@neurodevs/ndx-cli 0.1.32 → 0.1.33

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.
@@ -45,11 +45,18 @@ export default class CliCommandRunnerTest extends AbstractPackageTest {
45
45
  private static readonly packageName = generateId()
46
46
  private static readonly description = generateId()
47
47
  private static readonly keywords = [generateId(), generateId()]
48
+ private static readonly githubToken = generateId()
49
+
50
+ private static readonly defaultKeywords = ['nodejs', 'typescript', 'tdd']
51
+
52
+ private static get keywordsWithDefaults() {
53
+ return [...this.defaultKeywords, ...this.keywords]
54
+ }
48
55
 
49
56
  private static readonly createUiCommand = 'create.ui'
50
57
  private static readonly componentName = generateId()
51
58
 
52
- private static readonly githubToken = generateId()
59
+ private static readonly upgradePackageCommand = 'upgrade.package'
53
60
 
54
61
  protected static async beforeEach() {
55
62
  await super.beforeEach()
@@ -257,7 +264,7 @@ export default class CliCommandRunnerTest extends AbstractPackageTest {
257
264
  {
258
265
  name: this.packageName,
259
266
  description: this.description,
260
- keywords: this.keywordsWithDefaults(),
267
+ keywords: this.keywordsWithDefaults,
261
268
  gitNamespace: 'neurodevs',
262
269
  npmNamespace: 'neurodevs',
263
270
  installDir: this.expandHomeDir('~/dev'),
@@ -268,10 +275,6 @@ export default class CliCommandRunnerTest extends AbstractPackageTest {
268
275
  )
269
276
  }
270
277
 
271
- private static keywordsWithDefaults() {
272
- return ['nodejs', 'typescript', 'tdd', ...this.keywords]
273
- }
274
-
275
278
  @test()
276
279
  protected static async createPackageRunsNpmAutopackage() {
277
280
  await this.runCreatePackage()
@@ -279,7 +282,7 @@ export default class CliCommandRunnerTest extends AbstractPackageTest {
279
282
  assert.isEqual(
280
283
  FakeAutopackage.numCallsToRun,
281
284
  1,
282
- 'Did not call run on Autopackage!'
285
+ 'Did not call run on NpmAutopackage!'
283
286
  )
284
287
  }
285
288
 
@@ -549,6 +552,100 @@ export default class CliCommandRunnerTest extends AbstractPackageTest {
549
552
  )
550
553
  }
551
554
 
555
+ @test()
556
+ protected static async upgradePackageCreatesInstance() {
557
+ const instance = await this.runUpgradePackage()
558
+
559
+ assert.isTruthy(
560
+ instance,
561
+ `Failed to create instance for ${this.upgradePackageCommand}!`
562
+ )
563
+ }
564
+
565
+ @test()
566
+ protected static async upgradePackageCreatesNpmAutopackage() {
567
+ const infoFromPackageJson = {
568
+ name: this.packageName,
569
+ description: this.description,
570
+ keywords: this.keywordsWithDefaults,
571
+ }
572
+
573
+ setFakeReadFileResult(
574
+ 'package.json',
575
+ JSON.stringify(infoFromPackageJson)
576
+ )
577
+
578
+ await this.runUpgradePackage()
579
+
580
+ assert.isEqualDeep(
581
+ FakeAutopackage.callsToConstructor[0],
582
+ {
583
+ ...infoFromPackageJson,
584
+ gitNamespace: 'neurodevs',
585
+ npmNamespace: 'neurodevs',
586
+ installDir: this.expandHomeDir('~/dev'),
587
+ license: 'MIT',
588
+ author: 'Eric Yates <hello@ericthecurious.com>',
589
+ },
590
+ 'Did not create NpmAutopackage with expected options!'
591
+ )
592
+ }
593
+
594
+ @test()
595
+ protected static async upgradePackageRunsNpmAutopackage() {
596
+ await this.runUpgradePackage()
597
+
598
+ assert.isEqual(
599
+ FakeAutopackage.numCallsToRun,
600
+ 1,
601
+ 'Did not call run on NpmAutopackage!'
602
+ )
603
+ }
604
+
605
+ @test()
606
+ protected static async upgradePackageAddsDefaultKeywordsIfMissing() {
607
+ const infoFromPackageJson = {
608
+ name: this.packageName,
609
+ description: this.description,
610
+ keywords: [],
611
+ }
612
+
613
+ setFakeReadFileResult(
614
+ 'package.json',
615
+ JSON.stringify(infoFromPackageJson)
616
+ )
617
+
618
+ await this.runUpgradePackage()
619
+
620
+ assert.isEqualDeep(
621
+ FakeAutopackage.callsToConstructor[0]?.keywords,
622
+ this.defaultKeywords,
623
+ 'Did not add default keywords!'
624
+ )
625
+ }
626
+
627
+ @test()
628
+ protected static async upgradePackageDoesNotOverwriteKeywordsEvenIfDefaultsAreMissing() {
629
+ const infoFromPackageJson = {
630
+ name: this.packageName,
631
+ description: this.description,
632
+ keywords: [generateId(), generateId()],
633
+ }
634
+
635
+ setFakeReadFileResult(
636
+ 'package.json',
637
+ JSON.stringify(infoFromPackageJson)
638
+ )
639
+
640
+ await this.runUpgradePackage()
641
+
642
+ assert.isEqualDeep(
643
+ FakeAutopackage.callsToConstructor[0]?.keywords,
644
+ [...this.defaultKeywords, ...infoFromPackageJson.keywords],
645
+ 'Should not have overwritten keywords!'
646
+ )
647
+ }
648
+
552
649
  private static async runCreateUi(
553
650
  responses?: Record<string, string | boolean>
554
651
  ) {
@@ -645,6 +742,13 @@ export default class CliCommandRunnerTest extends AbstractPackageTest {
645
742
  return instance
646
743
  }
647
744
 
745
+ private static async runUpgradePackage() {
746
+ const instance = this.CliCommandRunner([this.upgradePackageCommand])
747
+ await instance.run()
748
+
749
+ return instance
750
+ }
751
+
648
752
  private static expandHomeDir(inputPath: string): string {
649
753
  return inputPath.startsWith('~')
650
754
  ? path.join(os.homedir(), inputPath.slice(1))
@@ -1,14 +1,11 @@
1
1
  import { exec as execSync } from 'child_process'
2
2
  import { mkdir, readFile, writeFile } from 'fs/promises'
3
- import os from 'os'
4
- import path from 'path'
5
3
  import { promisify } from 'util'
6
- import {
7
- ImplAutomodule,
8
- NpmAutopackage,
9
- UiAutomodule,
10
- } from '@neurodevs/meta-node'
11
4
  import prompts from 'prompts'
5
+ import CreateImplCommand from './commands/CreateImplCommand'
6
+ import CreatePackageCommand from './commands/CreatePackageCommand'
7
+ import CreateUiCommand from './commands/CreateUiCommand'
8
+ import UpgradePackageCommand from './commands/UpgradePackageCommand'
12
9
 
13
10
  export default class CliCommandRunner implements CommandRunner {
14
11
  public static Class?: CommandRunnerConstructor
@@ -21,21 +18,15 @@ export default class CliCommandRunner implements CommandRunner {
21
18
  private args: string[]
22
19
 
23
20
  private readonly createImplCommand = 'create.impl'
24
- private interfaceName!: string
25
- private implName!: string
26
-
27
21
  private readonly createPackageCommand = 'create.package'
28
- private packageName!: string
29
- private description!: string
30
- private keywords!: string[]
31
-
32
22
  private readonly createUiCommand = 'create.ui'
33
- private componentName!: string
23
+ private readonly upgradePackageCommand = 'upgrade.package'
34
24
 
35
25
  private readonly supportedCommands = [
36
26
  this.createImplCommand,
37
27
  this.createPackageCommand,
38
28
  this.createUiCommand,
29
+ this.upgradePackageCommand,
39
30
  ]
40
31
 
41
32
  protected constructor(args: string[]) {
@@ -76,384 +67,30 @@ export default class CliCommandRunner implements CommandRunner {
76
67
  case this.createUiCommand:
77
68
  await this.createUiModule()
78
69
  break
70
+ case this.upgradePackageCommand:
71
+ await this.upgradePackage()
72
+ break
79
73
  }
80
74
  }
81
75
 
82
76
  private async createImplModule() {
83
- const { interfaceName, implName } = await this.promptForAutomodule()
84
-
85
- this.interfaceName = interfaceName
86
- this.implName = implName
87
-
88
- if (!this.userInputExistsForCreateImpl) {
89
- return
90
- }
91
-
92
- await this.makeRequiredImplDirectories()
93
-
94
- const automodule = this.ImplAutomodule()
95
- await automodule.run()
96
- }
97
-
98
- private async promptForAutomodule() {
99
- return await this.prompts([
100
- {
101
- type: 'text',
102
- name: 'interfaceName',
103
- message: this.interfaceNameMessage,
104
- },
105
- {
106
- type: 'text',
107
- name: 'implName',
108
- message: this.implNameMessage,
109
- },
110
- ])
111
- }
112
-
113
- private readonly interfaceNameMessage =
114
- 'What should the interface be called? Example: YourInterface'
115
-
116
- private readonly implNameMessage =
117
- 'What should the implementation class be called? Example: YourInterfaceImpl'
118
-
119
- private get userInputExistsForCreateImpl() {
120
- return this.interfaceName && this.implName
121
- }
122
-
123
- private async makeRequiredImplDirectories() {
124
- await this.mkdir(this.implTestSaveDir, { recursive: true })
125
- await this.mkdir(this.implModuleSaveDir, { recursive: true })
126
- await this.mkdir(this.implFakeSaveDir, { recursive: true })
127
- }
128
-
129
- private readonly implTestSaveDir = 'src/__tests__/modules'
130
- private readonly implModuleSaveDir = 'src/modules'
131
-
132
- private get implFakeSaveDir() {
133
- return `src/testDoubles/${this.interfaceName}`
77
+ const command = new CreateImplCommand()
78
+ await command.run()
134
79
  }
135
80
 
136
81
  private async createPackage() {
137
- const { packageName, description, keywords } =
138
- await this.promptForAutopackage()
139
-
140
- this.packageName = packageName
141
- this.description = description
142
- this.keywords = keywords
143
-
144
- if (!this.userInputExistsForCreatePackage) {
145
- return
146
- }
147
-
148
- const autopackage = this.NpmAutopackage()
149
- await autopackage.run()
150
- }
151
-
152
- private async promptForAutopackage() {
153
- return await this.prompts([
154
- {
155
- type: 'text',
156
- name: 'packageName',
157
- message: this.packageNameMessage,
158
- },
159
- {
160
- type: 'text',
161
- name: 'description',
162
- message: this.descriptionMessage,
163
- },
164
- {
165
- type: 'text',
166
- name: 'keywords',
167
- message: this.keywordsMessage,
168
- initial: '',
169
- format: (value) =>
170
- value ? this.splitOnCommaOrWhitespace(value) : [],
171
- },
172
- ])
173
- }
174
-
175
- private readonly packageNameMessage =
176
- 'What should the package be called? Example: useful-package'
177
-
178
- private readonly descriptionMessage =
179
- 'What should the package description be? Example: A useful package.'
180
-
181
- private readonly keywordsMessage =
182
- 'Enter keywords (comma or space separated, lowercase, optional):'
183
-
184
- private splitOnCommaOrWhitespace(value: string) {
185
- return value
186
- .split(/[\s,]+/)
187
- .map((v: string) => v.trim())
188
- .filter(Boolean)
189
- }
190
-
191
- private get userInputExistsForCreatePackage() {
192
- return this.packageName && this.description
82
+ const command = new CreatePackageCommand()
83
+ await command.run()
193
84
  }
194
85
 
195
86
  private async createUiModule() {
196
- await this.installDependenciesIfNeeded()
197
-
198
- const { componentName } = await this.promptForUimodule()
199
-
200
- this.componentName = componentName
201
-
202
- if (!this.componentName) {
203
- return
204
- }
205
-
206
- await this.makeRequiredUiDirectories()
207
-
208
- const instance = this.UiAutomodule()
209
- await instance.run()
210
- }
211
-
212
- private async installDependenciesIfNeeded() {
213
- const isInstalled = await this.checkIfDependenciesAreInstalled()
214
-
215
- if (!isInstalled) {
216
- const { shouldInstall } = await this.promptForInstallDependencies()
217
-
218
- if (shouldInstall) {
219
- await this.installReactDependencies()
220
- await this.updateTsconfigForReact()
221
- await this.createSetupTestsFile()
222
- await this.addSetupTestsToPackageJson()
223
- await this.recompileForTsxFiles()
224
- }
225
- }
226
- }
227
-
228
- private async checkIfDependenciesAreInstalled() {
229
- const original = await this.loadPackageJson()
230
- const parsed = JSON.parse(original)
231
-
232
- const dependencies = Object.keys(parsed.dependencies ?? {})
233
-
234
- const areDepsInstalled = this.requiredDependencies.every((dep) =>
235
- dependencies.includes(dep)
236
- )
237
-
238
- const devDependencies = Object.keys(parsed.devDependencies ?? {})
239
-
240
- const areDevDepsInstalled = this.requiredDevDependencies.every((dep) =>
241
- devDependencies.includes(dep)
242
- )
243
-
244
- return areDepsInstalled && areDevDepsInstalled
245
- }
246
-
247
- private async loadPackageJson() {
248
- return await this.readFile(this.packageJsonPath, 'utf-8')
249
- }
250
-
251
- private readonly packageJsonPath = 'package.json'
252
-
253
- private readonly requiredDependencies = ['react', 'react-dom']
254
-
255
- private readonly requiredDevDependencies = [
256
- '@types/react',
257
- '@types/react-dom',
258
- '@types/jsdom',
259
- '@testing-library/react',
260
- '@testing-library/dom',
261
- '@testing-library/jest-dom',
262
- 'jsdom',
263
- ]
264
-
265
- private async promptForInstallDependencies() {
266
- return await this.prompts([
267
- {
268
- type: 'confirm',
269
- name: 'shouldInstall',
270
- message:
271
- 'Some required dependencies are missing! Press Enter to install, or any other key to abort.',
272
- initial: true,
273
- },
274
- ])
275
- }
276
-
277
- private async installReactDependencies() {
278
- await this.installDependencies()
279
- await this.installDevDependencies()
280
- }
281
-
282
- private async installDependencies() {
283
- console.log('Installing required dependencies...')
284
- await this.exec('yarn add react react-dom')
285
- }
286
-
287
- private async installDevDependencies() {
288
- console.log('Installing required dev dependencies...')
289
- await this.exec(
290
- 'yarn add -D @types/react @types/react-dom @types/jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom jsdom'
291
- )
292
- }
293
-
294
- private async updateTsconfigForReact() {
295
- console.log('Updating tsconfig.json for React...')
296
-
297
- const original = await this.loadTsconfig()
298
- const parsed = JSON.parse(original)
299
-
300
- const updated = JSON.stringify(
301
- {
302
- ...parsed,
303
- compilerOptions: {
304
- jsx: 'react-jsx',
305
- ...parsed.compilerOptions,
306
- },
307
- include: ['src'],
308
- },
309
- null,
310
- 4
311
- )
312
-
313
- await this.writeFile(this.tsconfigPath, updated)
314
- }
315
-
316
- private async loadTsconfig() {
317
- return await this.readFile(this.tsconfigPath, 'utf-8')
318
- }
319
-
320
- private readonly tsconfigPath = 'tsconfig.json'
321
-
322
- private async createSetupTestsFile() {
323
- console.log('Creating src/setupTests.ts...')
324
- await this.writeFile('src/__tests__/setupTests.ts', this.setupTestsFile)
325
- }
326
-
327
- private async addSetupTestsToPackageJson() {
328
- console.log('Adding setupTests.ts to package.json...')
329
-
330
- const original = await this.loadPackageJson()
331
- const parsed = JSON.parse(original)
332
-
333
- const updated = JSON.stringify(
334
- {
335
- ...parsed,
336
- jest: {
337
- ...parsed.jest,
338
- setupFiles: ['<rootDir>/build/__tests__/setupTests.js'],
339
- },
340
- },
341
- null,
342
- 4
343
- )
344
-
345
- await this.writeFile(this.packageJsonPath, updated)
346
- }
347
-
348
- private async recompileForTsxFiles() {
349
- console.log('Recompiling project for .tsx files...')
350
- await this.exec('npx tsc')
351
- }
352
-
353
- private async promptForUimodule() {
354
- return await this.prompts([
355
- {
356
- type: 'text',
357
- name: 'componentName',
358
- message: this.componentNameMessage,
359
- },
360
- ])
361
- }
362
-
363
- private readonly componentNameMessage =
364
- 'What should the component be called? Example: YourComponent'
365
-
366
- private async makeRequiredUiDirectories() {
367
- await this.mkdir(this.uiTestSaveDir, { recursive: true })
368
- await this.mkdir(this.uiModuleSaveDir, { recursive: true })
369
- await this.mkdir(this.uiFakeSaveDir, { recursive: true })
370
- }
371
-
372
- private expandHomeDir(inputPath: string): string {
373
- return inputPath.startsWith('~')
374
- ? path.join(os.homedir(), inputPath.slice(1))
375
- : inputPath
376
- }
377
-
378
- private readonly uiTestSaveDir = 'src/__tests__/ui'
379
- private readonly uiModuleSaveDir = 'src/ui'
380
-
381
- private get uiFakeSaveDir() {
382
- return `src/testDoubles/${this.componentName}`
383
- }
384
-
385
- private get exec() {
386
- return CliCommandRunner.exec
387
- }
388
-
389
- private get mkdir() {
390
- return CliCommandRunner.mkdir
391
- }
392
-
393
- private get prompts() {
394
- return CliCommandRunner.prompts
395
- }
396
-
397
- private get readFile() {
398
- return CliCommandRunner.readFile
399
- }
400
-
401
- private get writeFile() {
402
- return CliCommandRunner.writeFile
403
- }
404
-
405
- private readonly setupTestsFile = `
406
- import { JSDOM } from 'jsdom'
407
-
408
- const jsdom = new JSDOM('<!doctype html><html><body></body></html>', {
409
- url: 'http://localhost',
410
- })
411
-
412
- global.window = jsdom.window as unknown as Window & typeof globalThis
413
- global.document = jsdom.window.document
414
- global.navigator = jsdom.window.navigator
415
- global.HTMLElement = jsdom.window.HTMLElement
416
- global.getComputedStyle = jsdom.window.getComputedStyle
417
-
418
- global.ResizeObserver = class {
419
- public observe() {}
420
- public unobserve() {}
421
- public disconnect() {}
422
- }
423
-
424
- global.SVGElement = jsdom.window.SVGElement
425
- `
426
-
427
- private ImplAutomodule() {
428
- return ImplAutomodule.Create({
429
- testSaveDir: this.implTestSaveDir,
430
- moduleSaveDir: this.implModuleSaveDir,
431
- fakeSaveDir: this.implFakeSaveDir,
432
- interfaceName: this.interfaceName,
433
- implName: this.implName,
434
- })
435
- }
436
-
437
- private UiAutomodule() {
438
- return UiAutomodule.Create({
439
- testSaveDir: this.uiTestSaveDir,
440
- moduleSaveDir: this.uiModuleSaveDir,
441
- fakeSaveDir: this.uiFakeSaveDir,
442
- componentName: this.componentName,
443
- })
87
+ const command = new CreateUiCommand()
88
+ await command.run()
444
89
  }
445
90
 
446
- private NpmAutopackage() {
447
- return NpmAutopackage.Create({
448
- name: this.packageName,
449
- description: this.description,
450
- keywords: ['nodejs', 'typescript', 'tdd', ...this.keywords],
451
- gitNamespace: 'neurodevs',
452
- npmNamespace: 'neurodevs',
453
- installDir: this.expandHomeDir('~/dev'),
454
- license: 'MIT',
455
- author: 'Eric Yates <hello@ericthecurious.com>',
456
- })
91
+ private async upgradePackage() {
92
+ const command = new UpgradePackageCommand()
93
+ await command.run()
457
94
  }
458
95
  }
459
96
 
@@ -0,0 +1,81 @@
1
+ import { ImplAutomodule } from '@neurodevs/meta-node'
2
+ import CliCommandRunner from '../CliCommandRunner'
3
+
4
+ export default class CreateImplCommand {
5
+ private interfaceName!: string
6
+ private implName!: string
7
+
8
+ public constructor() {}
9
+
10
+ public async run() {
11
+ const { interfaceName, implName } = await this.promptForAutomodule()
12
+
13
+ this.interfaceName = interfaceName
14
+ this.implName = implName
15
+
16
+ if (!this.userInputExistsForCreateImpl) {
17
+ return
18
+ }
19
+
20
+ await this.makeRequiredImplDirectories()
21
+
22
+ const automodule = this.ImplAutomodule()
23
+ await automodule.run()
24
+ }
25
+
26
+ private async promptForAutomodule() {
27
+ return await this.prompts([
28
+ {
29
+ type: 'text',
30
+ name: 'interfaceName',
31
+ message: this.interfaceNameMessage,
32
+ },
33
+ {
34
+ type: 'text',
35
+ name: 'implName',
36
+ message: this.implNameMessage,
37
+ },
38
+ ])
39
+ }
40
+
41
+ private readonly interfaceNameMessage =
42
+ 'What should the interface be called? Example: YourInterface'
43
+
44
+ private readonly implNameMessage =
45
+ 'What should the implementation class be called? Example: YourInterfaceImpl'
46
+
47
+ private get userInputExistsForCreateImpl() {
48
+ return this.interfaceName && this.implName
49
+ }
50
+
51
+ private async makeRequiredImplDirectories() {
52
+ await this.mkdir(this.implTestSaveDir, { recursive: true })
53
+ await this.mkdir(this.implModuleSaveDir, { recursive: true })
54
+ await this.mkdir(this.implFakeSaveDir, { recursive: true })
55
+ }
56
+
57
+ private readonly implTestSaveDir = 'src/__tests__/modules'
58
+ private readonly implModuleSaveDir = 'src/modules'
59
+
60
+ private get implFakeSaveDir() {
61
+ return `src/testDoubles/${this.interfaceName}`
62
+ }
63
+
64
+ private get mkdir() {
65
+ return CliCommandRunner.mkdir
66
+ }
67
+
68
+ private get prompts() {
69
+ return CliCommandRunner.prompts
70
+ }
71
+
72
+ private ImplAutomodule() {
73
+ return ImplAutomodule.Create({
74
+ testSaveDir: this.implTestSaveDir,
75
+ moduleSaveDir: this.implModuleSaveDir,
76
+ fakeSaveDir: this.implFakeSaveDir,
77
+ interfaceName: this.interfaceName,
78
+ implName: this.implName,
79
+ })
80
+ }
81
+ }