Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
140 changes: 101 additions & 39 deletions src/code_transformer/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
83 changes: 83 additions & 0 deletions tests/code_transformer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'/)
})