diff --git a/packages/esbuild-plugin-ast-vue/README.md b/packages/esbuild-plugin-ast-vue/README.md index a640cf1..4515c98 100644 --- a/packages/esbuild-plugin-ast-vue/README.md +++ b/packages/esbuild-plugin-ast-vue/README.md @@ -1,23 +1,24 @@ -# esbuild-plugin-ast +# esbuild-plugin-ast-vue -A plugin to generate an AST representation of your `.js` files. The plugin use [Acorn](https://github.com/acornjs/acorn) to produce an `estree` compliant `AST` object. You can then apply transformations by providing a `visitor` object. +A plugin to generate an AST representation of your `.vue` files. The plugin use [Acorn](https://github.com/acornjs/acorn) to produce an `estree` compliant `AST` object. You can then apply transformations by providing a `visitor` object. In order to also parse your `.js` dependencies imported in your scripts, you should probably use the [@liip/esbuild-plugin-ast](https://github.com/liip/class-prefixer/tree/main/packages/esbuild-plugin-ast) in conjunction with this plugin since `esbuild` does not allow plugin composition. ## Installation ``` -npm i -D @liip/esbuild-plugin-ast +npm i -D @liip/esbuild-plugin-ast-vue @liip/esbuild-plugin-ast ``` ## Usage ```javascript import { astParser } from '@liip/esbuild-plugin-ast'; +import { astParserVue } from '@liip/esbuild-plugin-ast-vue'; ... await esbuild.context({ ... - plugins: [astParser(options)], + plugins: [astParserVue(vueParserOptions), astParser(parserOptions)], ... }); @@ -31,6 +32,7 @@ You can configure the way the plugin works by setting different options. ```typescript interface AstParserVueOptions extends AstParserOptions { + scriptNamespace?: string; templateVisitor: AstParserOptions['visitor']; templateOptions?: Pick< SFCTemplateCompileOptions, @@ -52,6 +54,10 @@ interface AstParserVueOptions extends AstParserOptions { } ``` +### scriptNamespace + +A string namespace used to tell `@liip/esbuild-plugin-ast` to parse `.js` dependencies from your `.vue` files. This is required since `esbuild` does not allow plugin composition. This namespace should also be provided to `astParser` in order to work correctly. + ### visitor An `ESTraverse.Visitor` object used to apply AST transformations. Check the [Estraverse documentation](https://github.com/estools/estraverse) form more information on the available API. diff --git a/packages/esbuild-plugin-ast-vue/package.json b/packages/esbuild-plugin-ast-vue/package.json index cc6a789..fc87838 100644 --- a/packages/esbuild-plugin-ast-vue/package.json +++ b/packages/esbuild-plugin-ast-vue/package.json @@ -48,6 +48,7 @@ "hash-sum": "^2.0.0" }, "peerDependencies": { + "@liip/esbuild-plugin-ast": "0.3.x", "esbuild": "0.19.x" } } diff --git a/packages/esbuild-plugin-ast-vue/src/__tests__/plugin.spec.ts b/packages/esbuild-plugin-ast-vue/src/__tests__/plugin.spec.ts index 89696ff..77f3378 100644 --- a/packages/esbuild-plugin-ast-vue/src/__tests__/plugin.spec.ts +++ b/packages/esbuild-plugin-ast-vue/src/__tests__/plugin.spec.ts @@ -2,6 +2,7 @@ import { mkdtemp, writeFile, rm, mkdir } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { astParser, AstParserOptions } from '@liip/esbuild-plugin-ast'; import { context, BuildContext, BuildOptions, OutputFile } from 'esbuild'; import { astParserVue, AstParserVueOptions } from '../plugin'; @@ -10,20 +11,23 @@ describe('astPluginVue', () => { const placeholder = 'astPluginVue'; const argument = 'ast-plugin-vue'; const virtualPackage = 'ast-plugin-vue'; - - const pluginOptions: AstParserVueOptions = { - visitors: { - enter(node) { - if ( - node.type === 'CallExpression' && - node.callee.type === 'Identifier' && - node.callee.name === placeholder - ) { - return node.arguments[0]; - } - return node; - }, + const namespace = 'ast-parser-vue'; + + const visitors: AstParserOptions['visitors'] = { + enter(node) { + if ( + node.type === 'CallExpression' && + node.callee.type === 'Identifier' && + node.callee.name === placeholder + ) { + return node.arguments[0]; + } + return node; }, + }; + + const vuePluginOptions: AstParserVueOptions = { + visitors, templateVisitor: { enter(node) { if ( @@ -50,7 +54,6 @@ describe('astPluginVue', () => { minify: false, write: false, external: ['vue'], - plugins: [astParserVue(pluginOptions)], }; let tmpDir: string; @@ -76,12 +79,6 @@ describe('astPluginVue', () => { entryPoint, `import testCase from './test-case.vue';\ntestCase();`, ); - - ctx = await context({ - ...esbuildConfig, - entryPoints: { index: entryPoint }, - minify: true, - }); }); afterEach(async () => { @@ -93,49 +90,103 @@ describe('astPluginVue', () => { await rm(tmpDir, { recursive: true, force: true }); }); - describe('Script section', () => { - it('should correctly handle Vue dependencies containing astPlugin placeholder', async () => { - const testCase = ``; - await writeFile(testFile, testCase); - - const result = await ctx.rebuild(); - - expect((result.outputFiles as OutputFile[])[0].text).toEqual( - expect.stringContaining(argument), - ); - expect((result.outputFiles as OutputFile[])[0].text).toEqual( - expect.not.stringContaining(placeholder), - ); + describe('Standalone usage', () => { + beforeEach(async () => { + ctx = await context({ + ...esbuildConfig, + plugins: [astParserVue(vuePluginOptions)], + entryPoints: { index: entryPoint }, + minify: true, + }); }); - it('should correctly handle Vue dependencies containing astPlugin placeholder in script setup', async () => { - const testCase = ``; - await writeFile(testFile, testCase); + describe('Script section', () => { + it('should correctly handle Vue dependencies containing astPlugin placeholder', async () => { + const testCase = ``; + await writeFile(testFile, testCase); + + const result = await ctx.rebuild(); + + expect((result.outputFiles as OutputFile[])[0].text).toEqual( + expect.stringContaining(argument), + ); + expect((result.outputFiles as OutputFile[])[0].text).toEqual( + expect.not.stringContaining(placeholder), + ); + }); + + it('should correctly handle Vue dependencies containing astPlugin placeholder in script setup', async () => { + const testCase = ``; + await writeFile(testFile, testCase); + + const result = await ctx.rebuild(); + + expect((result.outputFiles as OutputFile[])[0].text).toEqual( + expect.stringContaining(argument), + ); + expect((result.outputFiles as OutputFile[])[0].text).toEqual( + expect.not.stringContaining(placeholder), + ); + }); + + describe('Template section', () => { + it('should correctly handle Vue dependencies with static class in the template', async () => { + const testCase = ``; + await writeFile(testFile, testCase); + + const result = await ctx.rebuild(); + + expect((result.outputFiles as OutputFile[])[0].text).toEqual( + expect.stringContaining(`class:"${argument}"`), + ); + expect((result.outputFiles as OutputFile[])[0].text).toEqual( + expect.not.stringContaining(placeholder), + ); + }); + }); + }); + }); - const result = await ctx.rebuild(); + describe('Combined with esbuild-plugin-ast usage', () => { + beforeEach(async () => { + const pluginOptions: AstParserOptions = { + namespace, + visitors, + }; + + ctx = await context({ + ...esbuildConfig, + plugins: [ + astParserVue({ scriptNamespace: namespace, ...vuePluginOptions }), + astParser(pluginOptions), + ], + entryPoints: { index: entryPoint }, + minify: true, + }); + }); - expect((result.outputFiles as OutputFile[])[0].text).toEqual( - expect.stringContaining(argument), + it('should correctly handle dependencies containing astPlugin placeholder in script', async () => { + const testDepFile = join( + tmpDir, + `./packages/${virtualPackage}/test-dep.js`, ); - expect((result.outputFiles as OutputFile[])[0].text).toEqual( - expect.not.stringContaining(placeholder), - ); - }); - }); - describe('Template section', () => { - it('should correctly handle Vue dependencies with static class in the template', async () => { - const testCase = ``; + const testDep = `export function test() { return ${placeholder}('${argument}'); }`; + const testCase = ``; + + await writeFile(testDepFile, testDep); await writeFile(testFile, testCase); const result = await ctx.rebuild(); expect((result.outputFiles as OutputFile[])[0].text).toEqual( - expect.stringContaining(`class:"${argument}"`), + expect.stringContaining(argument), ); expect((result.outputFiles as OutputFile[])[0].text).toEqual( expect.not.stringContaining(placeholder), ); + + await rm(testDepFile); }); }); }); diff --git a/packages/esbuild-plugin-ast-vue/src/plugin.ts b/packages/esbuild-plugin-ast-vue/src/plugin.ts index 2477e9e..c5c502e 100644 --- a/packages/esbuild-plugin-ast-vue/src/plugin.ts +++ b/packages/esbuild-plugin-ast-vue/src/plugin.ts @@ -17,7 +17,8 @@ import { resolvePath, validateDependency } from './utils'; type ExtractNonArray = T extends Array ? never : T; -export interface AstParserVueOptions extends AstParserOptions { +export interface AstParserVueOptions + extends Omit { templateOptions?: Pick< SFCTemplateCompileOptions, | 'compiler' @@ -36,8 +37,11 @@ export interface AstParserVueOptions extends AstParserOptions { | 'postcssPlugins' >; templateVisitor: ExtractNonArray; + scriptNamespace?: string; } +type ScriptResolvedReturn = { path: string; namespace?: string }; + validateDependency(); export function astParserVue({ @@ -46,6 +50,7 @@ export function astParserVue({ styleOptions, visitors, templateVisitor, + scriptNamespace, }: AstParserVueOptions): Plugin { return { name: 'astParserVue', @@ -73,9 +78,15 @@ export function astParserVue({ * them in a specific namespace */ build.onResolve({ filter: /\.vue\?type=script/ }, (args) => { - return { + const resolved: ScriptResolvedReturn = { path: args.path, }; + + if (scriptNamespace) { + resolved.namespace = scriptNamespace; + } + + return resolved; }); build.onLoad({ filter: /\.vue\?type=script/ }, (args) => { diff --git a/packages/esbuild-plugin-ast/src/plugin.ts b/packages/esbuild-plugin-ast/src/plugin.ts index ca25500..2e052bc 100644 --- a/packages/esbuild-plugin-ast/src/plugin.ts +++ b/packages/esbuild-plugin-ast/src/plugin.ts @@ -10,16 +10,17 @@ import type { Plugin, OnResolveArgs } from 'esbuild'; export interface AstParserOptions { dependencies?: string[]; visitors: Visitor | Visitor[]; + namespace?: string; } export function astParser({ dependencies, visitors, + namespace = 'ast-parser', }: AstParserOptions): Plugin { return { name: 'astParser', setup(build) { - const namespace = 'ast-parser'; const excludeFileTypes = Object.keys( build.initialOptions.loader || {}, ).map((key) => key.slice(1)); @@ -109,7 +110,7 @@ export function astParser({ /** * Load, parse and modify any other imports that was placed in - * the `phx-class-prefixer` namespace + * the `ast-parser` namespace */ build.onLoad({ filter: /.*/, namespace }, async (args) => { const source = await readFile(args.path, 'utf-8');