@rnx-kit/cli 0.11.2 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +30 -22
  3. package/coverage/clover.xml +140 -16
  4. package/coverage/coverage-final.json +3 -2
  5. package/coverage/lcov-report/index.html +20 -20
  6. package/coverage/lcov-report/src/bundle/index.html +1 -1
  7. package/coverage/lcov-report/src/bundle/kit-config.ts.html +13 -13
  8. package/coverage/lcov-report/src/bundle/metro.ts.html +2 -2
  9. package/coverage/lcov-report/src/bundle/overrides.ts.html +4 -4
  10. package/coverage/lcov-report/src/copy-assets.ts.html +1387 -0
  11. package/coverage/lcov-report/src/index.html +25 -10
  12. package/coverage/lcov-report/src/metro-config.ts.html +4 -4
  13. package/coverage/lcov-report/src/typescript/index.html +1 -1
  14. package/coverage/lcov-report/src/typescript/project-cache.ts.html +1 -1
  15. package/coverage/lcov.info +262 -23
  16. package/lib/bundle/metro.js +1 -1
  17. package/lib/bundle/metro.js.map +1 -1
  18. package/lib/bundle/overrides.d.ts +1 -1
  19. package/lib/bundle/overrides.d.ts.map +1 -1
  20. package/lib/bundle/overrides.js +2 -2
  21. package/lib/bundle/overrides.js.map +1 -1
  22. package/lib/bundle.d.ts +1 -0
  23. package/lib/bundle.d.ts.map +1 -1
  24. package/lib/bundle.js +8 -0
  25. package/lib/bundle.js.map +1 -1
  26. package/lib/clean.d.ts.map +1 -1
  27. package/lib/clean.js +40 -18
  28. package/lib/clean.js.map +1 -1
  29. package/lib/copy-assets.d.ts +112 -0
  30. package/lib/copy-assets.d.ts.map +1 -0
  31. package/lib/copy-assets.js +353 -0
  32. package/lib/copy-assets.js.map +1 -0
  33. package/lib/index.d.ts +1 -0
  34. package/lib/index.d.ts.map +1 -1
  35. package/lib/index.js +4 -1
  36. package/lib/index.js.map +1 -1
  37. package/lib/metro-config.d.ts +2 -2
  38. package/lib/metro-config.d.ts.map +1 -1
  39. package/lib/metro-config.js +3 -3
  40. package/lib/metro-config.js.map +1 -1
  41. package/lib/start.js +1 -1
  42. package/lib/start.js.map +1 -1
  43. package/package.json +5 -2
  44. package/react-native.config.js +8 -1
  45. package/src/bundle/metro.ts +1 -1
  46. package/src/bundle/overrides.ts +3 -3
  47. package/src/bundle.ts +13 -1
  48. package/src/clean.ts +6 -7
  49. package/src/copy-assets.ts +434 -0
  50. package/src/index.ts +1 -0
  51. package/src/metro-config.ts +3 -3
  52. package/src/start.ts +1 -1
  53. package/test/__mocks__/fs-extra.js +20 -0
  54. package/test/bundle/kit-config.test.ts +23 -1
  55. package/test/bundle/metro.test.ts +1 -1
  56. package/test/bundle/overrides.test.ts +3 -13
  57. package/test/copy-assets.test.ts +123 -0
