@aerogel/cli 0.0.0-next.0dddfa5a33f1886a059ceb0950867ddcfa86480d → 0.0.0-next.1cf8c4f815a37b577f7a1c1e2538b0d08f4e585c

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 (63) hide show
  1. package/dist/aerogel-cli.js +292 -349
  2. package/dist/aerogel-cli.js.map +1 -1
  3. package/package.json +3 -2
  4. package/src/cli.ts +0 -2
  5. package/src/commands/create.test.ts +0 -23
  6. package/src/commands/create.ts +4 -8
  7. package/src/commands/generate-component.test.ts +0 -60
  8. package/src/commands/generate-component.ts +3 -79
  9. package/src/commands/install.test.ts +69 -6
  10. package/src/commands/install.ts +21 -6
  11. package/src/lib/App.ts +40 -41
  12. package/src/lib/Editor.ts +0 -1
  13. package/src/lib/File.mock.ts +6 -0
  14. package/src/lib/File.ts +32 -2
  15. package/src/lib/Shell.mock.ts +4 -0
  16. package/src/lib/utils/paths.ts +4 -0
  17. package/src/plugins/LocalFirst.ts +9 -0
  18. package/src/plugins/Plugin.ts +30 -32
  19. package/src/plugins/Solid.ts +8 -15
  20. package/src/plugins/Soukai.ts +6 -5
  21. package/src/testing/setup.ts +6 -0
  22. package/src/utils/package.ts +23 -0
  23. package/src/commands/generate-overrides.ts +0 -85
  24. package/templates/app/.github/workflows/ci.yml +0 -29
  25. package/templates/app/.gitignore.template +0 -2
  26. package/templates/app/.nvmrc +0 -1
  27. package/templates/app/.vscode/launch.json +0 -17
  28. package/templates/app/.vscode/settings.json +0 -10
  29. package/templates/app/cypress/cypress.config.ts +0 -14
  30. package/templates/app/cypress/e2e/app.cy.ts +0 -9
  31. package/templates/app/cypress/support/e2e.ts +0 -1
  32. package/templates/app/cypress/tsconfig.json +0 -11
  33. package/templates/app/index.html +0 -13
  34. package/templates/app/package.json +0 -66
  35. package/templates/app/postcss.config.js +0 -6
  36. package/templates/app/src/App.vue +0 -12
  37. package/templates/app/src/assets/css/main.css +0 -3
  38. package/templates/app/src/assets/public/robots.txt +0 -2
  39. package/templates/app/src/lang/en.yaml +0 -3
  40. package/templates/app/src/main.test.ts +0 -9
  41. package/templates/app/src/main.ts +0 -13
  42. package/templates/app/src/types/globals.d.ts +0 -2
  43. package/templates/app/src/types/shims.d.ts +0 -7
  44. package/templates/app/src/types/ts-reset.d.ts +0 -1
  45. package/templates/app/tailwind.config.js +0 -5
  46. package/templates/app/tsconfig.json +0 -12
  47. package/templates/app/vite.config.ts +0 -31
  48. package/templates/component-button/[component.name].vue +0 -42
  49. package/templates/component-button-story/[component.name].story.vue +0 -77
  50. package/templates/component-checkbox/[component.name].vue +0 -34
  51. package/templates/component-checkbox-story/[component.name].story.vue +0 -63
  52. package/templates/component-input/[component.name].vue +0 -17
  53. package/templates/component-input-story/[component.name].story.vue +0 -63
  54. package/templates/component-story/[component.name].story.vue +0 -7
  55. package/templates/overrides/components/index.ts +0 -15
  56. package/templates/overrides/components/overrides/AlertModal.vue +0 -11
  57. package/templates/overrides/components/overrides/ConfirmModal.vue +0 -20
  58. package/templates/overrides/components/overrides/ErrorReportModal.vue +0 -35
  59. package/templates/overrides/components/overrides/LoadingModal.vue +0 -12
  60. package/templates/overrides/components/overrides/ModalWrapper.vue +0 -22
  61. package/templates/overrides/components/overrides/SnackbarNotification.vue +0 -34
  62. package/templates/overrides-story/Overrides.story.vue +0 -86
  63. package/templates/postcss-pseudo-classes/postcss.config.js +0 -15
