mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-29 00:52:01 +00:00
refactor(nuxt): simplify and improve core plugins that parse ast (#30088)
This commit is contained in:
parent
a60d7fd51c
commit
3cb8b9fcb0
@ -1,7 +1,8 @@
|
||||
import { createUnplugin } from 'unplugin'
|
||||
import MagicString from 'magic-string'
|
||||
import type { Component } from 'nuxt/schema'
|
||||
import type { Program } from 'acorn'
|
||||
import { parseAndWalk, withLocations } from '../../core/utils/parse'
|
||||
|
||||
import { SX_RE, isVue } from '../../core/utils'
|
||||
|
||||
interface NameDevPluginOptions {
|
||||
@ -37,12 +38,15 @@ export const ComponentNamePlugin = (options: NameDevPluginOptions) => createUnpl
|
||||
|
||||
// Without setup function, vue compiler does not generate __name
|
||||
if (!s.hasChanged()) {
|
||||
const ast = this.parse(code) as Program
|
||||
const exportDefault = ast.body.find(node => node.type === 'ExportDefaultDeclaration')
|
||||
if (exportDefault) {
|
||||
const { start, end } = exportDefault.declaration
|
||||
parseAndWalk(code, id, function (node) {
|
||||
if (node.type !== 'ExportDefaultDeclaration') {
|
||||
return
|
||||
}
|
||||
|
||||
const { start, end } = withLocations(node.declaration)
|
||||
s.overwrite(start, end, `Object.assign(${code.slice(start, end)}, { __name: ${JSON.stringify(component.pascalName)} })`)
|
||||
}
|
||||
this.skip()
|
||||
})
|
||||
}
|
||||
|
||||
if (s.hasChanged()) {
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { parseURL } from 'ufo'
|
||||
import MagicString from 'magic-string'
|
||||
import { walk } from 'estree-walker'
|
||||
import type { AssignmentProperty, CallExpression, Identifier, Literal, MemberExpression, Node, ObjectExpression, Pattern, Program, Property, ReturnStatement, VariableDeclaration } from 'estree'
|
||||
import type { AssignmentProperty, CallExpression, ObjectExpression, Pattern, Property, ReturnStatement, VariableDeclaration } from 'estree'
|
||||
import type { Program } from 'acorn'
|
||||
import { createUnplugin } from 'unplugin'
|
||||
import type { Component } from '@nuxt/schema'
|
||||
import { resolve } from 'pathe'
|
||||
|
||||
import { parseAndWalk, walk, withLocations } from '../../core/utils/parse'
|
||||
import type { Node } from '../../core/utils/parse'
|
||||
import { distDir } from '../../dirs'
|
||||
|
||||
interface TreeShakeTemplatePluginOptions {
|
||||
@ -13,12 +16,9 @@ interface TreeShakeTemplatePluginOptions {
|
||||
getComponents (): Component[]
|
||||
}
|
||||
|
||||
type AcornNode<N extends Node> = N & { start: number, end: number }
|
||||
|
||||
const SSR_RENDER_RE = /ssrRenderComponent/
|
||||
const PLACEHOLDER_EXACT_RE = /^(?:fallback|placeholder)$/
|
||||
const CLIENT_ONLY_NAME_RE = /^(?:_unref\()?(?:_component_)?(?:Lazy|lazy_)?(?:client_only|ClientOnly\)?)$/
|
||||
const PARSER_OPTIONS = { sourceType: 'module', ecmaVersion: 'latest' }
|
||||
|
||||
export const TreeShakeTemplatePlugin = (options: TreeShakeTemplatePluginOptions) => createUnplugin(() => {
|
||||
const regexpMap = new WeakMap<Component[], [RegExp, RegExp, string[]]>()
|
||||
@ -29,7 +29,7 @@ export const TreeShakeTemplatePlugin = (options: TreeShakeTemplatePluginOptions)
|
||||
const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
||||
return pathname.endsWith('.vue')
|
||||
},
|
||||
transform (code) {
|
||||
transform (code, id) {
|
||||
const components = options.getComponents()
|
||||
|
||||
if (!regexpMap.has(components)) {
|
||||
@ -47,62 +47,55 @@ export const TreeShakeTemplatePlugin = (options: TreeShakeTemplatePluginOptions)
|
||||
const [COMPONENTS_RE, COMPONENTS_IDENTIFIERS_RE] = regexpMap.get(components)!
|
||||
if (!COMPONENTS_RE.test(code)) { return }
|
||||
|
||||
const codeAst = this.parse(code, PARSER_OPTIONS) as AcornNode<Program>
|
||||
|
||||
const componentsToRemoveSet = new Set<string>()
|
||||
|
||||
// remove client only components or components called in ClientOnly default slot
|
||||
walk(codeAst, {
|
||||
enter: (_node) => {
|
||||
const node = _node as AcornNode<Node>
|
||||
if (isSsrRender(node)) {
|
||||
const [componentCall, _, children] = node.arguments
|
||||
if (!componentCall) { return }
|
||||
const ast = parseAndWalk(code, id, (node) => {
|
||||
if (!isSsrRender(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (componentCall.type === 'Identifier' || componentCall.type === 'MemberExpression' || componentCall.type === 'CallExpression') {
|
||||
const componentName = getComponentName(node)
|
||||
const isClientComponent = COMPONENTS_IDENTIFIERS_RE.test(componentName)
|
||||
const isClientOnlyComponent = CLIENT_ONLY_NAME_RE.test(componentName)
|
||||
const [componentCall, _, children] = node.arguments
|
||||
if (!componentCall) { return }
|
||||
|
||||
if (isClientComponent && children?.type === 'ObjectExpression') {
|
||||
const slotsToRemove = isClientOnlyComponent ? children.properties.filter(prop => prop.type === 'Property' && prop.key.type === 'Identifier' && !PLACEHOLDER_EXACT_RE.test(prop.key.name)) as AcornNode<Property>[] : children.properties as AcornNode<Property>[]
|
||||
if (componentCall.type === 'Identifier' || componentCall.type === 'MemberExpression' || componentCall.type === 'CallExpression') {
|
||||
const componentName = getComponentName(node)
|
||||
if (!componentName || !COMPONENTS_IDENTIFIERS_RE.test(componentName) || children?.type !== 'ObjectExpression') { return }
|
||||
|
||||
for (const slot of slotsToRemove) {
|
||||
s.remove(slot.start, slot.end + 1)
|
||||
const removedCode = `({${code.slice(slot.start, slot.end + 1)}})`
|
||||
const currentCodeAst = this.parse(s.toString(), PARSER_OPTIONS) as Node
|
||||
const isClientOnlyComponent = CLIENT_ONLY_NAME_RE.test(componentName)
|
||||
const slotsToRemove = isClientOnlyComponent ? children.properties.filter(prop => prop.type === 'Property' && prop.key.type === 'Identifier' && !PLACEHOLDER_EXACT_RE.test(prop.key.name)) as Property[] : children.properties as Property[]
|
||||
|
||||
walk(this.parse(removedCode, PARSER_OPTIONS) as Node, {
|
||||
enter: (_node) => {
|
||||
const node = _node as AcornNode<CallExpression>
|
||||
if (isSsrRender(node)) {
|
||||
const name = getComponentName(node)
|
||||
for (const _slot of slotsToRemove) {
|
||||
const slot = withLocations(_slot)
|
||||
s.remove(slot.start, slot.end + 1)
|
||||
const removedCode = `({${code.slice(slot.start, slot.end + 1)}})`
|
||||
const currentState = s.toString()
|
||||
|
||||
// detect if the component is called else where
|
||||
const nameToRemove = isComponentNotCalledInSetup(currentCodeAst, name)
|
||||
if (nameToRemove) {
|
||||
componentsToRemoveSet.add(nameToRemove)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
parseAndWalk(removedCode, id, (node) => {
|
||||
if (!isSsrRender(node)) { return }
|
||||
const name = getComponentName(node)
|
||||
if (!name) { return }
|
||||
|
||||
// detect if the component is called else where
|
||||
const nameToRemove = isComponentNotCalledInSetup(currentState, id, name)
|
||||
if (nameToRemove) {
|
||||
componentsToRemoveSet.add(nameToRemove)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const componentsToRemove = [...componentsToRemoveSet]
|
||||
const removedNodes = new WeakSet<AcornNode<Node>>()
|
||||
const removedNodes = new WeakSet<Node>()
|
||||
|
||||
for (const componentName of componentsToRemove) {
|
||||
// remove import declaration if it exists
|
||||
removeImportDeclaration(codeAst, componentName, s)
|
||||
removeImportDeclaration(ast, componentName, s)
|
||||
// remove variable declaration
|
||||
removeVariableDeclarator(codeAst, componentName, s, removedNodes)
|
||||
removeVariableDeclarator(ast, componentName, s, removedNodes)
|
||||
// remove from setup return statement
|
||||
removeFromSetupReturn(codeAst, componentName, s)
|
||||
removeFromSetupReturn(ast, componentName, s)
|
||||
}
|
||||
|
||||
if (s.hasChanged()) {
|
||||
@ -129,7 +122,7 @@ function removeFromSetupReturn (codeAst: Program, name: string, magicString: Mag
|
||||
} else if (node.type === 'Property' && node.key.type === 'Identifier' && node.key.name === 'setup' && (node.value.type === 'FunctionExpression' || node.value.type === 'ArrowFunctionExpression')) {
|
||||
// walk into the setup function
|
||||
walkedInSetup = true
|
||||
if (node.value.body.type === 'BlockStatement') {
|
||||
if (node.value.body?.type === 'BlockStatement') {
|
||||
const returnStatement = node.value.body.body.find(statement => statement.type === 'ReturnStatement') as ReturnStatement
|
||||
if (returnStatement && returnStatement.argument?.type === 'ObjectExpression') {
|
||||
// remove from return statement
|
||||
@ -157,7 +150,8 @@ function removeFromSetupReturn (codeAst: Program, name: string, magicString: Mag
|
||||
function removePropertyFromObject (node: ObjectExpression, name: string, magicString: MagicString) {
|
||||
for (const property of node.properties) {
|
||||
if (property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === name) {
|
||||
magicString.remove((property as AcornNode<Property>).start, (property as AcornNode<Property>).end + 1)
|
||||
const _property = withLocations(property)
|
||||
magicString.remove(_property.start, _property.end + 1)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -167,26 +161,26 @@ function removePropertyFromObject (node: ObjectExpression, name: string, magicSt
|
||||
/**
|
||||
* is the node a call expression ssrRenderComponent()
|
||||
*/
|
||||
function isSsrRender (node: Node): node is AcornNode<CallExpression> {
|
||||
function isSsrRender (node: Node): node is CallExpression {
|
||||
return node.type === 'CallExpression' && node.callee.type === 'Identifier' && SSR_RENDER_RE.test(node.callee.name)
|
||||
}
|
||||
|
||||
function removeImportDeclaration (ast: Program, importName: string, magicString: MagicString): boolean {
|
||||
for (const node of ast.body) {
|
||||
if (node.type === 'ImportDeclaration') {
|
||||
const specifier = node.specifiers.find(s => s.local.name === importName)
|
||||
if (specifier) {
|
||||
if (node.specifiers.length > 1) {
|
||||
const specifierIndex = node.specifiers.findIndex(s => s.local.name === importName)
|
||||
if (specifierIndex > -1) {
|
||||
magicString.remove((node.specifiers[specifierIndex] as AcornNode<Node>).start, (node.specifiers[specifierIndex] as AcornNode<Node>).end + 1)
|
||||
node.specifiers.splice(specifierIndex, 1)
|
||||
}
|
||||
} else {
|
||||
magicString.remove((node as AcornNode<Node>).start, (node as AcornNode<Node>).end)
|
||||
}
|
||||
return true
|
||||
if (node.type !== 'ImportDeclaration' || !node.specifiers) {
|
||||
continue
|
||||
}
|
||||
const specifierIndex = node.specifiers.findIndex(s => s.local.name === importName)
|
||||
if (specifierIndex > -1) {
|
||||
if (node.specifiers!.length > 1) {
|
||||
const specifier = withLocations(node.specifiers![specifierIndex])
|
||||
magicString.remove(specifier.start, specifier.end + 1)
|
||||
node.specifiers!.splice(specifierIndex, 1)
|
||||
} else {
|
||||
const specifier = withLocations(node)
|
||||
magicString.remove(specifier.start, specifier.end)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
@ -197,62 +191,61 @@ function removeImportDeclaration (ast: Program, importName: string, magicString:
|
||||
* ImportDeclarations and VariableDeclarations are ignored
|
||||
* return the name of the component if is not called
|
||||
*/
|
||||
function isComponentNotCalledInSetup (codeAst: Node, name: string): string | void {
|
||||
if (name) {
|
||||
let found = false
|
||||
walk(codeAst, {
|
||||
enter (node) {
|
||||
if ((node.type === 'Property' && node.key.type === 'Identifier' && node.value.type === 'FunctionExpression' && node.key.name === 'setup') || (node.type === 'FunctionDeclaration' && (node.id?.name === '_sfc_ssrRender' || node.id?.name === 'ssrRender'))) {
|
||||
// walk through the setup function node or the ssrRender function
|
||||
walk(node, {
|
||||
enter (node) {
|
||||
if (found || node.type === 'VariableDeclaration') {
|
||||
this.skip()
|
||||
} else if (node.type === 'Identifier' && node.name === name) {
|
||||
found = true
|
||||
} else if (node.type === 'MemberExpression') {
|
||||
// dev only with $setup or _ctx
|
||||
found = (node.property.type === 'Literal' && node.property.value === name) || (node.property.type === 'Identifier' && node.property.name === name)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
if (!found) { return name }
|
||||
}
|
||||
function isComponentNotCalledInSetup (code: string, id: string, name: string): string | void {
|
||||
if (!name) { return }
|
||||
let found = false
|
||||
parseAndWalk(code, id, function (node) {
|
||||
if ((node.type === 'Property' && node.key.type === 'Identifier' && node.value.type === 'FunctionExpression' && node.key.name === 'setup') || (node.type === 'FunctionDeclaration' && (node.id?.name === '_sfc_ssrRender' || node.id?.name === 'ssrRender'))) {
|
||||
// walk through the setup function node or the ssrRender function
|
||||
walk(node, {
|
||||
enter (node) {
|
||||
if (found || node.type === 'VariableDeclaration') {
|
||||
this.skip()
|
||||
} else if (node.type === 'Identifier' && node.name === name) {
|
||||
found = true
|
||||
} else if (node.type === 'MemberExpression') {
|
||||
// dev only with $setup or _ctx
|
||||
found = (node.property.type === 'Literal' && node.property.value === name) || (node.property.type === 'Identifier' && node.property.name === name)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
if (!found) { return name }
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieve the component identifier being used on ssrRender callExpression
|
||||
* @param ssrRenderNode - ssrRender callExpression
|
||||
*/
|
||||
function getComponentName (ssrRenderNode: AcornNode<CallExpression>): string {
|
||||
const componentCall = ssrRenderNode.arguments[0] as Identifier | MemberExpression | CallExpression
|
||||
function getComponentName (ssrRenderNode: CallExpression): string | undefined {
|
||||
const componentCall = ssrRenderNode.arguments[0]
|
||||
if (!componentCall) { return }
|
||||
|
||||
if (componentCall.type === 'Identifier') {
|
||||
return componentCall.name
|
||||
} else if (componentCall.type === 'MemberExpression') {
|
||||
return (componentCall.property as Literal).value as string
|
||||
if (componentCall.property.type === 'Literal') {
|
||||
return componentCall.property.value as string
|
||||
}
|
||||
} else if (componentCall.type === 'CallExpression') {
|
||||
return getComponentName(componentCall)
|
||||
}
|
||||
return (componentCall.arguments[0] as Identifier).name
|
||||
}
|
||||
|
||||
/**
|
||||
* remove a variable declaration within the code
|
||||
*/
|
||||
function removeVariableDeclarator (codeAst: Node, name: string, magicString: MagicString, removedNodes: WeakSet<Node>): AcornNode<Node> | void {
|
||||
function removeVariableDeclarator (codeAst: Program, name: string, magicString: MagicString, removedNodes: WeakSet<Node>): Node | void {
|
||||
// remove variables
|
||||
walk(codeAst, {
|
||||
enter (node) {
|
||||
if (node.type === 'VariableDeclaration') {
|
||||
for (const declarator of node.declarations) {
|
||||
const toRemove = findMatchingPatternToRemove(declarator.id as AcornNode<Pattern>, node as AcornNode<VariableDeclaration>, name, removedNodes)
|
||||
if (toRemove) {
|
||||
magicString.remove(toRemove.start, toRemove.end + 1)
|
||||
removedNodes.add(toRemove)
|
||||
return toRemove
|
||||
}
|
||||
if (node.type !== 'VariableDeclaration') { return }
|
||||
for (const declarator of node.declarations) {
|
||||
const toRemove = withLocations(findMatchingPatternToRemove(declarator.id, node, name, removedNodes))
|
||||
if (toRemove) {
|
||||
magicString.remove(toRemove.start, toRemove.end + 1)
|
||||
removedNodes.add(toRemove)
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -262,13 +255,13 @@ function removeVariableDeclarator (codeAst: Node, name: string, magicString: Mag
|
||||
/**
|
||||
* find the Pattern to remove which the identifier is equal to the name parameter.
|
||||
*/
|
||||
function findMatchingPatternToRemove (node: AcornNode<Pattern>, toRemoveIfMatched: AcornNode<Node>, name: string, removedNodeSet: WeakSet<Node>): AcornNode<Node> | undefined {
|
||||
function findMatchingPatternToRemove (node: Pattern, toRemoveIfMatched: Node, name: string, removedNodeSet: WeakSet<Node>): Node | undefined {
|
||||
if (node.type === 'Identifier') {
|
||||
if (node.name === name) {
|
||||
return toRemoveIfMatched
|
||||
}
|
||||
} else if (node.type === 'ArrayPattern') {
|
||||
const elements = node.elements.filter((e): e is AcornNode<Pattern> => e !== null && !removedNodeSet.has(e))
|
||||
const elements = node.elements.filter((e): e is Pattern => e !== null && !removedNodeSet.has(e))
|
||||
|
||||
for (const element of elements) {
|
||||
const matched = findMatchingPatternToRemove(element, elements.length > 1 ? element : toRemoveIfMatched, name, removedNodeSet)
|
||||
@ -278,12 +271,12 @@ function findMatchingPatternToRemove (node: AcornNode<Pattern>, toRemoveIfMatche
|
||||
const properties = node.properties.filter((e): e is AssignmentProperty => e.type === 'Property' && !removedNodeSet.has(e))
|
||||
|
||||
for (const [index, property] of properties.entries()) {
|
||||
let nodeToRemove = property as AcornNode<Node>
|
||||
let nodeToRemove: Node = property
|
||||
if (properties.length < 2) {
|
||||
nodeToRemove = toRemoveIfMatched
|
||||
}
|
||||
|
||||
const matched = findMatchingPatternToRemove(property.value as AcornNode<Pattern>, nodeToRemove as AcornNode<Node>, name, removedNodeSet)
|
||||
const matched = findMatchingPatternToRemove(property.value, nodeToRemove, name, removedNodeSet)
|
||||
if (matched) {
|
||||
if (matched === property) {
|
||||
properties.splice(index, 1)
|
||||
@ -292,7 +285,7 @@ function findMatchingPatternToRemove (node: AcornNode<Pattern>, toRemoveIfMatche
|
||||
}
|
||||
}
|
||||
} else if (node.type === 'AssignmentPattern') {
|
||||
const matched = findMatchingPatternToRemove(node.left as AcornNode<Pattern>, toRemoveIfMatched, name, removedNodeSet)
|
||||
const matched = findMatchingPatternToRemove(node.left, toRemoveIfMatched, name, removedNodeSet)
|
||||
if (matched) { return matched }
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ import { RemovePluginMetadataPlugin } from './plugins/plugin-metadata'
|
||||
import { AsyncContextInjectionPlugin } from './plugins/async-context'
|
||||
import { ComposableKeysPlugin } from './plugins/composable-keys'
|
||||
import { resolveDeepImportsPlugin } from './plugins/resolve-deep-imports'
|
||||
import { prehydrateTransformPlugin } from './plugins/prehydrate'
|
||||
import { PrehydrateTransformPlugin } from './plugins/prehydrate'
|
||||
import { VirtualFSPlugin } from './plugins/virtual'
|
||||
|
||||
export function createNuxt (options: NuxtOptions): Nuxt {
|
||||
@ -283,7 +283,7 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
addVitePlugin(() => resolveDeepImportsPlugin(nuxt), { server: false })
|
||||
|
||||
// Add transform for `onPrehydrate` lifecycle hook
|
||||
addBuildPlugin(prehydrateTransformPlugin(nuxt))
|
||||
addBuildPlugin(PrehydrateTransformPlugin({ sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client }))
|
||||
|
||||
if (nuxt.options.experimental.localLayerAliases) {
|
||||
// Add layer aliasing support for ~, ~~, @ and @@ aliases
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { createUnplugin } from 'unplugin'
|
||||
import { isAbsolute, relative } from 'pathe'
|
||||
import type { Node } from 'estree-walker'
|
||||
import { walk } from 'estree-walker'
|
||||
import MagicString from 'magic-string'
|
||||
import { hash } from 'ohash'
|
||||
import type { CallExpression, Pattern } from 'estree'
|
||||
import type { Pattern } from 'estree'
|
||||
import { parseQuery, parseURL } from 'ufo'
|
||||
import escapeRE from 'escape-string-regexp'
|
||||
import { findStaticImports, parseStaticImport } from 'mlly'
|
||||
import { parseAndWalk, walk } from '../../core/utils/parse'
|
||||
|
||||
import { matchWithStringOrRegex } from '../utils/plugins'
|
||||
|
||||
interface ComposableKeysOptions {
|
||||
@ -52,23 +52,18 @@ export const ComposableKeysPlugin = (options: ComposableKeysOptions) => createUn
|
||||
const relativeID = isAbsolute(id) ? relative(options.rootDir, id) : id
|
||||
const { pathname: relativePathname } = parseURL(relativeID)
|
||||
|
||||
const ast = this.parse(script, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
}) as Node
|
||||
|
||||
// To handle variables hoisting we need a pre-pass to collect variable and function declarations with scope info.
|
||||
let scopeTracker = new ScopeTracker()
|
||||
const varCollector = new ScopedVarsCollector()
|
||||
walk(ast, {
|
||||
enter (_node) {
|
||||
if (_node.type === 'BlockStatement') {
|
||||
const ast = parseAndWalk(script, id, {
|
||||
enter (node) {
|
||||
if (node.type === 'BlockStatement') {
|
||||
scopeTracker.enterScope()
|
||||
varCollector.refresh(scopeTracker.curScopeKey)
|
||||
} else if (_node.type === 'FunctionDeclaration' && _node.id) {
|
||||
varCollector.addVar(_node.id.name)
|
||||
} else if (_node.type === 'VariableDeclarator') {
|
||||
varCollector.collect(_node.id)
|
||||
} else if (node.type === 'FunctionDeclaration' && node.id) {
|
||||
varCollector.addVar(node.id.name)
|
||||
} else if (node.type === 'VariableDeclarator') {
|
||||
varCollector.collect(node.id)
|
||||
}
|
||||
},
|
||||
leave (_node) {
|
||||
@ -81,13 +76,12 @@ export const ComposableKeysPlugin = (options: ComposableKeysOptions) => createUn
|
||||
|
||||
scopeTracker = new ScopeTracker()
|
||||
walk(ast, {
|
||||
enter (_node) {
|
||||
if (_node.type === 'BlockStatement') {
|
||||
enter (node) {
|
||||
if (node.type === 'BlockStatement') {
|
||||
scopeTracker.enterScope()
|
||||
}
|
||||
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
|
||||
const node: CallExpression = _node as CallExpression
|
||||
const name = 'name' in node.callee && node.callee.name
|
||||
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
|
||||
const name = node.callee.name
|
||||
if (!name || !keyedFunctions.has(name) || node.arguments.length >= maxLength) { return }
|
||||
|
||||
imports = imports || detectImportNames(script, composableMeta)
|
||||
@ -219,24 +213,23 @@ class ScopedVarsCollector {
|
||||
return false
|
||||
}
|
||||
|
||||
collect (n: Pattern) {
|
||||
const t = n.type
|
||||
if (t === 'Identifier') {
|
||||
this.addVar(n.name)
|
||||
} else if (t === 'RestElement') {
|
||||
this.collect(n.argument)
|
||||
} else if (t === 'AssignmentPattern') {
|
||||
this.collect(n.left)
|
||||
} else if (t === 'ArrayPattern') {
|
||||
n.elements.forEach(e => e && this.collect(e))
|
||||
} else if (t === 'ObjectPattern') {
|
||||
n.properties.forEach((p) => {
|
||||
if (p.type === 'RestElement') {
|
||||
this.collect(p)
|
||||
} else {
|
||||
this.collect(p.value)
|
||||
collect (pattern: Pattern) {
|
||||
if (pattern.type === 'Identifier') {
|
||||
this.addVar(pattern.name)
|
||||
} else if (pattern.type === 'RestElement') {
|
||||
this.collect(pattern.argument)
|
||||
} else if (pattern.type === 'AssignmentPattern') {
|
||||
this.collect(pattern.left)
|
||||
} else if (pattern.type === 'ArrayPattern') {
|
||||
for (const element of pattern.elements) {
|
||||
if (element) {
|
||||
this.collect(element.type === 'RestElement' ? element.argument : element)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else if (pattern.type === 'ObjectPattern') {
|
||||
for (const property of pattern.properties) {
|
||||
this.collect(property.type === 'RestElement' ? property.argument : property.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,5 @@
|
||||
import type { CallExpression, Literal, Property, SpreadElement } from 'estree'
|
||||
import type { Node } from 'estree-walker'
|
||||
import { walk } from 'estree-walker'
|
||||
import type { Literal, Property, SpreadElement } from 'estree'
|
||||
import { transform } from 'esbuild'
|
||||
import { parse } from 'acorn'
|
||||
import { defu } from 'defu'
|
||||
import { findExports } from 'mlly'
|
||||
import type { Nuxt } from '@nuxt/schema'
|
||||
@ -11,6 +8,8 @@ import MagicString from 'magic-string'
|
||||
import { normalize } from 'pathe'
|
||||
import { logger } from '@nuxt/kit'
|
||||
|
||||
import { parseAndWalk, withLocations } from '../../core/utils/parse'
|
||||
|
||||
import type { ObjectPlugin, PluginMeta } from '#app'
|
||||
|
||||
const internalOrderMap = {
|
||||
@ -47,36 +46,31 @@ export async function extractMetadata (code: string, loader = 'ts' as 'ts' | 'ts
|
||||
return metaCache[code]
|
||||
}
|
||||
const js = await transform(code, { loader })
|
||||
walk(parse(js.code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
}) as Node, {
|
||||
enter (_node) {
|
||||
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
|
||||
const node = _node as CallExpression & { start: number, end: number }
|
||||
const name = 'name' in node.callee && node.callee.name
|
||||
if (name !== 'defineNuxtPlugin' && name !== 'definePayloadPlugin') { return }
|
||||
parseAndWalk(js.code, `file.${loader}`, (node) => {
|
||||
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
|
||||
|
||||
if (name === 'definePayloadPlugin') {
|
||||
meta.order = internalOrderMap['user-revivers']
|
||||
const name = 'name' in node.callee && node.callee.name
|
||||
if (name !== 'defineNuxtPlugin' && name !== 'definePayloadPlugin') { return }
|
||||
|
||||
if (name === 'definePayloadPlugin') {
|
||||
meta.order = internalOrderMap['user-revivers']
|
||||
}
|
||||
|
||||
const metaArg = node.arguments[1]
|
||||
if (metaArg) {
|
||||
if (metaArg.type !== 'ObjectExpression') {
|
||||
throw new Error('Invalid plugin metadata')
|
||||
}
|
||||
meta = extractMetaFromObject(metaArg.properties)
|
||||
}
|
||||
|
||||
const metaArg = node.arguments[1]
|
||||
if (metaArg) {
|
||||
if (metaArg.type !== 'ObjectExpression') {
|
||||
throw new Error('Invalid plugin metadata')
|
||||
}
|
||||
meta = extractMetaFromObject(metaArg.properties)
|
||||
}
|
||||
const plugin = node.arguments[0]
|
||||
if (plugin?.type === 'ObjectExpression') {
|
||||
meta = defu(extractMetaFromObject(plugin.properties), meta)
|
||||
}
|
||||
|
||||
const plugin = node.arguments[0]
|
||||
if (plugin?.type === 'ObjectExpression') {
|
||||
meta = defu(extractMetaFromObject(plugin.properties), meta)
|
||||
}
|
||||
|
||||
meta.order = meta.order || orderMap[meta.enforce || 'default'] || orderMap.default
|
||||
delete meta.enforce
|
||||
},
|
||||
meta.order = meta.order || orderMap[meta.enforce || 'default'] || orderMap.default
|
||||
delete meta.enforce
|
||||
})
|
||||
metaCache[code] = meta
|
||||
return meta as Omit<PluginMeta, 'enforce'>
|
||||
@ -149,41 +143,33 @@ export const RemovePluginMetadataPlugin = (nuxt: Nuxt) => createUnplugin(() => {
|
||||
const wrapperNames = new Set(['defineNuxtPlugin', 'definePayloadPlugin'])
|
||||
|
||||
try {
|
||||
walk(this.parse(code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
}) as Node, {
|
||||
enter (_node) {
|
||||
if (_node.type === 'ImportSpecifier' && _node.imported.type === 'Identifier' && (_node.imported.name === 'defineNuxtPlugin' || _node.imported.name === 'definePayloadPlugin')) {
|
||||
wrapperNames.add(_node.local.name)
|
||||
}
|
||||
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
|
||||
const node = _node as CallExpression & { start: number, end: number }
|
||||
const name = 'name' in node.callee && node.callee.name
|
||||
if (!name || !wrapperNames.has(name)) { return }
|
||||
wrapped = true
|
||||
parseAndWalk(code, id, (node) => {
|
||||
if (node.type === 'ImportSpecifier' && node.imported.type === 'Identifier' && (node.imported.name === 'defineNuxtPlugin' || node.imported.name === 'definePayloadPlugin')) {
|
||||
wrapperNames.add(node.local.name)
|
||||
}
|
||||
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
|
||||
|
||||
// Remove metadata that already has been extracted
|
||||
if (!('order' in plugin) && !('name' in plugin)) { return }
|
||||
for (const [argIndex, _arg] of node.arguments.entries()) {
|
||||
if (_arg.type !== 'ObjectExpression') { continue }
|
||||
const name = 'name' in node.callee && node.callee.name
|
||||
if (!name || !wrapperNames.has(name)) { return }
|
||||
wrapped = true
|
||||
|
||||
const arg = _arg as typeof _arg & { start: number, end: number }
|
||||
for (const [propertyIndex, _property] of arg.properties.entries()) {
|
||||
if (_property.type === 'SpreadElement' || !('name' in _property.key)) { continue }
|
||||
// Remove metadata that already has been extracted
|
||||
if (!('order' in plugin) && !('name' in plugin)) { return }
|
||||
for (const [argIndex, arg] of node.arguments.entries()) {
|
||||
if (arg.type !== 'ObjectExpression') { continue }
|
||||
|
||||
const property = _property as typeof _property & { start: number, end: number }
|
||||
const propertyKey = _property.key.name
|
||||
if (propertyKey === 'order' || propertyKey === 'enforce' || propertyKey === 'name') {
|
||||
const _nextNode = arg.properties[propertyIndex + 1] || node.arguments[argIndex + 1]
|
||||
const nextNode = _nextNode as typeof _nextNode & { start: number, end: number }
|
||||
const nextIndex = nextNode?.start || (arg.end - 1)
|
||||
for (const [propertyIndex, property] of arg.properties.entries()) {
|
||||
if (property.type === 'SpreadElement' || !('name' in property.key)) { continue }
|
||||
|
||||
s.remove(property.start, nextIndex)
|
||||
}
|
||||
const propertyKey = property.key.name
|
||||
if (propertyKey === 'order' || propertyKey === 'enforce' || propertyKey === 'name') {
|
||||
const nextNode = arg.properties[propertyIndex + 1] || node.arguments[argIndex + 1]
|
||||
const nextIndex = withLocations(nextNode)?.start || (withLocations(arg).end - 1)
|
||||
|
||||
s.remove(withLocations(property).start, nextIndex)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
|
@ -1,16 +1,12 @@
|
||||
import { transform } from 'esbuild'
|
||||
import { parse } from 'acorn'
|
||||
import { walk } from 'estree-walker'
|
||||
import type { Node } from 'estree-walker'
|
||||
import type { Nuxt } from '@nuxt/schema'
|
||||
import { createUnplugin } from 'unplugin'
|
||||
import type { SimpleCallExpression } from 'estree'
|
||||
import MagicString from 'magic-string'
|
||||
|
||||
import { hash } from 'ohash'
|
||||
|
||||
import { parseAndWalk, withLocations } from '../../core/utils/parse'
|
||||
import { isJS, isVue } from '../utils'
|
||||
|
||||
export function prehydrateTransformPlugin (nuxt: Nuxt) {
|
||||
export function PrehydrateTransformPlugin (options: { sourcemap?: boolean } = {}) {
|
||||
return createUnplugin(() => ({
|
||||
name: 'nuxt:prehydrate-transform',
|
||||
transformInclude (id) {
|
||||
@ -22,33 +18,27 @@ export function prehydrateTransformPlugin (nuxt: Nuxt) {
|
||||
const s = new MagicString(code)
|
||||
const promises: Array<Promise<any>> = []
|
||||
|
||||
walk(parse(code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
ranges: true,
|
||||
}) as Node, {
|
||||
enter (_node) {
|
||||
if (_node.type !== 'CallExpression' || _node.callee.type !== 'Identifier') { return }
|
||||
const node = _node as SimpleCallExpression & { start: number, end: number }
|
||||
const name = 'name' in node.callee && node.callee.name
|
||||
if (name === 'onPrehydrate') {
|
||||
if (!node.arguments[0]) { return }
|
||||
if (node.arguments[0].type !== 'ArrowFunctionExpression' && node.arguments[0].type !== 'FunctionExpression') { return }
|
||||
parseAndWalk(code, id, (node) => {
|
||||
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') {
|
||||
return
|
||||
}
|
||||
if (node.callee.name === 'onPrehydrate') {
|
||||
const callback = withLocations(node.arguments[0])
|
||||
if (!callback) { return }
|
||||
if (callback.type !== 'ArrowFunctionExpression' && callback.type !== 'FunctionExpression') { return }
|
||||
|
||||
const needsAttr = node.arguments[0].params.length > 0
|
||||
const { start, end } = node.arguments[0] as Node & { start: number, end: number }
|
||||
const needsAttr = callback.params.length > 0
|
||||
|
||||
const p = transform(`forEach(${code.slice(start, end)})`, { loader: 'ts', minify: true })
|
||||
promises.push(p.then(({ code: result }) => {
|
||||
const cleaned = result.slice('forEach'.length).replace(/;\s+$/, '')
|
||||
const args = [JSON.stringify(cleaned)]
|
||||
if (needsAttr) {
|
||||
args.push(JSON.stringify(hash(result)))
|
||||
}
|
||||
s.overwrite(start, end, args.join(', '))
|
||||
}))
|
||||
}
|
||||
},
|
||||
const p = transform(`forEach(${code.slice(callback.start, callback.end)})`, { loader: 'ts', minify: true })
|
||||
promises.push(p.then(({ code: result }) => {
|
||||
const cleaned = result.slice('forEach'.length).replace(/;\s+$/, '')
|
||||
const args = [JSON.stringify(cleaned)]
|
||||
if (needsAttr) {
|
||||
args.push(JSON.stringify(hash(result)))
|
||||
}
|
||||
s.overwrite(callback.start, callback.end, args.join(', '))
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises).catch((e) => {
|
||||
@ -58,7 +48,7 @@ export function prehydrateTransformPlugin (nuxt: Nuxt) {
|
||||
if (s.hasChanged()) {
|
||||
return {
|
||||
code: s.toString(),
|
||||
map: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client
|
||||
map: options.sourcemap
|
||||
? s.generateMap({ hires: true })
|
||||
: undefined,
|
||||
}
|
||||
|
33
packages/nuxt/src/core/utils/parse.ts
Normal file
33
packages/nuxt/src/core/utils/parse.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { walk as _walk } from 'estree-walker'
|
||||
import type { Node, SyncHandler } from 'estree-walker'
|
||||
import type { Program as ESTreeProgram } from 'estree'
|
||||
import { parse } from 'acorn'
|
||||
import type { Program } from 'acorn'
|
||||
|
||||
export type { Node }
|
||||
|
||||
type WithLocations<T> = T & { start: number, end: number }
|
||||
type WalkerCallback = (this: ThisParameterType<SyncHandler>, node: WithLocations<Node>, parent: WithLocations<Node> | null, ctx: { key: string | number | symbol | null | undefined, index: number | null | undefined, ast: Program | Node }) => void
|
||||
|
||||
export function walk (ast: Program | Node, callback: { enter?: WalkerCallback, leave?: WalkerCallback }) {
|
||||
return _walk(ast as unknown as ESTreeProgram | Node, {
|
||||
enter (node, parent, key, index) {
|
||||
callback.enter?.call(this, node as WithLocations<Node>, parent as WithLocations<Node> | null, { key, index, ast })
|
||||
},
|
||||
leave (node, parent, key, index) {
|
||||
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: { enter?: WalkerCallback, leave?: WalkerCallback }): Program
|
||||
export function parseAndWalk (code: string, _sourceFilename: string, callback: { enter?: WalkerCallback, leave?: WalkerCallback } | WalkerCallback) {
|
||||
const ast = parse (code, { sourceType: 'module', ecmaVersion: 'latest', locations: true })
|
||||
walk(ast, typeof callback === 'function' ? { enter: callback } : callback)
|
||||
return ast
|
||||
}
|
||||
|
||||
export function withLocations<T> (node: T): WithLocations<T> {
|
||||
return node as WithLocations<T>
|
||||
}
|
@ -379,7 +379,7 @@ export default defineNuxtModule({
|
||||
const glob = pageToGlobMap[path]
|
||||
const code = path in nuxt.vfs ? nuxt.vfs[path]! : await readFile(path!, 'utf-8')
|
||||
try {
|
||||
const extractedRule = await extractRouteRules(code)
|
||||
const extractedRule = await extractRouteRules(code, path)
|
||||
if (extractedRule) {
|
||||
if (!glob) {
|
||||
const relativePath = relative(nuxt.options.srcDir, path)
|
||||
|
@ -3,13 +3,13 @@ import { createUnplugin } from 'unplugin'
|
||||
import { parseQuery, parseURL } from 'ufo'
|
||||
import type { StaticImport } from 'mlly'
|
||||
import { findExports, findStaticImports, parseStaticImport } from 'mlly'
|
||||
import type { CallExpression, Expression, Identifier } from 'estree'
|
||||
import type { Node } from 'estree-walker'
|
||||
import { walk } from 'estree-walker'
|
||||
import MagicString from 'magic-string'
|
||||
import { isAbsolute } from 'pathe'
|
||||
import { logger } from '@nuxt/kit'
|
||||
|
||||
import { parseAndWalk, withLocations } from '../../core/utils/parse'
|
||||
|
||||
interface PageMetaPluginOptions {
|
||||
dev?: boolean
|
||||
sourcemap?: boolean
|
||||
@ -36,7 +36,7 @@ if (import.meta.webpackHot) {
|
||||
})
|
||||
}`
|
||||
|
||||
export const PageMetaPlugin = (options: PageMetaPluginOptions) => createUnplugin(() => {
|
||||
export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnplugin(() => {
|
||||
return {
|
||||
name: 'nuxt:pages-macros-transform',
|
||||
enforce: 'post',
|
||||
@ -112,45 +112,38 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions) => createUnplugin
|
||||
}
|
||||
}
|
||||
|
||||
walk(this.parse(code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
}) as Node, {
|
||||
enter (_node) {
|
||||
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
|
||||
const node = _node as CallExpression & { start: number, end: number }
|
||||
const name = 'name' in node.callee && node.callee.name
|
||||
if (name !== 'definePageMeta') { return }
|
||||
parseAndWalk(code, id, (node) => {
|
||||
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
|
||||
if (!('name' in node.callee) || node.callee.name !== 'definePageMeta') { return }
|
||||
|
||||
const meta = node.arguments[0] as Expression & { start: number, end: number }
|
||||
const meta = withLocations(node.arguments[0])
|
||||
|
||||
let contents = `const __nuxt_page_meta = ${code!.slice(meta.start, meta.end) || 'null'}\nexport default __nuxt_page_meta` + (options.dev ? CODE_HMR : '')
|
||||
if (!meta) { return }
|
||||
|
||||
function addImport (name: string | false) {
|
||||
if (name && importMap.has(name)) {
|
||||
const importValue = importMap.get(name)!.code
|
||||
if (!addedImports.has(importValue)) {
|
||||
contents = importMap.get(name)!.code + '\n' + contents
|
||||
addedImports.add(importValue)
|
||||
}
|
||||
let contents = `const __nuxt_page_meta = ${code!.slice(meta.start, meta.end) || 'null'}\nexport default __nuxt_page_meta` + (options.dev ? CODE_HMR : '')
|
||||
|
||||
function addImport (name: string | false) {
|
||||
if (name && importMap.has(name)) {
|
||||
const importValue = importMap.get(name)!.code
|
||||
if (!addedImports.has(importValue)) {
|
||||
contents = importMap.get(name)!.code + '\n' + contents
|
||||
addedImports.add(importValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(meta, {
|
||||
enter (_node) {
|
||||
if (_node.type === 'CallExpression') {
|
||||
const node = _node as CallExpression & { start: number, end: number }
|
||||
addImport('name' in node.callee && node.callee.name)
|
||||
}
|
||||
if (_node.type === 'Identifier') {
|
||||
const node = _node as Identifier & { start: number, end: number }
|
||||
addImport(node.name)
|
||||
}
|
||||
},
|
||||
})
|
||||
walk(meta, {
|
||||
enter (node) {
|
||||
if (node.type === 'CallExpression' && 'name' in node.callee) {
|
||||
addImport(node.callee.name)
|
||||
}
|
||||
if (node.type === 'Identifier') {
|
||||
addImport(node.name)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
s.overwrite(0, code.length, contents)
|
||||
},
|
||||
s.overwrite(0, code.length, contents)
|
||||
})
|
||||
|
||||
if (!s.hasChanged() && !code.includes('__nuxt_page_meta')) {
|
||||
|
@ -1,48 +1,44 @@
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import type { Node } from 'estree-walker'
|
||||
import type { CallExpression } from 'estree'
|
||||
import { walk } from 'estree-walker'
|
||||
import { transform } from 'esbuild'
|
||||
import { parse } from 'acorn'
|
||||
import type { NuxtPage } from '@nuxt/schema'
|
||||
import type { NitroRouteConfig } from 'nitro/types'
|
||||
import { normalize } from 'pathe'
|
||||
|
||||
import { getLoader } from '../core/utils'
|
||||
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): Promise<NitroRouteConfig | null> {
|
||||
export async function extractRouteRules (code: string, path: string): Promise<NitroRouteConfig | null> {
|
||||
if (code in ruleCache) {
|
||||
return ruleCache[code] || null
|
||||
}
|
||||
if (!ROUTE_RULE_RE.test(code)) { return null }
|
||||
|
||||
let rule: NitroRouteConfig | null = null
|
||||
const contents = extractScriptContent(code)
|
||||
const loader = getLoader(path)
|
||||
if (!loader) { return null }
|
||||
|
||||
const contents = loader === 'vue' ? extractScriptContent(code) : [{ code, loader }]
|
||||
for (const script of contents) {
|
||||
if (rule) { break }
|
||||
|
||||
code = script?.code || code
|
||||
|
||||
const js = await transform(code, { loader: script?.loader || 'ts' })
|
||||
walk(parse(js.code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
}) as Node, {
|
||||
enter (_node) {
|
||||
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
|
||||
const node = _node as CallExpression & { start: number, end: number }
|
||||
const name = 'name' in node.callee && node.callee.name
|
||||
if (name === 'defineRouteRules') {
|
||||
const rulesString = js.code.slice(node.start, node.end)
|
||||
try {
|
||||
rule = JSON.parse(runInNewContext(rulesString.replace('defineRouteRules', 'JSON.stringify'), {}))
|
||||
} catch {
|
||||
throw new Error('[nuxt] Error parsing route rules. They should be JSON-serializable.')
|
||||
}
|
||||
|
||||
parseAndWalk(js.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)
|
||||
try {
|
||||
rule = JSON.parse(runInNewContext(rulesString.replace('defineRouteRules', 'JSON.stringify'), {}))
|
||||
} catch {
|
||||
throw new Error('[nuxt] Error parsing route rules. They should be JSON-serializable.')
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -8,11 +8,10 @@ import escapeRE from 'escape-string-regexp'
|
||||
import { filename } from 'pathe/utils'
|
||||
import { hash } from 'ohash'
|
||||
import { transform } from 'esbuild'
|
||||
import { parse } from 'acorn'
|
||||
import { walk } from 'estree-walker'
|
||||
import type { CallExpression, ExpressionStatement, ObjectExpression, Program, Property } from 'estree'
|
||||
import type { Property } from 'estree'
|
||||
import type { NuxtPage } from 'nuxt/schema'
|
||||
|
||||
import { parseAndWalk } from '../core/utils/parse'
|
||||
import { getLoader, uniqueBy } from '../core/utils'
|
||||
import { toArray } from '../utils'
|
||||
|
||||
@ -184,9 +183,9 @@ export async function augmentPages (routes: NuxtPage[], vfs: Record<string, stri
|
||||
}
|
||||
|
||||
const SFC_SCRIPT_RE = /<script(?<attrs>[^>]*)>(?<content>[\s\S]*?)<\/script[^>]*>/gi
|
||||
export function extractScriptContent (html: string) {
|
||||
export function extractScriptContent (sfc: string) {
|
||||
const contents: Array<{ loader: 'tsx' | 'ts', code: string }> = []
|
||||
for (const match of html.matchAll(SFC_SCRIPT_RE)) {
|
||||
for (const match of sfc.matchAll(SFC_SCRIPT_RE)) {
|
||||
if (match?.groups?.content) {
|
||||
contents.push({
|
||||
loader: match.groups.attrs?.includes('tsx') ? 'tsx' : 'ts',
|
||||
@ -222,7 +221,7 @@ export async function getRouteMeta (contents: string, absolutePath: string, extr
|
||||
return {}
|
||||
}
|
||||
|
||||
const extractedMeta = {} as Partial<Record<keyof NuxtPage, any>>
|
||||
const extractedMeta: Partial<Record<keyof NuxtPage, any>> = {}
|
||||
|
||||
const extractionKeys = new Set<keyof NuxtPage>([...defaultExtractionKeys, ...extraExtractionKeys as Array<keyof NuxtPage>])
|
||||
|
||||
@ -232,85 +231,79 @@ export async function getRouteMeta (contents: string, absolutePath: string, extr
|
||||
}
|
||||
|
||||
const js = await transform(script.code, { loader: script.loader })
|
||||
const ast = parse(js.code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
ranges: true,
|
||||
}) as unknown as Program
|
||||
|
||||
const dynamicProperties = new Set<keyof NuxtPage>()
|
||||
|
||||
let foundMeta = false
|
||||
|
||||
walk(ast, {
|
||||
enter (node) {
|
||||
if (foundMeta) { return }
|
||||
parseAndWalk(js.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 }
|
||||
if (node.type !== 'ExpressionStatement' || node.expression.type !== 'CallExpression' || node.expression.callee.type !== 'Identifier' || node.expression.callee.name !== 'definePageMeta') { return }
|
||||
|
||||
foundMeta = true
|
||||
const pageMetaArgument = ((node as ExpressionStatement).expression as CallExpression).arguments[0] as ObjectExpression
|
||||
foundMeta = true
|
||||
const pageMetaArgument = node.expression.arguments[0]
|
||||
if (pageMetaArgument?.type !== 'ObjectExpression') { return }
|
||||
|
||||
for (const key of extractionKeys) {
|
||||
const property = pageMetaArgument.properties.find(property => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === key) as Property
|
||||
if (!property) { continue }
|
||||
for (const key of extractionKeys) {
|
||||
const property = pageMetaArgument.properties.find((property): property is Property => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === key)
|
||||
if (!property) { continue }
|
||||
|
||||
if (property.value.type === 'ObjectExpression') {
|
||||
const valueString = js.code.slice(property.value.range![0], property.value.range![1])
|
||||
try {
|
||||
extractedMeta[key] = JSON.parse(runInNewContext(`JSON.stringify(${valueString})`, {}))
|
||||
} catch {
|
||||
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not JSON-serializable (reading \`${absolutePath}\`).`)
|
||||
dynamicProperties.add(key)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (property.value.type === 'ArrayExpression') {
|
||||
const values: string[] = []
|
||||
for (const element of property.value.elements) {
|
||||
if (!element) {
|
||||
continue
|
||||
}
|
||||
if (element.type !== 'Literal' || typeof element.value !== 'string') {
|
||||
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not an array of string literals (reading \`${absolutePath}\`).`)
|
||||
dynamicProperties.add(key)
|
||||
continue
|
||||
}
|
||||
values.push(element.value)
|
||||
}
|
||||
extractedMeta[key] = values
|
||||
continue
|
||||
}
|
||||
|
||||
if (property.value.type !== 'Literal' || (typeof property.value.value !== 'string' && typeof property.value.value !== 'boolean')) {
|
||||
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`)
|
||||
if (property.value.type === 'ObjectExpression') {
|
||||
const valueString = js.code.slice(property.value.range![0], property.value.range![1])
|
||||
try {
|
||||
extractedMeta[key] = JSON.parse(runInNewContext(`JSON.stringify(${valueString})`, {}))
|
||||
} catch {
|
||||
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not JSON-serializable (reading \`${absolutePath}\`).`)
|
||||
dynamicProperties.add(key)
|
||||
continue
|
||||
}
|
||||
extractedMeta[key] = property.value.value
|
||||
}
|
||||
|
||||
for (const property of pageMetaArgument.properties) {
|
||||
if (property.type !== 'Property') {
|
||||
continue
|
||||
}
|
||||
const isIdentifierOrLiteral = property.key.type === 'Literal' || property.key.type === 'Identifier'
|
||||
if (!isIdentifierOrLiteral) {
|
||||
continue
|
||||
}
|
||||
const name = property.key.type === 'Identifier' ? property.key.name : String(property.value)
|
||||
if (!extractionKeys.has(name as keyof NuxtPage)) {
|
||||
dynamicProperties.add('meta')
|
||||
break
|
||||
if (property.value.type === 'ArrayExpression') {
|
||||
const values: string[] = []
|
||||
for (const element of property.value.elements) {
|
||||
if (!element) {
|
||||
continue
|
||||
}
|
||||
if (element.type !== 'Literal' || typeof element.value !== 'string') {
|
||||
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not an array of string literals (reading \`${absolutePath}\`).`)
|
||||
dynamicProperties.add(key)
|
||||
continue
|
||||
}
|
||||
values.push(element.value)
|
||||
}
|
||||
extractedMeta[key] = values
|
||||
continue
|
||||
}
|
||||
|
||||
if (dynamicProperties.size) {
|
||||
extractedMeta.meta ??= {}
|
||||
extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties
|
||||
if (property.value.type !== 'Literal' || (typeof property.value.value !== 'string' && typeof property.value.value !== 'boolean')) {
|
||||
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`)
|
||||
dynamicProperties.add(key)
|
||||
continue
|
||||
}
|
||||
},
|
||||
extractedMeta[key] = property.value.value
|
||||
}
|
||||
|
||||
for (const property of pageMetaArgument.properties) {
|
||||
if (property.type !== 'Property') {
|
||||
continue
|
||||
}
|
||||
const isIdentifierOrLiteral = property.key.type === 'Literal' || property.key.type === 'Identifier'
|
||||
if (!isIdentifierOrLiteral) {
|
||||
continue
|
||||
}
|
||||
const name = property.key.type === 'Identifier' ? property.key.name : String(property.value)
|
||||
if (!extractionKeys.has(name as keyof NuxtPage)) {
|
||||
dynamicProperties.add('meta')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (dynamicProperties.size) {
|
||||
extractedMeta.meta ??= {}
|
||||
extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
50
packages/nuxt/test/component-names.test.ts
Normal file
50
packages/nuxt/test/component-names.test.ts
Normal file
@ -0,0 +1,50 @@
|
||||
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'
|
||||
|
||||
describe('component names', () => {
|
||||
const components = [{
|
||||
filePath: 'test.ts',
|
||||
pascalName: 'TestMe',
|
||||
}] as [Component]
|
||||
|
||||
const transformPlugin = ComponentNamePlugin({ sourcemap: false, getComponents: () => components }).raw({}, {} as any) as { transform: (code: string, id: string) => { code: string } | null }
|
||||
|
||||
it('should add correct default component names', () => {
|
||||
const sfc = `
|
||||
<script setup>
|
||||
onMounted(() => {
|
||||
window.a = 32
|
||||
})
|
||||
</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) ?? {}
|
||||
expect(code?.trim()).toMatchInlineSnapshot(`
|
||||
"export default Object.assign({
|
||||
setup(__props, { expose: __expose }) {
|
||||
__expose();
|
||||
|
||||
onMounted(() => {
|
||||
window.a = 32
|
||||
})
|
||||
|
||||
const __returned__ = { }
|
||||
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
|
||||
return __returned__
|
||||
}
|
||||
|
||||
}, { __name: "TestMe" })"
|
||||
`)
|
||||
})
|
||||
})
|
@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import * as Parser from 'acorn'
|
||||
|
||||
import { detectImportNames } from '../src/core/plugins/composable-keys'
|
||||
import { ComposableKeysPlugin, detectImportNames } from '../src/core/plugins/composable-keys'
|
||||
|
||||
describe('detectImportNames', () => {
|
||||
const keyedComposables = {
|
||||
@ -25,3 +26,57 @@ describe('detectImportNames', () => {
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('composable keys plugin', () => {
|
||||
const composables = [{
|
||||
name: 'useAsyncData',
|
||||
source: '#app',
|
||||
argumentLength: 2,
|
||||
}]
|
||||
const transformPlugin = ComposableKeysPlugin({ sourcemap: false, rootDir: '/', composables }).raw({}, {} as any) as { transform: (code: string, id: string) => { code: string } | null }
|
||||
|
||||
it('should add keyed hash when there is none already provided', () => {
|
||||
const code = `
|
||||
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(`
|
||||
"import { useAsyncData } from '#app'
|
||||
useAsyncData(() => {}, '$yXewDLZblH')"
|
||||
`)
|
||||
})
|
||||
|
||||
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`)
|
||||
})
|
||||
|
||||
it('should not add hash composables is imported from somewhere else', () => {
|
||||
const code = `
|
||||
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`)
|
||||
})
|
||||
})
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { compileScript, parse } from '@vue/compiler-sfc'
|
||||
import * as Parser from 'acorn'
|
||||
|
||||
import { PageMetaPlugin } from '../src/pages/plugins/page-meta'
|
||||
import { getRouteMeta, normalizeRoutes } from '../src/pages/utils'
|
||||
import type { NuxtPage } from '../schema'
|
||||
|
||||
@ -20,6 +24,43 @@ describe('page metadata', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('should parse JSX files', async () => {
|
||||
const fileContents = `
|
||||
export default {
|
||||
setup () {
|
||||
definePageMeta({ name: 'bar' })
|
||||
return () => <div></div>
|
||||
}
|
||||
}
|
||||
`
|
||||
const meta = await getRouteMeta(fileContents, `/app/pages/index.jsx`)
|
||||
expect(meta).toStrictEqual({
|
||||
name: 'bar',
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: https://github.com/nuxt/nuxt/pull/30066
|
||||
it.todo('should handle experimental decorators', async () => {
|
||||
const fileContents = `
|
||||
<script setup lang="ts">
|
||||
function something (_method: () => unknown) {
|
||||
return () => 'decorated'
|
||||
}
|
||||
class SomeClass {
|
||||
@something
|
||||
public someMethod () {
|
||||
return 'initial'
|
||||
}
|
||||
}
|
||||
definePageMeta({ name: 'bar' })
|
||||
</script>
|
||||
`
|
||||
const meta = await getRouteMeta(fileContents, `/app/pages/index.vue`)
|
||||
expect(meta).toStrictEqual({
|
||||
name: 'bar',
|
||||
})
|
||||
})
|
||||
|
||||
it('should use and invalidate cache', async () => {
|
||||
const fileContents = `<script setup>definePageMeta({ foo: 'bar' })</script>`
|
||||
const meta = await getRouteMeta(fileContents, filePath)
|
||||
@ -240,3 +281,33 @@ describe('normalizeRoutes', () => {
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('rewrite page meta', () => {
|
||||
const transformPlugin = PageMetaPlugin().raw({}, {} as any) as { transform: (code: string, id: string) => { code: string } | null }
|
||||
|
||||
it('should extract metadata from vue components', () => {
|
||||
const sfc = `
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
name: 'hi',
|
||||
other: 'value'
|
||||
})
|
||||
</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(`
|
||||
"const __nuxt_page_meta = {
|
||||
name: 'hi',
|
||||
other: 'value'
|
||||
}
|
||||
export default __nuxt_page_meta"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { parse } from 'acorn'
|
||||
import * as Parser from 'acorn'
|
||||
|
||||
import { RemovePluginMetadataPlugin, extractMetadata } from '../src/core/plugins/plugin-metadata'
|
||||
import { checkForCircularDependencies } from '../src/core/app'
|
||||
@ -40,7 +40,14 @@ describe('plugin-metadata', () => {
|
||||
'export const plugin = {}',
|
||||
]
|
||||
for (const plugin of invalidPlugins) {
|
||||
expect(transformPlugin.transform.call({ parse }, plugin, 'my-plugin.mjs').code).toBe('export default () => {}')
|
||||
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 () => {}')
|
||||
}
|
||||
})
|
||||
|
||||
@ -52,7 +59,14 @@ describe('plugin-metadata', () => {
|
||||
setup: () => {},
|
||||
}, { order: 10, name: test })
|
||||
`
|
||||
expect(transformPlugin.transform.call({ parse }, plugin, 'my-plugin.mjs').code).toMatchInlineSnapshot(`
|
||||
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(`
|
||||
"
|
||||
export default defineNuxtPlugin({
|
||||
setup: () => {},
|
||||
|
40
packages/nuxt/test/prehydrate.test.ts
Normal file
40
packages/nuxt/test/prehydrate.test.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { PrehydrateTransformPlugin } from '../src/core/plugins/prehydrate'
|
||||
|
||||
describe('prehydrate', () => {
|
||||
const transformPlugin = PrehydrateTransformPlugin().raw({}, {} as any) as { transform: (code: string, id: string) => Promise<{ code: string } | null> }
|
||||
|
||||
it('should extract and minify code in onPrehydrate', async () => {
|
||||
const snippet = `
|
||||
onPrehydrate(() => {
|
||||
console.log('hello world')
|
||||
})
|
||||
`
|
||||
const snippet2 = `
|
||||
export default {
|
||||
async setup () {
|
||||
onPrehydrate(() => {
|
||||
console.log('hello world')
|
||||
})
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
for (const item of [snippet, snippet2]) {
|
||||
const { code } = await transformPlugin.transform(item, 'test.ts') ?? {}
|
||||
expect(code).toContain(`onPrehydrate("(()=>{console.log(\\"hello world\\")})")`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should add hash if required', async () => {
|
||||
const snippet = `
|
||||
onPrehydrate((attr) => {
|
||||
console.log('hello world')
|
||||
})
|
||||
`
|
||||
|
||||
const { code } = await transformPlugin.transform(snippet, 'test.ts') ?? {}
|
||||
expect(code?.trim()).toMatchInlineSnapshot(`"onPrehydrate("(o=>{console.log(\\"hello world\\")})", "rifMBArY0d")"`)
|
||||
})
|
||||
})
|
56
packages/nuxt/test/route-rules.test.ts
Normal file
56
packages/nuxt/test/route-rules.test.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { extractRouteRules } from '../src/pages/route-rules'
|
||||
|
||||
describe('route-rules', () => {
|
||||
it('should extract route rules from pages', async () => {
|
||||
for (const [path, code] of Object.entries(examples)) {
|
||||
const result = await extractRouteRules(code, path)
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
'prerender': true,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const examples = {
|
||||
// vue component with two script blocks
|
||||
'app.vue': `
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineRouteRules({
|
||||
prerender: true
|
||||
})
|
||||
</script>
|
||||
`,
|
||||
// vue component with a normal script block, and defineRouteRules ambiently
|
||||
'component.vue': `
|
||||
|
||||
<script>
|
||||
defineRouteRules({
|
||||
prerender: true
|
||||
})
|
||||
export default {
|
||||
setup() {}
|
||||
}
|
||||
</script>
|
||||
`,
|
||||
// TODO: JS component with defineRouteRules within a setup function
|
||||
// 'component.ts': `
|
||||
// export default {
|
||||
// setup() {
|
||||
// defineRouteRules({
|
||||
// prerender: true
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// `,
|
||||
}
|
@ -3,7 +3,7 @@ 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 { Parser } from 'acorn'
|
||||
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'
|
||||
@ -81,16 +81,7 @@ async function SFCCompile (name: string, source: string, options: Options, ssr =
|
||||
define: {},
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
const result = await (plugin.transform! as Function).call({
|
||||
parse: (code: string, opts: any = {}) => Parser.parse(code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
locations: true,
|
||||
...opts,
|
||||
}),
|
||||
}, source, name, {
|
||||
ssr,
|
||||
})
|
||||
const result = await (plugin.transform! as Function)(source, name, { ssr })
|
||||
|
||||
return typeof result === 'string' ? result : result?.code
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user