Skip to content

Commit

Permalink
fix(metro-service): cli-plugin-metro has been deprecated
Browse files Browse the repository at this point in the history
Sync to the latest changes and don't depend on it for bundling.

`@react-native-community/cli-plugin-metro` is being moved into the
`react-native` repository. In the process, it was renamed and its API
surface has been reduced to the bare minimum. `buildBundleWithConfig`,
which we need to pass our custom config to the bundler, has also been
axed. For more details, see
facebook/react-native#38795.
  • Loading branch information
tido64 committed Aug 7, 2023
1 parent 6d05950 commit 32f73c5
Show file tree
Hide file tree
Showing 11 changed files with 386 additions and 151 deletions.
12 changes: 12 additions & 0 deletions .changeset/long-avocados-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@rnx-kit/metro-service": patch
---

`@react-native-community/cli-plugin-metro` has been deprecated. Sync to the
latest changes and don't depend on it for bundling.

`@react-native-community/cli-plugin-metro` is being moved into the
`react-native` repository. In the process, it was renamed and its API surface
has been reduced to the bare minimum. `buildBundleWithConfig`, which we need to
pass our custom config to the bundler, has also been axed. For more details, see
https://github.com/facebook/react-native/pull/38795.
80 changes: 20 additions & 60 deletions packages/metro-service/src/asset/android.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,25 @@
// https://github.com/react-native-community/cli/blob/716555851b442a83a1bf5e0db27b6226318c9a69/packages/cli-plugin-metro/src/commands/bundle/getAssetDestPathAndroid.ts

import * as path from "path";
import { getResourceIdentifier } from "./assetPathUtils";
import type { PackagerAsset } from "./types";

/**
* Decide if two numbers, integer or decimal, are "approximately" equal.
* They're equal if they are close enough to be within the given tolerance.
*
* This is useful for comparing decimal values, as they aren't precise enough
* to use equality.
*
* @param lhs First value to compare
* @param rhs Second value to compare
* @param tolerance Number indicating how far apart the first and second values can be before they are considered not equal.
* @returns True if the difference between the first and second value is less than the tolerance
*/
export function isApproximatelyEqual(
lhs: number,
rhs: number,
tolerance: number
): boolean {
return Math.abs(lhs - rhs) < tolerance;
}

