Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor saveAssets code to allow out of tree overrides #2591

Merged
merged 7 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
);
};
32 changes: 32 additions & 0 deletions packages/metro-service/src/asset/default.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

acoates-ms marked this conversation as resolved.
Show resolved Hide resolved
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