From 95f0cfa47336b200019b0dd8043d424a2fa1b3fa Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Sat, 17 Feb 2024 16:09:24 +0100 Subject: [PATCH 1/2] feat: add `addVitePlugin` method --- src/code_transformer/main.ts | 140 ++++++++++++++++++++++++--------- tests/code_transformer.spec.ts | 83 +++++++++++++++++++ 2 files changed, 184 insertions(+), 39 deletions(-) diff --git a/src/code_transformer/main.ts b/src/code_transformer/main.ts index 44f02958..ddcc2c2f 100644 --- a/src/code_transformer/main.ts +++ b/src/code_transformer/main.ts @@ -158,6 +158,54 @@ export class CodeTransformer { } } + /** + * Add the given import declarations to the source file + * and merge named imports with the existing import + */ + #addImportDeclarations( + file: SourceFile, + importDeclarations: { isNamed: boolean; module: string; identifier: string }[] + ) { + const existingImports = file.getImportDeclarations() + + importDeclarations.forEach((importDeclaration) => { + const existingImport = existingImports.find( + (mod) => mod.getModuleSpecifierValue() === importDeclaration.module + ) + + /** + * Add a new named import to existing import for the + * same module + */ + if (existingImport && importDeclaration.isNamed) { + if ( + !existingImport + .getNamedImports() + .find((namedImport) => namedImport.getName() === importDeclaration.identifier) + ) { + existingImport.addNamedImport(importDeclaration.identifier) + } + return + } + + /** + * Ignore default import when the same module is already imported. + * The chances are the existing default import and the importDeclaration + * identifiers are not the same. But we should not modify existing source + */ + if (existingImport) { + return + } + + file.addImportDeclaration({ + ...(importDeclaration.isNamed + ? { namedImports: [importDeclaration.identifier] } + : { defaultImport: importDeclaration.identifier }), + moduleSpecifier: importDeclaration.module, + }) + }) + } + /** * Write a leading comment */ @@ -297,46 +345,9 @@ export class CodeTransformer { const file = this.#project.getSourceFileOrThrow(testBootstrapUrl) /** - * Add the import declaration + * Add the import declarations */ - const existingImports = file.getImportDeclarations() - - importDeclarations.forEach((importDeclaration) => { - const existingImport = existingImports.find( - (mod) => mod.getModuleSpecifierValue() === importDeclaration.module - ) - - /** - * Add a new named import to existing import for the - * same module - */ - if (existingImport && importDeclaration.isNamed) { - if ( - !existingImport - .getNamedImports() - .find((namedImport) => namedImport.getName() === importDeclaration.identifier) - ) { - existingImport.addNamedImport(importDeclaration.identifier) - } - return - } - - /** - * Ignore default import when the same module is already imported. - * The chances are the existing default import and the importDeclaration - * identifiers are not the same. But we should not modify existing source - */ - if (existingImport) { - return - } - - file.addImportDeclaration({ - ...(importDeclaration.isNamed - ? { namedImports: [importDeclaration.identifier] } - : { defaultImport: importDeclaration.identifier }), - moduleSpecifier: importDeclaration.module, - }) - }) + this.#addImportDeclarations(file, importDeclarations) /** * Insert the plugin call in the `plugins` array @@ -358,6 +369,57 @@ export class CodeTransformer { await file.save() } + /** + * Add a new Vite plugin + */ + async addVitePlugin( + pluginCall: string, + importDeclarations: { isNamed: boolean; module: string; identifier: string }[] + ) { + /** + * Get the `vite.config.ts` source file + */ + const viteConfigTsUrl = fileURLToPath(new URL('./vite.config.ts', this.#cwd)) + + const file = this.#project.getSourceFile(viteConfigTsUrl) + if (!file) { + throw new Error( + 'Cannot find vite.config.ts file. Make sure to rename vite.config.js to vite.config.ts' + ) + } + + /** + * Add the import declarations + */ + this.#addImportDeclarations(file, importDeclarations) + + /** + * Get the default export options + */ + const defaultExport = file.getDefaultExportSymbol() + if (!defaultExport) { + throw new Error('Cannot find the default export in vite.config.ts') + } + + const options = defaultExport + .getDeclarations()[0] + .getChildrenOfKind(SyntaxKind.ObjectLiteralExpression)[0] + + const pluginsArray = options + .getPropertyOrThrow('plugins') + .getFirstChildByKindOrThrow(SyntaxKind.ArrayLiteralExpression) + + /** + * Add plugin call to the plugins array + */ + if (!pluginsArray.getElements().find((element) => element.getText() === pluginCall)) { + pluginsArray.addElement(pluginCall) + } + + file.formatText(this.#editorSettings) + await file.save() + } + /** * Adds a policy to the list of `policies` object configured * inside the `app/policies/main.ts` file. diff --git a/tests/code_transformer.spec.ts b/tests/code_transformer.spec.ts index 35b73567..38edb9ad 100644 --- a/tests/code_transformer.spec.ts +++ b/tests/code_transformer.spec.ts @@ -779,3 +779,86 @@ test.group('Code transformer | addPolicies', (group) => { ]) }).throws(/Expected to find an initializer of kind \'ObjectLiteralExpression\'./) }) + +test.group('Code transformer | addVitePlugin', (group) => { + group.each.setup(async ({ context }) => setupFakeAdonisproject(context.fs)) + + test('add vite plugin to vite.config.ts file', async ({ assert, fs }) => { + await fs.create( + 'vite.config.ts', + `export default { + plugins: [], + }` + ) + + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addVitePlugin('vue({ foo: 32 })', [ + { identifier: 'vue', module: 'vue', isNamed: false }, + { identifier: 'foo', module: 'foo', isNamed: true }, + ]) + + const file = await fs.contents('vite.config.ts') + assert.snapshot(file).matchInline(` + "import vue from 'vue' + import { foo } from 'foo' + + export default { + plugins: [vue({ foo: 32 })], + } + " + `) + }) + + test('ignore duplicates when adding vite plugin', async ({ assert, fs }) => { + await fs.create( + 'vite.config.ts', + `export default { + plugins: [], + }` + ) + + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addVitePlugin('vue({ foo: 32 })', [ + { identifier: 'vue', module: 'vue', isNamed: false }, + { identifier: 'foo', module: 'foo', isNamed: true }, + ]) + + await transformer.addVitePlugin('vue({ foo: 32 })', [ + { identifier: 'vue', module: 'vue', isNamed: false }, + { identifier: 'foo', module: 'foo', isNamed: true }, + ]) + + const file = await fs.contents('vite.config.ts') + assert.snapshot(file).matchInline(` + "import vue from 'vue' + import { foo } from 'foo' + + export default { + plugins: [vue({ foo: 32 })], + } + " + `) + }) + + test('throw error when vite.config.ts file is missing', async ({ fs }) => { + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addVitePlugin('vue()', [{ identifier: 'vue', module: 'vue', isNamed: false }]) + }).throws(/Cannot find vite\.config\.ts file/) + + test('throw if no default export found', async ({ fs }) => { + await fs.create('vite.config.ts', `export const plugins = []`) + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addVitePlugin('vue()', [{ identifier: 'vue', module: 'vue', isNamed: false }]) + }).throws(/Cannot find the default export/) + + test('throw if plugins property is not found', async ({ fs }) => { + await fs.create('vite.config.ts', `export default {}`) + const transformer = new CodeTransformer(fs.baseUrl) + + await transformer.addVitePlugin('vue()', [{ identifier: 'vue', module: 'vue', isNamed: false }]) + }).throws(/Expected to find property named 'plugins'/) +}) From db8b35a07c066e1f00be3cbb13570b9218d0cd5d Mon Sep 17 00:00:00 2001 From: Julien Ripouteau Date: Sat, 17 Feb 2024 17:17:14 +0100 Subject: [PATCH 2/2] chore: update readme --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index 92fc27b7..0de7e35d 100644 --- a/README.md +++ b/README.md @@ -390,6 +390,45 @@ export const plugins: Config['plugins'] = [ ] ``` +### addVitePlugin + +Register a Vite plugin to the `vite.config.ts` file. + +> [!IMPORTANT] +> This codemod expects the `vite.config.ts` file to exist and must have the `export default defineConfig` function call. + +```ts +const transformer = new CodeTransformer(appRoot) +const imports = [ + { + isNamed: false, + module: '@vitejs/plugin-vue', + identifier: 'vue' + }, +] +const pluginUsage = 'vue({ jsx: true })' + +try { + await transformer.addVitePlugin(pluginUsage, imports) +} catch (error) { + console.error('Unable to register vite plugin') + console.error(error) +} +``` + +Output + +```ts +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [ + vue({ jsx: true }) + ] +}) +``` + ### addPolicies Register AdonisJS bouncer policies to the list of `policies` object exported from the `app/policies/main.ts` file.