export function getAndroidAssetSuffix(
asset: PackagerAsset,
scale: number
): string {
export function getAndroidAssetSuffix(scale: number): string {
const tolerance = 0.01;
if (isApproximatelyEqual(scale, 0.75, tolerance)) {
return "ldpi";
} else if (isApproximatelyEqual(scale, 1, tolerance)) {
return "mdpi";
} else if (isApproximatelyEqual(scale, 1.5, tolerance)) {
return "hdpi";
} else if (isApproximatelyEqual(scale, 2, tolerance)) {
return "xhdpi";
} else if (isApproximatelyEqual(scale, 3, tolerance)) {
return "xxhdpi";
} else if (isApproximatelyEqual(scale, 4, tolerance)) {
const scaleApprox = scale + tolerance;
if (scaleApprox >= 4) {
return "xxxhdpi";
} else if (scaleApprox >= 3) {
return "xxhdpi";
} else if (scaleApprox >= 2) {
return "xhdpi";
} else if (scaleApprox >= 1.5) {
return "hdpi";
} else if (scaleApprox >= 1) {
return "mdpi";
} else {
return "ldpi";
}

throw new Error(
`Don't know which android drawable suffix to use for asset: ${JSON.stringify(
asset
)}`
);
}

// See https://developer.android.com/guide/topics/resources/drawable-resource.html
Expand All @@ -64,31 +39,16 @@ function getAndroidResourceFolderName(
if (!drawableFileTypes.has(asset.type)) {
return "raw";
}
return `drawable-${getAndroidAssetSuffix(asset, scale)}`;
}

function getBasePath(asset: PackagerAsset): string {
let basePath = asset.httpServerLocation;
if (basePath[0] === "/") {
basePath = basePath.substring(1);
}
return basePath;
}

function getAndroidResourceIdentifier(asset: PackagerAsset): string {
const folderPath = getBasePath(asset);
return `${folderPath}/${asset.name}`
.toLowerCase()
.replace(/\//g, "_") // Encode folder structure in file name
.replace(/([^a-z0-9_])/g, "") // Remove illegal chars
.replace(/^assets_/, ""); // Remove "assets_" prefix
const suffix = getAndroidAssetSuffix(scale);
const androidFolder = `drawable-${suffix}`;
return androidFolder;
}

export function getAssetDestPathAndroid(
asset: PackagerAsset,
scale: number
): string {
const androidFolder = getAndroidResourceFolderName(asset, scale);
const fileName = getAndroidResourceIdentifier(asset);
const fileName = getResourceIdentifier(asset);
return path.join(androidFolder, `${fileName}.${asset.type}`);
}
20 changes: 20 additions & 0 deletions packages/metro-service/src/asset/assetPathUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// https://github.com/react-native-community/cli/blob/716555851b442a83a1bf5e0db27b6226318c9a69/packages/cli-plugin-metro/src/commands/bundle/assetPathUtils.ts

import type { PackagerAsset } from "./types";

function getBasePath(asset: PackagerAsset): string {
let basePath = asset.httpServerLocation;
if (basePath[0] === "/") {
basePath = basePath.substring(1);
}
return basePath;
}

export function getResourceIdentifier(asset: PackagerAsset): string {
const folderPath = getBasePath(asset);
return `${folderPath}/${asset.name}`
.toLowerCase()
.replace(/\//g, "_") // Encode folder structure in file name
.replace(/([^a-z0-9_])/g, "") // Remove illegal chars
.replace(/^assets_/, ""); // Remove "assets_" prefix
}
2 changes: 2 additions & 0 deletions packages/metro-service/src/asset/filter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// https://github.com/react-native-community/cli/blob/716555851b442a83a1bf5e0db27b6226318c9a69/packages/cli-plugin-metro/src/commands/bundle/filterPlatformAssetScales.ts

const ALLOWED_SCALES: { [key: string]: number[] } = {
ios: [1, 2, 3],
};
Expand Down
69 changes: 68 additions & 1 deletion packages/metro-service/src/asset/ios.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,73 @@
// https://github.com/react-native-community/cli/blob/716555851b442a83a1bf5e0db27b6226318c9a69/packages/cli-plugin-metro/src/commands/bundle/assetCatalogIOS.ts
// https://github.com/react-native-community/cli/blob/716555851b442a83a1bf5e0db27b6226318c9a69/packages/cli-plugin-metro/src/commands/bundle/getAssetDestPathIOS.ts

import fs from "fs";
import type { AssetData } from "metro";
import path from "path";
import { getResourceIdentifier } from "./assetPathUtils";
import type { PackagerAsset } from "./types";

type ImageSet = {
basePath: string;
files: { name: string; src: string; scale: number }[];
};

export function cleanAssetCatalog(catalogDir: string): void {
const files = fs
.readdirSync(catalogDir)
.filter((file) => file.endsWith(".imageset"));
for (const file of files) {
fs.rmSync(path.join(catalogDir, file));
}
}

export function getImageSet(
catalogDir: string,
asset: AssetData,
scales: readonly number[]
): ImageSet {
const fileName = getResourceIdentifier(asset);
return {
basePath: path.join(catalogDir, `${fileName}.imageset`),
files: scales.map((scale, idx) => {
const suffix = scale === 1 ? "" : `@${scale}x`;
return {
name: `${fileName + suffix}.${asset.type}`,
scale,
src: asset.files[idx],
};
}),
};
}

export function isCatalogAsset(asset: AssetData): boolean {
return asset.type === "png" || asset.type === "jpg" || asset.type === "jpeg";
}

export function writeImageSet(imageSet: ImageSet): void {
fs.mkdirSync(imageSet.basePath, { recursive: true });

for (const file of imageSet.files) {
const dest = path.join(imageSet.basePath, file.name);
fs.copyFileSync(file.src, dest);
}

fs.writeFileSync(
path.join(imageSet.basePath, "Contents.json"),
JSON.stringify({
images: imageSet.files.map((file) => ({
filename: file.name,
idiom: "universal",
scale: `${file.scale}x`,
})),
info: {
author: "xcode",
version: 1,
},
})
);
}

export function getAssetDestPathIOS(
asset: PackagerAsset,
scale: number
Expand All @@ -11,7 +78,7 @@ export function getAssetDestPathIOS(
// Assets can have relative paths outside of the project root.
// Replace `../` with `_` to make sure they don't end up outside of
// the expected assets directory.
asset.httpServerLocation.substr(1).replace(/\.\.\//g, "_"),
asset.httpServerLocation.substring(1).replace(/\.\.\//g, "_"),
fileName
);
}
105 changes: 86 additions & 19 deletions packages/metro-service/src/asset/write.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,84 @@
// https://github.com/react-native-community/cli/blob/716555851b442a83a1bf5e0db27b6226318c9a69/packages/cli-plugin-metro/src/commands/bundle/saveAssets.ts

import { error, info, warn } from "@rnx-kit/console";
import * as fs from "fs";
import type { AssetData } from "metro";
import * as path from "path";
import { getAssetDestPathAndroid } from "./android";
import { filterPlatformAssetScales } from "./filter";
import { getAssetDestPathIOS } from "./ios";
import {
cleanAssetCatalog,
getAssetDestPathIOS,
getImageSet,
isCatalogAsset,
writeImageSet,
} from "./ios";

function copy(src: string, dest: string): void {
function copy(
src: string,
dest: string,
callback: (error: NodeJS.ErrnoException) => void
): void {
const destDir = path.dirname(dest);
fs.mkdirSync(destDir, { recursive: true, mode: 0o755 });
fs.copyFileSync(src, dest);
fs.mkdir(destDir, { recursive: true }, (err?) => {
if (err) {
callback(err);
return;
}
fs.createReadStream(src)
.pipe(fs.createWriteStream(dest))
.on("finish", callback);
});
}

function copyAll(filesToCopy: Record<string, string>): void {
function copyAll(filesToCopy: Record<string, string>) {
const queue = Object.keys(filesToCopy);
if (queue.length === 0) {
return;
return Promise.resolve();
}

console.info(`Copying ${queue.length} asset files`);
for (const src of queue) {
copy(src, filesToCopy[src]);
}
info(`Copying ${queue.length} asset files`);
return new Promise<void>((resolve, reject) => {
const copyNext = (error?: NodeJS.ErrnoException) => {
if (error) {
reject(error);
return;
}
if (queue.length === 0) {
info("Done copying assets");
resolve();
} else {
// queue.length === 0 is checked in previous branch, so this is string
const src = queue.shift() as string;
const dest = filesToCopy[src];
copy(src, dest, copyNext);
}
};
copyNext();
});
}

export async function saveAssets(
assets: readonly AssetData[],
export function saveAssets(
assets: ReadonlyArray<AssetData>,
platform: string,
assetsDest: string | undefined
assetsDest: string | undefined,
assetCatalogDest: string | undefined
): Promise<void> {
if (!assetsDest) {
console.warn("Assets destination folder is not set, skipping...");
warn("Assets destination folder is not set, skipping...");
return Promise.resolve();
}

const filesToCopy: Record<string, string> = Object.create(null); // Map src -> dest

const getAssetDestPath =
platform === "android" ? getAssetDestPathAndroid : getAssetDestPathIOS;

const filesToCopy: Record<string, string> = Object.create(null); // Map src -> dest
assets.forEach((asset) => {
const addAssetToCopy = (asset: AssetData) => {
const validScales = new Set(
filterPlatformAssetScales(platform, asset.scales)
);

asset.scales.forEach((scale, idx) => {
if (!validScales.has(scale)) {
return;
Expand All @@ -49,8 +87,37 @@ export async function saveAssets(
const dest = path.join(assetsDest, getAssetDestPath(asset, scale));
filesToCopy[src] = dest;
});
});
};

if (platform === "ios" && assetCatalogDest != null) {
// Use iOS Asset Catalog for images. This will allow Apple app thinning to
// remove unused scales from the optimized bundle.
const catalogDir = path.join(assetCatalogDest, "RNAssets.xcassets");
if (!fs.existsSync(catalogDir)) {
error(
`Could not find asset catalog 'RNAssets.xcassets' in ${assetCatalogDest}. Make sure to create it if it does not exist.`
);
return Promise.reject();
}

info("Adding images to asset catalog", catalogDir);
cleanAssetCatalog(catalogDir);
for (const asset of assets) {
if (isCatalogAsset(asset)) {
const imageSet = getImageSet(
catalogDir,
asset,
filterPlatformAssetScales(platform, asset.scales)
);
writeImageSet(imageSet);
} else {
addAssetToCopy(asset);
}
}
info("Done adding images to asset catalog");
} else {
assets.forEach(addAssetToCopy);
}

copyAll(filesToCopy);
return Promise.resolve();
return copyAll(filesToCopy);
}
Loading

0 comments on commit 32f73c5

Please sign in to comment.