feat(nuxt): use oxc-parser instead of esbuild + acorn (#30066)

This commit is contained in:
Daniel Roe 2025-03-02 08:36:54 +00:00
parent fa480e0a0a
commit c4e6e8c117
No known key found for this signature in database
GPG Key ID: CBC814C393D93268
14 changed files with 203 additions and 262 deletions

View File

@ -81,7 +81,6 @@
"@unhead/ssr": "^1.11.20",
"@unhead/vue": "^1.11.20",
"@vue/shared": "^3.5.13",
"acorn": "^8.14.0",
"c12": "^3.0.2",
"chokidar": "^4.0.3",
"compatx": "^0.1.8",
@ -111,6 +110,7 @@
"ofetch": "^1.4.1",
"ohash": "^2.0.8",
"on-change": "^5.0.1",
"oxc-parser": "^0.53.0",
"pathe": "^2.0.3",
"perfect-debounce": "^1.0.0",
"pkg-types": "^2.0.1",

View File

@ -1,8 +1,7 @@
import { pathToFileURL } from 'node:url'
import { parseURL } from 'ufo'
import MagicString from 'magic-string'
import type { AssignmentProperty, CallExpression, ObjectExpression, Pattern, Property, ReturnStatement, VariableDeclaration } from 'estree'
import type { Program } from 'acorn'
import type { AssignmentProperty, CallExpression, ObjectExpression, Pattern, Program, Property, ReturnStatement, VariableDeclaration } from 'estree'
import { createUnplugin } from 'unplugin'
import type { Component } from '@nuxt/schema'
import { resolve } from 'pathe'

View File

@ -7,7 +7,7 @@ import MagicString from 'magic-string'
import { normalize } from 'pathe'
import type { ObjectPlugin, PluginMeta } from 'nuxt/app'
import { parseAndWalk, transform, withLocations } from '../../core/utils/parse'
import { parseAndWalk, withLocations } from '../../core/utils/parse'
import { logger } from '../../utils'
const internalOrderMap = {
@ -38,7 +38,7 @@ export const orderMap: Record<NonNullable<ObjectPlugin['enforce']>, number> = {
}
const metaCache: Record<string, Omit<PluginMeta, 'enforce'>> = {}
export async function extractMetadata (code: string, loader = 'ts' as 'ts' | 'tsx') {
export function extractMetadata (code: string, loader = 'ts' as 'ts' | 'tsx') {
let meta: PluginMeta = {}
if (metaCache[code]) {
return metaCache[code]
@ -46,8 +46,7 @@ export async function extractMetadata (code: string, loader = 'ts' as 'ts' | 'ts
if (code.match(/defineNuxtPlugin\s*\([\w(]/)) {
return {}
}
const js = await transform(code, { loader })
parseAndWalk(js.code, `file.${loader}`, (node) => {
parseAndWalk(code, `file.${loader}`, (node) => {
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
const name = 'name' in node.callee && node.callee.name

View File

@ -1,8 +1,7 @@
import { walk as _walk } from 'estree-walker'
import type { Node, SyncHandler } from 'estree-walker'
import type { ArrowFunctionExpression, CatchClause, Program as ESTreeProgram, FunctionDeclaration, FunctionExpression, Identifier, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier, VariableDeclaration } from 'estree'
import { parse } from 'acorn'
import type { Program } from 'acorn'
import type { ArrowFunctionExpression, CatchClause, FunctionDeclaration, FunctionExpression, Identifier, ImportDefaultSpecifier, ImportNamespaceSpecifier, ImportSpecifier, Program, VariableDeclaration } from 'estree'
import { parseSync } from 'oxc-parser'
import { type SameShape, type TransformOptions, type TransformResult, transform as esbuildTransform } from 'esbuild'
import { tryUseNuxt } from '@nuxt/kit'
@ -22,7 +21,7 @@ interface WalkOptions {
}
export function walk (ast: Program | Node, callback: Partial<WalkOptions>) {
return _walk(ast as unknown as ESTreeProgram | Node, {
return _walk(ast, {
enter (node, parent, key, index) {
// @ts-expect-error - accessing a protected property
callback.scopeTracker?.processNodeEnter(node as WithLocations<Node>)
@ -33,15 +32,16 @@ export function walk (ast: Program | Node, callback: Partial<WalkOptions>) {
callback.scopeTracker?.processNodeLeave(node as WithLocations<Node>)
callback.leave?.call(this, node as WithLocations<Node>, parent as WithLocations<Node> | null, { key, index, ast })
},
}) as Program | Node | null
})
}
export function parseAndWalk (code: string, sourceFilename: string, callback: WalkerCallback): Program
export function parseAndWalk (code: string, sourceFilename: string, object: Partial<WalkOptions>): Program
export function parseAndWalk (code: string, sourceFilename: string, callback: Partial<WalkOptions> | WalkerCallback) {
const ast = parse (code, { sourceType: 'module', ecmaVersion: 'latest', locations: true, sourceFile: sourceFilename })
walk(ast, typeof callback === 'function' ? { enter: callback } : callback)
return ast
const lang = sourceFilename.match(/\.[cm]?([jt]sx?)$/)?.[1] as 'js' | 'ts' | 'jsx' | 'tsx' | undefined
const ast = parseSync(sourceFilename, code, { sourceType: 'module', lang })
walk(ast.program as unknown as Program, typeof callback === 'function' ? { enter: callback } : callback)
return ast.program as unknown as Program
}
export function withLocations<T> (node: T): WithLocations<T> {

View File

@ -212,7 +212,7 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
}
}
const ast = parseAndWalk(code, id, {
const ast = parseAndWalk(code, id + (query.lang ? '.' + query.lang : '.ts'), {
scopeTracker,
})

View File

@ -4,13 +4,13 @@ import type { NitroRouteConfig } from 'nitropack'
import { normalize } from 'pathe'
import { getLoader } from '../core/utils'
import { parseAndWalk, transform } from '../core/utils/parse'
import { parseAndWalk } from '../core/utils/parse'
import { extractScriptContent, pathToNitroGlob } from './utils'
const ROUTE_RULE_RE = /\bdefineRouteRules\(/
const ruleCache: Record<string, NitroRouteConfig | null> = {}
export async function extractRouteRules (code: string, path: string): Promise<NitroRouteConfig | null> {
export function extractRouteRules (code: string, path: string): NitroRouteConfig | null {
if (code in ruleCache) {
return ruleCache[code] || null
}
@ -26,12 +26,10 @@ export async function extractRouteRules (code: string, path: string): Promise<Ni
code = script?.code || code
const js = await transform(code, { loader: script?.loader || 'ts' })
parseAndWalk(js.code, 'file.' + (script?.loader || 'ts'), (node) => {
parseAndWalk(code, 'file.' + (script?.loader || 'ts'), (node) => {
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
if (node.callee.name === 'defineRouteRules') {
const rulesString = js.code.slice(node.start, node.end)
const rulesString = code.slice(node.start, node.end)
try {
rule = JSON.parse(runInNewContext(rulesString.replace('defineRouteRules', 'JSON.stringify'), {}))
} catch {

View File

@ -11,7 +11,7 @@ import type { Property } from 'estree'
import type { NuxtPage } from 'nuxt/schema'
import { klona } from 'klona'
import { parseAndWalk, transform, withLocations } from '../core/utils/parse'
import { parseAndWalk, withLocations } from '../core/utils/parse'
import { getLoader, uniqueBy } from '../core/utils'
import { logger, toArray } from '../utils'
@ -207,7 +207,7 @@ const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
const pageContentsCache: Record<string, string> = {}
const metaCache: Record<string, Partial<Record<keyof NuxtPage, any>>> = {}
export async function getRouteMeta (contents: string, absolutePath: string, extraExtractionKeys: string[] = []): Promise<Partial<Record<keyof NuxtPage, any>>> {
export function getRouteMeta (contents: string, absolutePath: string, extraExtractionKeys: string[] = []): Partial<Record<keyof NuxtPage, any>> {
// set/update pageContentsCache, invalidate metaCache on cache mismatch
if (!(absolutePath in pageContentsCache) || pageContentsCache[absolutePath] !== contents) {
pageContentsCache[absolutePath] = contents
@ -234,13 +234,11 @@ export async function getRouteMeta (contents: string, absolutePath: string, extr
continue
}
const js = await transform(script.code, { loader: script.loader })
const dynamicProperties = new Set<keyof NuxtPage>()
let foundMeta = false
parseAndWalk(js.code, absolutePath.replace(/\.\w+$/, '.' + script.loader), (node) => {
parseAndWalk(script.code, absolutePath.replace(/\.\w+$/, '.' + script.loader), (node) => {
if (foundMeta) { return }
if (node.type !== 'ExpressionStatement' || node.expression.type !== 'CallExpression' || node.expression.callee.type !== 'Identifier' || node.expression.callee.name !== 'definePageMeta') { return }
@ -259,7 +257,7 @@ export async function getRouteMeta (contents: string, absolutePath: string, extr
const propertyValue = withLocations(property.value)
if (propertyValue.type === 'ObjectExpression') {
const valueString = js.code.slice(propertyValue.start, propertyValue.end)
const valueString = script.code.slice(propertyValue.start, propertyValue.end)
try {
extractedMeta[key] = JSON.parse(runInNewContext(`JSON.stringify(${valueString})`, {}))
} catch {

View File

@ -1,7 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { Component } from '@nuxt/schema'
import { compileScript, parse } from '@vue/compiler-sfc'
import * as Parser from 'acorn'
import { ComponentNamePlugin } from '../src/components/plugins/component-names'
@ -43,14 +42,7 @@ onMounted(() => {
</script>
`
const res = compileScript(parse(sfc).descriptor, { id: 'test.vue' })
const { code } = transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, res.content, components[0].filePath) ?? {}
const { code } = transformPlugin.transform(res.content, components[0].filePath) ?? {}
expect(code?.trim()).toMatchInlineSnapshot(`
"export default Object.assign({
setup(__props, { expose: __expose }) {

View File

@ -1,5 +1,4 @@
import { describe, expect, it } from 'vitest'
import * as Parser from 'acorn'
import { ComposableKeysPlugin, detectImportNames } from '../src/core/plugins/composable-keys'
@ -40,14 +39,7 @@ describe('composable keys plugin', () => {
import { useAsyncData } from '#app'
useAsyncData(() => {})
`
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, code, 'plugin.ts')?.code.trim()).toMatchInlineSnapshot(`
expect(transformPlugin.transform(code, 'plugin.ts')?.code.trim()).toMatchInlineSnapshot(`
"import { useAsyncData } from '#app'
useAsyncData(() => {}, '$HJiaryoL2y')"
`)
@ -55,14 +47,7 @@ useAsyncData(() => {})
it('should not add hash when one exists', () => {
const code = `useAsyncData(() => {}, 'foo')`
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, code, 'plugin.ts')?.code.trim()).toMatchInlineSnapshot(`undefined`)
expect(transformPlugin.transform(code, 'plugin.ts')?.code.trim()).toMatchInlineSnapshot(`undefined`)
})
it('should not add hash composables is imported from somewhere else', () => {
@ -70,13 +55,6 @@ useAsyncData(() => {})
const useAsyncData = () => {}
useAsyncData(() => {})
`
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, code, 'plugin.ts')?.code.trim()).toMatchInlineSnapshot(`undefined`)
expect(transformPlugin.transform(code, 'plugin.ts')?.code.trim()).toMatchInlineSnapshot(`undefined`)
})
})

View File

@ -1,8 +1,7 @@
import { type MockedFunction, describe, expect, it, vi } from 'vitest'
import { compileScript, parse } from '@vue/compiler-sfc'
import * as Parser from 'acorn'
import { klona } from 'klona'
import { transform as esbuildTransform } from 'esbuild'
import { PageMetaPlugin } from '../src/pages/plugins/page-meta'
import { getRouteMeta, normalizeRoutes } from '../src/pages/utils'
import type { NuxtPage } from '../schema'
@ -12,15 +11,15 @@ const filePath = '/app/pages/index.vue'
vi.mock('klona', { spy: true })
describe('page metadata', () => {
it('should not extract metadata from empty files', async () => {
expect(await getRouteMeta('', filePath)).toEqual({})
expect(await getRouteMeta('<template><div>Hi</div></template>', filePath)).toEqual({})
it('should not extract metadata from empty files', () => {
expect(getRouteMeta('', filePath)).toEqual({})
expect(getRouteMeta('<template><div>Hi</div></template>', filePath)).toEqual({})
})
it('should extract metadata from JS/JSX files', async () => {
it('should extract metadata from JS/JSX files', () => {
const fileContents = `definePageMeta({ name: 'bar' })`
for (const ext of ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs']) {
const meta = await getRouteMeta(fileContents, `/app/pages/index.${ext}`)
const meta = getRouteMeta(fileContents, `/app/pages/index.${ext}`)
expect(meta).toStrictEqual({
'name': 'bar',
'meta': {
@ -30,7 +29,7 @@ describe('page metadata', () => {
}
})
it('should parse JSX files', async () => {
it('should parse JSX files', () => {
const fileContents = `
export default {
setup () {
@ -39,7 +38,7 @@ export default {
}
}
`
const meta = await getRouteMeta(fileContents, `/app/pages/index.jsx`)
const meta = getRouteMeta(fileContents, `/app/pages/index.jsx`)
// TODO: remove in v4
delete meta.meta
expect(meta).toStrictEqual({
@ -47,14 +46,14 @@ export default {
})
})
it('should parse lang="jsx" from vue files', async () => {
it('should parse lang="jsx" from vue files', () => {
const fileContents = `
<script setup lang="jsx">
const foo = <></>;
definePageMeta({ name: 'bar' })
</script>`
const meta = await getRouteMeta(fileContents, `/app/pages/index.vue`)
const meta = getRouteMeta(fileContents, `/app/pages/index.vue`)
expect(meta).toStrictEqual({
'meta': {
'__nuxt_dynamic_meta_key': new Set(['meta']),
@ -63,8 +62,7 @@ export default {
})
})
// TODO: https://github.com/nuxt/nuxt/pull/30066
it.todo('should handle experimental decorators', async () => {
it('should handle experimental decorators', () => {
const fileContents = `
<script setup lang="ts">
function something (_method: () => unknown) {
@ -79,31 +77,31 @@ class SomeClass {
definePageMeta({ name: 'bar' })
</script>
`
const meta = await getRouteMeta(fileContents, `/app/pages/index.vue`)
const meta = getRouteMeta(fileContents, `/app/pages/index.vue`)
expect(meta).toStrictEqual({
name: 'bar',
})
})
it('should use and invalidate cache', async () => {
it('should use and invalidate cache', () => {
const _klona = klona as unknown as MockedFunction<typeof klona>
_klona.mockImplementation(obj => obj)
const fileContents = `<script setup>definePageMeta({ foo: 'bar' })</script>`
const meta = await getRouteMeta(fileContents, filePath)
expect(meta === await getRouteMeta(fileContents, filePath)).toBeTruthy()
expect(meta === await getRouteMeta(fileContents, '/app/pages/other.vue')).toBeFalsy()
expect(meta === await getRouteMeta('<template><div>Hi</div></template>' + fileContents, filePath)).toBeFalsy()
const meta = getRouteMeta(fileContents, filePath)
expect(meta === getRouteMeta(fileContents, filePath)).toBeTruthy()
expect(meta === getRouteMeta(fileContents, '/app/pages/other.vue')).toBeFalsy()
expect(meta === getRouteMeta('<template><div>Hi</div></template>' + fileContents, filePath)).toBeFalsy()
_klona.mockReset()
})
it('should not share state between page metadata', async () => {
it('should not share state between page metadata', () => {
const fileContents = `<script setup>definePageMeta({ foo: 'bar' })</script>`
const meta = await getRouteMeta(fileContents, filePath)
expect(meta === await getRouteMeta(fileContents, filePath)).toBeFalsy()
const meta = getRouteMeta(fileContents, filePath)
expect(meta === getRouteMeta(fileContents, filePath)).toBeFalsy()
})
it('should extract serialisable metadata', async () => {
const meta = await getRouteMeta(`
it('should extract serialisable metadata', () => {
const meta = getRouteMeta(`
<script setup>
definePageMeta({
path: '/some-custom-path',
@ -144,8 +142,8 @@ definePageMeta({ name: 'bar' })
`)
})
it('should extract serialisable metadata from files with multiple blocks', async () => {
const meta = await getRouteMeta(`
it('should extract serialisable metadata from files with multiple blocks', () => {
const meta = getRouteMeta(`
<script lang="ts">
export default {
name: 'thing'
@ -179,8 +177,8 @@ definePageMeta({ name: 'bar' })
`)
})
it('should extract serialisable metadata in options api', async () => {
const meta = await getRouteMeta(`
it('should extract serialisable metadata in options api', () => {
const meta = getRouteMeta(`
<script>
export default {
setup() {
@ -207,8 +205,8 @@ definePageMeta({ name: 'bar' })
`)
})
it('should extract serialisable metadata all quoted', async () => {
const meta = await getRouteMeta(`
it('should extract serialisable metadata all quoted', () => {
const meta = getRouteMeta(`
<script setup>
definePageMeta({
"otherValue": {
@ -229,8 +227,8 @@ definePageMeta({ name: 'bar' })
`)
})
it('should extract configured extra meta', async () => {
const meta = await getRouteMeta(`
it('should extract configured extra meta', () => {
const meta = getRouteMeta(`
<script setup>
definePageMeta({
foo: 'bar',
@ -249,9 +247,9 @@ definePageMeta({ name: 'bar' })
})
describe('normalizeRoutes', () => {
it('should produce valid route objects when used with extracted meta', async () => {
it('should produce valid route objects when used with extracted meta', () => {
const page: NuxtPage = { path: '/', file: filePath }
Object.assign(page, await getRouteMeta(`
Object.assign(page, getRouteMeta(`
<script setup>
definePageMeta({
name: 'some-custom-name',
@ -340,14 +338,7 @@ definePageMeta({
</script>
`
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
expect(transformPlugin.transform(res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
"const __nuxt_page_meta = {
name: 'hi',
other: 'value'
@ -374,14 +365,7 @@ definePageMeta({
</script>
`
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
expect(transformPlugin.transform(res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
"function isNumber(value) {
return value && !isNaN(Number(value))
}
@ -408,14 +392,7 @@ definePageMeta({
</script>
`
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
expect(transformPlugin.transform(res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
"import { validateIdParam } from './utils'
const __nuxt_page_meta = {
@ -443,14 +420,7 @@ definePageMeta({
</script>
`
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
expect(transformPlugin.transform(res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
"const __nuxt_page_meta = {
middleware: () => {
const useState = (key) => ({ value: { isLoggedIn: false } })
@ -485,14 +455,7 @@ definePageMeta({
</script>
`
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
expect(transformPlugin.transform(res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
"const __nuxt_page_meta = {
middleware: () => {
function isLoggedIn() {
@ -535,14 +498,7 @@ definePageMeta({
</script>
`
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
expect(transformPlugin.transform(res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
"import { useState } from '#app/composables/state'
const __nuxt_page_meta = {
@ -584,14 +540,7 @@ definePageMeta({
</script>
`
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
expect(transformPlugin.transform(res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
"import { useState } from '#app/composables/state'
const __nuxt_page_meta = {
@ -608,7 +557,7 @@ definePageMeta({
`)
})
it('should work with esbuild.keepNames = true', async () => {
it('should work when keeping names = true', () => {
const sfc = `
<script setup lang="ts">
import { foo } from './utils'
@ -629,37 +578,24 @@ definePageMeta({
</script>
`
const compiled = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
const res = await esbuildTransform(compiled.content, {
loader: 'ts',
keepNames: true,
})
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, res.code, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
"import { foo } from "./utils";
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
const checkNum = /* @__PURE__ */ __name((value) => {
return !isNaN(Number(foo(value)));
}, "checkNum");
function isNumber(value) {
return value && checkNum(value);
}
expect(transformPlugin.transform(compiled.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
"import { foo } from './utils'
const checkNum = (value) => {
return !isNaN(Number(foo(value)))
}
function isNumber (value) {
return value && checkNum(value)
}
const __nuxt_page_meta = {
validate: /* @__PURE__ */ __name(({ params }) => {
return isNumber(params.id);
}, "validate")
}
validate: ({ params }) => {
return isNumber(params.id)
},
}
export default __nuxt_page_meta"
`)
})
it('should throw for await expressions', async () => {
it('should throw for await expressions', () => {
const sfc = `
<script setup lang="ts">
const asyncValue = await Promise.resolve('test')
@ -670,21 +606,11 @@ definePageMeta({
</script>
`
const compiled = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
const res = await esbuildTransform(compiled.content, {
loader: 'ts',
})
let wasErrorThrown = false
try {
transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, res.code, 'component.vue?macro=true')
transformPlugin.transform(compiled.content, 'component.vue?macro=true')
} catch (e) {
if (e instanceof Error) {
expect(e.message).toMatch(/await in definePageMeta/)
@ -751,14 +677,7 @@ const hoisted = ref('hoisted')
</script>
`
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
expect(transformPlugin.transform(res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
"const foo = 'foo'
const num = 1
const bar = { bar: 'bar' }.bar, baz = { baz: 'baz' }.baz, x = { foo }

View File

@ -1,33 +1,29 @@
import { describe, expect, it, vi } from 'vitest'
import * as Parser from 'acorn'
import { RemovePluginMetadataPlugin, extractMetadata } from '../src/core/plugins/plugin-metadata'
import { checkForCircularDependencies } from '../src/core/app'
describe('plugin-metadata', () => {
it('should extract metadata from object-syntax plugins', async () => {
const properties = Object.entries({
name: 'test',
enforce: 'post',
hooks: { 'app:mounted': () => {} },
setup: () => { return { provide: { jsx: '[JSX]' } } },
order: 1,
const properties = Object.entries({
name: 'test',
enforce: 'post',
hooks: { 'app:mounted': () => {} },
setup: () => { return { provide: { jsx: '[JSX]' } } },
order: 1,
})
it.each(properties)('should extract metadata from object-syntax plugins', (k, value) => {
const obj = [...properties.filter(([key]) => key !== k), [k, value]]
const meta = extractMetadata([
'export default defineNuxtPlugin({',
...obj.map(([key, value]) => `${key}: ${typeof value === 'function' ? value.toString().replace('"[JSX]"', '() => <span>JSX</span>') : JSON.stringify(value)},`),
'})',
].join('\n'), 'tsx')
expect(meta).toEqual({
'name': 'test',
'order': 1,
})
for (const item of properties) {
const obj = [...properties.filter(([key]) => key !== item[0]), item]
const meta = await extractMetadata([
'export default defineNuxtPlugin({',
...obj.map(([key, value]) => `${key}: ${typeof value === 'function' ? value.toString().replace('"[JSX]"', '() => <span>JSX</span>') : JSON.stringify(value)},`),
'})',
].join('\n'), 'tsx')
expect(meta).toEqual({
'name': 'test',
'order': 1,
})
}
})
const transformPlugin: any = RemovePluginMetadataPlugin({
@ -41,14 +37,7 @@ describe('plugin-metadata', () => {
'export default function (ctx, inject) {}',
]
for (const plugin of invalidPlugins) {
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, plugin, 'my-plugin.mjs').code).toBe('export default () => {}')
expect(transformPlugin.transform(plugin, 'my-plugin.mjs').code).toBe('export default () => {}')
}
})
@ -60,14 +49,7 @@ describe('plugin-metadata', () => {
setup: () => {},
}, { order: 10, name: test })
`
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, plugin, 'my-plugin.mjs').code).toMatchInlineSnapshot(`
expect(transformPlugin.transform(plugin, 'my-plugin.mjs').code).toMatchInlineSnapshot(`
"
export default defineNuxtPlugin({
setup: () => {},

View File

@ -3,9 +3,9 @@ import { describe, expect, it } from 'vitest'
import { extractRouteRules } from '../src/pages/route-rules'
describe('route-rules', () => {
it('should extract route rules from pages', async () => {
it('should extract route rules from pages', () => {
for (const [path, code] of Object.entries(examples)) {
const result = await extractRouteRules(code, path)
const result = extractRouteRules(code, path)
expect(result).toStrictEqual({
'prerender': true,
@ -33,7 +33,6 @@ defineRouteRules({
`,
// vue component with a normal script block, and defineRouteRules ambiently
'component.vue': `
<script>
defineRouteRules({
prerender: true
@ -43,14 +42,14 @@ export default {
}
</script>
`,
// TODO: JS component with defineRouteRules within a setup function
// 'component.ts': `
// export default {
// setup() {
// defineRouteRules({
// prerender: true
// })
// }
// }
// `,
// JS component with defineRouteRules within a setup function
'component.ts': `
export default {
setup() {
defineRouteRules({
prerender: true
})
}
}
`,
}

View File

@ -3,7 +3,6 @@ import path from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import * as VueCompilerSFC from 'vue/compiler-sfc'
import type { Plugin } from 'vite'
import * as Parser from 'acorn'
import type { Options } from '@vitejs/plugin-vue'
import _vuePlugin from '@vitejs/plugin-vue'
import { TreeShakeTemplatePlugin } from '../src/components/plugins/tree-shake'
@ -56,14 +55,7 @@ const treeshakeTemplatePlugin = TreeShakeTemplatePlugin({
const treeshake = async (source: string): Promise<string> => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const result = await (treeshakeTemplatePlugin.transform! as Function).call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, source)
const result = await (treeshakeTemplatePlugin.transform! as Function)(source, 'test.ts')
return typeof result === 'string' ? result : result?.code
}

View File

@ -368,9 +368,6 @@ importers:
'@vue/shared':
specifier: 3.5.13
version: 3.5.13
acorn:
specifier: ^8.14.0
version: 8.14.0
c12:
specifier: ^3.0.2
version: 3.0.2(magicast@0.3.5)
@ -458,6 +455,9 @@ importers:
on-change:
specifier: ^5.0.1
version: 5.0.1
oxc-parser:
specifier: ^0.53.0
version: 0.53.0
pathe:
specifier: ^2.0.3
version: 2.0.3
@ -2273,6 +2273,49 @@ packages:
'@one-ini/wasm@0.1.1':
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
'@oxc-parser/binding-darwin-arm64@0.53.0':
resolution: {integrity: sha512-wamfZjKPBIbORIL9XeGPvvLVvYvfl/1sOZqKWGFFp+LKBoOqx3X7la8+76SDjgIGkAUyl/FTO2lclFe2h87JWg==}
cpu: [arm64]
os: [darwin]
'@oxc-parser/binding-darwin-x64@0.53.0':
resolution: {integrity: sha512-geeBJOf1FtTQgmgKEUdOEnHP9pTIXSo7HDIJ1tiIO6j6u3j9zss8wFX2khH2XqAxLekSC87x8Bu6iB22ZDqajQ==}
cpu: [x64]
os: [darwin]
'@oxc-parser/binding-linux-arm64-gnu@0.53.0':
resolution: {integrity: sha512-1f5ErSzs5vGSzfMAm+puhKr5J3zqmwtRqoAiW8oZe1TXBk8PSQGaHl43mY8fhfX+ErGwmTdj3z0Q/Y341rkf8Q==}
cpu: [arm64]
os: [linux]
'@oxc-parser/binding-linux-arm64-musl@0.53.0':
resolution: {integrity: sha512-wZ8rNwybcSzzoJzmR1xLUOrum3rYcW2/h4sJSzykI59rzo3Hw21F45EEgCddn3svcgMPK2qW/hcX9SKQ5Ru71Q==}
cpu: [arm64]
os: [linux]
'@oxc-parser/binding-linux-x64-gnu@0.53.0':
resolution: {integrity: sha512-R1Y2hamxRtD1j9sEbyiPTl9rQoyub3tOVaPRG8hSJosymrrfKotrK/S3RNkUWfY5UjKTRFc1c+he3FDMZCRamQ==}
cpu: [x64]
os: [linux]
'@oxc-parser/binding-linux-x64-musl@0.53.0':
resolution: {integrity: sha512-3BlzMhRvfUmF1DNzIcN/TjEqrm9vKHEfPipgZJHG9uh1cr+o5e+whBYhQ2JJ0cd07i7FcNq+gKIjCwv31JNonw==}
cpu: [x64]
os: [linux]
'@oxc-parser/binding-win32-arm64-msvc@0.53.0':
resolution: {integrity: sha512-m7LYYdg8l1h4ozYSHPvuoC4oBEBKBEYniHwHrwRxYoNDlqzkQtnvE/lBwMZnXDbd+STwx1ba7ukxV2XNpv58Wg==}
cpu: [arm64]
os: [win32]
'@oxc-parser/binding-win32-x64-msvc@0.53.0':
resolution: {integrity: sha512-7EhG3JJRttLxfyPriaO7J+2mNQ3tKG25/VkkPW30aH5YL6TKQFUijM/Lh7UW7nRXdvr5Oqn4yVjnDTi8PapFmw==}
cpu: [x64]
os: [win32]
'@oxc-project/types@0.53.0':
resolution: {integrity: sha512-8JXXVoHnRLcl6kDBboSfAmAkKeb6PSvSc5qSJxiOFzFx0ZCLAbUDmuwR2hkBnY7kQS3LmNXaONq1BFAmwTyeZw==}
'@parcel/watcher-android-arm64@2.5.1':
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
engines: {node: '>= 10.0.0'}
@ -5928,6 +5971,9 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
oxc-parser@0.53.0:
resolution: {integrity: sha512-NjUiQx1ns2l9uIh9aAzXkEakP7GD00rza4FC/cVwxpY/sSvzHlhbuTaRX/91fmVOKJkA3PEMks43UeJCzcLApg==}
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
@ -9041,6 +9087,32 @@ snapshots:
'@one-ini/wasm@0.1.1': {}
'@oxc-parser/binding-darwin-arm64@0.53.0':
optional: true
'@oxc-parser/binding-darwin-x64@0.53.0':
optional: true
'@oxc-parser/binding-linux-arm64-gnu@0.53.0':
optional: true
'@oxc-parser/binding-linux-arm64-musl@0.53.0':
optional: true
'@oxc-parser/binding-linux-x64-gnu@0.53.0':
optional: true
'@oxc-parser/binding-linux-x64-musl@0.53.0':
optional: true
'@oxc-parser/binding-win32-arm64-msvc@0.53.0':
optional: true
'@oxc-parser/binding-win32-x64-msvc@0.53.0':
optional: true
'@oxc-project/types@0.53.0': {}
'@parcel/watcher-android-arm64@2.5.1':
optional: true
@ -13518,6 +13590,19 @@ snapshots:
type-check: 0.4.0
word-wrap: 1.2.5
oxc-parser@0.53.0:
dependencies:
'@oxc-project/types': 0.53.0
optionalDependencies:
'@oxc-parser/binding-darwin-arm64': 0.53.0
'@oxc-parser/binding-darwin-x64': 0.53.0
'@oxc-parser/binding-linux-arm64-gnu': 0.53.0
'@oxc-parser/binding-linux-arm64-musl': 0.53.0
'@oxc-parser/binding-linux-x64-gnu': 0.53.0
'@oxc-parser/binding-linux-x64-musl': 0.53.0
'@oxc-parser/binding-win32-arm64-msvc': 0.53.0
'@oxc-parser/binding-win32-x64-msvc': 0.53.0
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0