@pwrdrvr/microapps-publish 0.3.5 → 0.4.0-alpha.10
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/README.md +67 -72
- package/dist/commands/delete.d.ts +3 -0
- package/dist/commands/delete.d.ts.map +1 -1
- package/dist/commands/delete.js +27 -10
- package/dist/commands/delete.js.map +1 -1
- package/dist/commands/nextjs-version.d.ts +2 -0
- package/dist/commands/nextjs-version.d.ts.map +1 -1
- package/dist/commands/nextjs-version.js +20 -5
- package/dist/commands/nextjs-version.js.map +1 -1
- package/dist/commands/preflight.d.ts +3 -0
- package/dist/commands/preflight.d.ts.map +1 -1
- package/dist/commands/preflight.js +27 -10
- package/dist/commands/preflight.js.map +1 -1
- package/dist/commands/publish-static.d.ts +6 -1
- package/dist/commands/publish-static.d.ts.map +1 -1
- package/dist/commands/publish-static.js +125 -48
- package/dist/commands/publish-static.js.map +1 -1
- package/dist/commands/publish.d.ts +7 -0
- package/dist/commands/publish.d.ts.map +1 -1
- package/dist/commands/publish.js +118 -41
- package/dist/commands/publish.js.map +1 -1
- package/dist/config/Application.js.map +1 -1
- package/dist/config/Config.js.map +1 -1
- package/dist/config/Deployer.js.map +1 -1
- package/dist/lib/DeployClient.d.ts +1 -1
- package/dist/lib/DeployClient.d.ts.map +1 -1
- package/dist/lib/S3TransferUtility.d.ts +2 -2
- package/dist/lib/S3TransferUtility.d.ts.map +1 -1
- package/dist/lib/S3TransferUtility.js +27 -8
- package/dist/lib/S3TransferUtility.js.map +1 -1
- package/dist/lib/S3Uploader.d.ts +1 -1
- package/dist/lib/S3Uploader.d.ts.map +1 -1
- package/dist/lib/S3Uploader.js +4 -3
- package/dist/lib/S3Uploader.js.map +1 -1
- package/package.json +13 -7
- package/src/commands/delete.ts +29 -9
- package/src/commands/nextjs-version.ts +19 -5
- package/src/commands/preflight.ts +29 -9
- package/src/commands/publish-static.ts +155 -64
- package/src/commands/publish.ts +150 -55
- package/src/lib/DeployClient.ts +1 -1
- package/src/lib/S3TransferUtility.spec.ts +15 -0
- package/src/lib/S3TransferUtility.ts +30 -12
- package/src/lib/S3Uploader.ts +2 -2
- package/src/lib/__snapshots__/S3TransferUtility.spec.ts.snap +12 -0
package/src/commands/publish.ts
CHANGED
|
@@ -4,14 +4,19 @@ import * as lambda from '@aws-sdk/client-lambda';
|
|
|
4
4
|
import * as s3 from '@aws-sdk/client-s3';
|
|
5
5
|
import * as sts from '@aws-sdk/client-sts';
|
|
6
6
|
import { Command, flags as flagsParser } from '@oclif/command';
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
8
|
+
const pMap = require('p-map');
|
|
7
9
|
import * as path from 'path';
|
|
8
10
|
import { pathExists, createReadStream } from 'fs-extra';
|
|
9
11
|
import { Listr, ListrTask } from 'listr2';
|
|
10
12
|
import { createVersions, IVersions } from '../lib/Versions';
|
|
11
13
|
import { Config, IConfig } from '../config/Config';
|
|
12
|
-
import DeployClient, {
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
import DeployClient, {
|
|
15
|
+
DeployVersionArgs,
|
|
16
|
+
IDeployVersionPreflightResult,
|
|
17
|
+
} from '../lib/DeployClient';
|
|
18
|
+
import { S3Uploader } from '../lib/S3Uploader';
|
|
19
|
+
import { S3TransferUtility } from '../lib/S3TransferUtility';
|
|
15
20
|
import { Upload } from '@aws-sdk/lib-storage';
|
|
16
21
|
import { contentType } from 'mime-types';
|
|
17
22
|
import { TaskWrapper } from 'listr2/dist/lib/task-wrapper';
|
|
@@ -22,8 +27,6 @@ const lambdaClient = new lambda.LambdaClient({
|
|
|
22
27
|
maxAttempts: 8,
|
|
23
28
|
});
|
|
24
29
|
|
|
25
|
-
type DeployVersionArgs = Parameters<typeof DeployClient.DeployVersionLite>[0];
|
|
26
|
-
|
|
27
30
|
interface IContext {
|
|
28
31
|
preflightResult: IDeployVersionPreflightResult;
|
|
29
32
|
files: string[];
|
|
@@ -66,38 +69,78 @@ export class PublishCommand extends Command {
|
|
|
66
69
|
char: 'v',
|
|
67
70
|
}),
|
|
68
71
|
help: flagsParser.help(),
|
|
72
|
+
// Deprecated
|
|
69
73
|
deployerLambdaName: flagsParser.string({
|
|
74
|
+
multiple: false,
|
|
75
|
+
required: false,
|
|
76
|
+
hidden: true,
|
|
77
|
+
}),
|
|
78
|
+
'deployer-lambda-name': flagsParser.string({
|
|
70
79
|
char: 'd',
|
|
71
80
|
multiple: false,
|
|
72
|
-
|
|
81
|
+
exactlyOne: ['deployer-lambda-name', 'deployerLambdaName'],
|
|
73
82
|
description: 'Name of the deployer lambda function',
|
|
74
83
|
}),
|
|
84
|
+
// Deprecated
|
|
75
85
|
newVersion: flagsParser.string({
|
|
86
|
+
multiple: false,
|
|
87
|
+
required: false,
|
|
88
|
+
hidden: true,
|
|
89
|
+
}),
|
|
90
|
+
'new-version': flagsParser.string({
|
|
76
91
|
char: 'n',
|
|
77
92
|
multiple: false,
|
|
78
|
-
|
|
93
|
+
exactlyOne: ['new-version', 'newVersion'],
|
|
79
94
|
description: 'New semantic version to apply',
|
|
80
95
|
}),
|
|
96
|
+
// Deprecated
|
|
81
97
|
appLambdaName: flagsParser.string({
|
|
98
|
+
multiple: false,
|
|
99
|
+
required: false,
|
|
100
|
+
hidden: true,
|
|
101
|
+
exclusive: ['app-lambda-name'],
|
|
102
|
+
}),
|
|
103
|
+
'app-lambda-name': flagsParser.string({
|
|
82
104
|
char: 'l',
|
|
83
105
|
multiple: false,
|
|
84
106
|
required: false,
|
|
85
107
|
description: 'ARN of lambda version, alias, or function (name or ARN) to deploy',
|
|
86
108
|
}),
|
|
109
|
+
// Deprecated
|
|
87
110
|
appName: flagsParser.string({
|
|
88
|
-
char: 'a',
|
|
89
111
|
multiple: false,
|
|
90
112
|
required: false,
|
|
113
|
+
hidden: true,
|
|
114
|
+
exclusive: ['app-name'],
|
|
115
|
+
}),
|
|
116
|
+
'app-name': flagsParser.string({
|
|
117
|
+
char: 'a',
|
|
118
|
+
multiple: false,
|
|
119
|
+
exactlyOne: ['app-name', 'appName'],
|
|
91
120
|
description: 'MicroApps app name (this becomes the path the app is rooted at)',
|
|
92
121
|
}),
|
|
122
|
+
// Deprecated
|
|
93
123
|
staticAssetsPath: flagsParser.string({
|
|
124
|
+
multiple: false,
|
|
125
|
+
required: false,
|
|
126
|
+
hidden: true,
|
|
127
|
+
exclusive: ['static-assets-path'],
|
|
128
|
+
}),
|
|
129
|
+
'static-assets-path': flagsParser.string({
|
|
94
130
|
char: 's',
|
|
95
131
|
multiple: false,
|
|
96
132
|
required: false,
|
|
97
133
|
description:
|
|
98
134
|
'Path to files to be uploaded to S3 static bucket at app/version/ path. Do include app/version/ in path if files are already "rooted" under that path locally.',
|
|
99
135
|
}),
|
|
136
|
+
// Deprecated
|
|
100
137
|
defaultFile: flagsParser.string({
|
|
138
|
+
multiple: false,
|
|
139
|
+
required: false,
|
|
140
|
+
hidden: true,
|
|
141
|
+
exclusive: ['default-file'],
|
|
142
|
+
}),
|
|
143
|
+
'default-file': flagsParser.string({
|
|
101
144
|
char: 'i',
|
|
102
145
|
multiple: false,
|
|
103
146
|
required: false,
|
|
@@ -111,7 +154,13 @@ export class PublishCommand extends Command {
|
|
|
111
154
|
description:
|
|
112
155
|
'Allow overwrite - Warn but do not fail if version exists. Discouraged outside of test envs if cacheable static files have changed.',
|
|
113
156
|
}),
|
|
157
|
+
// Deprecated
|
|
114
158
|
noCache: flagsParser.boolean({
|
|
159
|
+
required: false,
|
|
160
|
+
default: false,
|
|
161
|
+
hidden: true,
|
|
162
|
+
}),
|
|
163
|
+
'no-cache': flagsParser.boolean({
|
|
115
164
|
required: false,
|
|
116
165
|
default: false,
|
|
117
166
|
description: 'Force revalidation of CloudFront and browser caching of static assets',
|
|
@@ -149,14 +198,22 @@ export class PublishCommand extends Command {
|
|
|
149
198
|
const RUNNING = ''; //chalk.reset.inverse.yellow.bold(RUNNING_TEXT) + ' ';
|
|
150
199
|
|
|
151
200
|
const { flags: parsedFlags } = this.parse(PublishCommand);
|
|
152
|
-
const appLambdaName =
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
201
|
+
const appLambdaName =
|
|
202
|
+
parsedFlags.appLambdaName ?? parsedFlags['app-lambda-name'] ?? config.app.lambdaName;
|
|
203
|
+
const appName = parsedFlags.appName ?? parsedFlags['app-name'] ?? config.app.name;
|
|
204
|
+
const deployerLambdaName =
|
|
205
|
+
parsedFlags.deployerLambdaName ??
|
|
206
|
+
parsedFlags['deployer-lambda-name'] ??
|
|
207
|
+
config.deployer.lambdaName;
|
|
208
|
+
const semVer = parsedFlags.newVersion ?? parsedFlags['new-version'] ?? config.app.semVer;
|
|
209
|
+
const staticAssetsPath =
|
|
210
|
+
parsedFlags.staticAssetsPath ??
|
|
211
|
+
parsedFlags['static-assets-path'] ??
|
|
212
|
+
config.app.staticAssetsPath;
|
|
213
|
+
const defaultFile =
|
|
214
|
+
parsedFlags.defaultFile ?? parsedFlags['default-file'] ?? config.app.defaultFile;
|
|
158
215
|
const overwrite = parsedFlags.overwrite;
|
|
159
|
-
const noCache = parsedFlags.noCache;
|
|
216
|
+
const noCache = parsedFlags.noCache ?? parsedFlags['no-cache'];
|
|
160
217
|
|
|
161
218
|
// Override the config value
|
|
162
219
|
config.deployer.lambdaName = deployerLambdaName;
|
|
@@ -331,11 +388,11 @@ export class PublishCommand extends Command {
|
|
|
331
388
|
{
|
|
332
389
|
enabled: () => !!config.app.staticAssetsPath,
|
|
333
390
|
title: 'Enumerate Files to Upload to S3',
|
|
334
|
-
task:
|
|
391
|
+
task: (ctx, task) => {
|
|
335
392
|
const origTitle = task.title;
|
|
336
393
|
task.title = RUNNING + origTitle;
|
|
337
394
|
|
|
338
|
-
ctx.files =
|
|
395
|
+
ctx.files = S3TransferUtility.GetFiles(S3Uploader.TempDir);
|
|
339
396
|
|
|
340
397
|
task.title = origTitle;
|
|
341
398
|
},
|
|
@@ -343,7 +400,7 @@ export class PublishCommand extends Command {
|
|
|
343
400
|
{
|
|
344
401
|
enabled: () => !!config.app.staticAssetsPath,
|
|
345
402
|
title: 'Upload Static Files to S3',
|
|
346
|
-
task: (ctx, task) => {
|
|
403
|
+
task: async (ctx, task) => {
|
|
347
404
|
const origTitle = task.title;
|
|
348
405
|
task.title = RUNNING + origTitle;
|
|
349
406
|
|
|
@@ -363,50 +420,88 @@ export class PublishCommand extends Command {
|
|
|
363
420
|
|
|
364
421
|
// Setup caching on static assets
|
|
365
422
|
// NoCache - Only used for test deploys, requires browser and CloudFront to refetch every time
|
|
366
|
-
// Overwrite - Reduces default cache time period from 24 hours to 15 minutes
|
|
367
423
|
// Default - 24 hours
|
|
368
424
|
const CacheControl = noCache
|
|
369
425
|
? 'max-age=0, must-revalidate, public'
|
|
370
|
-
: overwrite
|
|
371
|
-
? `max-age=${15 * 60}, public`
|
|
372
426
|
: `max-age=${24 * 60 * 60}, public`;
|
|
373
427
|
|
|
374
428
|
const pathWithoutAppAndVer = path.join(S3Uploader.TempDir, destinationPrefix);
|
|
375
429
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
430
|
+
// Listr causes OOM if passes a list of, say, 5,000 to 20,000 files
|
|
431
|
+
if (ctx.files.length > 200) {
|
|
432
|
+
const fileCountMsgInterval = Math.floor(ctx.files.length / 10);
|
|
433
|
+
let filesPublished = 0;
|
|
434
|
+
|
|
435
|
+
await pMap(
|
|
436
|
+
ctx.files,
|
|
437
|
+
async (filePath: string) => {
|
|
438
|
+
// Can't use tasks for each file
|
|
439
|
+
const relFilePath = path.relative(pathWithoutAppAndVer, filePath);
|
|
440
|
+
|
|
441
|
+
if (
|
|
442
|
+
ctx.files.length > 1000 &&
|
|
443
|
+
(filesPublished % fileCountMsgInterval === 0 ||
|
|
444
|
+
filesPublished === ctx.files.length)
|
|
445
|
+
) {
|
|
446
|
+
task.output = `Uploaded ${filesPublished} of ${ctx.files.length} files`;
|
|
447
|
+
} else if (ctx.files.length <= 1000) {
|
|
448
|
+
task.output = `Uploading ${relFilePath}`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const upload = new Upload({
|
|
452
|
+
client: s3Client,
|
|
453
|
+
leavePartsOnError: false,
|
|
454
|
+
params: {
|
|
455
|
+
Bucket: bucketName,
|
|
456
|
+
Key: path.relative(S3Uploader.TempDir, filePath),
|
|
457
|
+
Body: createReadStream(filePath),
|
|
458
|
+
ContentType:
|
|
459
|
+
contentType(path.basename(filePath)) || 'application/octet-stream',
|
|
460
|
+
CacheControl,
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
await upload.done();
|
|
464
|
+
filesPublished++;
|
|
465
|
+
},
|
|
466
|
+
{ concurrency: 40 },
|
|
467
|
+
);
|
|
468
|
+
} else {
|
|
469
|
+
const tasks: ListrTask<IContext>[] = ctx.files.map((filePath) => ({
|
|
470
|
+
task: async (ctx: IContext, subtask) => {
|
|
471
|
+
const relFilePath = path.relative(pathWithoutAppAndVer, filePath);
|
|
472
|
+
|
|
473
|
+
const origTitle = relFilePath;
|
|
474
|
+
subtask.title = RUNNING + origTitle;
|
|
475
|
+
|
|
476
|
+
const upload = new Upload({
|
|
477
|
+
client: s3Client,
|
|
478
|
+
leavePartsOnError: false,
|
|
479
|
+
params: {
|
|
480
|
+
Bucket: bucketName,
|
|
481
|
+
Key: path.relative(S3Uploader.TempDir, filePath),
|
|
482
|
+
Body: createReadStream(filePath),
|
|
483
|
+
ContentType:
|
|
484
|
+
contentType(path.basename(filePath)) || 'application/octet-stream',
|
|
485
|
+
CacheControl,
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
await upload.done();
|
|
489
|
+
|
|
490
|
+
subtask.title = origTitle;
|
|
491
|
+
},
|
|
492
|
+
}));
|
|
493
|
+
|
|
494
|
+
task.title = origTitle;
|
|
495
|
+
|
|
496
|
+
return task.newListr(tasks, {
|
|
497
|
+
concurrent: 20,
|
|
498
|
+
rendererOptions: {
|
|
499
|
+
clearOutput: false,
|
|
500
|
+
showErrorMessage: true,
|
|
501
|
+
showTimer: true,
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
}
|
|
410
505
|
},
|
|
411
506
|
},
|
|
412
507
|
{
|
package/src/lib/DeployClient.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface IDeployVersionPreflightResult {
|
|
|
19
19
|
response: IDeployVersionPreflightResponse;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export type DeployVersionArgs = Parameters<typeof DeployClient.DeployVersionLite
|
|
22
|
+
export type DeployVersionArgs = Parameters<typeof DeployClient.DeployVersionLite>[0];
|
|
23
23
|
|
|
24
24
|
export default class DeployClient {
|
|
25
25
|
static readonly _client = new lambda.LambdaClient({
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { S3TransferUtility } from './S3TransferUtility';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
describe('S3TransferUtility', () => {
|
|
5
|
+
it('collects the files', () => {
|
|
6
|
+
const testFilesRoot = path.join(__dirname, '../../tests/files');
|
|
7
|
+
const files = S3TransferUtility.GetFiles(path.join(__dirname, '../../tests/files'));
|
|
8
|
+
|
|
9
|
+
expect(files).toBeDefined();
|
|
10
|
+
expect(files).toHaveLength(6);
|
|
11
|
+
|
|
12
|
+
const relativeFiles = files.map((file) => path.relative(testFilesRoot, file));
|
|
13
|
+
expect(relativeFiles).toMatchSnapshot();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// From: https://stackoverflow.com/a/65862128/878903
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { createReadStream, readdirSync } from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import * as s3 from '@aws-sdk/client-s3';
|
|
8
8
|
import { Upload } from '@aws-sdk/lib-storage';
|
|
@@ -10,7 +10,7 @@ import type { IDeployVersionPreflightResponse } from '@pwrdrvr/microapps-deploye
|
|
|
10
10
|
import { contentType } from 'mime-types';
|
|
11
11
|
import pMap from 'p-map';
|
|
12
12
|
|
|
13
|
-
export
|
|
13
|
+
export class S3TransferUtility {
|
|
14
14
|
/**
|
|
15
15
|
* @deprecated 2021-11-27
|
|
16
16
|
*
|
|
@@ -36,7 +36,7 @@ export default class S3TransferUtility {
|
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
// console.log('Uploading files to S3');
|
|
39
|
-
const files =
|
|
39
|
+
const files = S3TransferUtility.GetFiles(s3Path);
|
|
40
40
|
// const pathWithoutAppAndVer = path.join(s3Path, destPrefixPath);
|
|
41
41
|
// for (const filePath of files) {
|
|
42
42
|
// const relFilePath = path.relative(pathWithoutAppAndVer, filePath);
|
|
@@ -82,14 +82,32 @@ export default class S3TransferUtility {
|
|
|
82
82
|
* @param dir
|
|
83
83
|
* @returns
|
|
84
84
|
*/
|
|
85
|
-
public static
|
|
86
|
-
const dirents =
|
|
87
|
-
const files =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
public static GetFiles(dir: string): string[] {
|
|
86
|
+
const dirents = readdirSync(dir, { withFileTypes: true });
|
|
87
|
+
const files: string[] = [];
|
|
88
|
+
const toScan = dirents.map((dirent) => ({
|
|
89
|
+
path: path.resolve(dir, dirent.name),
|
|
90
|
+
isDirectory: dirent.isDirectory(),
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
// Iteratively collect all the files in the directories
|
|
94
|
+
// Do not use function call recursion
|
|
95
|
+
while (toScan.length > 0) {
|
|
96
|
+
const dirOrFile = toScan.pop();
|
|
97
|
+
|
|
98
|
+
if (dirOrFile.isDirectory) {
|
|
99
|
+
const direntsChild = readdirSync(dirOrFile.path, { withFileTypes: true });
|
|
100
|
+
const toScanChild = direntsChild.map((dirent) => ({
|
|
101
|
+
path: path.resolve(dirOrFile.path, dirent.name),
|
|
102
|
+
isDirectory: dirent.isDirectory(),
|
|
103
|
+
}));
|
|
104
|
+
Array.prototype.push.apply(toScan, toScanChild);
|
|
105
|
+
} else {
|
|
106
|
+
files.push(dirOrFile.path);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Reverse the array so that the files are in the same order as the original
|
|
111
|
+
return files.reverse();
|
|
94
112
|
}
|
|
95
113
|
}
|
package/src/lib/S3Uploader.ts
CHANGED
|
@@ -2,9 +2,9 @@ import * as path from 'path';
|
|
|
2
2
|
import type { IDeployVersionPreflightResponse } from '@pwrdrvr/microapps-deployer-lib';
|
|
3
3
|
import * as fs from 'fs-extra';
|
|
4
4
|
import { IConfig } from '../config/Config';
|
|
5
|
-
import S3TransferUtility from './S3TransferUtility';
|
|
5
|
+
import { S3TransferUtility } from './S3TransferUtility';
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export class S3Uploader {
|
|
8
8
|
/**
|
|
9
9
|
* Copy files to local upload directory
|
|
10
10
|
* @param config
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`S3TransferUtility collects the files 1`] = `
|
|
4
|
+
[
|
|
5
|
+
"dir1/.touch",
|
|
6
|
+
"dir2/dir21/dir211/some211-1.json",
|
|
7
|
+
"dir2/dir21/dir211/some211-2.json",
|
|
8
|
+
"dir2/dir21/some21.json",
|
|
9
|
+
"dir2/dir22/some22.json",
|
|
10
|
+
"some.json",
|
|
11
|
+
]
|
|
12
|
+
`;
|