@rnx-kit/cli 0.11.2 → 0.12.0
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.
- package/CHANGELOG.md +11 -0
- package/README.md +9 -1
- package/coverage/clover.xml +128 -4
- package/coverage/coverage-final.json +2 -1
- package/coverage/lcov-report/index.html +20 -20
- package/coverage/lcov-report/src/bundle/index.html +1 -1
- package/coverage/lcov-report/src/bundle/kit-config.ts.html +1 -1
- package/coverage/lcov-report/src/bundle/metro.ts.html +1 -1
- package/coverage/lcov-report/src/bundle/overrides.ts.html +1 -1
- package/coverage/lcov-report/src/copy-assets.ts.html +1387 -0
- package/coverage/lcov-report/src/index.html +25 -10
- package/coverage/lcov-report/src/metro-config.ts.html +2 -2
- package/coverage/lcov-report/src/typescript/index.html +1 -1
- package/coverage/lcov-report/src/typescript/project-cache.ts.html +1 -1
- package/coverage/lcov.info +239 -0
- package/lib/copy-assets.d.ts +111 -0
- package/lib/copy-assets.d.ts.map +1 -0
- package/lib/copy-assets.js +352 -0
- package/lib/copy-assets.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -1
- package/lib/index.js.map +1 -1
- package/lib/metro-config.d.ts +1 -1
- package/lib/metro-config.js +1 -1
- package/package.json +5 -2
- package/react-native.config.js +2 -0
- package/src/copy-assets.ts +434 -0
- package/src/index.ts +1 -0
- package/src/metro-config.ts +1 -1
- package/test/__mocks__/fs-extra.js +20 -0
- package/test/copy-assets.test.ts +113 -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
|
+
function versionOf(pkgName: string): string {
|
|
73
|
+
const { version } = readPackage(`${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
package/src/metro-config.ts
CHANGED
|
@@ -109,7 +109,7 @@ 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
|
|
112
|
+
* @param experimental_treeShake When true, experimental tree shaking is enabled.
|
|
113
113
|
*/
|
|
114
114
|
export function customizeMetroConfig(
|
|
115
115
|
metroConfigReadonly: InputConfigT,
|
|
@@ -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;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { copyAssets, gatherConfigs } 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
|
+
});
|