@@ -1,4 +1,4 @@
1
- import { arrayFilter, arrayFrom, stringToSlug } from '@noeldemartin/utils';
1
+ import { arrayFrom, stringToSlug } from '@noeldemartin/utils';
2
2
  import { Node, SyntaxKind } from 'ts-morph';
3
3
  import type { ArrayLiteralExpression, CallExpression, SourceFile } from 'ts-morph';
4
4
 
@@ -9,7 +9,6 @@ import Template from '@aerogel/cli/lib/Template';
9
9
  import { app } from '@aerogel/cli/lib/utils/app';
10
10
  import { editFiles, findDescendant } from '@aerogel/cli/lib/utils/edit';
11
11
  import { templatePath } from '@aerogel/cli/lib/utils/paths';
12
- import type { CommandOptions } from '@aerogel/cli/commands/Command';
13
12
 
14
13
  export interface Options {
15
14
  button?: boolean;
@@ -26,52 +25,21 @@ export class GenerateComponentCommand extends Command {
26
25
  ['path', 'Component path (relative to components folder; extension not necessary)'],
27
26
  ];
28
27
 
29
- protected static override options: CommandOptions = {
30
- button: {
31
- description: 'Create a custom button',
32
- type: 'boolean',
33
- },
34
- checkbox: {
35
- description: 'Create a custom checkbox',
36
- type: 'boolean',
37
- },
38
- input: {
39
- description: 'Create a custom input',
40
- type: 'boolean',
41
- },
42
- story: {
43
- description: 'Create component story using Histoire',
44
- type: 'boolean',
45
- },
46
- };
47
-
48
28
  private path: string;
49
- private options: Options;
50
29
 
51
- constructor(path: string, options: Options = {}) {
30
+ constructor(path: string) {
52
31
  super();
53
32
 
54
33
  this.path = path;
55
- this.options = options;
56
- }
57
-
58
- protected override async validate(): Promise<void> {
59
- const components = arrayFilter([this.options.button, this.options.input, this.options.checkbox]).length;
60
-
61
- if (components > 1) {
62
- Log.fail('Can only use one of \'button\', \'input\', or \'checkbox\' flags!');
63
- }
64
34
  }
65
35
 
66
36
  protected override async run(): Promise<void> {
67
37
  this.assertAerogelOrDirectory('src/components');
68
- this.assertHistoireInstalled();
69
38
 
70
39
  const files = new Set<string>();
71
40
  const [directoryName, componentName] = this.parsePathComponents();
72
41
 
73
42
  await this.createComponent(directoryName, componentName, files);
74
- await this.createStory(directoryName, componentName, files);
75
43
  await this.declareComponents();
76
44
 
77
45
  const filesList = arrayFrom(files)
@@ -81,33 +49,13 @@ export class GenerateComponentCommand extends Command {
81
49
  Log.info(`${componentName} component created successfully! The following files were created:\n\n${filesList}`);
82
50
  }
83
51
 
84
- protected assertHistoireInstalled(): void {
85
- if (!this.options.story) {
86
- return;
87
- }
88
-
89
- if (!File.contains('package.json', '"histoire"') && !File.contains('package.json', '"@aerogel/histoire"')) {
90
- Log.fail(`
91
- Histoire is not installed yet! You can install it running:
92
- npx gel install histoire
93
- `);
94
- }
95
- }
96
-
97
52
  protected async createComponent(directoryName: string, componentName: string, files: Set<string>): Promise<void> {
98
53
  await Log.animate('Creating component', async () => {
99
54
  if (File.exists(`src/components/${this.path}.vue`)) {
100
55
  Log.fail(`${this.path} component already exists!`);
101
56
  }
102
57
 
103
- const templateName = this.options.input
104
- ? 'component-input'
105
- : this.options.button
106
- ? 'component-button'
107
- : this.options.checkbox
108
- ? 'component-checkbox'
109
- : 'component';
110
- const componentFiles = Template.instantiate(templatePath(templateName), `src/components/${directoryName}`, {
58
+ const componentFiles = Template.instantiate(templatePath('component'), `src/components/${directoryName}`, {
111
59
  component: {
112
60
  name: componentName,
113
61
  slug: stringToSlug(componentName),
@@ -118,30 +66,6 @@ export class GenerateComponentCommand extends Command {
118
66
  });
119
67
  }
120
68
 
121
- protected async createStory(directoryName: string, componentName: string, files: Set<string>): Promise<void> {
122
- if (!this.options.story) {
123
- return;
124
- }
125
-
126
- await Log.animate('Creating story', async () => {
127
- const templateName = this.options.input
128
- ? 'component-input-story'
129
- : this.options.button
130
- ? 'component-button-story'
131
- : this.options.checkbox
132
- ? 'component-checkbox-story'
133
- : 'component-story';
134
- const storyFiles = Template.instantiate(templatePath(templateName), `src/components/${directoryName}`, {
135
- component: {
136
- name: componentName,
137
- slug: stringToSlug(componentName),
138
- },
139
- });
140
-
141
- storyFiles.forEach((file) => files.add(file));
142
- });
143
- }
144
-
145
69
  protected async declareComponents(): Promise<void> {
146
70
  if (!editFiles()) {
147
71
  return;
@@ -1,27 +1,90 @@
1
- import { describe, it } from 'vitest';
1
+ import { describe, expect, it } from 'vitest';
2
2
 
3
3
  import ShellMock from '@aerogel/cli/lib/Shell.mock';
4
+ import FileMock from '@aerogel/cli/lib/File.mock';
4
5
 
5
6
  import { InstallCommand } from './install';
6
7
 
7
8
  describe('Install plugin command', () => {
8
9
 
9
10
  it('installs solid', async () => {
11
+ // Arrange
12
+ stubPackageJson();
13
+
10
14
  // Act
11
15
  await InstallCommand.run('solid');
12
16
 
13
17
  // Assert
14
- ShellMock.expectRan('npm install soukai-solid@next --save-exact');
15
- ShellMock.expectRan('npm install @aerogel/plugin-solid@next --save-exact');
18
+ ShellMock.expectRan('pnpm install --no-save');
19
+
20
+ expectPackageJson({
21
+ dependencies: {
22
+ '@aerogel/core': 'next',
23
+ '@aerogel/plugin-solid': 'next',
24
+ '@noeldemartin/solid-utils': 'next',
25
+ 'soukai-solid': 'next',
26
+ 'vue': '^3.5.13',
27
+ },
28
+ });
16
29
  });
17
30
 
18
31
  it('installs soukai', async () => {
32
+ // Arrange
33
+ stubPackageJson();
34
+
19
35
  // Act
20
- await InstallCommand.run('soukai');
36
+ await InstallCommand.run('soukai', { skipInstall: true });
21
37
 
22
38
  // Assert
23
- ShellMock.expectRan('npm install soukai@next --save-exact');
24
- ShellMock.expectRan('npm install @aerogel/plugin-soukai@next --save-exact');
39
+ ShellMock.expectNotRan('pnpm install --no-save');
40
+
41
+ expectPackageJson({
42
+ dependencies: {
43
+ '@aerogel/core': 'next',
44
+ '@aerogel/plugin-soukai': 'next',
45
+ 'soukai': 'next',
46
+ 'vue': '^3.5.13',
47
+ },
48
+ });
49
+ });
50
+
51
+ it('installs local-first', async () => {
52
+ // Arrange
53
+ stubPackageJson();
54
+
55
+ // Act
56
+ await InstallCommand.run('local-first');
57
+
58
+ // Assert
59
+ ShellMock.expectRan('pnpm install --no-save');
60
+
61
+ expectPackageJson({
62
+ dependencies: {
63
+ '@aerogel/core': 'next',
64
+ '@aerogel/plugin-local-first': 'next',
65
+ 'vue': '^3.5.13',
66
+ },
67
+ });
25
68
  });
26
69
 
27
70
  });
71
+
72
+ function stubPackageJson(): void {
73
+ FileMock.stub(
74
+ 'package.json',
75
+ JSON.stringify(
76
+ {
77
+ dependencies: {
78
+ '@aerogel/core': 'next',
79
+ 'vue': '^3.5.13',
80
+ },
81
+ },
82
+ null,
83
+ 2,
84
+ ),
85
+ );
86
+ }
87
+
88
+ function expectPackageJson(packageJson: object): void {
89
+ expect(JSON.parse(FileMock.read('package.json') ?? '{}')).toEqual(packageJson);
90
+ }
@@ -1,32 +1,47 @@
1
1
  import Command from '@aerogel/cli/commands/Command';
2
+ import LocalFirst from '@aerogel/cli/plugins/LocalFirst';
2
3
  import Log from '@aerogel/cli/lib/Log';
3
- import { Solid } from '@aerogel/cli/plugins/Solid';
4
- import { Soukai } from '@aerogel/cli/plugins/Soukai';
4
+ import Solid from '@aerogel/cli/plugins/Solid';
5
+ import Soukai from '@aerogel/cli/plugins/Soukai';
5
6
  import type Plugin from '@aerogel/cli/plugins/Plugin';
7
+ import type { CommandOptions } from '@aerogel/cli/commands/Command';
6
8
 
7
- const plugins = [new Soukai(), new Solid()].reduce(
9
+ const plugins = [new Soukai(), new Solid(), new LocalFirst()].reduce(
8
10
  (pluginsObject, plugin) => Object.assign(pluginsObject, { [plugin.name]: plugin }),
9
11
  {} as Record<string, Plugin>,
10
12
  );
13
+ export interface Options {
14
+ skipInstall?: boolean;
15
+ }
11
16
 
12
17
  export class InstallCommand extends Command {
13
18
 
14
19
  protected static override command: string = 'install';
15
20
  protected static override description: string = 'Install an AerogelJS plugin';
16
21
  protected static override parameters: [string, string][] = [['plugin', 'Plugin to install']];
17
-
22
+ protected static override options: CommandOptions = {
23
+ skipInstall: {
24
+ type: 'boolean',
25
+ description: 'Skip installing dependencies, just add them to package.json',
26
+ },
27
+ };
28
+
29
+ private options: Options;
18
30
  private plugin: Plugin;
19
31
 
20
- constructor(plugin: string) {
32
+ constructor(plugin: string, options: Options = {}) {
21
33
  super();
22
34
 
35
+ this.options = options;
23
36
  this.plugin =
24
37
  plugins[plugin] ??
25
38
  Log.fail(`Plugin '${plugin}' doesn't exist. Available plugins: ${Object.keys(plugins).join(', ')}`);
26
39
  }
27
40
 
28
41
  protected override async run(): Promise<void> {
29
- await this.plugin.install();
42
+ await this.plugin.install({
43
+ skipInstall: this.options.skipInstall,
44
+ });
30
45
  }
31
46
 
32
47
  }
package/src/lib/App.ts CHANGED
@@ -1,19 +1,11 @@
1
- import { stringToSlug } from '@noeldemartin/utils';
1
+ import { resolve } from 'node:path';
2
2
 
3
3
  import File from '@aerogel/cli/lib/File';
4
4
  import Log from '@aerogel/cli/lib/Log';
5
5
  import Template from '@aerogel/cli/lib/Template';
6
6
  import { packNotFound, packagePackPath, packagePath, templatePath } from '@aerogel/cli/lib/utils/paths';
7
7
  import { Editor } from '@aerogel/cli/lib/Editor';
8
-
9
- interface Dependencies {
10
- aerogelCli: string;
11
- aerogelCore: string;
12
- aerogelCypress: string;
13
- aerogelPluginI18n: string;
14
- aerogelPluginSoukai: string;
15
- aerogelVite: string;
16
- }
8
+ import { simpleGit } from 'simple-git';
17
9
 
18
10
  export interface Options {
19
11
  local?: boolean;
@@ -27,28 +19,42 @@ export default class App {
27
19
  protected options: Options = {},
28
20
  ) {}
29
21
 
30
- public create(path: string): void {
22
+ public async create(path: string): Promise<void> {
31
23
  if (File.exists(path) && (!File.isDirectory(path) || !File.isEmptyDirectory(path))) {
32
24
  Log.fail(`Folder at '${path}' already exists!`);
33
25
  }
34
26
 
35
- Template.instantiate(templatePath('app'), path, {
36
- app: {
37
- name: this.name,
38
- slug: stringToSlug(this.name),
39
- },
40
- dependencies: this.getDependencies(),
41
- contentPath: this.options.linkedLocal
42
- ? `${packagePath('core')}/dist/**/*.js`
43
- : './node_modules/@aerogel/core/dist/**/*.js',
27
+ // Clone repository
28
+ await simpleGit().clone('https://github.com/NoelDeMartin/aerogel-template.git', path, {
29
+ '--depth': 1,
44
30
  });
31
+
32
+ File.delete(resolve(path, '.git'));
33
+
34
+ // Apply replacements
35
+ const dependencies = this.getDependencies();
36
+
37
+ File.replace(
38
+ resolve(path, 'vite.config.ts'),
39
+ 'Aerogel({ name: \'Aerogel\' })',
40
+ `Aerogel({ name: '${this.name}' })`,
41
+ );
42
+
43
+ File.replace(resolve(path, 'src/lang/en.yaml'), 'title: \'App\'', `title: '${this.name}'`);
44
+
45
+ for (const [name, version] of Object.entries(dependencies)) {
46
+ File.replace(resolve(path, 'package.json'), new RegExp(`"${name}": ".*?"`, 'g'), `"${name}": "${version}"`);
47
+ }
48
+
49
+ // Copy template
50
+ Template.instantiate(templatePath('app'), path, { app: { name: this.name } });
45
51
  }
46
52
 
47
53
  public edit(): Editor {
48
54
  return new Editor();
49
55
  }
50
56
 
51
- protected getDependencies(): Dependencies {
57
+ protected getDependencies(): Record<string, string> {
52
58
  const withFilePrefix = <T extends Record<string, string>>(paths: T) =>
53
59
  Object.entries(paths).reduce(
54
60
  (pathsWithFile, [name, path]) => Object.assign(pathsWithFile, { [name]: `file:${path}` }) as T,
@@ -57,34 +63,27 @@ export default class App {
57
63
 
58
64
  if (this.options.linkedLocal) {
59
65
  return withFilePrefix({
60
- aerogelCli: packagePath('cli'),
61
- aerogelCore: packagePath('core'),
62
- aerogelCypress: packagePath('cypress'),
63
- aerogelPluginI18n: packagePath('plugin-i18n'),
64
- aerogelPluginSoukai: packagePath('plugin-soukai'),
65
- aerogelVite: packagePath('vite'),
66
+ '@aerogel/cli': packagePath('cli'),
67
+ '@aerogel/core': packagePath('core'),
68
+ '@aerogel/cypress': packagePath('cypress'),
69
+ '@aerogel/plugin-i18n': packagePath('plugin-i18n'),
70
+ '@aerogel/plugin-soukai': packagePath('plugin-soukai'),
71
+ '@aerogel/vite': packagePath('vite'),
66
72
  });
67
73
  }
68
74
 
69
75
  if (this.options.local) {
70
76
  return withFilePrefix({
71
- aerogelCli: packagePackPath('cli') ?? packNotFound('cli'),
72
- aerogelCore: packagePackPath('core') ?? packNotFound('core'),
73
- aerogelCypress: packagePackPath('cypress') ?? packNotFound('cypress'),
74
- aerogelPluginI18n: packagePackPath('plugin-i18n') ?? packNotFound('plugin-i18n'),
75
- aerogelPluginSoukai: packagePackPath('plugin-soukai') ?? packNotFound('plugin-soukai'),
76
- aerogelVite: packagePackPath('vite') ?? packNotFound('vite'),
77
+ '@aerogel/cli': packagePackPath('cli') ?? packNotFound('cli'),
78
+ '@aerogel/core': packagePackPath('core') ?? packNotFound('core'),
79
+ '@aerogel/cypress': packagePackPath('cypress') ?? packNotFound('cypress'),
80
+ '@aerogel/plugin-i18n': packagePackPath('plugin-i18n') ?? packNotFound('plugin-i18n'),
81
+ '@aerogel/plugin-soukai': packagePackPath('plugin-soukai') ?? packNotFound('plugin-soukai'),
82
+ '@aerogel/vite': packagePackPath('vite') ?? packNotFound('vite'),
77
83
  });
78
84
  }
79
85
 
80
- return {
81
- aerogelCli: 'next',
82
- aerogelCore: 'next',
83
- aerogelCypress: 'next',
84
- aerogelPluginI18n: 'next',
85
- aerogelPluginSoukai: 'next',
86
- aerogelVite: 'next',
87
- };
86
+ return {};
88
87
  }
89
88
 
90
89
  }
package/src/lib/Editor.ts CHANGED
@@ -16,7 +16,6 @@ export class Editor {
16
16
  this.modifiedFiles = new Set();
17
17
 
18
18
  this.project.addSourceFilesAtPaths('src/**/*.ts');
19
- this.project.addSourceFilesAtPaths('tailwind.config.js');
20
19
  this.project.addSourceFilesAtPaths('vite.config.ts');
21
20
  this.project.addSourceFilesAtPaths('package.json');
22
21
  }
@@ -8,6 +8,12 @@ export class FileMockService extends FileService {
8
8
 
9
9
  private virtualFilesystem: Record<string, string | { directory: true }> = {};
10
10
 
11
+ public override delete(path: string): void {
12
+ if (path in this.virtualFilesystem) {
13
+ delete this.virtualFilesystem[path];
14
+ }
15
+ }
16
+
11
17
  public override exists(path: string): boolean {
12
18
  return super.exists(path) || path in this.virtualFilesystem;
13
19
  }
package/src/lib/File.ts CHANGED
@@ -1,7 +1,17 @@
1
- import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
- import { facade } from '@noeldemartin/utils';
1
+ import {
2
+ existsSync,
3
+ lstatSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ readdirSync,
7
+ rmdirSync,
8
+ unlinkSync,
9
+ writeFileSync,
10
+ } from 'node:fs';
3
11
  import { dirname, resolve } from 'node:path';
4
12
 
13
+ import { facade } from '@noeldemartin/utils';
14
+
5
15
  export class FileService {
6
16
 
7
17
  public contains(path: string, contents: string): boolean {
@@ -12,6 +22,16 @@ export class FileService {
12
22
  return existsSync(path);
13
23
  }
14
24
 
25
+ public delete(path: string): void {
26
+ if (this.isDirectory(path)) {
27
+ rmdirSync(path, { recursive: true });
28
+
29
+ return;
30
+ }
31
+
32
+ unlinkSync(path);
33
+ }
34
+
15
35
  public isSymlink(path: string): boolean {
16
36
  const stats = lstatSync(path);
17
37
 
@@ -26,6 +46,16 @@ export class FileService {
26
46
  return readFileSync(path).toString();
27
47
  }
28
48
 
49
+ public replace(path: string, search: string | RegExp, replacement: string): void {
50
+ const contents = this.read(path);
51
+
52
+ if (!contents) {
53
+ return;
54
+ }
55
+
56
+ this.write(path, contents.replaceAll(search, replacement));
57
+ }
58
+
29
59
  public getFiles(directoryPath: string): string[] {
30
60
  const children = readdirSync(directoryPath, { withFileTypes: true });
31
61
  const files: string[] = [];
@@ -15,6 +15,10 @@ export class ShellServiceMock extends ShellService {
15
15
  expect(this.history, `expected '${command}' command to have been executed`).toContain(command);
16
16
  }
17
17
 
18
+ public expectNotRan(command: string): void {
19
+ expect(this.history, `expected '${command}' command to not have been executed`).not.toContain(command);
20
+ }
21
+
18
22
  }
19
23
 
20
24
  export default facade(ShellServiceMock);
@@ -6,6 +6,10 @@ import File from '@aerogel/cli/lib/File';
6
6
  import Log from '@aerogel/cli/lib/Log';
7
7
 
8
8
  export function basePath(path: string = ''): string {
9
+ if (process.env.AEROGEL_BASE_PATH) {
10
+ return resolve(process.env.AEROGEL_BASE_PATH, path);
11
+ }
12
+
9
13
  if (
10
14
  File.contains(
11
15
  fileURLToPath(new URL(/* @vite-ignore */ '../../../package.json', import.meta.url)),
@@ -0,0 +1,9 @@
1
+ import Plugin from '@aerogel/cli/plugins/Plugin';
2
+
3
+ export default class LocalFirst extends Plugin {
4
+
5
+ constructor() {
6
+ super('local-first');
7
+ }
8
+
9
+ }
@@ -1,10 +1,12 @@
1
1
  import { Node, SyntaxKind } from 'ts-morph';
2
+ import { stringToCamelCase } from '@noeldemartin/utils';
2
3
  import type { ArrayLiteralExpression, ImportDeclarationStructure, OptionalKind, SourceFile } from 'ts-morph';
3
4
 
4
5
  import Log from '@aerogel/cli/lib/Log';
5
6
  import Shell from '@aerogel/cli/lib/Shell';
6
7
  import File from '@aerogel/cli/lib/File';
7
8
  import { app, isLinkedLocalApp, isLocalApp } from '@aerogel/cli/lib/utils/app';
9
+ import { addNpmDependency } from '@aerogel/cli/utils/package';
8
10
  import { editFiles, findDescendant, when } from '@aerogel/cli/lib/utils/edit';
9
11
  import { packNotFound, packagePackPath, packagePath } from '@aerogel/cli/lib/utils/paths';
10
12
  import type { Editor } from '@aerogel/cli/lib/Editor';
@@ -17,15 +19,21 @@ export default abstract class Plugin {
17
19
  this.name = name;
18
20
  }
19
21
 
20
- public async install(): Promise<void> {
22
+ public async install(options: { skipInstall?: boolean } = {}): Promise<void> {
21
23
  this.assertNotInstalled();
22
24
 
23
25
  await this.beforeInstall();
24
- await this.installDependencies();
26
+ await this.addDependencies();
27
+
28
+ if (!options.skipInstall) {
29
+ await this.installDependencies();
30
+ }
25
31
 
26
32
  if (editFiles()) {
27
33
  const editor = app().edit();
28
34
 
35
+ editor.addSourceFile('package.json');
36
+
29
37
  await this.updateFiles(editor);
30
38
  await editor.format();
31
39
  }
@@ -49,23 +57,32 @@ export default abstract class Plugin {
49
57
  // Placeholder for overrides, don't place any functionality here.
50
58
  }
51
59
 
60
+ protected async addDependencies(): Promise<void> {
61
+ Log.info('Adding plugin dependencies');
62
+ this.addNpmDependencies();
63
+ }
64
+
52
65
  protected async installDependencies(): Promise<void> {
53
66
  await Log.animate('Installing plugin dependencies', async () => {
54
- await this.installNpmDependencies();
67
+ await Shell.run('pnpm install --no-save');
55
68
  });
56
69
  }
57
70
 
58
71
  protected async updateFiles(editor: Editor): Promise<void> {
59
- if (!this.isForDevelopment()) {
60
- await this.updateBootstrapConfig(editor);
72
+ if (this.isForDevelopment()) {
73
+ return;
61
74
  }
62
- }
63
75
 
64
- protected async installNpmDependencies(): Promise<void> {
65
- const flags = this.isForDevelopment() ? '--save-dev' : '';
76
+ await this.updateBootstrapConfig(editor);
77
+ }
66
78
 
79
+ protected addNpmDependencies(): void {
67
80
  if (isLinkedLocalApp()) {
68
- await Shell.run(`npm install file:${packagePath(this.getLocalPackageName())} ${flags}`);
81
+ addNpmDependency(
82
+ this.getNpmPackageName(),
83
+ `file:${packagePath(this.getLocalPackageName())}`,
84
+ this.isForDevelopment(),
85
+ );
69
86
 
70
87
  return;
71
88
  }
@@ -73,12 +90,12 @@ export default abstract class Plugin {
73
90
  if (isLocalApp()) {
74
91
  const packPath = packagePackPath(this.getLocalPackageName()) ?? packNotFound(this.getLocalPackageName());
75
92
 
76
- await Shell.run(`npm install file:${packPath} ${flags}`);
93
+ addNpmDependency(this.getNpmPackageName(), `file:${packPath}`, this.isForDevelopment());
77
94
 
78
95
  return;
79
96
  }
80
97
 
81
- await Shell.run(`npm install ${this.getNpmPackageName()}@next --save-exact ${flags}`);
98
+ addNpmDependency(this.getNpmPackageName(), 'next', this.isForDevelopment());
82
99
  }
83
100
 
84
101
  protected async updateBootstrapConfig(editor: Editor): Promise<void> {
@@ -101,25 +118,6 @@ export default abstract class Plugin {
101
118
  });
102
119
  }
103
120
 
104
- protected async updateTailwindConfig(editor: Editor, options: { content: string }): Promise<void> {
105
- await Log.animate('Updating tailwind configuration', async () => {
106
- const tailwindConfig = editor.requireSourceFile('tailwind.config.js');
107
- const contentArray = this.getTailwindContentArray(tailwindConfig);
108
-
109
- if (!contentArray) {
110
- return Log.fail(`
111
- Could not find content array in tailwind config, please add the following manually:
112
-
113
- ${options.content}
114
- `);
115
- }
116
-
117
- contentArray.addElement(options.content);
118
-
119
- await editor.save(tailwindConfig);
120
- });
121
- }
122
-
123
121
  protected getBootstrapPluginsDeclaration(mainConfig: SourceFile): ArrayLiteralExpression | null {
124
122
  const bootstrapAppCall = findDescendant(mainConfig, {
125
123
  guard: Node.isCallExpression,
@@ -154,7 +152,7 @@ export default abstract class Plugin {
154
152
 
155
153
  protected getBootstrapImport(): OptionalKind<ImportDeclarationStructure> {
156
154
  return {
157
- defaultImport: this.name,
155
+ defaultImport: stringToCamelCase(this.name),
158
156
  moduleSpecifier: `@aerogel/plugin-${this.name}`,
159
157
  };
160
158
  }
@@ -172,7 +170,7 @@ export default abstract class Plugin {
172
170
  }
173
171
 
174
172
  protected getBootstrapConfig(): string {
175
- return `${this.name}()`;
173
+ return `${stringToCamelCase(this.name)}()`;
176
174
  }
177
175
 
178
176
  }