Skip to content

Commit f7f314a

Browse files
authored
Merge pull request #73 from adonisjs/feat/add-vite-plugin
feat: add `addVitePlugin` method
2 parents f6bb870 + db8b35a commit f7f314a

File tree

3 files changed

+223
-39
lines changed

3 files changed

+223
-39
lines changed

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,45 @@ export const plugins: Config['plugins'] = [
390390
]
391391
```
392392

393+
### addVitePlugin
394+
395+
Register a Vite plugin to the `vite.config.ts` file.
396+
397+
> [!IMPORTANT]
398+
> This codemod expects the `vite.config.ts` file to exist and must have the `export default defineConfig` function call.
399+
400+
```ts
401+
const transformer = new CodeTransformer(appRoot)
402+
const imports = [
403+
{
404+
isNamed: false,
405+
module: '@vitejs/plugin-vue',
406+
identifier: 'vue'
407+
},
408+
]
409+
const pluginUsage = 'vue({ jsx: true })'
410+
411+
try {
412+
await transformer.addVitePlugin(pluginUsage, imports)
413+
} catch (error) {
414+
console.error('Unable to register vite plugin')
415+
console.error(error)
416+
}
417+
```
418+
419+
Output
420+
421+
```ts
422+
import { defineConfig } from 'vite'
423+
import vue from '@vitejs/plugin-vue'
424+
425+
export default defineConfig({
426+
plugins: [
427+
vue({ jsx: true })
428+
]
429+
})
430+
```
431+
393432
### addPolicies
394433
Register AdonisJS bouncer policies to the list of `policies` object exported from the `app/policies/main.ts` file.
395434

src/code_transformer/main.ts

Lines changed: 101 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,54 @@ export class CodeTransformer {
158158
}
159159
}
160160

161+
/**
162+
* Add the given import declarations to the source file
163+
* and merge named imports with the existing import
164+
*/
165+
#addImportDeclarations(
166+
file: SourceFile,
167+
importDeclarations: { isNamed: boolean; module: string; identifier: string }[]
168+
) {
169+
const existingImports = file.getImportDeclarations()
170+
171+
importDeclarations.forEach((importDeclaration) => {
172+
const existingImport = existingImports.find(
173+
(mod) => mod.getModuleSpecifierValue() === importDeclaration.module
174+
)
175+
176+
/**
177+
* Add a new named import to existing import for the
178+
* same module
179+
*/
180+
if (existingImport && importDeclaration.isNamed) {
181+
if (
182+
!existingImport
183+
.getNamedImports()
184+
.find((namedImport) => namedImport.getName() === importDeclaration.identifier)
185+
) {
186+
existingImport.addNamedImport(importDeclaration.identifier)
187+
}
188+
return
189+
}
190+
191+
/**
192+
* Ignore default import when the same module is already imported.
193+
* The chances are the existing default import and the importDeclaration
194+
* identifiers are not the same. But we should not modify existing source
195+
*/
196+
if (existingImport) {
197+
return
198+
}
199+
200+
file.addImportDeclaration({
201+
...(importDeclaration.isNamed
202+
? { namedImports: [importDeclaration.identifier] }
203+
: { defaultImport: importDeclaration.identifier }),
204+
moduleSpecifier: importDeclaration.module,
205+
})
206+
})
207+
}
208+
161209
/**
162210
* Write a leading comment
163211
*/
@@ -297,46 +345,9 @@ export class CodeTransformer {
297345
const file = this.#project.getSourceFileOrThrow(testBootstrapUrl)
298346

299347
/**
300-
* Add the import declaration
348+
* Add the import declarations
301349
*/
302-
const existingImports = file.getImportDeclarations()
303-
304-
importDeclarations.forEach((importDeclaration) => {
305-
const existingImport = existingImports.find(
306-
(mod) => mod.getModuleSpecifierValue() === importDeclaration.module
307-
)
308-
309-
/**
310-
* Add a new named import to existing import for the
311-
* same module
312-
*/
313-
if (existingImport && importDeclaration.isNamed) {
314-
if (
315-
!existingImport
316-
.getNamedImports()
317-
.find((namedImport) => namedImport.getName() === importDeclaration.identifier)
318-
) {
319-
existingImport.addNamedImport(importDeclaration.identifier)
320-
}
321-
return
322-
}
323-
324-
/**
325-
* Ignore default import when the same module is already imported.
326-
* The chances are the existing default import and the importDeclaration
327-
* identifiers are not the same. But we should not modify existing source
328-
*/
329-
if (existingImport) {
330-
return
331-
}
332-
333-
file.addImportDeclaration({
334-
...(importDeclaration.isNamed
335-
? { namedImports: [importDeclaration.identifier] }
336-
: { defaultImport: importDeclaration.identifier }),
337-
moduleSpecifier: importDeclaration.module,
338-
})
339-
})
350+
this.#addImportDeclarations(file, importDeclarations)
340351

341352
/**
342353
* Insert the plugin call in the `plugins` array
@@ -358,6 +369,57 @@ export class CodeTransformer {
358369
await file.save()
359370
}
360371

372+
/**
373+
* Add a new Vite plugin
374+
*/
375+
async addVitePlugin(
376+
pluginCall: string,
377+
importDeclarations: { isNamed: boolean; module: string; identifier: string }[]
378+
) {
379+
/**
380+
* Get the `vite.config.ts` source file
381+
*/
382+
const viteConfigTsUrl = fileURLToPath(new URL('./vite.config.ts', this.#cwd))
383+
384+
const file = this.#project.getSourceFile(viteConfigTsUrl)
385+
if (!file) {
386+
throw new Error(
387+
'Cannot find vite.config.ts file. Make sure to rename vite.config.js to vite.config.ts'
388+
)
389+
}
390+
391+
/**
392+
* Add the import declarations
393+
*/
394+
this.#addImportDeclarations(file, importDeclarations)
395+
396+
/**
397+
* Get the default export options
398+
*/
399+
const defaultExport = file.getDefaultExportSymbol()
400+
if (!defaultExport) {
401+
throw new Error('Cannot find the default export in vite.config.ts')
402+
}
403+
404+
const options = defaultExport
405+
.getDeclarations()[0]
406+
.getChildrenOfKind(SyntaxKind.ObjectLiteralExpression)[0]
407+
408+
const pluginsArray = options
409+
.getPropertyOrThrow('plugins')
410+
.getFirstChildByKindOrThrow(SyntaxKind.ArrayLiteralExpression)
411+
412+
/**
413+
* Add plugin call to the plugins array
414+
*/
415+
if (!pluginsArray.getElements().find((element) => element.getText() === pluginCall)) {
416+
pluginsArray.addElement(pluginCall)
417+
}
418+
419+
file.formatText(this.#editorSettings)
420+
await file.save()
421+
}
422+
361423
/**
362424
* Adds a policy to the list of `policies` object configured
363425
* inside the `app/policies/main.ts` file.

tests/code_transformer.spec.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,3 +779,86 @@ test.group('Code transformer | addPolicies', (group) => {
779779
])
780780
}).throws(/Expected to find an initializer of kind \'ObjectLiteralExpression\'./)
781781
})
782+
783+
test.group('Code transformer | addVitePlugin', (group) => {
784+
group.each.setup(async ({ context }) => setupFakeAdonisproject(context.fs))
785+
786+
test('add vite plugin to vite.config.ts file', async ({ assert, fs }) => {
787+
await fs.create(
788+
'vite.config.ts',
789+
`export default {
790+
plugins: [],
791+
}`
792+
)
793+
794+
const transformer = new CodeTransformer(fs.baseUrl)
795+
796+
await transformer.addVitePlugin('vue({ foo: 32 })', [
797+
{ identifier: 'vue', module: 'vue', isNamed: false },
798+
{ identifier: 'foo', module: 'foo', isNamed: true },
799+
])
800+
801+
const file = await fs.contents('vite.config.ts')
802+
assert.snapshot(file).matchInline(`
803+
"import vue from 'vue'
804+
import { foo } from 'foo'
805+
806+
export default {
807+
plugins: [vue({ foo: 32 })],
808+
}
809+
"
810+
`)
811+
})
812+
813+
test('ignore duplicates when adding vite plugin', async ({ assert, fs }) => {
814+
await fs.create(
815+
'vite.config.ts',
816+
`export default {
817+
plugins: [],
818+
}`
819+
)
820+
821+
const transformer = new CodeTransformer(fs.baseUrl)
822+
823+
await transformer.addVitePlugin('vue({ foo: 32 })', [
824+
{ identifier: 'vue', module: 'vue', isNamed: false },
825+
{ identifier: 'foo', module: 'foo', isNamed: true },
826+
])
827+
828+
await transformer.addVitePlugin('vue({ foo: 32 })', [
829+
{ identifier: 'vue', module: 'vue', isNamed: false },
830+
{ identifier: 'foo', module: 'foo', isNamed: true },
831+
])
832+
833+
const file = await fs.contents('vite.config.ts')
834+
assert.snapshot(file).matchInline(`
835+
"import vue from 'vue'
836+
import { foo } from 'foo'
837+
838+
export default {
839+
plugins: [vue({ foo: 32 })],
840+
}
841+
"
842+
`)
843+
})
844+
845+
test('throw error when vite.config.ts file is missing', async ({ fs }) => {
846+
const transformer = new CodeTransformer(fs.baseUrl)
847+
848+
await transformer.addVitePlugin('vue()', [{ identifier: 'vue', module: 'vue', isNamed: false }])
849+
}).throws(/Cannot find vite\.config\.ts file/)
850+
851+
test('throw if no default export found', async ({ fs }) => {
852+
await fs.create('vite.config.ts', `export const plugins = []`)
853+
const transformer = new CodeTransformer(fs.baseUrl)
854+
855+
await transformer.addVitePlugin('vue()', [{ identifier: 'vue', module: 'vue', isNamed: false }])
856+
}).throws(/Cannot find the default export/)
857+
858+
test('throw if plugins property is not found', async ({ fs }) => {
859+
await fs.create('vite.config.ts', `export default {}`)
860+
const transformer = new CodeTransformer(fs.baseUrl)
861+
862+
await transformer.addVitePlugin('vue()', [{ identifier: 'vue', module: 'vue', isNamed: false }])
863+
}).throws(/Expected to find property named 'plugins'/)
864+
})

0 commit comments

Comments
 (0)