Merge branch 'main' into fix/21721-spa-loading

This commit is contained in:
Nikolay 2024-11-30 01:06:57 +07:00 committed by GitHub
commit 24a0253fbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 939 additions and 698 deletions

View File

@ -46,59 +46,3 @@ For example, referencing an image file that will be processed if a build tool is
::note ::note
Nuxt won't serve files in the [`assets/`](/docs/guide/directory-structure/assets) directory at a static URL like `/assets/my-file.png`. If you need a static URL, use the [`public/`](#public-directory) directory. Nuxt won't serve files in the [`assets/`](/docs/guide/directory-structure/assets) directory at a static URL like `/assets/my-file.png`. If you need a static URL, use the [`public/`](#public-directory) directory.
:: ::
### Global Styles Imports
To globally insert statements in your Nuxt components styles, you can use the [`Vite`](/docs/api/nuxt-config#vite) option at your [`nuxt.config`](/docs/api/nuxt-config) file.
#### Example
In this example, there is a [sass partial](https://sass-lang.com/documentation/at-rules/use#partials) file containing color variables to be used by your Nuxt [pages](/docs/guide/directory-structure/pages) and [components](/docs/guide/directory-structure/components)
::code-group
```scss [assets/_colors.scss]
$primary: #49240F;
$secondary: #E4A79D;
```
```sass [assets/_colors.sass]
$primary: #49240F
$secondary: #E4A79D
```
::
In your `nuxt.config`
::code-group
```ts twoslash [SCSS]
export default defineNuxtConfig({
vite: {
css: {
preprocessorOptions: {
scss: {
additionalData: '@use "~/assets/_colors.scss" as *;'
}
}
}
}
})
```
```ts twoslash [SASS]
export default defineNuxtConfig({
vite: {
css: {
preprocessorOptions: {
sass: {
additionalData: '@use "~/assets/_colors.sass" as *\n'
}
}
}
}
})
```
::

View File

@ -1,5 +1,5 @@
--- ---
title: 'Data fetching' title: 'Data Fetching'
description: Nuxt provides composables to handle data fetching within your application. description: Nuxt provides composables to handle data fetching within your application.
navigation.icon: i-ph-plugs-connected navigation.icon: i-ph-plugs-connected
--- ---

View File

@ -35,7 +35,7 @@ Then, run [`nuxi typecheck`](/docs/api/commands/typecheck) command to check your
npx nuxi typecheck npx nuxi typecheck
``` ```
To enable type-checking at build time, you can also use the [`typescript.typeCheck`](/docs/api/nuxt-config#typecheck) option in your `nuxt.config` file: To enable type-checking at build or development time, you can also use the [`typescript.typeCheck`](/docs/api/nuxt-config#typecheck) option in your `nuxt.config` file:
```ts twoslash [nuxt.config.ts] ```ts twoslash [nuxt.config.ts]
export default defineNuxtConfig({ export default defineNuxtConfig({

View File

@ -363,6 +363,26 @@ Headers that are **not meant to be forwarded** will **not be included** in the r
`transfer-encoding`, `connection`, `keep-alive`, `upgrade`, `expect`, `host`, `accept` `transfer-encoding`, `connection`, `keep-alive`, `upgrade`, `expect`, `host`, `accept`
:: ::
### Awaiting Promises After Response
When handling server requests, you might need to perform asynchronous tasks that shouldn't block the response to the client (for example, caching and logging). You can use `event.waitUntil` to await a promise in the background without delaying the response.
The `event.waitUntil` method accepts a promise that will be awaited before the handler terminates, ensuring the task is completed even if the server would otherwise terminate the handler right after the response is sent. This integrates with runtime providers to leverage their native capabilities for handling asynchronous operations after the response is sent.
```ts [server/api/background-task.ts]
const timeConsumingBackgroundTask = async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
};
export default eventHandler((event) => {
// schedule a background task without blocking the response
event.waitUntil(timeConsumingBackgroundTask())
// immediately send the response to the client
return 'done'
});
```
## Advanced Usage ## Advanced Usage
### Nitro Config ### Nitro Config

View File

@ -395,6 +395,35 @@ In addition, any changes to files within `srcDir` will trigger a rebuild of the
A maximum of 10 cache tarballs are kept. A maximum of 10 cache tarballs are kept.
:: ::
## extraPageMetaExtractionKeys
The `definePageMeta()` macro is a useful way to collect build-time meta about pages. Nuxt itself provides a set list of supported keys which is used to power some of the internal features such as redirects, page aliases and custom paths.
This option allows passing additional keys to extract from the page metadata when using `scanPageMeta`.
```vue
<script lang="ts" setup>
definePageMeta({
foo: 'bar'
})
</script>
```
```ts
export default defineNuxtConfig({
experimental: {
extraPageMetaExtractionKeys: ['foo'],
},
hooks: {
'pages:resolved' (ctx) {
// ✅ foo is available
},
},
})
```
This allows modules to access additional metadata from the page metadata in the build context. If you are using this within a module, it's recommended also to [augment the `NuxtPage` types with your keys](/docs/guide/directory-structure/pages#typing-custom-metadata).
## normalizeComponentNames ## normalizeComponentNames
Ensure that auto-generated Vue component names match the full component name Ensure that auto-generated Vue component names match the full component name

View File

@ -30,7 +30,7 @@ If you're using a custom useFetch wrapper, do not await it in the composable, as
:: ::
::note ::note
`data`, `status` and `error` are Vue refs and they should be accessed with `.value` when used within the `<script setup>`, while `refresh`/`execute` and `clear` are plain functions.. `data`, `status`, and `error` are Vue refs, and they should be accessed with `.value` when used within the `<script setup>`, while `refresh`/`execute` and `clear` are plain functions.
:: ::
Using the `query` option, you can add search parameters to your query. This option is extended from [unjs/ofetch](https://github.com/unjs/ofetch) and is using [unjs/ufo](https://github.com/unjs/ufo) to create the URL. Objects are automatically stringified. Using the `query` option, you can add search parameters to your query. This option is extended from [unjs/ofetch](https://github.com/unjs/ofetch) and is using [unjs/ufo](https://github.com/unjs/ufo) to create the URL. Objects are automatically stringified.

View File

@ -39,7 +39,7 @@
"@nuxt/schema": "workspace:*", "@nuxt/schema": "workspace:*",
"@nuxt/vite-builder": "workspace:*", "@nuxt/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"@types/node": "22.10.0", "@types/node": "22.10.1",
"@unhead/dom": "1.11.13", "@unhead/dom": "1.11.13",
"@unhead/schema": "1.11.13", "@unhead/schema": "1.11.13",
"@unhead/shared": "1.11.13", "@unhead/shared": "1.11.13",
@ -72,7 +72,7 @@
"@nuxt/test-utils": "3.14.4", "@nuxt/test-utils": "3.14.4",
"@nuxt/webpack-builder": "workspace:*", "@nuxt/webpack-builder": "workspace:*",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/node": "22.10.0", "@types/node": "22.10.1",
"@types/semver": "7.5.8", "@types/semver": "7.5.8",
"@unhead/schema": "1.11.13", "@unhead/schema": "1.11.13",
"@unhead/vue": "1.11.13", "@unhead/vue": "1.11.13",
@ -90,14 +90,14 @@
"eslint-plugin-perfectionist": "4.1.2", "eslint-plugin-perfectionist": "4.1.2",
"eslint-typegen": "0.3.2", "eslint-typegen": "0.3.2",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e", "h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"happy-dom": "15.11.6", "happy-dom": "15.11.7",
"jiti": "2.4.0", "jiti": "2.4.0",
"knip": "5.38.1", "knip": "5.38.2",
"markdownlint-cli": "0.43.0", "markdownlint-cli": "0.43.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxi": "3.15.0", "nuxi": "3.16.0",
"nuxt": "workspace:*", "nuxt": "workspace:*",
"nuxt-content-twoslash": "0.1.1", "nuxt-content-twoslash": "0.1.2",
"ofetch": "1.4.1", "ofetch": "1.4.1",
"pathe": "1.1.2", "pathe": "1.1.2",
"playwright-core": "1.49.0", "playwright-core": "1.49.0",

View File

@ -95,7 +95,7 @@
"mlly": "^1.7.3", "mlly": "^1.7.3",
"nanotar": "^0.1.1", "nanotar": "^0.1.1",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d", "nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxi": "^3.15.0", "nuxi": "^3.16.0",
"nypm": "^0.4.0", "nypm": "^0.4.0",
"ofetch": "^1.4.1", "ofetch": "^1.4.1",
"ohash": "^1.1.4", "ohash": "^1.1.4",

View File

@ -1,7 +1,8 @@
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import type { Component } from 'nuxt/schema' 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' import { SX_RE, isVue } from '../../core/utils'
interface NameDevPluginOptions { interface NameDevPluginOptions {
@ -37,12 +38,15 @@ export const ComponentNamePlugin = (options: NameDevPluginOptions) => createUnpl
// Without setup function, vue compiler does not generate __name // Without setup function, vue compiler does not generate __name
if (!s.hasChanged()) { if (!s.hasChanged()) {
const ast = this.parse(code) as Program parseAndWalk(code, id, function (node) {
const exportDefault = ast.body.find(node => node.type === 'ExportDefaultDeclaration') if (node.type !== 'ExportDefaultDeclaration') {
if (exportDefault) { return
const { start, end } = exportDefault.declaration
s.overwrite(start, end, `Object.assign(${code.slice(start, end)}, { __name: ${JSON.stringify(component.pascalName)} })`)
} }
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()) { if (s.hasChanged()) {

View File

@ -1,11 +1,14 @@
import { pathToFileURL } from 'node:url' import { pathToFileURL } from 'node:url'
import { parseURL } from 'ufo' import { parseURL } from 'ufo'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { walk } from 'estree-walker' import type { AssignmentProperty, CallExpression, ObjectExpression, Pattern, Property, ReturnStatement, VariableDeclaration } from 'estree'
import type { AssignmentProperty, CallExpression, Identifier, Literal, MemberExpression, Node, ObjectExpression, Pattern, Program, Property, ReturnStatement, VariableDeclaration } from 'estree' import type { Program } from 'acorn'
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import type { Component } from '@nuxt/schema' import type { Component } from '@nuxt/schema'
import { resolve } from 'pathe' import { resolve } from 'pathe'
import { parseAndWalk, walk, withLocations } from '../../core/utils/parse'
import type { Node } from '../../core/utils/parse'
import { distDir } from '../../dirs' import { distDir } from '../../dirs'
interface TreeShakeTemplatePluginOptions { interface TreeShakeTemplatePluginOptions {
@ -13,12 +16,9 @@ interface TreeShakeTemplatePluginOptions {
getComponents (): Component[] getComponents (): Component[]
} }
type AcornNode<N extends Node> = N & { start: number, end: number }
const SSR_RENDER_RE = /ssrRenderComponent/ const SSR_RENDER_RE = /ssrRenderComponent/
const PLACEHOLDER_EXACT_RE = /^(?:fallback|placeholder)$/ const PLACEHOLDER_EXACT_RE = /^(?:fallback|placeholder)$/
const CLIENT_ONLY_NAME_RE = /^(?:_unref\()?(?:_component_)?(?:Lazy|lazy_)?(?:client_only|ClientOnly\)?)$/ 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(() => { export const TreeShakeTemplatePlugin = (options: TreeShakeTemplatePluginOptions) => createUnplugin(() => {
const regexpMap = new WeakMap<Component[], [RegExp, RegExp, string[]]>() const regexpMap = new WeakMap<Component[], [RegExp, RegExp, string[]]>()
@ -29,7 +29,7 @@ export const TreeShakeTemplatePlugin = (options: TreeShakeTemplatePluginOptions)
const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href)) const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href))
return pathname.endsWith('.vue') return pathname.endsWith('.vue')
}, },
transform (code) { transform (code, id) {
const components = options.getComponents() const components = options.getComponents()
if (!regexpMap.has(components)) { if (!regexpMap.has(components)) {
@ -47,62 +47,55 @@ export const TreeShakeTemplatePlugin = (options: TreeShakeTemplatePluginOptions)
const [COMPONENTS_RE, COMPONENTS_IDENTIFIERS_RE] = regexpMap.get(components)! const [COMPONENTS_RE, COMPONENTS_IDENTIFIERS_RE] = regexpMap.get(components)!
if (!COMPONENTS_RE.test(code)) { return } if (!COMPONENTS_RE.test(code)) { return }
const codeAst = this.parse(code, PARSER_OPTIONS) as AcornNode<Program>
const componentsToRemoveSet = new Set<string>() const componentsToRemoveSet = new Set<string>()
// remove client only components or components called in ClientOnly default slot // remove client only components or components called in ClientOnly default slot
walk(codeAst, { const ast = parseAndWalk(code, id, (node) => {
enter: (_node) => { if (!isSsrRender(node)) {
const node = _node as AcornNode<Node> return
if (isSsrRender(node)) { }
const [componentCall, _, children] = node.arguments const [componentCall, _, children] = node.arguments
if (!componentCall) { return } if (!componentCall) { return }
if (componentCall.type === 'Identifier' || componentCall.type === 'MemberExpression' || componentCall.type === 'CallExpression') { if (componentCall.type === 'Identifier' || componentCall.type === 'MemberExpression' || componentCall.type === 'CallExpression') {
const componentName = getComponentName(node) const componentName = getComponentName(node)
const isClientComponent = COMPONENTS_IDENTIFIERS_RE.test(componentName) if (!componentName || !COMPONENTS_IDENTIFIERS_RE.test(componentName) || children?.type !== 'ObjectExpression') { return }
const isClientOnlyComponent = CLIENT_ONLY_NAME_RE.test(componentName) 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[]
if (isClientComponent && children?.type === 'ObjectExpression') { for (const _slot of slotsToRemove) {
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>[] const slot = withLocations(_slot)
for (const slot of slotsToRemove) {
s.remove(slot.start, slot.end + 1) s.remove(slot.start, slot.end + 1)
const removedCode = `({${code.slice(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 currentState = s.toString()
walk(this.parse(removedCode, PARSER_OPTIONS) as Node, { parseAndWalk(removedCode, id, (node) => {
enter: (_node) => { if (!isSsrRender(node)) { return }
const node = _node as AcornNode<CallExpression>
if (isSsrRender(node)) {
const name = getComponentName(node) const name = getComponentName(node)
if (!name) { return }
// detect if the component is called else where // detect if the component is called else where
const nameToRemove = isComponentNotCalledInSetup(currentCodeAst, name) const nameToRemove = isComponentNotCalledInSetup(currentState, id, name)
if (nameToRemove) { if (nameToRemove) {
componentsToRemoveSet.add(nameToRemove) componentsToRemoveSet.add(nameToRemove)
} }
}
},
}) })
} }
} }
}
}
},
}) })
const componentsToRemove = [...componentsToRemoveSet] const componentsToRemove = [...componentsToRemoveSet]
const removedNodes = new WeakSet<AcornNode<Node>>() const removedNodes = new WeakSet<Node>()
for (const componentName of componentsToRemove) { for (const componentName of componentsToRemove) {
// remove import declaration if it exists // remove import declaration if it exists
removeImportDeclaration(codeAst, componentName, s) removeImportDeclaration(ast, componentName, s)
// remove variable declaration // remove variable declaration
removeVariableDeclarator(codeAst, componentName, s, removedNodes) removeVariableDeclarator(ast, componentName, s, removedNodes)
// remove from setup return statement // remove from setup return statement
removeFromSetupReturn(codeAst, componentName, s) removeFromSetupReturn(ast, componentName, s)
} }
if (s.hasChanged()) { 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')) { } 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 // walk into the setup function
walkedInSetup = true 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 const returnStatement = node.value.body.body.find(statement => statement.type === 'ReturnStatement') as ReturnStatement
if (returnStatement && returnStatement.argument?.type === 'ObjectExpression') { if (returnStatement && returnStatement.argument?.type === 'ObjectExpression') {
// remove from return statement // remove from return statement
@ -157,7 +150,8 @@ function removeFromSetupReturn (codeAst: Program, name: string, magicString: Mag
function removePropertyFromObject (node: ObjectExpression, name: string, magicString: MagicString) { function removePropertyFromObject (node: ObjectExpression, name: string, magicString: MagicString) {
for (const property of node.properties) { for (const property of node.properties) {
if (property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === name) { 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 return true
} }
} }
@ -167,28 +161,28 @@ function removePropertyFromObject (node: ObjectExpression, name: string, magicSt
/** /**
* is the node a call expression ssrRenderComponent() * 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) return node.type === 'CallExpression' && node.callee.type === 'Identifier' && SSR_RENDER_RE.test(node.callee.name)
} }
function removeImportDeclaration (ast: Program, importName: string, magicString: MagicString): boolean { function removeImportDeclaration (ast: Program, importName: string, magicString: MagicString): boolean {
for (const node of ast.body) { for (const node of ast.body) {
if (node.type === 'ImportDeclaration') { if (node.type !== 'ImportDeclaration' || !node.specifiers) {
const specifier = node.specifiers.find(s => s.local.name === importName) continue
if (specifier) { }
if (node.specifiers.length > 1) {
const specifierIndex = node.specifiers.findIndex(s => s.local.name === importName) const specifierIndex = node.specifiers.findIndex(s => s.local.name === importName)
if (specifierIndex > -1) { if (specifierIndex > -1) {
magicString.remove((node.specifiers[specifierIndex] as AcornNode<Node>).start, (node.specifiers[specifierIndex] as AcornNode<Node>).end + 1) if (node.specifiers!.length > 1) {
node.specifiers.splice(specifierIndex, 1) const specifier = withLocations(node.specifiers![specifierIndex])
} magicString.remove(specifier.start, specifier.end + 1)
node.specifiers!.splice(specifierIndex, 1)
} else { } else {
magicString.remove((node as AcornNode<Node>).start, (node as AcornNode<Node>).end) const specifier = withLocations(node)
magicString.remove(specifier.start, specifier.end)
} }
return true return true
} }
} }
}
return false return false
} }
@ -197,11 +191,10 @@ function removeImportDeclaration (ast: Program, importName: string, magicString:
* ImportDeclarations and VariableDeclarations are ignored * ImportDeclarations and VariableDeclarations are ignored
* return the name of the component if is not called * return the name of the component if is not called
*/ */
function isComponentNotCalledInSetup (codeAst: Node, name: string): string | void { function isComponentNotCalledInSetup (code: string, id: string, name: string): string | void {
if (name) { if (!name) { return }
let found = false let found = false
walk(codeAst, { parseAndWalk(code, id, function (node) {
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'))) { 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 through the setup function node or the ssrRender function
walk(node, { walk(node, {
@ -217,42 +210,42 @@ function isComponentNotCalledInSetup (codeAst: Node, name: string): string | voi
}, },
}) })
} }
},
}) })
if (!found) { return name } if (!found) { return name }
}
} }
/** /**
* retrieve the component identifier being used on ssrRender callExpression * retrieve the component identifier being used on ssrRender callExpression
* @param ssrRenderNode - ssrRender callExpression * @param ssrRenderNode - ssrRender callExpression
*/ */
function getComponentName (ssrRenderNode: AcornNode<CallExpression>): string { function getComponentName (ssrRenderNode: CallExpression): string | undefined {
const componentCall = ssrRenderNode.arguments[0] as Identifier | MemberExpression | CallExpression const componentCall = ssrRenderNode.arguments[0]
if (!componentCall) { return }
if (componentCall.type === 'Identifier') { if (componentCall.type === 'Identifier') {
return componentCall.name return componentCall.name
} else if (componentCall.type === 'MemberExpression') { } 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 * 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 // remove variables
walk(codeAst, { walk(codeAst, {
enter (node) { enter (node) {
if (node.type === 'VariableDeclaration') { if (node.type !== 'VariableDeclaration') { return }
for (const declarator of node.declarations) { for (const declarator of node.declarations) {
const toRemove = findMatchingPatternToRemove(declarator.id as AcornNode<Pattern>, node as AcornNode<VariableDeclaration>, name, removedNodes) const toRemove = withLocations(findMatchingPatternToRemove(declarator.id, node, name, removedNodes))
if (toRemove) { if (toRemove) {
magicString.remove(toRemove.start, toRemove.end + 1) magicString.remove(toRemove.start, toRemove.end + 1)
removedNodes.add(toRemove) removedNodes.add(toRemove)
return 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. * 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.type === 'Identifier') {
if (node.name === name) { if (node.name === name) {
return toRemoveIfMatched return toRemoveIfMatched
} }
} else if (node.type === 'ArrayPattern') { } 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) { for (const element of elements) {
const matched = findMatchingPatternToRemove(element, elements.length > 1 ? element : toRemoveIfMatched, name, removedNodeSet) 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)) const properties = node.properties.filter((e): e is AssignmentProperty => e.type === 'Property' && !removedNodeSet.has(e))
for (const [index, property] of properties.entries()) { for (const [index, property] of properties.entries()) {
let nodeToRemove = property as AcornNode<Node> let nodeToRemove: Node = property
if (properties.length < 2) { if (properties.length < 2) {
nodeToRemove = toRemoveIfMatched 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) {
if (matched === property) { if (matched === property) {
properties.splice(index, 1) properties.splice(index, 1)
@ -292,7 +285,7 @@ function findMatchingPatternToRemove (node: AcornNode<Pattern>, toRemoveIfMatche
} }
} }
} else if (node.type === 'AssignmentPattern') { } 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 } if (matched) { return matched }
} }
} }

View File

@ -42,7 +42,7 @@ import { RemovePluginMetadataPlugin } from './plugins/plugin-metadata'
import { AsyncContextInjectionPlugin } from './plugins/async-context' import { AsyncContextInjectionPlugin } from './plugins/async-context'
import { ComposableKeysPlugin } from './plugins/composable-keys' import { ComposableKeysPlugin } from './plugins/composable-keys'
import { resolveDeepImportsPlugin } from './plugins/resolve-deep-imports' import { resolveDeepImportsPlugin } from './plugins/resolve-deep-imports'
import { prehydrateTransformPlugin } from './plugins/prehydrate' import { PrehydrateTransformPlugin } from './plugins/prehydrate'
import { VirtualFSPlugin } from './plugins/virtual' import { VirtualFSPlugin } from './plugins/virtual'
export function createNuxt (options: NuxtOptions): Nuxt { export function createNuxt (options: NuxtOptions): Nuxt {
@ -283,7 +283,7 @@ async function initNuxt (nuxt: Nuxt) {
addVitePlugin(() => resolveDeepImportsPlugin(nuxt), { server: false }) addVitePlugin(() => resolveDeepImportsPlugin(nuxt), { server: false })
// Add transform for `onPrehydrate` lifecycle hook // 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) { if (nuxt.options.experimental.localLayerAliases) {
// Add layer aliasing support for ~, ~~, @ and @@ aliases // Add layer aliasing support for ~, ~~, @ and @@ aliases

View File

@ -1,14 +1,14 @@
import { pathToFileURL } from 'node:url' import { pathToFileURL } from 'node:url'
import { createUnplugin } from 'unplugin' import { createUnplugin } from 'unplugin'
import { isAbsolute, relative } from 'pathe' import { isAbsolute, relative } from 'pathe'
import type { Node } from 'estree-walker'
import { walk } from 'estree-walker'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { hash } from 'ohash' import { hash } from 'ohash'
import type { CallExpression, Pattern } from 'estree' import type { Pattern } from 'estree'
import { parseQuery, parseURL } from 'ufo' import { parseQuery, parseURL } from 'ufo'
import escapeRE from 'escape-string-regexp' import escapeRE from 'escape-string-regexp'
import { findStaticImports, parseStaticImport } from 'mlly' import { findStaticImports, parseStaticImport } from 'mlly'
import { parseAndWalk, walk } from '../../core/utils/parse'
import { matchWithStringOrRegex } from '../utils/plugins' import { matchWithStringOrRegex } from '../utils/plugins'
interface ComposableKeysOptions { interface ComposableKeysOptions {
@ -52,23 +52,18 @@ export const ComposableKeysPlugin = (options: ComposableKeysOptions) => createUn
const relativeID = isAbsolute(id) ? relative(options.rootDir, id) : id const relativeID = isAbsolute(id) ? relative(options.rootDir, id) : id
const { pathname: relativePathname } = parseURL(relativeID) 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. // To handle variables hoisting we need a pre-pass to collect variable and function declarations with scope info.
let scopeTracker = new ScopeTracker() let scopeTracker = new ScopeTracker()
const varCollector = new ScopedVarsCollector() const varCollector = new ScopedVarsCollector()
walk(ast, { const ast = parseAndWalk(script, id, {
enter (_node) { enter (node) {
if (_node.type === 'BlockStatement') { if (node.type === 'BlockStatement') {
scopeTracker.enterScope() scopeTracker.enterScope()
varCollector.refresh(scopeTracker.curScopeKey) varCollector.refresh(scopeTracker.curScopeKey)
} else if (_node.type === 'FunctionDeclaration' && _node.id) { } else if (node.type === 'FunctionDeclaration' && node.id) {
varCollector.addVar(_node.id.name) varCollector.addVar(node.id.name)
} else if (_node.type === 'VariableDeclarator') { } else if (node.type === 'VariableDeclarator') {
varCollector.collect(_node.id) varCollector.collect(node.id)
} }
}, },
leave (_node) { leave (_node) {
@ -81,13 +76,12 @@ export const ComposableKeysPlugin = (options: ComposableKeysOptions) => createUn
scopeTracker = new ScopeTracker() scopeTracker = new ScopeTracker()
walk(ast, { walk(ast, {
enter (_node) { enter (node) {
if (_node.type === 'BlockStatement') { if (node.type === 'BlockStatement') {
scopeTracker.enterScope() scopeTracker.enterScope()
} }
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return } if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
const node: CallExpression = _node as CallExpression const name = node.callee.name
const name = 'name' in node.callee && node.callee.name
if (!name || !keyedFunctions.has(name) || node.arguments.length >= maxLength) { return } if (!name || !keyedFunctions.has(name) || node.arguments.length >= maxLength) { return }
imports = imports || detectImportNames(script, composableMeta) imports = imports || detectImportNames(script, composableMeta)
@ -219,24 +213,23 @@ class ScopedVarsCollector {
return false return false
} }
collect (n: Pattern) { collect (pattern: Pattern) {
const t = n.type if (pattern.type === 'Identifier') {
if (t === 'Identifier') { this.addVar(pattern.name)
this.addVar(n.name) } else if (pattern.type === 'RestElement') {
} else if (t === 'RestElement') { this.collect(pattern.argument)
this.collect(n.argument) } else if (pattern.type === 'AssignmentPattern') {
} else if (t === 'AssignmentPattern') { this.collect(pattern.left)
this.collect(n.left) } else if (pattern.type === 'ArrayPattern') {
} else if (t === 'ArrayPattern') { for (const element of pattern.elements) {
n.elements.forEach(e => e && this.collect(e)) if (element) {
} else if (t === 'ObjectPattern') { this.collect(element.type === 'RestElement' ? element.argument : element)
n.properties.forEach((p) => { }
if (p.type === 'RestElement') { }
this.collect(p) } else if (pattern.type === 'ObjectPattern') {
} else { for (const property of pattern.properties) {
this.collect(p.value) this.collect(property.type === 'RestElement' ? property.argument : property.value)
} }
})
} }
} }
} }

View File

@ -1,8 +1,5 @@
import type { CallExpression, Literal, Property, SpreadElement } from 'estree' import type { Literal, Property, SpreadElement } from 'estree'
import type { Node } from 'estree-walker'
import { walk } from 'estree-walker'
import { transform } from 'esbuild' import { transform } from 'esbuild'
import { parse } from 'acorn'
import { defu } from 'defu' import { defu } from 'defu'
import { findExports } from 'mlly' import { findExports } from 'mlly'
import type { Nuxt } from '@nuxt/schema' import type { Nuxt } from '@nuxt/schema'
@ -11,6 +8,8 @@ import MagicString from 'magic-string'
import { normalize } from 'pathe' import { normalize } from 'pathe'
import { logger } from '@nuxt/kit' import { logger } from '@nuxt/kit'
import { parseAndWalk, withLocations } from '../../core/utils/parse'
import type { ObjectPlugin, PluginMeta } from '#app' import type { ObjectPlugin, PluginMeta } from '#app'
const internalOrderMap = { const internalOrderMap = {
@ -47,13 +46,9 @@ export async function extractMetadata (code: string, loader = 'ts' as 'ts' | 'ts
return metaCache[code] return metaCache[code]
} }
const js = await transform(code, { loader }) const js = await transform(code, { loader })
walk(parse(js.code, { parseAndWalk(js.code, `file.${loader}`, (node) => {
sourceType: 'module', if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
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 const name = 'name' in node.callee && node.callee.name
if (name !== 'defineNuxtPlugin' && name !== 'definePayloadPlugin') { return } if (name !== 'defineNuxtPlugin' && name !== 'definePayloadPlugin') { return }
@ -76,7 +71,6 @@ export async function extractMetadata (code: string, loader = 'ts' as 'ts' | 'ts
meta.order = meta.order || orderMap[meta.enforce || 'default'] || orderMap.default meta.order = meta.order || orderMap[meta.enforce || 'default'] || orderMap.default
delete meta.enforce delete meta.enforce
},
}) })
metaCache[code] = meta metaCache[code] = meta
return meta as Omit<PluginMeta, 'enforce'> return meta as Omit<PluginMeta, 'enforce'>
@ -149,41 +143,33 @@ export const RemovePluginMetadataPlugin = (nuxt: Nuxt) => createUnplugin(() => {
const wrapperNames = new Set(['defineNuxtPlugin', 'definePayloadPlugin']) const wrapperNames = new Set(['defineNuxtPlugin', 'definePayloadPlugin'])
try { try {
walk(this.parse(code, { parseAndWalk(code, id, (node) => {
sourceType: 'module', if (node.type === 'ImportSpecifier' && node.imported.type === 'Identifier' && (node.imported.name === 'defineNuxtPlugin' || node.imported.name === 'definePayloadPlugin')) {
ecmaVersion: 'latest', wrapperNames.add(node.local.name)
}) 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 } if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
const node = _node as CallExpression & { start: number, end: number }
const name = 'name' in node.callee && node.callee.name const name = 'name' in node.callee && node.callee.name
if (!name || !wrapperNames.has(name)) { return } if (!name || !wrapperNames.has(name)) { return }
wrapped = true wrapped = true
// Remove metadata that already has been extracted // Remove metadata that already has been extracted
if (!('order' in plugin) && !('name' in plugin)) { return } if (!('order' in plugin) && !('name' in plugin)) { return }
for (const [argIndex, _arg] of node.arguments.entries()) { for (const [argIndex, arg] of node.arguments.entries()) {
if (_arg.type !== 'ObjectExpression') { continue } if (arg.type !== 'ObjectExpression') { continue }
const arg = _arg as typeof _arg & { start: number, end: number } for (const [propertyIndex, property] of arg.properties.entries()) {
for (const [propertyIndex, _property] of arg.properties.entries()) { if (property.type === 'SpreadElement' || !('name' in property.key)) { continue }
if (_property.type === 'SpreadElement' || !('name' in _property.key)) { continue }
const property = _property as typeof _property & { start: number, end: number } const propertyKey = property.key.name
const propertyKey = _property.key.name
if (propertyKey === 'order' || propertyKey === 'enforce' || propertyKey === 'name') { if (propertyKey === 'order' || propertyKey === 'enforce' || propertyKey === 'name') {
const _nextNode = arg.properties[propertyIndex + 1] || node.arguments[argIndex + 1] const nextNode = arg.properties[propertyIndex + 1] || node.arguments[argIndex + 1]
const nextNode = _nextNode as typeof _nextNode & { start: number, end: number } const nextIndex = withLocations(nextNode)?.start || (withLocations(arg).end - 1)
const nextIndex = nextNode?.start || (arg.end - 1)
s.remove(property.start, nextIndex) s.remove(withLocations(property).start, nextIndex)
} }
} }
} }
},
}) })
} catch (e) { } catch (e) {
logger.error(e) logger.error(e)

View File

@ -1,16 +1,12 @@
import { transform } from 'esbuild' 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 { createUnplugin } from 'unplugin'
import type { SimpleCallExpression } from 'estree'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { hash } from 'ohash' import { hash } from 'ohash'
import { parseAndWalk, withLocations } from '../../core/utils/parse'
import { isJS, isVue } from '../utils' import { isJS, isVue } from '../utils'
export function prehydrateTransformPlugin (nuxt: Nuxt) { export function PrehydrateTransformPlugin (options: { sourcemap?: boolean } = {}) {
return createUnplugin(() => ({ return createUnplugin(() => ({
name: 'nuxt:prehydrate-transform', name: 'nuxt:prehydrate-transform',
transformInclude (id) { transformInclude (id) {
@ -22,33 +18,27 @@ export function prehydrateTransformPlugin (nuxt: Nuxt) {
const s = new MagicString(code) const s = new MagicString(code)
const promises: Array<Promise<any>> = [] const promises: Array<Promise<any>> = []
walk(parse(code, { parseAndWalk(code, id, (node) => {
sourceType: 'module', if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') {
ecmaVersion: 'latest', return
ranges: true, }
}) as Node, { if (node.callee.name === 'onPrehydrate') {
enter (_node) { const callback = withLocations(node.arguments[0])
if (_node.type !== 'CallExpression' || _node.callee.type !== 'Identifier') { return } if (!callback) { return }
const node = _node as SimpleCallExpression & { start: number, end: number } if (callback.type !== 'ArrowFunctionExpression' && callback.type !== 'FunctionExpression') { return }
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 }
const needsAttr = node.arguments[0].params.length > 0 const needsAttr = callback.params.length > 0
const { start, end } = node.arguments[0] as Node & { start: number, end: number }
const p = transform(`forEach(${code.slice(start, end)})`, { loader: 'ts', minify: true }) const p = transform(`forEach(${code.slice(callback.start, callback.end)})`, { loader: 'ts', minify: true })
promises.push(p.then(({ code: result }) => { promises.push(p.then(({ code: result }) => {
const cleaned = result.slice('forEach'.length).replace(/;\s+$/, '') const cleaned = result.slice('forEach'.length).replace(/;\s+$/, '')
const args = [JSON.stringify(cleaned)] const args = [JSON.stringify(cleaned)]
if (needsAttr) { if (needsAttr) {
args.push(JSON.stringify(hash(result))) args.push(JSON.stringify(hash(result)))
} }
s.overwrite(start, end, args.join(', ')) s.overwrite(callback.start, callback.end, args.join(', '))
})) }))
} }
},
}) })
await Promise.all(promises).catch((e) => { await Promise.all(promises).catch((e) => {
@ -58,7 +48,7 @@ export function prehydrateTransformPlugin (nuxt: Nuxt) {
if (s.hasChanged()) { if (s.hasChanged()) {
return { return {
code: s.toString(), code: s.toString(),
map: nuxt.options.sourcemap.server || nuxt.options.sourcemap.client map: options.sourcemap
? s.generateMap({ hires: true }) ? s.generateMap({ hires: true })
: undefined, : undefined,
} }

View 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>
}

View File

@ -67,7 +67,7 @@ const globalProps = {
type: Boolean, type: Boolean,
default: undefined, default: undefined,
}, },
style: String, style: [String, Object, Array],
tabindex: String, tabindex: String,
title: String, title: String,
translate: String, translate: String,

View File

@ -379,7 +379,7 @@ export default defineNuxtModule({
const glob = pageToGlobMap[path] const glob = pageToGlobMap[path]
const code = path in nuxt.vfs ? nuxt.vfs[path]! : await readFile(path!, 'utf-8') const code = path in nuxt.vfs ? nuxt.vfs[path]! : await readFile(path!, 'utf-8')
try { try {
const extractedRule = await extractRouteRules(code) const extractedRule = await extractRouteRules(code, path)
if (extractedRule) { if (extractedRule) {
if (!glob) { if (!glob) {
const relativePath = relative(nuxt.options.srcDir, path) const relativePath = relative(nuxt.options.srcDir, path)

View File

@ -3,13 +3,13 @@ import { createUnplugin } from 'unplugin'
import { parseQuery, parseURL } from 'ufo' import { parseQuery, parseURL } from 'ufo'
import type { StaticImport } from 'mlly' import type { StaticImport } from 'mlly'
import { findExports, findStaticImports, parseStaticImport } 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 { walk } from 'estree-walker'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { isAbsolute } from 'pathe' import { isAbsolute } from 'pathe'
import { logger } from '@nuxt/kit' import { logger } from '@nuxt/kit'
import { parseAndWalk, withLocations } from '../../core/utils/parse'
interface PageMetaPluginOptions { interface PageMetaPluginOptions {
dev?: boolean dev?: boolean
sourcemap?: boolean sourcemap?: boolean
@ -36,7 +36,7 @@ if (import.meta.webpackHot) {
}) })
}` }`
export const PageMetaPlugin = (options: PageMetaPluginOptions) => createUnplugin(() => { export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnplugin(() => {
return { return {
name: 'nuxt:pages-macros-transform', name: 'nuxt:pages-macros-transform',
enforce: 'post', enforce: 'post',
@ -112,17 +112,13 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions) => createUnplugin
} }
} }
walk(this.parse(code, { parseAndWalk(code, id, (node) => {
sourceType: 'module', if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
ecmaVersion: 'latest', if (!('name' in node.callee) || node.callee.name !== 'definePageMeta') { return }
}) 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 }
const meta = node.arguments[0] as Expression & { start: number, end: number } const meta = withLocations(node.arguments[0])
if (!meta) { return }
let contents = `const __nuxt_page_meta = ${code!.slice(meta.start, meta.end) || 'null'}\nexport default __nuxt_page_meta` + (options.dev ? CODE_HMR : '') let contents = `const __nuxt_page_meta = ${code!.slice(meta.start, meta.end) || 'null'}\nexport default __nuxt_page_meta` + (options.dev ? CODE_HMR : '')
@ -137,20 +133,17 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions) => createUnplugin
} }
walk(meta, { walk(meta, {
enter (_node) { enter (node) {
if (_node.type === 'CallExpression') { if (node.type === 'CallExpression' && 'name' in node.callee) {
const node = _node as CallExpression & { start: number, end: number } addImport(node.callee.name)
addImport('name' in node.callee && node.callee.name)
} }
if (_node.type === 'Identifier') { if (node.type === 'Identifier') {
const node = _node as Identifier & { start: number, end: number }
addImport(node.name) addImport(node.name)
} }
}, },
}) })
s.overwrite(0, code.length, contents) s.overwrite(0, code.length, contents)
},
}) })
if (!s.hasChanged() && !code.includes('__nuxt_page_meta')) { if (!s.hasChanged() && !code.includes('__nuxt_page_meta')) {

View File

@ -1,40 +1,37 @@
import { runInNewContext } from 'node:vm' 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 { transform } from 'esbuild'
import { parse } from 'acorn'
import type { NuxtPage } from '@nuxt/schema' import type { NuxtPage } from '@nuxt/schema'
import type { NitroRouteConfig } from 'nitro/types' import type { NitroRouteConfig } from 'nitro/types'
import { normalize } from 'pathe' import { normalize } from 'pathe'
import { getLoader } from '../core/utils'
import { parseAndWalk } from '../core/utils/parse'
import { extractScriptContent, pathToNitroGlob } from './utils' import { extractScriptContent, pathToNitroGlob } from './utils'
const ROUTE_RULE_RE = /\bdefineRouteRules\(/ const ROUTE_RULE_RE = /\bdefineRouteRules\(/
const ruleCache: Record<string, NitroRouteConfig | null> = {} 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) { if (code in ruleCache) {
return ruleCache[code] || null return ruleCache[code] || null
} }
if (!ROUTE_RULE_RE.test(code)) { return null } if (!ROUTE_RULE_RE.test(code)) { return null }
let rule: NitroRouteConfig | null = 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) { for (const script of contents) {
if (rule) { break } if (rule) { break }
code = script?.code || code code = script?.code || code
const js = await transform(code, { loader: script?.loader || 'ts' }) const js = await transform(code, { loader: script?.loader || 'ts' })
walk(parse(js.code, {
sourceType: 'module', parseAndWalk(js.code, 'file.' + (script?.loader || 'ts'), (node) => {
ecmaVersion: 'latest', if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
}) as Node, { if (node.callee.name === 'defineRouteRules') {
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) const rulesString = js.code.slice(node.start, node.end)
try { try {
rule = JSON.parse(runInNewContext(rulesString.replace('defineRouteRules', 'JSON.stringify'), {})) rule = JSON.parse(runInNewContext(rulesString.replace('defineRouteRules', 'JSON.stringify'), {}))
@ -42,7 +39,6 @@ export async function extractRouteRules (code: string): Promise<NitroRouteConfig
throw new Error('[nuxt] Error parsing route rules. They should be JSON-serializable.') throw new Error('[nuxt] Error parsing route rules. They should be JSON-serializable.')
} }
} }
},
}) })
} }

View File

@ -8,11 +8,10 @@ import escapeRE from 'escape-string-regexp'
import { filename } from 'pathe/utils' import { filename } from 'pathe/utils'
import { hash } from 'ohash' import { hash } from 'ohash'
import { transform } from 'esbuild' import { transform } from 'esbuild'
import { parse } from 'acorn' import type { Property } from 'estree'
import { walk } from 'estree-walker'
import type { CallExpression, ExpressionStatement, ObjectExpression, Program, Property } from 'estree'
import type { NuxtPage } from 'nuxt/schema' import type { NuxtPage } from 'nuxt/schema'
import { parseAndWalk } from '../core/utils/parse'
import { getLoader, uniqueBy } from '../core/utils' import { getLoader, uniqueBy } from '../core/utils'
import { toArray } from '../utils' import { toArray } from '../utils'
@ -71,13 +70,14 @@ export async function resolvePagesRoutes (): Promise<NuxtPage[]> {
return pages return pages
} }
const augmentCtx = { extraExtractionKeys: nuxt.options.experimental.extraPageMetaExtractionKeys }
if (shouldAugment === 'after-resolve') { if (shouldAugment === 'after-resolve') {
await nuxt.callHook('pages:extend', pages) await nuxt.callHook('pages:extend', pages)
await augmentPages(pages, nuxt.vfs) await augmentPages(pages, nuxt.vfs, augmentCtx)
} else { } else {
const augmentedPages = await augmentPages(pages, nuxt.vfs) const augmentedPages = await augmentPages(pages, nuxt.vfs, augmentCtx)
await nuxt.callHook('pages:extend', pages) await nuxt.callHook('pages:extend', pages)
await augmentPages(pages, nuxt.vfs, { pagesToSkip: augmentedPages }) await augmentPages(pages, nuxt.vfs, { pagesToSkip: augmentedPages, ...augmentCtx })
augmentedPages?.clear() augmentedPages?.clear()
} }
@ -158,13 +158,15 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
interface AugmentPagesContext { interface AugmentPagesContext {
pagesToSkip?: Set<string> pagesToSkip?: Set<string>
augmentedPages?: Set<string> augmentedPages?: Set<string>
extraExtractionKeys?: string[]
} }
export async function augmentPages (routes: NuxtPage[], vfs: Record<string, string>, ctx: AugmentPagesContext = {}) { export async function augmentPages (routes: NuxtPage[], vfs: Record<string, string>, ctx: AugmentPagesContext = {}) {
ctx.augmentedPages ??= new Set() ctx.augmentedPages ??= new Set()
for (const route of routes) { for (const route of routes) {
if (route.file && !ctx.pagesToSkip?.has(route.file)) { if (route.file && !ctx.pagesToSkip?.has(route.file)) {
const fileContent = route.file in vfs ? vfs[route.file]! : fs.readFileSync(await resolvePath(route.file), 'utf-8') const fileContent = route.file in vfs ? vfs[route.file]! : fs.readFileSync(await resolvePath(route.file), 'utf-8')
const routeMeta = await getRouteMeta(fileContent, route.file) const routeMeta = await getRouteMeta(fileContent, route.file, ctx.extraExtractionKeys)
if (route.meta) { if (route.meta) {
routeMeta.meta = { ...routeMeta.meta, ...route.meta } routeMeta.meta = { ...routeMeta.meta, ...route.meta }
} }
@ -181,9 +183,9 @@ export async function augmentPages (routes: NuxtPage[], vfs: Record<string, stri
} }
const SFC_SCRIPT_RE = /<script(?<attrs>[^>]*)>(?<content>[\s\S]*?)<\/script[^>]*>/gi 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 }> = [] 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) { if (match?.groups?.content) {
contents.push({ contents.push({
loader: match.groups.attrs?.includes('tsx') ? 'tsx' : 'ts', loader: match.groups.attrs?.includes('tsx') ? 'tsx' : 'ts',
@ -196,12 +198,12 @@ export function extractScriptContent (html: string) {
} }
const PAGE_META_RE = /definePageMeta\([\s\S]*?\)/ const PAGE_META_RE = /definePageMeta\([\s\S]*?\)/
const extractionKeys = ['name', 'path', 'props', 'alias', 'redirect'] as const const defaultExtractionKeys = ['name', 'path', 'props', 'alias', 'redirect'] as const
const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
const pageContentsCache: Record<string, string> = {} const pageContentsCache: Record<string, string> = {}
const metaCache: Record<string, Partial<Record<keyof NuxtPage, any>>> = {} const metaCache: Record<string, Partial<Record<keyof NuxtPage, any>>> = {}
export async function getRouteMeta (contents: string, absolutePath: string): Promise<Partial<Record<keyof NuxtPage, any>>> { export async function getRouteMeta (contents: string, absolutePath: string, extraExtractionKeys: string[] = []): Promise<Partial<Record<keyof NuxtPage, any>>> {
// set/update pageContentsCache, invalidate metaCache on cache mismatch // set/update pageContentsCache, invalidate metaCache on cache mismatch
if (!(absolutePath in pageContentsCache) || pageContentsCache[absolutePath] !== contents) { if (!(absolutePath in pageContentsCache) || pageContentsCache[absolutePath] !== contents) {
pageContentsCache[absolutePath] = contents pageContentsCache[absolutePath] = contents
@ -219,7 +221,9 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
return {} 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>])
for (const script of scriptBlocks) { for (const script of scriptBlocks) {
if (!PAGE_META_RE.test(script.code)) { if (!PAGE_META_RE.test(script.code)) {
@ -227,27 +231,22 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
} }
const js = await transform(script.code, { loader: script.loader }) 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>() const dynamicProperties = new Set<keyof NuxtPage>()
let foundMeta = false let foundMeta = false
walk(ast, { parseAndWalk(js.code, absolutePath.replace(/\.\w+$/, '.' + script.loader), (node) => {
enter (node) {
if (foundMeta) { return } 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 foundMeta = true
const pageMetaArgument = ((node as ExpressionStatement).expression as CallExpression).arguments[0] as ObjectExpression const pageMetaArgument = node.expression.arguments[0]
if (pageMetaArgument?.type !== 'ObjectExpression') { return }
for (const key of extractionKeys) { for (const key of extractionKeys) {
const property = pageMetaArgument.properties.find(property => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === key) as Property 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) { continue }
if (property.value.type === 'ObjectExpression') { if (property.value.type === 'ObjectExpression') {
@ -295,7 +294,7 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
continue continue
} }
const name = property.key.type === 'Identifier' ? property.key.name : String(property.value) const name = property.key.type === 'Identifier' ? property.key.name : String(property.value)
if (!(extractionKeys as unknown as string[]).includes(name)) { if (!extractionKeys.has(name as keyof NuxtPage)) {
dynamicProperties.add('meta') dynamicProperties.add('meta')
break break
} }
@ -305,7 +304,6 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
extractedMeta.meta ??= {} extractedMeta.meta ??= {}
extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties
} }
},
}) })
} }

View 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" })"
`)
})
})

View File

@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest' 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', () => { describe('detectImportNames', () => {
const keyedComposables = { 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`)
})
})

View File

@ -1,4 +1,8 @@
import { describe, expect, it } from 'vitest' 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 { getRouteMeta, normalizeRoutes } from '../src/pages/utils'
import type { NuxtPage } from '../schema' 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 () => { it('should use and invalidate cache', async () => {
const fileContents = `<script setup>definePageMeta({ foo: 'bar' })</script>` const fileContents = `<script setup>definePageMeta({ foo: 'bar' })</script>`
const meta = await getRouteMeta(fileContents, filePath) const meta = await getRouteMeta(fileContents, filePath)
@ -142,6 +183,24 @@ describe('page metadata', () => {
} }
`) `)
}) })
it('should extract configured extra meta', async () => {
const meta = await getRouteMeta(`
<script setup>
definePageMeta({
foo: 'bar',
bar: true,
})
</script>
`, filePath, ['bar', 'foo'])
expect(meta).toMatchInlineSnapshot(`
{
"bar": true,
"foo": "bar",
}
`)
})
}) })
describe('normalizeRoutes', () => { describe('normalizeRoutes', () => {
@ -222,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"
`)
})
})

View File

@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest' 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 { RemovePluginMetadataPlugin, extractMetadata } from '../src/core/plugins/plugin-metadata'
import { checkForCircularDependencies } from '../src/core/app' import { checkForCircularDependencies } from '../src/core/app'
@ -40,7 +40,14 @@ describe('plugin-metadata', () => {
'export const plugin = {}', 'export const plugin = {}',
] ]
for (const plugin of invalidPlugins) { 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: () => {}, setup: () => {},
}, { order: 10, name: test }) }, { 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({ export default defineNuxtPlugin({
setup: () => {}, setup: () => {},

View 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")"`)
})
})

View 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
// })
// }
// }
// `,
}

View File

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

View File

@ -308,6 +308,16 @@ export default defineUntypedSchema({
}, },
}, },
/**
* Configure additional keys to extract from the page metadata when using `scanPageMeta`.
*
* This allows modules to access additional metadata from the page metadata. It's recommended
* to augment the NuxtPage types with your keys.
*
* @type {string[]}
*/
extraPageMetaExtractionKeys: [],
/** /**
* Automatically share payload _data_ between pages that are prerendered. This can result in a significant * Automatically share payload _data_ between pages that are prerendered. This can result in a significant
* performance improvement when prerendering sites that use `useAsyncData` or `useFetch` and fetch the same * performance improvement when prerendering sites that use `useAsyncData` or `useFetch` and fetch the same

File diff suppressed because it is too large Load Diff