@pwrdrvr/microapps-publish 0.0.20 → 0.0.21

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 (68) hide show
  1. package/bin/run +5 -0
  2. package/dist/commands/nextjs-docker-auto.d.ts +38 -0
  3. package/dist/commands/nextjs-docker-auto.d.ts.map +1 -0
  4. package/dist/commands/nextjs-docker-auto.js +415 -0
  5. package/dist/commands/nextjs-docker-auto.js.map +1 -0
  6. package/dist/commands/nextjs-version-restore.d.ts +14 -0
  7. package/dist/commands/nextjs-version-restore.d.ts.map +1 -0
  8. package/dist/commands/nextjs-version-restore.js +58 -0
  9. package/dist/commands/nextjs-version-restore.js.map +1 -0
  10. package/dist/commands/nextjs-version.d.ts +16 -0
  11. package/dist/commands/nextjs-version.d.ts.map +1 -0
  12. package/dist/commands/nextjs-version.js +94 -0
  13. package/dist/commands/nextjs-version.js.map +1 -0
  14. package/dist/commands/preflight.d.ts +15 -0
  15. package/dist/commands/preflight.d.ts.map +1 -0
  16. package/dist/commands/preflight.js +111 -0
  17. package/dist/commands/preflight.js.map +1 -0
  18. package/dist/commands/publish.d.ts +23 -0
  19. package/dist/commands/publish.d.ts.map +1 -0
  20. package/dist/commands/publish.js +300 -0
  21. package/dist/commands/publish.js.map +1 -0
  22. package/dist/config/Config.d.ts +0 -2
  23. package/dist/config/Config.d.ts.map +1 -1
  24. package/dist/config/Config.js +0 -5
  25. package/dist/config/Config.js.map +1 -1
  26. package/dist/index.d.ts +1 -64
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3 -480
  29. package/dist/index.js.map +1 -1
  30. package/dist/{DeployClient.d.ts → lib/DeployClient.d.ts} +14 -6
  31. package/dist/lib/DeployClient.d.ts.map +1 -0
  32. package/dist/{DeployClient.js → lib/DeployClient.js} +15 -5
  33. package/dist/lib/DeployClient.js.map +1 -0
  34. package/dist/{S3TransferUtility.d.ts → lib/S3TransferUtility.d.ts} +0 -0
  35. package/dist/lib/S3TransferUtility.d.ts.map +1 -0
  36. package/dist/{S3TransferUtility.js → lib/S3TransferUtility.js} +0 -0
  37. package/dist/lib/S3TransferUtility.js.map +1 -0
  38. package/dist/{S3Uploader.d.ts → lib/S3Uploader.d.ts} +1 -1
  39. package/dist/lib/S3Uploader.d.ts.map +1 -0
  40. package/dist/{S3Uploader.js → lib/S3Uploader.js} +10 -10
  41. package/dist/lib/S3Uploader.js.map +1 -0
  42. package/dist/lib/Versions.d.ts +27 -0
  43. package/dist/lib/Versions.d.ts.map +1 -0
  44. package/dist/lib/Versions.js +76 -0
  45. package/dist/lib/Versions.js.map +1 -0
  46. package/package.json +11 -2
  47. package/src/commands/nextjs-docker-auto.ts +498 -0
  48. package/src/commands/nextjs-version-restore.ts +73 -0
  49. package/src/commands/nextjs-version.ts +113 -0
  50. package/src/commands/preflight.ts +121 -0
  51. package/src/commands/publish.ts +358 -0
  52. package/src/config/Config.ts +0 -4
  53. package/src/index.ts +1 -596
  54. package/src/{DeployClient.ts → lib/DeployClient.ts} +21 -12
  55. package/src/{S3TransferUtility.ts → lib/S3TransferUtility.ts} +0 -0
  56. package/src/{S3Uploader.ts → lib/S3Uploader.ts} +3 -3
  57. package/src/lib/Versions.ts +95 -0
  58. package/dist/DeployClient.d.ts.map +0 -1
  59. package/dist/DeployClient.js.map +0 -1
  60. package/dist/S3TransferUtility.d.ts.map +0 -1
  61. package/dist/S3TransferUtility.js.map +0 -1
  62. package/dist/S3Uploader.d.ts.map +0 -1
  63. package/dist/S3Uploader.js.map +0 -1
  64. package/dist/config/FileStore.d.ts +0 -7
  65. package/dist/config/FileStore.d.ts.map +0 -1
  66. package/dist/config/FileStore.js +0 -17
  67. package/dist/config/FileStore.js.map +0 -1
  68. package/src/config/FileStore.ts +0 -14
