Skip to content

Commit

Permalink
Refactor saveAssets code to allow out of tree overrides (#2591)
Browse files Browse the repository at this point in the history
  • Loading branch information
acoates-ms authored Aug 9, 2023
1 parent 642f233 commit 2edf436
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 81 deletions.
5 changes: 5 additions & 0 deletions .changeset/metal-ladybugs-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rnx-kit/metro-service": minor
---

Refactor saveAssets code to allow out of tree overrides
1 change: 1 addition & 0 deletions packages/metro-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
},
"depcheck": {
"ignoreMatches": [
"@office-iss/react-native-win32",
"metro-babel-transformer"
]
},
Expand Down
14 changes: 13 additions & 1 deletion packages/metro-service/src/asset/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

export function getAndroidAssetSuffix(scale: number): string {
const tolerance = 0.01;
Expand Down Expand Up @@ -52,3 +52,15 @@ export function getAssetDestPathAndroid(
const fileName = getResourceIdentifier(asset);
return path.join(androidFolder, `${fileName}.${asset.type}`);
}

export const saveAssetsAndroid: SaveAssetsPlugin = (
assets,
_platform,
_assetsDest,
_assetCatalogDest,
addAssetToCopy
) => {
assets.forEach((asset) =>
addAssetToCopy(asset, undefined, getAssetDestPathAndroid)
);
};
24 changes: 24 additions & 0 deletions packages/metro-service/src/asset/default.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import path from "path";
import type { PackagerAsset, SaveAssetsPlugin } from "./types";

export function getAssetDestPath(asset: PackagerAsset, scale: number): string {
const suffix = scale === 1 ? "" : `@${scale}x`;
const fileName = `${asset.name + suffix}.${asset.type}`;
return path.join(
// 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, "_"),
fileName
);
}

export const saveAssetsDefault: SaveAssetsPlugin = (
assets,
_platform,
_assetsDest,
_assetCatalogDest,
addAssetToCopy
) => {
assets.forEach((asset) => addAssetToCopy(asset, undefined, getAssetDestPath));
};
7 changes: 1 addition & 6 deletions packages/metro-service/src/asset/filter.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
// 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],
};

export function filterPlatformAssetScales(
platform: string,
allowlist: ReadonlyArray<number> | undefined,
scales: readonly number[]
): readonly number[] {
const allowlist: number[] = ALLOWED_SCALES[platform];
if (!allowlist) {
return scales;
}
Expand Down
60 changes: 45 additions & 15 deletions packages/metro-service/src/asset/ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

import fs from "fs";
import type { AssetData } from "metro";
import { error, info } from "@rnx-kit/console";
import path from "path";
import { getResourceIdentifier } from "./assetPathUtils";
import type { PackagerAsset } from "./types";
import { filterPlatformAssetScales } from "./filter";
import { getAssetDestPath } from "./default";
import type { SaveAssetsPlugin } from "./types";

type ImageSet = {
basePath: string;
Expand Down Expand Up @@ -68,17 +71,44 @@ export function writeImageSet(imageSet: ImageSet): void {
);
}

export function getAssetDestPathIOS(
asset: PackagerAsset,
scale: number
): string {
const suffix = scale === 1 ? "" : `@${scale}x`;
const fileName = `${asset.name + suffix}.${asset.type}`;
return path.join(
// 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.substring(1).replace(/\.\.\//g, "_"),
fileName
);
}
const ALLOWED_SCALES = [1, 2, 3];

export const saveAssetsIOS: SaveAssetsPlugin = (
assets,
_platform,
_assetsDest,
assetCatalogDest,
addAssetToCopy
) => {
if (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;
}

info("Adding images to asset catalog", catalogDir);
cleanAssetCatalog(catalogDir);
for (const asset of assets) {
if (isCatalogAsset(asset)) {
const imageSet = getImageSet(
catalogDir,
asset,
filterPlatformAssetScales(ALLOWED_SCALES, asset.scales)
);
writeImageSet(imageSet);
} else {
addAssetToCopy(asset, ALLOWED_SCALES, getAssetDestPath);
}
}
info("Done adding images to asset catalog");
} else {
assets.forEach((asset) =>
addAssetToCopy(asset, ALLOWED_SCALES, getAssetDestPath)
);
}
};
14 changes: 14 additions & 0 deletions packages/metro-service/src/asset/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import type { AssetData } from "metro";

export type PackagerAsset = {
httpServerLocation: string;
name: string;
type: string;
};

export type SaveAssetsPlugin = (
assets: ReadonlyArray<AssetData>,
platform: string,
assetsDest: string | undefined,
assetCatalogDest: string | undefined,
addAssetToCopy: (
asset: AssetData,
allowedScales: number[] | undefined,
getAssetDestPath: (asset: AssetData, scale: number) => string
) => void
) => void;
71 changes: 26 additions & 45 deletions packages/metro-service/src/asset/write.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
// 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 { 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 {
cleanAssetCatalog,
getAssetDestPathIOS,
getImageSet,
isCatalogAsset,
writeImageSet,
} from "./ios";
import type { SaveAssetsPlugin } from "./types";

