From c4e6e8c117944c793fd88fdd969607f131dba7d7 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sun, 2 Mar 2025 08:36:54 +0000 Subject: [PATCH] feat(nuxt): use `oxc-parser` instead of esbuild + acorn (#30066) --- packages/nuxt/package.json | 2 +- .../nuxt/src/components/plugins/tree-shake.ts | 3 +- .../nuxt/src/core/plugins/plugin-metadata.ts | 7 +- packages/nuxt/src/core/utils/parse.ts | 16 +- packages/nuxt/src/pages/plugins/page-meta.ts | 2 +- packages/nuxt/src/pages/route-rules.ts | 10 +- packages/nuxt/src/pages/utils.ts | 10 +- packages/nuxt/test/component-names.test.ts | 10 +- packages/nuxt/test/composable-keys.test.ts | 28 +-- packages/nuxt/test/page-metadata.test.ts | 191 +++++------------- packages/nuxt/test/plugin-metadata.test.ts | 60 ++---- packages/nuxt/test/route-rules.test.ts | 25 ++- packages/nuxt/test/treeshake-client.test.ts | 10 +- pnpm-lock.yaml | 91 ++++++++- 14 files changed, 203 insertions(+), 262 deletions(-) diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index e663d63f44..68453f6629 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -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", diff --git a/packages/nuxt/src/components/plugins/tree-shake.ts b/packages/nuxt/src/components/plugins/tree-shake.ts index d24b5f1c00..103709b5f8 100644 --- a/packages/nuxt/src/components/plugins/tree-shake.ts +++ b/packages/nuxt/src/components/plugins/tree-shake.ts @@ -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' diff --git a/packages/nuxt/src/core/plugins/plugin-metadata.ts b/packages/nuxt/src/core/plugins/plugin-metadata.ts index 43a1b89635..7a4f354b53 100644 --- a/packages/nuxt/src/core/plugins/plugin-metadata.ts +++ b/packages/nuxt/src/core/plugins/plugin-metadata.ts @@ -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, number> = { } const metaCache: Record> = {} -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 diff --git a/packages/nuxt/src/core/utils/parse.ts b/packages/nuxt/src/core/utils/parse.ts index 95910140e8..ded886c536 100644 --- a/packages/nuxt/src/core/utils/parse.ts +++ b/packages/nuxt/src/core/utils/parse.ts @@ -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) { - 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) @@ -33,15 +32,16 @@ export function walk (ast: Program | Node, callback: Partial) { callback.scopeTracker?.processNodeLeave(node as WithLocations) callback.leave?.call(this, node as WithLocations, parent as WithLocations | 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): Program export function parseAndWalk (code: string, sourceFilename: string, callback: Partial | 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 (node: T): WithLocations { diff --git a/packages/nuxt/src/pages/plugins/page-meta.ts b/packages/nuxt/src/pages/plugins/page-meta.ts index 0a6cd606bc..d7be7da293 100644 --- a/packages/nuxt/src/pages/plugins/page-meta.ts +++ b/packages/nuxt/src/pages/plugins/page-meta.ts @@ -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, }) diff --git a/packages/nuxt/src/pages/route-rules.ts b/packages/nuxt/src/pages/route-rules.ts index c1127c0ced..b0092536fd 100644 --- a/packages/nuxt/src/pages/route-rules.ts +++ b/packages/nuxt/src/pages/route-rules.ts @@ -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 = {} -export async function extractRouteRules (code: string, path: string): Promise { +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 { + 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 { diff --git a/packages/nuxt/src/pages/utils.ts b/packages/nuxt/src/pages/utils.ts index 6f095ac699..b44b608297 100644 --- a/packages/nuxt/src/pages/utils.ts +++ b/packages/nuxt/src/pages/utils.ts @@ -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 = {} const metaCache: Record>> = {} -export async function getRouteMeta (contents: string, absolutePath: string, extraExtractionKeys: string[] = []): Promise>> { +export function getRouteMeta (contents: string, absolutePath: string, extraExtractionKeys: string[] = []): Partial> { // 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() 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 { diff --git a/packages/nuxt/test/component-names.test.ts b/packages/nuxt/test/component-names.test.ts index cc8a2662c9..ee0cb10995 100644 --- a/packages/nuxt/test/component-names.test.ts +++ b/packages/nuxt/test/component-names.test.ts @@ -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(() => { ` 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 }) { diff --git a/packages/nuxt/test/composable-keys.test.ts b/packages/nuxt/test/composable-keys.test.ts index 13bbcbed96..990fc6ea71 100644 --- a/packages/nuxt/test/composable-keys.test.ts +++ b/packages/nuxt/test/composable-keys.test.ts @@ -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`) }) }) diff --git a/packages/nuxt/test/page-metadata.test.ts b/packages/nuxt/test/page-metadata.test.ts index 33c94adbea..632861cf71 100644 --- a/packages/nuxt/test/page-metadata.test.ts +++ b/packages/nuxt/test/page-metadata.test.ts @@ -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('', filePath)).toEqual({}) + it('should not extract metadata from empty files', () => { + expect(getRouteMeta('', filePath)).toEqual({}) + expect(getRouteMeta('', 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 = ` ` - 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 = ` ` - 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 _klona.mockImplementation(obj => obj) const fileContents = `` - 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('' + 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('' + fileContents, filePath)).toBeFalsy() _klona.mockReset() }) - it('should not share state between page metadata', async () => { + it('should not share state between page metadata', () => { const fileContents = `` - 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(` ` 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({ ` 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({ ` 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({ ` 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({ ` 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({ ` 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({ ` 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 = ` ` 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 = ` ` 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') ` 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 } diff --git a/packages/nuxt/test/plugin-metadata.test.ts b/packages/nuxt/test/plugin-metadata.test.ts index 0615955bef..29da31a0f7 100644 --- a/packages/nuxt/test/plugin-metadata.test.ts +++ b/packages/nuxt/test/plugin-metadata.test.ts @@ -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]"', '() => JSX') : 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]"', '() => JSX') : 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: () => {}, diff --git a/packages/nuxt/test/route-rules.test.ts b/packages/nuxt/test/route-rules.test.ts index a5431fab37..c313494318 100644 --- a/packages/nuxt/test/route-rules.test.ts +++ b/packages/nuxt/test/route-rules.test.ts @@ -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': ` - `, -// 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 + }) + } +} + `, } diff --git a/packages/nuxt/test/treeshake-client.test.ts b/packages/nuxt/test/treeshake-client.test.ts index 298c72d02c..f9b8a1f9d1 100644 --- a/packages/nuxt/test/treeshake-client.test.ts +++ b/packages/nuxt/test/treeshake-client.test.ts @@ -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 => { // 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 } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index adaff2d8f1..47f78306c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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