@@ -0,0 +1,434 @@
1
+ import type { Config as CLIConfig } from "@react-native-community/cli-types";
2
+ import { error, info, warn } from "@rnx-kit/console";
3
+ import { isNonEmptyArray } from "@rnx-kit/tools-language/array";
4
+ import type { PackageManifest } from "@rnx-kit/tools-node/package";
5
+ import { findPackageDir, readPackage } from "@rnx-kit/tools-node/package";
6
+ import type { AllPlatforms } from "@rnx-kit/tools-react-native";
7
+ import { parsePlatform } from "@rnx-kit/tools-react-native";
8
+ import { spawnSync } from "child_process";
9
+ import * as fs from "fs-extra";
10
+ import * as os from "os";
11
+ import * as path from "path";
12
+
13
+ export type AndroidArchive = {
14
+ targetName: string;
15
+ version?: string;
16
+ output?: string;
17
+ };
18
+
19
+ export type NativeAssets = {
20
+ assets?: string[];
21
+ strings?: string[];
22
+ aar?: AndroidArchive & {
23
+ env?: Record<string, string | number>;
24
+ dependencies?: Record<string, AndroidArchive>;
25
+ };
26
+ xcassets?: string[];
27
+ };
28
+
29
+ export type Options = {
30
+ platform: AllPlatforms;
31
+ assetsDest: string;
32
+ bundleAar: boolean;
33
+ xcassetsDest?: string;
34
+ [key: string]: unknown;
35
+ };
36
+
37
+ export type Context = {
38
+ projectRoot: string;
39
+ manifest: PackageManifest;
40
+ options: Options;
41
+ };
42
+
43
+ export type AssetsConfig = {
44
+ getAssets?: (context: Context) => Promise<NativeAssets>;
45
+ };
46
+
47
+ function ensureOption(options: Options, opt: string, flag = opt) {
48
+ if (options[opt] == null) {
49
+ error(`Missing required option: --${flag}`);
50
+ process.exit(1);
51
+ }
52
+ }
53
+
54
+ function findGradleProject(projectRoot: string): string | undefined {
55
+ if (fs.existsSync(path.join(projectRoot, "android", "build.gradle"))) {
56
+ return path.join(projectRoot, "android");
57
+ }
58
+ if (fs.existsSync(path.join(projectRoot, "build.gradle"))) {
59
+ return projectRoot;
60
+ }
61
+ return undefined;
62
+ }
63
+
64
+ function isAssetsConfig(config: unknown): config is AssetsConfig {
65
+ return typeof config === "object" && config !== null && "getAssets" in config;
66
+ }
67
+
68
+ function keysOf(record: Record<string, unknown> | undefined): string[] {
69
+ return record ? Object.keys(record) : [];
70
+ }
71
+
72
+ export function versionOf(pkgName: string): string {
73
+ const { version } = readPackage(require.resolve(`${pkgName}/package.json`));
74
+ return version;
75
+ }
76
+
77
+ function getAndroidPaths(
78
+ context: Context,
79
+ packageName: string,
80
+ { targetName, version, output }: AndroidArchive
81
+ ) {
82
+ const projectRoot = path.dirname(
83
+ require.resolve(`${packageName}/package.json`)
84
+ );
85
+
86
+ switch (packageName) {
87
+ case "hermes-engine":
88
+ return {
89
+ projectRoot,
90
+ output: path.join(projectRoot, "android", "hermes-release.aar"),
91
+ destination: path.join(
92
+ context.options.assetsDest,
93
+ "aar",
94
+ `hermes-release-${versionOf(packageName)}.aar`
95
+ ),
96
+ };
97
+
98
+ case "react-native":
99
+ return {
100
+ projectRoot,
101
+ output: path.join(projectRoot, "android"),
102
+ destination: path.join(
103
+ context.options.assetsDest,
104
+ "aar",
105
+ "react-native"
106
+ ),
107
+ };
108
+
109
+ default: {
110
+ const androidProject = findGradleProject(projectRoot);
111
+ return {
112
+ projectRoot,
113
+ androidProject,
114
+ output:
115
+ output ||
116
+ (androidProject &&
117
+ path.join(
118
+ androidProject,
119
+ "build",
120
+ "outputs",
121
+ "aar",
122
+ `${targetName}-release.aar`
123
+ )),
124
+ destination: path.join(
125
+ context.options.assetsDest,
126
+ "aar",
127
+ `${targetName}-${version || versionOf(packageName)}.aar`
128
+ ),
129
+ };
130
+ }
131
+ }
132
+ }
133
+
134
+ async function assembleAarBundle(
135
+ context: Context,
136
+ packageName: string,
137
+ { aar }: NativeAssets
138
+ ): Promise<void> {
139
+ if (!aar) {
140
+ return;
141
+ }
142
+
143
+ const findUp = require("find-up");
144
+ const gradlew = await findUp(
145
+ os.platform() === "win32" ? "gradlew.bat" : "gradlew"
146
+ );
147
+ if (!gradlew) {
148
+ warn(`Skipped \`${packageName}\`: cannot find \`gradlew\``);
149
+ return;
150
+ }
151
+
152
+ const { androidProject, output } = getAndroidPaths(context, packageName, aar);
153
+ if (!androidProject || !output) {
154
+ warn(`Skipped \`${packageName}\`: cannot find \`build.gradle\``);
155
+ return;
156
+ }
157
+
158
+ const { targetName, version, env, dependencies } = aar;
159
+ const targets = [`:${targetName}:assembleRelease`];
160
+ const targetsToCopy: [string, string][] = [];
161
+ if (dependencies) {
162
+ for (const [dependencyName, aar] of Object.entries(dependencies)) {
163
+ const { output, destination } = getAndroidPaths(
164
+ context,
165
+ dependencyName,
166
+ aar
167
+ );
168
+ if (output) {
169
+ if (!fs.existsSync(output)) {
170
+ targets.push(`:${aar.targetName}:assembleRelease`);
171
+ targetsToCopy.push([output, destination]);
172
+ } else if (!fs.existsSync(destination)) {
173
+ targetsToCopy.push([output, destination]);
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ // Run only one Gradle task at a time
180
+ spawnSync(gradlew, targets, {
181
+ cwd: androidProject,
182
+ stdio: "inherit",
183
+ env: {
184
+ ENABLE_HERMES: "true",
185
+ NODE_MODULES_PATH: path.join(process.cwd(), "node_modules"),
186
+ REACT_NATIVE_VERSION: versionOf("react-native"),
187
+ ...process.env,
188
+ ...env,
189
+ },
190
+ });
191
+
192
+ const destination = path.join(context.options.assetsDest, "aar");
193
+ await fs.ensureDir(destination);
194
+
195
+ const aarVersion = version || versionOf(packageName);
196
+ const dest = path.join(destination, `${targetName}-${aarVersion}.aar`);
197
+ await Promise.all([
198
+ fs.copy(output, dest),
199
+ ...targetsToCopy.map(([src, dest]) => fs.copy(src, dest)),
200
+ ]);
201
+ }
202
+
203
+ async function copyFiles(files: unknown, destination: string): Promise<void> {
204
+ if (!isNonEmptyArray<string>(files)) {
205
+ return;
206
+ }
207
+
208
+ await fs.ensureDir(destination);
209
+ await Promise.all(
210
+ files.map((file) => {
211
+ const basename = path.basename(file);
212
+ return fs.copy(file, `${destination}/${basename}`);
213
+ })
214
+ );
215
+ }
216
+
217
+ async function copyXcodeAssets(
218
+ xcassets: unknown,
219
+ destination: string
220
+ ): Promise<void> {
221
+ if (!isNonEmptyArray<string>(xcassets)) {
222
+ return;
223
+ }
224
+
225
+ await fs.ensureDir(destination);
226
+ await Promise.all(
227
+ xcassets.map((catalog) => {
228
+ const dest = `${destination}/${path.basename(catalog)}`;
229
+ return fs.copy(catalog, dest);
230
+ })
231
+ );
232
+ }
233
+
234
+ export async function copyAssets(
235
+ { options: { assetsDest, xcassetsDest } }: Context,
236
+ packageName: string,
237
+ { assets, strings, xcassets }: NativeAssets
238
+ ): Promise<void> {
239
+ const tasks = [
240
+ copyFiles(assets, `${assetsDest}/assets/${packageName}`),
241
+ copyFiles(strings, `${assetsDest}/strings/${packageName}`),
242
+ ];
243
+
244
+ if (typeof xcassetsDest === "string") {
245
+ tasks.push(copyXcodeAssets(xcassets, xcassetsDest));
246
+ }
247
+
248
+ await Promise.all(tasks);
249
+ }
250
+
251
+ export async function gatherConfigs({
252
+ projectRoot,
253
+ manifest,
254
+ }: Context): Promise<Record<string, AssetsConfig | null> | undefined> {
255
+ const { dependencies, devDependencies } = manifest;
256
+ const packages = [...keysOf(dependencies), ...keysOf(devDependencies)];
257
+ if (packages.length === 0) {
258
+ return;
259
+ }
260
+
261
+ const resolveOptions = { paths: [projectRoot] };
262
+ const assetsConfigs: Record<string, AssetsConfig | null> = {};
263
+
264
+ for (const pkg of packages) {
265
+ try {
266
+ const pkgPath = path.dirname(
267
+ require.resolve(`${pkg}/package.json`, resolveOptions)
268
+ );
269
+ const reactNativeConfig = `${pkgPath}/react-native.config.js`;
270
+ if (fs.existsSync(reactNativeConfig)) {
271
+ const { nativeAssets } = require(reactNativeConfig);
272
+ if (nativeAssets) {
273
+ assetsConfigs[pkg] = nativeAssets;
274
+ }
275
+ }
276
+ } catch (err) {
277
+ warn(err);
278
+ }
279
+ }
280
+
281
+ // Overrides from project config
282
+ const reactNativeConfig = `${projectRoot}/react-native.config.js`;
283
+ if (fs.existsSync(reactNativeConfig)) {
284
+ const { nativeAssets } = require(reactNativeConfig);
285
+ const overrides = Object.entries(nativeAssets);
286
+ for (const [pkgName, config] of overrides) {
287
+ if (config === null || isAssetsConfig(config)) {
288
+ assetsConfigs[pkgName] = config;
289
+ }
290
+ }
291
+ }
292
+
293
+ return assetsConfigs;
294
+ }
295
+
296
+ /**
297
+ * Copies additional assets not picked by bundlers into desired directory.
298
+ *
299
+ * The way this works is by scanning all direct dependencies of the current
300
+ * project for a file, `react-native.config.js`, whose contents include a
301
+ * field, `nativeAssets`, and a function that returns assets to copy:
302
+ *
303
+ * ```js
304
+ * // react-native.config.js
305
+ * module.exports = {
306
+ * nativeAssets: {
307
+ * getAssets: (context) => {
308
+ * return {
309
+ * assets: [],
310
+ * strings: [],
311
+ * xcassets: [],
312
+ * };
313
+ * }
314
+ * }
315
+ * };
316
+ * ```
317
+ *
318
+ * We also allow the project itself to override this where applicable. The
319
+ * format is similar and looks like this:
320
+ *
321
+ * ```js
322
+ * // react-native.config.js
323
+ * module.exports = {
324
+ * nativeAssets: {
325
+ * "some-library": {
326
+ * getAssets: (context) => {
327
+ * return {
328
+ * assets: [],
329
+ * strings: [],
330
+ * xcassets: [],
331
+ * };
332
+ * }
333
+ * },
334
+ * "another-library": {
335
+ * getAssets: (context) => {
336
+ * return {
337
+ * assets: [],
338
+ * strings: [],
339
+ * xcassets: [],
340
+ * };
341
+ * }
342
+ * }
343
+ * }
344
+ * };
345
+ * ```
346
+ *
347
+ * @param options Options dictate what gets copied where
348
+ */
349
+ export async function copyProjectAssets(options: Options): Promise<void> {
350
+ const projectRoot = findPackageDir() || process.cwd();
351
+ const content = await fs.readFile(`${projectRoot}/package.json`, {
352
+ encoding: "utf-8",
353
+ });
354
+ const manifest: PackageManifest = JSON.parse(content);
355
+ const context = { projectRoot, manifest, options };
356
+ const assetConfigs = await gatherConfigs(context);
357
+ if (!assetConfigs) {
358
+ return;
359
+ }
360
+
361
+ const dependencies = Object.entries(assetConfigs);
362
+ for (const [packageName, config] of dependencies) {
363
+ if (!isAssetsConfig(config)) {
364
+ continue;
365
+ }
366
+
367
+ const { getAssets } = config;
368
+ if (typeof getAssets !== "function") {
369
+ warn(`Skipped \`${packageName}\`: getAssets is not a function`);
370
+ continue;
371
+ }
372
+
373
+ const assets = await getAssets(context);
374
+ if (options.bundleAar && assets.aar) {
375
+ info(`Assembling "${packageName}"`);
376
+ await assembleAarBundle(context, packageName, assets);
377
+ } else {
378
+ info(`Copying assets for "${packageName}"`);
379
+ await copyAssets(context, packageName, assets);
380
+ }
381
+ }
382
+
383
+ if (options.bundleAar) {
384
+ const dummyAar = { targetName: "dummy" };
385
+ const copyTasks = [];
386
+ for (const dependencyName of ["hermes-engine", "react-native"]) {
387
+ const { output, destination } = getAndroidPaths(
388
+ context,
389
+ dependencyName,
390
+ dummyAar
391
+ );
392
+ if (
393
+ output &&
394
+ (!fs.existsSync(destination) || fs.statSync(destination).isDirectory())
395
+ ) {
396
+ info(`Copying Android Archive of "${dependencyName}"`);
397
+ copyTasks.push(fs.copy(output, destination));
398
+ }
399
+ }
400
+ await Promise.all(copyTasks);
401
+ }
402
+ }
403
+
404
+ export const rnxCopyAssetsCommand = {
405
+ name: "rnx-copy-assets",
406
+ description:
407
+ "Copies additional assets not picked by bundlers into desired directory.",
408
+ func: (_argv: string[], _config: CLIConfig, options: Options) => {
409
+ ensureOption(options, "platform");
410
+ ensureOption(options, "assetsDest", "assets-dest");
411
+ return copyProjectAssets(options);
412
+ },
413
+ options: [
414
+ {
415
+ name: "--platform <string>",
416
+ description: "platform to target",
417
+ parse: parsePlatform,
418
+ },
419
+ {
420
+ name: "--assets-dest <string>",
421
+ description: "path of the directory to copy assets into",
422
+ },
423
+ {
424
+ name: "--bundle-aar <boolean>",
425
+ description: "whether to bundle AARs of dependencies",
426
+ default: false,
427
+ },
428
+ {
429
+ name: "--xcassets-dest <string>",
430
+ description:
431
+ "path of the directory to copy Xcode asset catalogs into. Asset catalogs will only be copied if a destination path is specified.",
432
+ },
433
+ ],
434
+ };
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { rnxBundle } from "./bundle";
2
+ export { copyProjectAssets, rnxCopyAssetsCommand } from "./copy-assets";
2
3
  export { rnxDepCheck, rnxDepCheckCommand } from "./dep-check";
3
4
  export { rnxStart } from "./start";
4
5
  export { rnxTest, rnxTestCommand } from "./test";
@@ -109,14 +109,14 @@ const emptySerializerHook = (_graph: Graph, _delta: DeltaResult): void => {
109
109
  * @param detectCyclicDependencies When true, cyclic dependency checking is enabled with a default set of options. Otherwise the object allows for fine-grained control over the detection process.
110
110
  * @param detectDuplicateDependencies When true, duplicate dependency checking is enabled with a default set of options. Otherwise, the object allows for fine-grained control over the detection process.
111
111
  * @param typescriptValidation When true, TypeScript type-checking is enabled with a default set of options. Otherwise, the object allows for fine-grained control over the type-checking process.
112
- * @param experimental_treeShake When true, experimental tree-shaking is enabled.
112
+ * @param treeShake When true, tree shaking is enabled.
113
113
  */
114
114
  export function customizeMetroConfig(
115
115
  metroConfigReadonly: InputConfigT,
116
116
  detectCyclicDependencies: boolean | CyclicDetectorOptions,
117
117
  detectDuplicateDependencies: boolean | DuplicateDetectorOptions,
118
118
  typescriptValidation: boolean | TypeScriptValidationOptions,
119
- experimental_treeShake: boolean
119
+ treeShake: boolean
120
120
  ): void {
121
121
  // We will be making changes to the Metro configuration. Coerce from a
122
122
  // type with readonly props to a type where the props are writeable.
@@ -134,7 +134,7 @@ export function customizeMetroConfig(
134
134
  plugins.push(CyclicDependencies());
135
135
  }
136
136
 
137
- if (experimental_treeShake) {
137
+ if (treeShake) {
138
138
  metroConfig.serializer.customSerializer = MetroSerializerEsbuild(plugins);
139
139
  Object.assign(metroConfig.transformer, esbuildTransformerConfig);
140
140
  } else if (plugins.length > 0) {
package/src/start.ts CHANGED
@@ -131,7 +131,7 @@ export async function rnxStart(
131
131
  serverConfig.detectCyclicDependencies,
132
132
  serverConfig.detectDuplicateDependencies,
133
133
  serverConfig.typescriptValidation ? typescriptValidationOptions : false,
134
- serverConfig.experimental_treeShake
134
+ serverConfig.treeShake
135
135
  );
136
136
 
137
137
  // create middleware -- a collection of plugins which handle incoming
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+
3
+ const fs = jest.createMockFromModule("fs-extra");
4
+
5
+ const { vol } = require("memfs");
6
+
7
+ /** @type {(newMockFiles: { [filename: string]: string }) => void} */
8
+ fs.__setMockFiles = (files) => {
9
+ vol.reset();
10
+ vol.fromJSON(files);
11
+ };
12
+
13
+ fs.__toJSON = () => vol.toJSON();
14
+
15
+ fs.copy = (...args) => vol.promises.copyFile(...args);
16
+ fs.ensureDir = (dir) => vol.promises.mkdir(dir, { recursive: true });
17
+ fs.pathExists = (...args) => Promise.resolve(vol.existsSync(...args));
18
+ fs.readFile = (...args) => vol.promises.readFile(...args);
19
+
20
+ module.exports = fs;
@@ -80,7 +80,7 @@ describe("CLI > Bundle > Kit Config > getKitBundleConfigs", () => {
80
80
  detectCyclicDependencies: true,
81
81
  detectDuplicateDependencies: true,
82
82
  typescriptValidation: true,
83
- experimental_treeShake: true,
83
+ treeShake: true,
84
84
  targets: ["ios", "android"],
85
85
  platforms: {
86
86
  ios: {
@@ -123,4 +123,26 @@ describe("CLI > Bundle > Kit Config > getKitBundleConfigs", () => {
123
123
  platform: "android",
124
124
  });
125
125
  });
126
+
127
+ test("uses deprecated experimental_treeShake", () => {
128
+ const d: Record<string, unknown> = { ...definition };
129
+ delete d.treeShake;
130
+ d.experimental_treeShake = true;
131
+ rnxKitConfig.__setMockConfig({
132
+ bundle: {
133
+ ...d,
134
+ },
135
+ });
136
+ consoleWarnSpy.mockReset();
137
+
138
+ const kitBundleConfigs = getKitBundleConfigs(undefined, "ios");
139
+ expect(kitBundleConfigs[0].treeShake).toBe(true);
140
+ expect(consoleWarnSpy).toBeCalledTimes(1);
141
+ expect(consoleWarnSpy).toBeCalledWith(
142
+ expect.stringContaining("deprecated")
143
+ );
144
+ expect(consoleWarnSpy).toBeCalledWith(
145
+ expect.stringContaining("experimental_treeShake")
146
+ );
147
+ });
126
148
  });
@@ -8,7 +8,7 @@ describe("CLI > Bundle > Metro > createMetroBundleArgs", () => {
8
8
  detectCyclicDependencies: true,
9
9
  detectDuplicateDependencies: true,
10
10
  typescriptValidation: true,
11
- experimental_treeShake: true,
11
+ treeShake: true,
12
12
  entryPath: "out/entry.js",
13
13
  distPath: "out",
14
14
  assetsPath: "out/assets",
@@ -7,7 +7,7 @@ describe("CLI > Bundle > Overrides > applyKitBundleConfigOverrides", () => {
7
7
  detectCyclicDependencies: true,
8
8
  detectDuplicateDependencies: true,
9
9
  typescriptValidation: true,
10
- experimental_treeShake: true,
10
+ treeShake: true,
11
11
  entryPath: "dist/index.js",
12
12
  distPath: "dist",
13
13
  assetsPath: "dist",
@@ -63,17 +63,7 @@ describe("CLI > Bundle > Overrides > applyKitBundleConfigOverrides", () => {
63
63
  testOverride("sourcemapSourcesRoot", "out");
64
64
  });
65
65
 
66
- test("set experimental_treeShake using override experimentalTreeShake", () => {
67
- const copy = { ...config };
68
- applyKitBundleConfigOverrides(
69
- {
70
- experimentalTreeShake: true,
71
- },
72
- [copy]
73
- );
74
- expect(copy).toEqual({
75
- ...config,
76
- experimental_treeShake: true,
77
- });
66
+ test("changes treeShake using an override", () => {
67
+ testOverride("treeShake", true);
78
68
  });
79
69
  });
@@ -0,0 +1,123 @@
1
+ import fs from "fs-extra";
2
+ import * as path from "path";
3
+ import { copyAssets, gatherConfigs, versionOf } from "../src/copy-assets";
4
+
5
+ const options = {
6
+ platform: "ios" as const,
7
+ assetsDest: "dist",
8
+ bundleAar: false,
9
+ xcassetsDest: "xcassets",
10
+ };
11
+
12
+ const context = {
13
+ projectRoot: path.resolve(__dirname, ".."),
14
+ manifest: {
15
+ name: "@rnx-kit/cli",
16
+ version: "0.0.0-dev",
17
+ },
18
+ options,
19
+ };
20
+
21
+ function findFiles() {
22
+ // @ts-ignore `__toJSON`
23
+ return Object.entries(fs.__toJSON());
24
+ }
25
+
26
+ function mockFiles(files: Record<string, string> = {}) {
27
+ // @ts-ignore `__setMockFiles`
28
+ fs.__setMockFiles(files);
29
+ }
30
+
31
+ describe("copyAssets", () => {
32
+ afterEach(() => {
33
+ mockFiles();
34
+ });
35
+
36
+ afterAll(() => {
37
+ jest.clearAllMocks();
38
+ });
39
+
40
+ test("returns early if there is nothing to copy", async () => {
41
+ await copyAssets(context, "test", {});
42
+ expect(findFiles()).toEqual([]);
43
+ });
44
+
45
+ test("copies assets", async () => {
46
+ const filename = "arnolds-greatest-movies.md";
47
+ const content = "all of them";
48
+ mockFiles({ [filename]: content });
49
+
50
+ await copyAssets(context, "test", { assets: [filename] });
51
+
52
+ expect(findFiles()).toEqual([
53
+ [expect.stringContaining(filename), content],
54
+ [
55
+ expect.stringMatching(
56
+ `dist[/\\\\]assets[/\\\\]test[/\\\\]${filename}$`
57
+ ),
58
+ content,
59
+ ],
60
+ ]);
61
+ });
62
+
63
+ test("copies strings", async () => {
64
+ const filename = "arnolds-greatest-lines.md";
65
+ const content = "all of them";
66
+ mockFiles({ [filename]: content });
67
+
68
+ await copyAssets(context, "test", { strings: [filename] });
69
+
70
+ expect(findFiles()).toEqual([
71
+ [expect.stringContaining(filename), content],
72
+ [
73
+ expect.stringMatching(
74
+ `dist[/\\\\]strings[/\\\\]test[/\\\\]${filename}$`
75
+ ),
76
+ content,
77
+ ],
78
+ ]);
79
+ });
80
+
81
+ test("copies Xcode asset catalogs", async () => {
82
+ const filename = "arnolds-greatest-assets.xcassets";
83
+ const content = "all of them";
84
+ mockFiles({ [filename]: content });
85
+
86
+ await copyAssets(context, "test", { xcassets: [filename] });
87
+
88
+ expect(findFiles()).toEqual([
89
+ [expect.stringContaining(filename), content],
90
+ [expect.stringMatching(`xcassets[/\\\\]${filename}$`), content],
91
+ ]);
92
+ });
93
+
94
+ test("does not copy Xcode asset catalogs if destination path is unset", async () => {
95
+ const filename = "arnolds-greatest-assets.xcassets";
96
+ const content = "all of them";
97
+ mockFiles({ [filename]: content });
98
+
99
+ await copyAssets(
100
+ { ...context, options: { ...options, xcassetsDest: undefined } },
101
+ "test",
102
+ { xcassets: [filename] }
103
+ );
104
+
105
+ expect(findFiles()).toEqual([[expect.stringContaining(filename), content]]);
106
+ });
107
+ });
108
+
109
+ describe("gatherConfigs", () => {
110
+ test("returns early if there is nothing to copy", async () => {
111
+ expect(await gatherConfigs(context)).toBeUndefined();
112
+ });
113
+ });
114
+
115
+ describe("versionOf", () => {
116
+ test("returns the version of specified package", () => {
117
+ expect(versionOf("@rnx-kit/tools-node")).toMatch(/^\d+[.\d]+$/);
118
+ });
119
+
120
+ test("throws if package is not installed", () => {
121
+ expect(() => versionOf("some-package-that-does-not-exist")).toThrow();
122
+ });
123
+ });