function copy(
src: string,
Expand Down Expand Up @@ -62,7 +55,8 @@ export function saveAssets(
assets: ReadonlyArray<AssetData>,
platform: string,
assetsDest: string | undefined,
assetCatalogDest: string | undefined
assetCatalogDest: string | undefined,
saveAssetsPlugin: SaveAssetsPlugin
): Promise<void> {
if (!assetsDest) {
warn("Assets destination folder is not set, skipping...");
Expand All @@ -71,53 +65,40 @@ export function saveAssets(

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

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

const addAssetToCopy = (asset: AssetData) => {
const addAssetToCopy = (
asset: AssetData,
allowedScales: number[] | undefined,
getAssetDestPath: (asset: AssetData, scale: number) => string
) => {
const validScales = new Set(
filterPlatformAssetScales(platform, asset.scales)
filterPlatformAssetScales(allowedScales, asset.scales)
);

asset.scales.forEach((scale, idx) => {
asset.scales.forEach((scale: number, idx: number) => {
if (!validScales.has(scale)) {
return;
}
const src = asset.files[idx];
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);
asset.scales.forEach((scale, idx) => {
if (!validScales.has(scale)) {
return;
}
}
info("Done adding images to asset catalog");
} else {
assets.forEach(addAssetToCopy);
}
const src = asset.files[idx];
const dest = path.join(assetsDest, getAssetDestPath(asset, scale));
filesToCopy[src] = dest;
});
};

saveAssetsPlugin(
assets,
platform,
assetsDest,
assetCatalogDest,
addAssetToCopy
);
return copyAll(filesToCopy);
}
40 changes: 39 additions & 1 deletion packages/metro-service/src/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import Server from "metro/src/Server";
import Bundle from "metro/src/shared/output/bundle";
import path from "path";
import { saveAssets } from "./asset";
import type { SaveAssetsPlugin } from "./asset/types";
import { saveAssetsAndroid } from "./asset/android";
import { saveAssetsDefault } from "./asset/default";
import { saveAssetsIOS } from "./asset/ios";
import { ensureBabelConfig } from "./babel";

export type BundleArgs = {
Expand Down Expand Up @@ -41,6 +45,34 @@ type RequestOptions = {
unstable_transformProfile?: BundleOptions["unstable_transformProfile"];
};

// Eventually this will be part of the rn config, but we require it on older rn versions for win32 and the cli doesn't allow extra config properties.
// See https://github.com/react-native-community/cli/pull/2002
function getSaveAssetsPlugin(
platform: string,
projectRoot: string
): SaveAssetsPlugin {
if (platform === "win32") {
try {
const saveAssetsPlugin = require.resolve(
"@office-iss/react-native-win32/saveAssetPlugin",
{ paths: [projectRoot] }
);
return require(saveAssetsPlugin);
} catch (_) {
/* empty */
}
}

switch (platform) {
case "ios":
return saveAssetsIOS;
case "android":
return saveAssetsAndroid;
default:
return saveAssetsDefault;
}
}

export async function bundle(
args: BundleArgs,
config: ConfigT,
Expand All @@ -49,6 +81,11 @@ export async function bundle(
// ensure Metro can find Babel config
ensureBabelConfig(config);

const saveAssetsPlugin = getSaveAssetsPlugin(
args.platform,
config.projectRoot
);

if (config.resolver.platforms.indexOf(args.platform) === -1) {
error(
`Invalid platform ${
Expand Down Expand Up @@ -107,7 +144,8 @@ export async function bundle(
outputAssets,
args.platform,
args.assetsDest,
args.assetCatalogDest
args.assetCatalogDest,
saveAssetsPlugin
);
} finally {
server.end();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import * as path from "path";
import { getAssetDestPathIOS } from "../../src/asset/ios";
import { getAssetDestPath } from "../../src/asset/default";

describe("getAssetDestPathIOS", () => {
describe("getAssetDestPath", () => {
test("should build correct path", () => {
const asset = {
name: "icon",
type: "png",
httpServerLocation: "/assets/test",
};

expect(getAssetDestPathIOS(asset, 1)).toBe(
expect(getAssetDestPath(asset, 1)).toBe(
path.normalize("assets/test/icon.png")
);
});
Expand All @@ -21,10 +21,10 @@ describe("getAssetDestPathIOS", () => {
httpServerLocation: "/assets/test",
};

expect(getAssetDestPathIOS(asset, 2)).toBe(
expect(getAssetDestPath(asset, 2)).toBe(
path.normalize("assets/test/icon@2x.png")
);
expect(getAssetDestPathIOS(asset, 3)).toBe(
expect(getAssetDestPath(asset, 3)).toBe(
path.normalize("assets/test/icon@3x.png")
);
});
Expand All @@ -36,7 +36,7 @@ describe("getAssetDestPathIOS", () => {
httpServerLocation: "/assets/../../test",
};

expect(getAssetDestPathIOS(asset, 1)).toBe(
expect(getAssetDestPath(asset, 1)).toBe(
path.normalize("assets/__test/icon.png")
);
});
Expand Down
Loading

0 comments on commit 2edf436

Please sign in to comment.