From 42948682d4f32be986806f3b517785126dbb0647 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen <4123478+tido64@users.noreply.github.com> Date: Tue, 19 Sep 2023 11:13:14 +0200 Subject: [PATCH] fix(metro-service): consume `@react-native/assets-registry` --- .changeset/fuzzy-cheetahs-burn.md | 5 + .../metro-service/copy-assets-registry.mjs | 40 ++++++++ packages/metro-service/package.json | 3 + packages/metro-service/src/asset/android.ts | 47 +-------- .../metro-service/src/asset/assetPathUtils.ts | 20 ---- packages/metro-service/src/asset/ios.ts | 7 +- packages/metro-service/src/asset/types.ts | 11 +-- packages/metro-service/src/asset/write.ts | 5 +- .../src/assets-registry/path-support.d.ts | 9 ++ .../src/assets-registry/path-support.js | 95 +++++++++++++++++++ .../src/assets-registry/registry.d.ts | 13 +++ .../src/assets-registry/registry.js | 30 ++++++ .../metro-service/test/asset/android.test.ts | 47 +-------- yarn.lock | 46 +++++++++ 14 files changed, 256 insertions(+), 122 deletions(-) create mode 100644 .changeset/fuzzy-cheetahs-burn.md create mode 100644 packages/metro-service/copy-assets-registry.mjs delete mode 100644 packages/metro-service/src/asset/assetPathUtils.ts create mode 100644 packages/metro-service/src/assets-registry/path-support.d.ts create mode 100644 packages/metro-service/src/assets-registry/path-support.js create mode 100644 packages/metro-service/src/assets-registry/registry.d.ts create mode 100644 packages/metro-service/src/assets-registry/registry.js diff --git a/.changeset/fuzzy-cheetahs-burn.md b/.changeset/fuzzy-cheetahs-burn.md new file mode 100644 index 000000000..d13dbb7e3 --- /dev/null +++ b/.changeset/fuzzy-cheetahs-burn.md @@ -0,0 +1,5 @@ +--- +"@rnx-kit/metro-service": patch +--- + +Reuse code from `@react-native/assets-registry` diff --git a/packages/metro-service/copy-assets-registry.mjs b/packages/metro-service/copy-assets-registry.mjs new file mode 100644 index 000000000..446a1a71c --- /dev/null +++ b/packages/metro-service/copy-assets-registry.mjs @@ -0,0 +1,40 @@ +// @ts-check +import flowRemoveTypes from "flow-remove-types"; +import * as fs from "node:fs"; +import { createRequire } from "node:module"; +import * as path from "node:path"; + +function main() { + const require = createRequire(import.meta.url); + + const flowOptions = { all: true, pretty: true }; + /** @type {{ encoding: "utf-8" }} */ + const fsOptions = { encoding: "utf-8" }; + /** @type {fs.NoParamCallback} */ + const rethrow = (err) => { + if (err) { + throw err; + } + }; + + const assetsRegistry = path.dirname( + require.resolve("@react-native/assets-registry/package.json") + ); + + for (const filename of ["path-support.js", "registry.js"]) { + const p = path.join(assetsRegistry, filename); + fs.readFile(p, fsOptions, (err, source) => { + rethrow(err); + const t = [ + `// THIS FILE WAS GENERATED BY '${path.basename(import.meta.url)}'`, + "/* eslint-disable @typescript-eslint/ban-ts-comment */", + "// @ts-nocheck", + "// prettier-ignore", + flowRemoveTypes(source, flowOptions).toString(), + ].join("\n"); + fs.writeFile(path.join("src", "assets-registry", filename), t, rethrow); + }); + } +} + +main(); diff --git a/packages/metro-service/package.json b/packages/metro-service/package.json index 37b8d072e..f81a6a893 100644 --- a/packages/metro-service/package.json +++ b/packages/metro-service/package.json @@ -15,6 +15,7 @@ "directory": "packages/metro-service" }, "scripts": { + "prepare": "node copy-assets-registry.mjs", "build": "rnx-kit-scripts build", "format": "rnx-kit-scripts format", "lint": "rnx-kit-scripts lint", @@ -53,10 +54,12 @@ }, "devDependencies": { "@react-native-community/cli-types": "^11.0.0", + "@react-native/assets-registry": "^0.73.0", "@rnx-kit/scripts": "*", "@types/node": "^18.0.0", "@types/node-fetch": "^2.6.5", "eslint": "^8.0.0", + "flow-remove-types": "~2.206.0", "jest": "^29.2.1", "metro": "^0.76.5", "metro-config": "^0.76.5", diff --git a/packages/metro-service/src/asset/android.ts b/packages/metro-service/src/asset/android.ts index daea6ba18..c9542f674 100644 --- a/packages/metro-service/src/asset/android.ts +++ b/packages/metro-service/src/asset/android.ts @@ -1,55 +1,18 @@ // 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 { + getAndroidResourceFolderName, + getAndroidResourceIdentifier, +} from "../assets-registry/path-support"; import type { PackagerAsset, SaveAssetsPlugin } from "./types"; -export function getAndroidAssetSuffix(scale: number): string { - const tolerance = 0.01; - 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"; - } -} - -// See https://developer.android.com/guide/topics/resources/drawable-resource.html -const drawableFileTypes = new Set([ - "gif", - "jpeg", - "jpg", - "png", - "webp", - "xml", -]); - -function getAndroidResourceFolderName( - asset: PackagerAsset, - scale: number -): string { - if (!drawableFileTypes.has(asset.type)) { - return "raw"; - } - 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 = getResourceIdentifier(asset); + const fileName = getAndroidResourceIdentifier(asset); return path.join(androidFolder, `${fileName}.${asset.type}`); } diff --git a/packages/metro-service/src/asset/assetPathUtils.ts b/packages/metro-service/src/asset/assetPathUtils.ts deleted file mode 100644 index cedad8528..000000000 --- a/packages/metro-service/src/asset/assetPathUtils.ts +++ /dev/null @@ -1,20 +0,0 @@ -// 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 -} diff --git a/packages/metro-service/src/asset/ios.ts b/packages/metro-service/src/asset/ios.ts index df381fc39..b711916df 100644 --- a/packages/metro-service/src/asset/ios.ts +++ b/packages/metro-service/src/asset/ios.ts @@ -3,12 +3,11 @@ import { error, info } from "@rnx-kit/console"; import fs from "fs"; -import type { AssetData } from "metro"; import path from "path"; -import { getResourceIdentifier } from "./assetPathUtils"; +import { getAndroidResourceIdentifier } from "../assets-registry/path-support"; import { getAssetDestPath } from "./default"; import { filterPlatformAssetScales } from "./filter"; -import type { SaveAssetsPlugin } from "./types"; +import type { AssetData, SaveAssetsPlugin } from "./types"; type ImageSet = { basePath: string; @@ -29,7 +28,7 @@ export function getImageSet( asset: AssetData, scales: readonly number[] ): ImageSet { - const fileName = getResourceIdentifier(asset); + const fileName = getAndroidResourceIdentifier(asset); return { basePath: path.join(catalogDir, `${fileName}.imageset`), files: scales.map((scale, idx) => { diff --git a/packages/metro-service/src/asset/types.ts b/packages/metro-service/src/asset/types.ts index 57ee89e3f..785ad19f9 100644 --- a/packages/metro-service/src/asset/types.ts +++ b/packages/metro-service/src/asset/types.ts @@ -1,13 +1,8 @@ import type { AssetData } from "metro"; - -export type PackagerAsset = { - httpServerLocation: string; - name: string; - type: string; -}; +import type { PackagerAsset } from "../assets-registry/registry"; export type SaveAssetsPlugin = ( - assets: ReadonlyArray, + assets: readonly AssetData[], platform: string, assetsDest: string | undefined, assetCatalogDest: string | undefined, @@ -17,3 +12,5 @@ export type SaveAssetsPlugin = ( getAssetDestPath: (asset: AssetData, scale: number) => string ) => void ) => void; + +export { AssetData, PackagerAsset }; diff --git a/packages/metro-service/src/asset/write.ts b/packages/metro-service/src/asset/write.ts index 6d40cf401..249a937bc 100644 --- a/packages/metro-service/src/asset/write.ts +++ b/packages/metro-service/src/asset/write.ts @@ -2,10 +2,9 @@ import { info, warn } from "@rnx-kit/console"; import * as fs from "fs"; -import type { AssetData } from "metro"; import * as path from "path"; import { filterPlatformAssetScales } from "./filter"; -import type { SaveAssetsPlugin } from "./types"; +import type { AssetData, SaveAssetsPlugin } from "./types"; function copy( src: string, @@ -52,7 +51,7 @@ function copyAll(filesToCopy: Record) { } export function saveAssets( - assets: ReadonlyArray, + assets: readonly AssetData[], platform: string, assetsDest: string | undefined, assetCatalogDest: string | undefined, diff --git a/packages/metro-service/src/assets-registry/path-support.d.ts b/packages/metro-service/src/assets-registry/path-support.d.ts new file mode 100644 index 000000000..f04f30110 --- /dev/null +++ b/packages/metro-service/src/assets-registry/path-support.d.ts @@ -0,0 +1,9 @@ +import type { PackagerAsset } from "./registry"; +export declare function getAndroidResourceFolderName( + asset: PackagerAsset, + scale: number +): string | "raw"; +export declare function getAndroidResourceIdentifier( + asset: PackagerAsset +): string; +export declare function getBasePath(asset: PackagerAsset): string; diff --git a/packages/metro-service/src/assets-registry/path-support.js b/packages/metro-service/src/assets-registry/path-support.js new file mode 100644 index 000000000..0901938bb --- /dev/null +++ b/packages/metro-service/src/assets-registry/path-support.js @@ -0,0 +1,95 @@ +// THIS FILE WAS GENERATED BY 'copy-assets-registry.mjs' +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-nocheck +// prettier-ignore +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * strict + */ + +'use strict'; + + +const androidScaleSuffix = { + '0.75': 'ldpi', + '1': 'mdpi', + '1.5': 'hdpi', + '2': 'xhdpi', + '3': 'xxhdpi', + '4': 'xxxhdpi', +}; + +const ANDROID_BASE_DENSITY = 160; + +/** + * FIXME: using number to represent discrete scale numbers is fragile in essence because of + * floating point numbers imprecision. + */ +function getAndroidAssetSuffix(scale) { + if (scale.toString() in androidScaleSuffix) { + return androidScaleSuffix[scale.toString()]; + } + // NOTE: Android Gradle Plugin does not fully support the nnndpi format. + // See https://issuetracker.google.com/issues/72884435 + if (Number.isFinite(scale) && scale > 0) { + return Math.round(scale * ANDROID_BASE_DENSITY) + 'dpi'; + } + throw new Error('no such scale ' + scale.toString()); +} + +// See https://developer.android.com/guide/topics/resources/drawable-resource.html +const drawableFileTypes = new Set([ + 'gif', + 'jpeg', + 'jpg', + 'ktx', + 'png', + 'svg', + 'webp', + 'xml', +]); + +function getAndroidResourceFolderName( + asset, + scale, +) { + if (!drawableFileTypes.has(asset.type)) { + return 'raw'; + } + const suffix = getAndroidAssetSuffix(scale); + if (!suffix) { + throw new Error( + "Don't know which android drawable suffix to use for scale: " + + scale + + '\nAsset: ' + + JSON.stringify(asset, null, '\t') + + '\nPossible scales are:' + + JSON.stringify(androidScaleSuffix, null, '\t'), + ); + } + return 'drawable-' + suffix; +} + +function getAndroidResourceIdentifier(asset) { + return (getBasePath(asset) + '/' + asset.name) + .toLowerCase() + .replace(/\//g, '_') // Encode folder structure in file name + .replace(/([^a-z0-9_])/g, '') // Remove illegal chars + .replace(/^assets_/, ''); // Remove "assets_" prefix +} + +function getBasePath(asset) { + const basePath = asset.httpServerLocation; + return basePath.startsWith('/') ? basePath.substr(1) : basePath; +} + +module.exports = { + getAndroidResourceFolderName, + getAndroidResourceIdentifier, + getBasePath, +}; diff --git a/packages/metro-service/src/assets-registry/registry.d.ts b/packages/metro-service/src/assets-registry/registry.d.ts new file mode 100644 index 000000000..217743c90 --- /dev/null +++ b/packages/metro-service/src/assets-registry/registry.d.ts @@ -0,0 +1,13 @@ +export type PackagerAsset = { + readonly __packager_asset: boolean; + readonly fileSystemLocation: string; + readonly httpServerLocation: string; + readonly width?: null | undefined | number; + readonly height?: null | undefined | number; + readonly scales: number[]; + readonly hash: string; + readonly name: string; + readonly type: string; +}; +export declare function registerAsset(asset: PackagerAsset): number; +export declare function getAssetByID(assetId: number): PackagerAsset; diff --git a/packages/metro-service/src/assets-registry/registry.js b/packages/metro-service/src/assets-registry/registry.js new file mode 100644 index 000000000..f9828cec7 --- /dev/null +++ b/packages/metro-service/src/assets-registry/registry.js @@ -0,0 +1,30 @@ +// THIS FILE WAS GENERATED BY 'copy-assets-registry.mjs' +/* eslint-disable @typescript-eslint/ban-ts-comment */ +// @ts-nocheck +// prettier-ignore +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * strict + * @format + */ + +'use strict'; + + +const assets = []; + +function registerAsset(asset) { + // `push` returns new array length, so the first asset will + // get id 1 (not 0) to make the value truthy + return assets.push(asset); +} + +function getAssetByID(assetId) { + return assets[assetId - 1]; +} + +module.exports = {registerAsset, getAssetByID}; diff --git a/packages/metro-service/test/asset/android.test.ts b/packages/metro-service/test/asset/android.test.ts index 26364c36a..c9961d66c 100644 --- a/packages/metro-service/test/asset/android.test.ts +++ b/packages/metro-service/test/asset/android.test.ts @@ -1,50 +1,5 @@ import * as path from "path"; -import { - getAndroidAssetSuffix, - getAssetDestPathAndroid, -} from "../../src/asset/android"; - -describe("getAndroidAssetSuffix", () => { - test("returns 'ldpi' for scale 0.75", () => { - expect(getAndroidAssetSuffix(0.75)).toEqual("ldpi"); - }); - - test("returns 'mdpi' for scale 1", () => { - expect(getAndroidAssetSuffix(1)).toEqual("mdpi"); - }); - - test("returns 'hdpi' for scale 1.5", () => { - expect(getAndroidAssetSuffix(1.5)).toEqual("hdpi"); - }); - - test("returns 'xhdpi' for scale 2", () => { - expect(getAndroidAssetSuffix(2)).toEqual("xhdpi"); - }); - - test("returns 'xxhdpi' for scale 3", () => { - expect(getAndroidAssetSuffix(3)).toEqual("xxhdpi"); - }); - - test("returns 'xxxhdpi' for scale 4", () => { - expect(getAndroidAssetSuffix(4)).toEqual("xxxhdpi"); - }); - - test("returns 'ldpi' for scale 0.741", () => { - expect(getAndroidAssetSuffix(0.741)).toEqual("ldpi"); - }); - - test("returns 'ldpi' for scale 0.759", () => { - expect(getAndroidAssetSuffix(0.759)).toEqual("ldpi"); - }); - - test("returns 'ldpi' for scale 0.70", () => { - expect(getAndroidAssetSuffix(0.7)).toEqual("ldpi"); - }); - - test("returns 'mdpi' for scale 1.23", () => { - expect(getAndroidAssetSuffix(1.23)).toEqual("mdpi"); - }); -}); +import { getAssetDestPathAndroid } from "../../src/asset/android"; describe("getAssetDestPathAndroid", () => { test("should use the right destination folder", () => { diff --git a/yarn.lock b/yarn.lock index 09eeffaf8..247a7a6fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3285,6 +3285,13 @@ __metadata: languageName: node linkType: hard +"@react-native/assets-registry@npm:^0.73.0": + version: 0.73.0 + resolution: "@react-native/assets-registry@npm:0.73.0" + checksum: 27548b90c79d7c34d6551173f99374c929cefa7e236d429e03baa63d570bf916abb8f08ce1771bc3a097f38384231e6bf2aa9f5db7a35c10e0c7a819981a1430 + languageName: node + linkType: hard + "@react-native/assets@npm:1.0.0": version: 1.0.0 resolution: "@react-native/assets@npm:1.0.0" @@ -3863,6 +3870,7 @@ __metadata: resolution: "@rnx-kit/metro-service@workspace:packages/metro-service" dependencies: "@react-native-community/cli-types": ^11.0.0 + "@react-native/assets-registry": ^0.73.0 "@rnx-kit/console": ^1.0.0 "@rnx-kit/scripts": "*" "@rnx-kit/tools-language": ^2.0.0 @@ -3870,6 +3878,7 @@ __metadata: "@types/node-fetch": ^2.6.5 chalk: ^4.1.0 eslint: ^8.0.0 + flow-remove-types: ~2.206.0 jest: ^29.2.1 metro: ^0.76.5 metro-config: ^0.76.5 @@ -7530,6 +7539,20 @@ __metadata: languageName: node linkType: hard +"flow-remove-types@npm:~2.206.0": + version: 2.206.0 + resolution: "flow-remove-types@npm:2.206.0" + dependencies: + flow-parser: ^0.206.0 + pirates: ^3.0.2 + vlq: ^0.2.1 + bin: + flow-node: flow-node + flow-remove-types: flow-remove-types + checksum: 54b848d0ce9b7f2c7611ff3ba6230d52ccfef4b73408fd88c4c9c25a83c8a3a128de076c5dab9d2ce5cd3f2588f06c3250b9699eabb4d9bc32c8432f0b7ee157 + languageName: node + linkType: hard + "fn.name@npm:1.x.x": version: 1.1.0 resolution: "fn.name@npm:1.1.0" @@ -10724,6 +10747,13 @@ __metadata: languageName: node linkType: hard +"node-modules-regexp@npm:^1.0.0": + version: 1.0.0 + resolution: "node-modules-regexp@npm:1.0.0" + checksum: 99541903536c5ce552786f0fca7f06b88df595e62e423c21fa86a1674ee2363dad1f7482d1bec20b4bd9fa5f262f88e6e5cb788fc56411113f2fe2e97783a3a7 + languageName: node + linkType: hard + "node-releases@npm:^2.0.12": version: 2.0.12 resolution: "node-releases@npm:2.0.12" @@ -11421,6 +11451,15 @@ __metadata: languageName: node linkType: hard +"pirates@npm:^3.0.2": + version: 3.0.2 + resolution: "pirates@npm:3.0.2" + dependencies: + node-modules-regexp: ^1.0.0 + checksum: 73bc9d8a1859c0854740541d1afd0c87b62d3d63fdaec38c3ac7f57d9d70b9f6f325f5ffcac11b8935c8d9249ead41c9c59632ed098b225a96219274ca6248c8 + languageName: node + linkType: hard + "pirates@npm:^4.0.4, pirates@npm:^4.0.5": version: 4.0.5 resolution: "pirates@npm:4.0.5" @@ -13805,6 +13844,13 @@ __metadata: languageName: node linkType: hard +"vlq@npm:^0.2.1": + version: 0.2.3 + resolution: "vlq@npm:0.2.3" + checksum: 2231d8caeb5b2c1a438677ab029e9a94aa6fb61ab05819c72691b792aea0456dab29576aff5ae29309ee45bad0a309e832dc45173119bca1393f3b87709d8f8d + languageName: node + linkType: hard + "vlq@npm:^1.0.0": version: 1.0.1 resolution: "vlq@npm:1.0.1"