@@ -0,0 +1,498 @@
1
+ import 'reflect-metadata';
2
+ import { exec } from 'child_process';
3
+ import * as util from 'util';
4
+ import * as lambda from '@aws-sdk/client-lambda';
5
+ import * as s3 from '@aws-sdk/client-s3';
6
+ import * as sts from '@aws-sdk/client-sts';
7
+ import { Command, flags as flagsParser } from '@oclif/command';
8
+ import * as chalk from 'chalk';
9
+ import * as path from 'path';
10
+ import { promises as fs, pathExists, createReadStream } from 'fs-extra';
11
+ import { Listr, ListrTask } from 'listr2';
12
+ import { Config, IConfig } from '../config/Config';
13
+ import DeployClient, { IDeployVersionPreflightResult } from '../lib/DeployClient';
14
+ import S3Uploader from '../lib/S3Uploader';
15
+ import S3TransferUtility from '../lib/S3TransferUtility';
16
+ import { Upload } from '@aws-sdk/lib-storage';
17
+ import { contentType } from 'mime-types';
18
+ import { TaskWrapper } from 'listr2/dist/lib/task-wrapper';
19
+ import { DefaultRenderer } from 'listr2/dist/renderer/default.renderer';
20
+ import {
21
+ createVersions,
22
+ IFileToModify,
23
+ IVersions,
24
+ restoreFiles,
25
+ writeNewVersions,
26
+ } from '../lib/Versions';
27
+ const asyncSetTimeout = util.promisify(setTimeout);
28
+ const asyncExec = util.promisify(exec);
29
+
30
+ const lambdaClient = new lambda.LambdaClient({
31
+ maxAttempts: 8,
32
+ });
33
+
34
+ interface IContext {
35
+ preflightResult: IDeployVersionPreflightResult;
36
+ files: string[];
37
+ }
38
+
39
+ export class DockerAutoCommand extends Command {
40
+ static description =
41
+ 'Fully automatic publishing of Docker-based Lambda function using Next.js and serverless-nextjs-router';
42
+
43
+ static examples = [
44
+ `$ microapps-publish nextjs-docker-auto -d microapps-deployer-dev -n 0.0.14 -r microapps-app-release-dev-repo
45
+ ✔ Logging into ECR [2s]
46
+ ✔ Modifying Config Files [0.0s]
47
+ ✔ Preflight Version Check [1s]
48
+ ✔ Serverless Next.js Build [1m16s]
49
+ ✔ Publish to ECR [32s]
50
+ ✔ Deploy to Lambda [11s]
51
+ ✔ Confirm Static Assets Folder Exists [0.0s]
52
+ ✔ Copy Static Files to Local Upload Dir [0.0s]
53
+ ✔ Enumerate Files to Upload to S3 [0.0s]
54
+ ✔ Upload Static Files to S3 [1s]
55
+ ✔ Creating MicroApp Application: release [0.2s]
56
+ ✔ Creating MicroApp Version: 0.0.14 [1s]
57
+ `,
58
+ ];
59
+
60
+ static flags = {
61
+ version: flagsParser.version({
62
+ char: 'v',
63
+ }),
64
+ help: flagsParser.help(),
65
+ deployerLambdaName: flagsParser.string({
66
+ char: 'd',
67
+ multiple: false,
68
+ required: true,
69
+ description: 'Name of the deployer lambda function',
70
+ }),
71
+ newVersion: flagsParser.string({
72
+ char: 'n',
73
+ multiple: false,
74
+ required: true,
75
+ description: 'New semantic version to apply',
76
+ }),
77
+ repoName: flagsParser.string({
78
+ char: 'r',
79
+ multiple: false,
80
+ required: true,
81
+ description: 'Name (not URI) of the Docker repo for the app',
82
+ }),
83
+ leaveCopy: flagsParser.boolean({
84
+ char: 'l',
85
+ default: false,
86
+ required: false,
87
+ description: 'Leave a copy of the modifed files as .modified',
88
+ }),
89
+ };
90
+
91
+ private VersionAndAlias: IVersions;
92
+ private IMAGE_TAG = '';
93
+ private IMAGE_URI = '';
94
+ private FILES_TO_MODIFY: IFileToModify[];
95
+ private _restoreFilesStarted = false;
96
+
97
+ async run(): Promise<void> {
98
+ const RUNNING_TEXT = ' RUNS ';
99
+ const RUNNING = chalk.reset.inverse.yellow.bold(RUNNING_TEXT) + ' ';
100
+
101
+ const { flags: parsedFlags } = this.parse(DockerAutoCommand);
102
+ const version = parsedFlags.newVersion;
103
+ const leaveFiles = parsedFlags.leaveCopy;
104
+ const lambdaName = parsedFlags.deployerLambdaName;
105
+ const ecrRepo = parsedFlags.repoName;
106
+
107
+ // Override the config value
108
+ const config = Config.instance;
109
+ config.deployer.lambdaName = lambdaName;
110
+ config.app.semVer = version;
111
+
112
+ // Get the account ID and region from STS
113
+ // TODO: Move this to the right place
114
+ if (config.app.awsAccountID === 0 || config.app.awsRegion === '') {
115
+ const stsClient = new sts.STSClient({
116
+ maxAttempts: 8,
117
+ });
118
+ const stsResponse = await stsClient.send(new sts.GetCallerIdentityCommand({}));
119
+ if (config.app.awsAccountID === 0) {
120
+ config.app.awsAccountID = parseInt(stsResponse.Account, 10);
121
+ }
122
+ if (config.app.awsRegion === '') {
123
+ config.app.awsRegion = stsClient.config.region as string;
124
+ }
125
+ }
126
+ if (config.app.ecrHost === '') {
127
+ config.app.ecrHost = `${config.app.awsAccountID}.dkr.ecr.${config.app.awsRegion}.amazonaws.com`;
128
+ }
129
+ if (ecrRepo) {
130
+ config.app.ecrRepoName = ecrRepo;
131
+ } else if (config.app.ecrRepoName === '') {
132
+ config.app.ecrRepoName = `microapps-app-${config.app.name}${Config.envLevel}-repo`;
133
+ }
134
+
135
+ this.VersionAndAlias = createVersions(version);
136
+ const versionOnly = { version: this.VersionAndAlias.version };
137
+
138
+ this.FILES_TO_MODIFY = [
139
+ { path: 'package.json', versions: versionOnly },
140
+ { path: 'next.config.js', versions: versionOnly },
141
+ ];
142
+
143
+ // Install handler to ensure that we restore files
144
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
145
+ process.on('SIGINT', async () => {
146
+ if (this._restoreFilesStarted) {
147
+ return;
148
+ } else {
149
+ this._restoreFilesStarted = true;
150
+ }
151
+ this.log('Caught Ctrl-C, restoring files');
152
+ await S3Uploader.removeTempDirIfExists();
153
+ await restoreFiles(this.FILES_TO_MODIFY);
154
+ });
155
+
156
+ if (config === undefined) {
157
+ this.error('Failed to load the config file');
158
+ }
159
+ if (config.app.staticAssetsPath === undefined) {
160
+ this.error('StaticAssetsPath must be specified in the config file');
161
+ }
162
+
163
+ //
164
+ // Setup Tasks
165
+ //
166
+
167
+ const tasks = new Listr<IContext>(
168
+ [
169
+ {
170
+ title: 'Logging into ECR',
171
+ task: async (ctx, task) => {
172
+ const origTitle = task.title;
173
+ task.title = RUNNING + origTitle;
174
+
175
+ await this.loginToECR(config);
176
+
177
+ task.title = origTitle;
178
+ },
179
+ },
180
+ {
181
+ title: 'Modifying Config Files',
182
+ task: async (ctx, task) => {
183
+ const origTitle = task.title;
184
+ task.title = RUNNING + origTitle;
185
+
186
+ // Modify the existing files with the new version
187
+ for (const fileToModify of this.FILES_TO_MODIFY) {
188
+ task.output = `Patching version (${this.VersionAndAlias.version}) into ${fileToModify.path}`;
189
+ if (!(await writeNewVersions(fileToModify.path, fileToModify.versions, leaveFiles))) {
190
+ task.output = `Failed modifying file: ${fileToModify.path}`;
191
+ }
192
+ }
193
+
194
+ task.title = origTitle;
195
+ },
196
+ },
197
+ {
198
+ title: 'Preflight Version Check',
199
+ task: async (ctx, task) => {
200
+ const origTitle = task.title;
201
+ task.title = RUNNING + origTitle;
202
+
203
+ // Confirm the Version Does Not Exist in Published State
204
+ task.output = `Checking if deployed app/version already exists for ${config.app.name}/${version}`;
205
+ ctx.preflightResult = await DeployClient.DeployVersionPreflight({
206
+ config,
207
+ output: (message: string) => (task.output = message),
208
+ });
209
+ if (ctx.preflightResult.exists) {
210
+ task.output = `Warning: App/Version already exists: ${config.app.name}/${config.app.semVer}`;
211
+ }
212
+
213
+ task.title = origTitle;
214
+ },
215
+ },
216
+ {
217
+ title: 'Serverless Next.js Build',
218
+ task: async (ctx, task) => {
219
+ const origTitle = task.title;
220
+ task.title = RUNNING + origTitle;
221
+
222
+ task.output = `Invoking serverless next.js build for ${config.app.name}/${version}`;
223
+
224
+ // Run the serverless next.js build
225
+ await asyncExec('serverless');
226
+
227
+ if (config.app.serverlessNextRouterPath !== undefined) {
228
+ task.output = 'Copying Serverless Next.js router to build output directory';
229
+ await fs.copyFile(
230
+ config.app.serverlessNextRouterPath,
231
+ './.serverless_nextjs/index.js',
232
+ );
233
+ }
234
+
235
+ task.title = origTitle;
236
+ },
237
+ },
238
+ {
239
+ title: 'Publish to ECR',
240
+ task: async (ctx: IContext, task: TaskWrapper<IContext, typeof DefaultRenderer>) => {
241
+ const origTitle = task.title;
242
+ task.title = RUNNING + origTitle;
243
+
244
+ // Docker, build, tag, push to ECR
245
+ // Note: Need to already have AWS env vars set
246
+ await this.publishToECR(config, task);
247
+
248
+ task.title = origTitle;
249
+ },
250
+ },
251
+ {
252
+ title: 'Deploy to Lambda',
253
+ task: async (ctx, task) => {
254
+ const origTitle = task.title;
255
+ task.title = RUNNING + origTitle;
256
+
257
+ // Update the Lambda function
258
+ await this.deployToLambda(config, this.VersionAndAlias, task);
259
+
260
+ task.title = origTitle;
261
+ },
262
+ },
263
+ {
264
+ title: 'Confirm Static Assets Folder Exists',
265
+ task: async (ctx, task) => {
266
+ const origTitle = task.title;
267
+ task.title = RUNNING + origTitle;
268
+
269
+ // Check that Static Assets Folder exists
270
+ if (!(await pathExists(config.app.staticAssetsPath))) {
271
+ this.error(`Static asset path does not exist: ${config.app.staticAssetsPath}`);
272
+ }
273
+
274
+ task.title = origTitle;
275
+ },
276
+ },
277
+ {
278
+ title: 'Copy Static Files to Local Upload Dir',
279
+ task: async (ctx, task) => {
280
+ const origTitle = task.title;
281
+ task.title = RUNNING + origTitle;
282
+
283
+ // Copy files to local dir to be uploaded
284
+ await S3Uploader.CopyToUploadDir(config, ctx.preflightResult.response.s3UploadUrl);
285
+
286
+ task.title = origTitle;
287
+ },
288
+ },
289
+ {
290
+ title: 'Enumerate Files to Upload to S3',
291
+ task: async (ctx, task) => {
292
+ const origTitle = task.title;
293
+ task.title = RUNNING + origTitle;
294
+
295
+ ctx.files = (await S3TransferUtility.GetFiles(S3Uploader.TempDir)) as string[];
296
+
297
+ task.title = origTitle;
298
+ },
299
+ },
300
+ {
301
+ title: 'Upload Static Files to S3',
302
+ task: (ctx, task) => {
303
+ const origTitle = task.title;
304
+ task.title = RUNNING + origTitle;
305
+
306
+ const { bucketName, destinationPrefix } = S3Uploader.ParseUploadPath(
307
+ ctx.preflightResult.response.s3UploadUrl,
308
+ );
309
+
310
+ // Use temp credentials for S3
311
+ const s3Client = new s3.S3Client({
312
+ maxAttempts: 16,
313
+ credentials: {
314
+ accessKeyId: ctx.preflightResult.response.awsCredentials.accessKeyId,
315
+ secretAccessKey: ctx.preflightResult.response.awsCredentials.secretAccessKey,
316
+ sessionToken: ctx.preflightResult.response.awsCredentials.sessionToken,
317
+ },
318
+ });
319
+
320
+ const pathWithoutAppAndVer = path.join(S3Uploader.TempDir, destinationPrefix);
321
+
322
+ const tasks: ListrTask<IContext>[] = ctx.files.map((filePath) => ({
323
+ task: async (ctx: IContext, subtask) => {
324
+ const relFilePath = path.relative(pathWithoutAppAndVer, filePath);
325
+
326
+ const origTitle = relFilePath;
327
+ subtask.title = RUNNING + origTitle;
328
+
329
+ const upload = new Upload({
330
+ client: s3Client,
331
+ leavePartsOnError: false,
332
+ params: {
333
+ Bucket: bucketName,
334
+ Key: path.relative(S3Uploader.TempDir, filePath),
335
+ Body: createReadStream(filePath),
336
+ ContentType: contentType(path.basename(filePath)) || 'application/octet-stream',
337
+ CacheControl: 'max-age=86400; public',
338
+ },
339
+ });
340
+ await upload.done();
341
+
342
+ subtask.title = origTitle;
343
+ },
344
+ }));
345
+
346
+ task.title = origTitle;
347
+
348
+ return task.newListr(tasks, {
349
+ concurrent: 8,
350
+ rendererOptions: {
351
+ clearOutput: false,
352
+ showErrorMessage: true,
353
+ showTimer: true,
354
+ },
355
+ });
356
+ },
357
+ },
358
+ {
359
+ title: `Creating MicroApp Application: ${config.app.name}`,
360
+ task: async (ctx, task) => {
361
+ const origTitle = task.title;
362
+ task.title = RUNNING + origTitle;
363
+
364
+ // Call Deployer to Create App if Not Exists
365
+ await DeployClient.CreateApp(config);
366
+
367
+ task.title = origTitle;
368
+ },
369
+ },
370
+ {
371
+ title: `Creating MicroApp Version: ${config.app.semVer}`,
372
+ task: async (ctx, task) => {
373
+ const origTitle = task.title;
374
+ task.title = RUNNING + origTitle;
375
+
376
+ // Call Deployer to Deploy AppName/Version
377
+ await DeployClient.DeployVersion(config, (message: string) => (task.output = message));
378
+
379
+ task.title = origTitle;
380
+ },
381
+ },
382
+ ],
383
+ {
384
+ rendererOptions: {
385
+ showTimer: true,
386
+ },
387
+ },
388
+ );
389
+
390
+ try {
391
+ await tasks.run();
392
+ // this.log(`Published: ${config.app.name}/${config.app.semVer}`);
393
+ } catch (error) {
394
+ this.log(`Caught exception: ${error.message}`);
395
+ } finally {
396
+ await S3Uploader.removeTempDirIfExists();
397
+ await restoreFiles(this.FILES_TO_MODIFY);
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Login to ECR for Lambda Docker functions
403
+ * @param config
404
+ * @returns
405
+ */
406
+ private async loginToECR(config: IConfig): Promise<boolean> {
407
+ this.IMAGE_TAG = `${config.app.ecrRepoName}:${this.VersionAndAlias.version}`;
408
+ this.IMAGE_URI = `${config.app.ecrHost}/${this.IMAGE_TAG}`;
409
+
410
+ try {
411
+ await asyncExec(
412
+ `aws ecr get-login-password --region ${config.app.awsRegion} | docker login --username AWS --password-stdin ${config.app.ecrHost}`,
413
+ );
414
+ } catch (error) {
415
+ throw new Error(`ECR Login Failed: ${error.message}`);
416
+ }
417
+
418
+ return true;
419
+ }
420
+
421
+ /**
422
+ * Publish to ECR for Lambda Docker function
423
+ * @param config
424
+ */
425
+ private async publishToECR(
426
+ config: IConfig,
427
+ task: TaskWrapper<IContext, typeof DefaultRenderer>,
428
+ ): Promise<void> {
429
+ task.output = 'Starting Docker build';
430
+ await asyncExec(`docker build -f Dockerfile -t ${this.IMAGE_TAG} .`);
431
+ await asyncExec(`docker tag ${this.IMAGE_TAG} ${config.app.ecrHost}/${this.IMAGE_TAG}`);
432
+ task.output = 'Starting Docker push to ECR';
433
+ await asyncExec(`docker push ${config.app.ecrHost}/${this.IMAGE_TAG}`);
434
+ }
435
+
436
+ /**
437
+ * Publish an app version to Lambda
438
+ * @param config
439
+ * @param versions
440
+ */
441
+ private async deployToLambda(
442
+ config: IConfig,
443
+ versions: IVersions,
444
+ task: TaskWrapper<IContext, typeof DefaultRenderer>,
445
+ ): Promise<void> {
446
+ // Create Lambda version
447
+ task.output = 'Updating Lambda code to point to new Docker image';
448
+ const resultUpdate = await lambdaClient.send(
449
+ new lambda.UpdateFunctionCodeCommand({
450
+ FunctionName: config.app.lambdaName,
451
+ ImageUri: this.IMAGE_URI,
452
+ Publish: true,
453
+ }),
454
+ );
455
+ // await lambdaClient.send(
456
+ // new lambda.PublishVersionCommand({
457
+ // FunctionName: config.app.lambdaName,
458
+ // }),
459
+ // );
460
+ const lambdaVersion = resultUpdate.Version;
461
+ task.output = `Lambda version created: ${resultUpdate.Version}`;
462
+
463
+ let lastUpdateStatus = resultUpdate.LastUpdateStatus;
464
+ for (let i = 0; i < 5; i++) {
465
+ // When the function is created the status will be "Pending"
466
+ // and we have to wait until it's done creating
467
+ // before we can point an alias to it
468
+ if (lastUpdateStatus === 'Successful') {
469
+ task.output = `Lambda function updated, version: ${lambdaVersion}`;
470
+ break;
471
+ }
472
+
473
+ // If it didn't work, wait and try again
474
+ await asyncSetTimeout(1000 * i);
475
+
476
+ const resultGet = await lambdaClient.send(
477
+ new lambda.GetFunctionCommand({
478
+ FunctionName: config.app.lambdaName,
479
+ Qualifier: lambdaVersion,
480
+ }),
481
+ );
482
+
483
+ // Save the last update status so we can check on re-loop
484
+ lastUpdateStatus = resultGet?.Configuration?.LastUpdateStatus;
485
+ }
486
+
487
+ // Create Lambda alias point
488
+ task.output = `Creating the lambda alias for the new version: ${lambdaVersion}`;
489
+ const resultLambdaAlias = await lambdaClient.send(
490
+ new lambda.CreateAliasCommand({
491
+ FunctionName: config.app.lambdaName,
492
+ Name: versions.alias,
493
+ FunctionVersion: lambdaVersion,
494
+ }),
495
+ );
496
+ task.output = `Lambda alias created, name: ${resultLambdaAlias.Name}`;
497
+ }
498
+ }
@@ -0,0 +1,73 @@
1
+ import 'reflect-metadata';
2
+ import { Command, flags as flagsParser } from '@oclif/command';
3
+ import * as chalk from 'chalk';
4
+ import { Listr } from 'listr2';
5
+ import { createVersions, IVersions, restoreFiles } from '../lib/Versions';
6
+
7
+ const RUNNING_TEXT = ' RUNS ';
8
+ const RUNNING = chalk.reset.inverse.yellow.bold(RUNNING_TEXT) + ' ';
9
+
10
+ export class NextJSVersionRestoreCommand extends Command {
11
+ static description = 'Restore next.config.js';
12
+
13
+ static examples = [
14
+ `$ microapps-publish nextjs-version-restore
15
+ ✔ Restoring Modified Config Files [0.0s]
16
+ `,
17
+ ];
18
+
19
+ static flags = {
20
+ version: flagsParser.version({
21
+ char: 'v',
22
+ }),
23
+ help: flagsParser.help(),
24
+ };
25
+
26
+ private VersionAndAlias: IVersions;
27
+ private FILES_TO_MODIFY: {
28
+ path: string;
29
+ versions: IVersions;
30
+ }[];
31
+
32
+ async run(): Promise<void> {
33
+ const { flags: parsedFlags } = this.parse(NextJSVersionRestoreCommand);
34
+
35
+ this.VersionAndAlias = createVersions('0.0.0');
36
+ const versionOnly = { version: this.VersionAndAlias.version };
37
+
38
+ this.FILES_TO_MODIFY = [{ path: 'next.config.js', versions: versionOnly }];
39
+
40
+ // TODO: Pick and validate the appname/semver from the config and flags
41
+
42
+ //
43
+ // Setup Tasks
44
+ //
45
+
46
+ const tasks = new Listr(
47
+ [
48
+ {
49
+ title: 'Restoring Modified Config Files',
50
+ task: async (ctx, task) => {
51
+ const origTitle = task.title;
52
+ task.title = RUNNING + origTitle;
53
+
54
+ await restoreFiles(this.FILES_TO_MODIFY);
55
+
56
+ task.title = origTitle;
57
+ },
58
+ },
59
+ ],
60
+ {
61
+ rendererOptions: {
62
+ showTimer: true,
63
+ },
64
+ },
65
+ );
66
+
67
+ try {
68
+ await tasks.run();
69
+ } catch (error) {
70
+ this.log(`Caught exception: ${error.message}`);
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,113 @@
1
+ import 'reflect-metadata';
2
+ import { Command, flags as flagsParser } from '@oclif/command';
3
+ import * as chalk from 'chalk';
4
+ import { Listr } from 'listr2';
5
+ import { Config } from '../config/Config';
6
+ import { createVersions, IVersions, restoreFiles, writeNewVersions } from '../lib/Versions';
7
+
8
+ const RUNNING_TEXT = ' RUNS ';
9
+ const RUNNING = chalk.reset.inverse.yellow.bold(RUNNING_TEXT) + ' ';
10
+
11
+ export class NextJSVersionCommand extends Command {
12
+ static description = 'Apply version to next.config.js overtop of 0.0.0 placeholder';
13
+
14
+ static examples = [
15
+ `$ microapps-publish nextjs-version -n 0.0.13
16
+ ✔ Modifying Config Files [0.0s]
17
+ `,
18
+ ];
19
+
20
+ static flags = {
21
+ version: flagsParser.version({
22
+ char: 'v',
23
+ }),
24
+ help: flagsParser.help(),
25
+ newVersion: flagsParser.string({
26
+ char: 'n',
27
+ multiple: false,
28
+ required: true,
29
+ description: 'New semantic version to apply',
30
+ }),
31
+ leaveCopy: flagsParser.boolean({
32
+ char: 'l',
33
+ default: false,
34
+ required: false,
35
+ description: 'Leave a copy of the modifed files as .modified',
36
+ }),
37
+ };
38
+
39
+ private VersionAndAlias: IVersions;
40
+ private FILES_TO_MODIFY: {
41
+ path: string;
42
+ versions: IVersions;
43
+ }[];
44
+
45
+ async run(): Promise<void> {
46
+ const { flags: parsedFlags } = this.parse(NextJSVersionCommand);
47
+ const version = parsedFlags.newVersion;
48
+ const leaveFiles = parsedFlags.leaveCopy;
49
+
50
+ // Override the config value
51
+ const config = Config.instance;
52
+ config.app.semVer = version;
53
+
54
+ this.VersionAndAlias = createVersions(version);
55
+ const versionOnly = { version: this.VersionAndAlias.version };
56
+
57
+ this.FILES_TO_MODIFY = [{ path: 'next.config.js', versions: versionOnly }];
58
+
59
+ // TODO: Pick and validate the appname/semver from the config and flags
60
+
61
+ if (config === undefined) {
62
+ this.error('Failed to load the config file');
63
+ }
64
+
65
+ //
66
+ // Setup Tasks
67
+ //
68
+
69
+ const tasks = new Listr(
70
+ [
71
+ {
72
+ title: 'Restoring Modified Config Files',
73
+ task: async (ctx, task) => {
74
+ const origTitle = task.title;
75
+ task.title = RUNNING + origTitle;
76
+
77
+ await restoreFiles(this.FILES_TO_MODIFY);
78
+
79
+ task.title = origTitle;
80
+ },
81
+ },
82
+ {
83
+ title: 'Modifying Config Files',
84
+ task: async (ctx, task) => {
85
+ const origTitle = task.title;
86
+ task.title = RUNNING + origTitle;
87
+
88
+ // Modify the existing files with the new version
89
+ for (const fileToModify of this.FILES_TO_MODIFY) {
90
+ task.output = `Patching version (${this.VersionAndAlias.version}) into ${fileToModify.path}`;
91
+ if (!(await writeNewVersions(fileToModify.path, fileToModify.versions, leaveFiles))) {
92
+ task.output = `Failed modifying file: ${fileToModify.path}`;
93
+ }
94
+ }
95
+
96
+ task.title = origTitle;
97
+ },
98
+ },
99
+ ],
100
+ {
101
+ rendererOptions: {
102
+ showTimer: true,
103
+ },
104
+ },
105
+ );
106
+
107
+ try {
108
+ await tasks.run();
109
+ } catch (error) {
110
+ this.log(`Caught exception: ${error.message}`);
111
+ }
112
+ }
113
+ }