mirror of
https://github.com/nuxt/nuxt.git
synced 2025-01-22 11:22:43 +00:00
Merge branch 'main' into fix/21721-spa-loading
This commit is contained in:
commit
24a0253fbb
@ -46,59 +46,3 @@ For example, referencing an image file that will be processed if a build tool is
|
||||
::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.
|
||||
::
|
||||
|
||||
### 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
::
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: 'Data fetching'
|
||||
title: 'Data Fetching'
|
||||
description: Nuxt provides composables to handle data fetching within your application.
|
||||
navigation.icon: i-ph-plugs-connected
|
||||
---
|
||||
|
@ -35,7 +35,7 @@ Then, run [`nuxi typecheck`](/docs/api/commands/typecheck) command to check your
|
||||
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]
|
||||
export default defineNuxtConfig({
|
||||
|
@ -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`
|
||||
::
|
||||
|
||||
### 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
|
||||
|
||||
### Nitro Config
|
||||
|
@ -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.
|
||||
::
|
||||
|
||||
## 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
|
||||
|
||||
Ensure that auto-generated Vue component names match the full component name
|
||||
|
@ -30,7 +30,7 @@ If you're using a custom useFetch wrapper, do not await it in the composable, as
|
||||
::
|
||||
|
||||
::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.
|
||||
|
12
package.json
12
package.json
@ -39,7 +39,7 @@
|
||||
"@nuxt/schema": "workspace:*",
|
||||
"@nuxt/vite-builder": "workspace:*",
|
||||
"@nuxt/webpack-builder": "workspace:*",
|
||||
"@types/node": "22.10.0",
|
||||
"@types/node": "22.10.1",
|
||||
"@unhead/dom": "1.11.13",
|
||||
"@unhead/schema": "1.11.13",
|
||||
"@unhead/shared": "1.11.13",
|
||||
@ -72,7 +72,7 @@
|
||||
"@nuxt/test-utils": "3.14.4",
|
||||
"@nuxt/webpack-builder": "workspace:*",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/node": "22.10.0",
|
||||
"@types/node": "22.10.1",
|
||||
"@types/semver": "7.5.8",
|
||||
"@unhead/schema": "1.11.13",
|
||||
"@unhead/vue": "1.11.13",
|
||||
@ -90,14 +90,14 @@
|
||||
"eslint-plugin-perfectionist": "4.1.2",
|
||||
"eslint-typegen": "0.3.2",
|
||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||
"happy-dom": "15.11.6",
|
||||
"happy-dom": "15.11.7",
|
||||
"jiti": "2.4.0",
|
||||
"knip": "5.38.1",
|
||||
"knip": "5.38.2",
|
||||
"markdownlint-cli": "0.43.0",
|
||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
|
||||
"nuxi": "3.15.0",
|
||||
"nuxi": "3.16.0",
|
||||
"nuxt": "workspace:*",
|
||||
"nuxt-content-twoslash": "0.1.1",
|
||||
"nuxt-content-twoslash": "0.1.2",
|
||||
"ofetch": "1.4.1",
|
||||
"pathe": "1.1.2",
|
||||
"playwright-core": "1.49.0",
|
||||
|
@ -95,7 +95,7 @@
|
||||
"mlly": "^1.7.3",
|
||||
"nanotar": "^0.1.1",
|
||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
|
||||
"nuxi": "^3.15.0",
|
||||
"nuxi": "^3.16.0",
|
||||
"nypm": "^0.4.0",
|
||||
"ofetch": "^1.4.1",
|
||||
"ohash": "^1.1.4",
|
||||
|
@ -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
|
||||
s.overwrite(start, end, `Object.assign(${code.slice(start, end)}, { __name: ${JSON.stringify(component.pascalName)} })`)
|
||||
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 ast = parseAndWalk(code, id, (node) => {
|
||||
if (!isSsrRender(node)) {
|
||||
return
|
||||
}
|
||||
|
||||
const [componentCall, _, children] = node.arguments
|
||||
if (!componentCall) { return }
|
||||
|
||||
if (componentCall.type === 'Identifier' || componentCall.type === 'MemberExpression' || componentCall.type === 'CallExpression') {
|
||||
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 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') {
|
||||
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>[]
|
||||
|
||||
for (const slot of slotsToRemove) {
|
||||
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 currentCodeAst = this.parse(s.toString(), PARSER_OPTIONS) as Node
|
||||
const currentState = s.toString()
|
||||
|
||||
walk(this.parse(removedCode, PARSER_OPTIONS) as Node, {
|
||||
enter: (_node) => {
|
||||
const node = _node as AcornNode<CallExpression>
|
||||
if (isSsrRender(node)) {
|
||||
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(currentCodeAst, name)
|
||||
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,28 +161,28 @@ 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) {
|
||||
if (node.type !== 'ImportDeclaration' || !node.specifiers) {
|
||||
continue
|
||||
}
|
||||
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)
|
||||
}
|
||||
if (node.specifiers!.length > 1) {
|
||||
const specifier = withLocations(node.specifiers![specifierIndex])
|
||||
magicString.remove(specifier.start, specifier.end + 1)
|
||||
node.specifiers!.splice(specifierIndex, 1)
|
||||
} 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 false
|
||||
}
|
||||
|
||||
@ -197,11 +191,10 @@ 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) {
|
||||
function isComponentNotCalledInSetup (code: string, id: string, name: string): string | void {
|
||||
if (!name) { return }
|
||||
let found = false
|
||||
walk(codeAst, {
|
||||
enter (node) {
|
||||
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, {
|
||||
@ -217,42 +210,42 @@ function isComponentNotCalledInSetup (codeAst: Node, name: string): string | voi
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
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') {
|
||||
if (node.type !== 'VariableDeclaration') { return }
|
||||
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) {
|
||||
magicString.remove(toRemove.start, toRemove.end + 1)
|
||||
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.
|
||||
*/
|
||||
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,13 +46,9 @@ 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 }
|
||||
parseAndWalk(js.code, `file.${loader}`, (node) => {
|
||||
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
|
||||
|
||||
const name = 'name' in node.callee && node.callee.name
|
||||
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
|
||||
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)
|
||||
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 as CallExpression).callee.type !== 'Identifier') { return }
|
||||
const node = _node as CallExpression & { start: number, end: number }
|
||||
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
|
||||
|
||||
const name = 'name' in node.callee && node.callee.name
|
||||
if (!name || !wrapperNames.has(name)) { return }
|
||||
wrapped = true
|
||||
|
||||
// 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 }
|
||||
for (const [argIndex, arg] of node.arguments.entries()) {
|
||||
if (arg.type !== 'ObjectExpression') { continue }
|
||||
|
||||
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 }
|
||||
for (const [propertyIndex, property] of arg.properties.entries()) {
|
||||
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') {
|
||||
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)
|
||||
const nextNode = arg.properties[propertyIndex + 1] || node.arguments[argIndex + 1]
|
||||
const nextIndex = withLocations(nextNode)?.start || (withLocations(arg).end - 1)
|
||||
|
||||
s.remove(property.start, nextIndex)
|
||||
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 })
|
||||
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(start, end, args.join(', '))
|
||||
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>
|
||||
}
|
@ -67,7 +67,7 @@ const globalProps = {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
style: String,
|
||||
style: [String, Object, Array],
|
||||
tabindex: String,
|
||||
title: String,
|
||||
translate: String,
|
||||
|
@ -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,17 +112,13 @@ 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])
|
||||
|
||||
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 : '')
|
||||
|
||||
@ -137,20 +133,17 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions) => createUnplugin
|
||||
}
|
||||
|
||||
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)
|
||||
enter (node) {
|
||||
if (node.type === 'CallExpression' && 'name' in node.callee) {
|
||||
addImport(node.callee.name)
|
||||
}
|
||||
if (_node.type === 'Identifier') {
|
||||
const node = _node as Identifier & { start: number, end: number }
|
||||
if (node.type === 'Identifier') {
|
||||
addImport(node.name)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
s.overwrite(0, code.length, contents)
|
||||
},
|
||||
})
|
||||
|
||||
if (!s.hasChanged() && !code.includes('__nuxt_page_meta')) {
|
||||
|
@ -1,40 +1,37 @@
|
||||
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') {
|
||||
|
||||
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'), {}))
|
||||
@ -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.')
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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'
|
||||
|
||||
@ -71,13 +70,14 @@ export async function resolvePagesRoutes (): Promise<NuxtPage[]> {
|
||||
return pages
|
||||
}
|
||||
|
||||
const augmentCtx = { extraExtractionKeys: nuxt.options.experimental.extraPageMetaExtractionKeys }
|
||||
if (shouldAugment === 'after-resolve') {
|
||||
await nuxt.callHook('pages:extend', pages)
|
||||
await augmentPages(pages, nuxt.vfs)
|
||||
await augmentPages(pages, nuxt.vfs, augmentCtx)
|
||||
} else {
|
||||
const augmentedPages = await augmentPages(pages, nuxt.vfs)
|
||||
const augmentedPages = await augmentPages(pages, nuxt.vfs, augmentCtx)
|
||||
await nuxt.callHook('pages:extend', pages)
|
||||
await augmentPages(pages, nuxt.vfs, { pagesToSkip: augmentedPages })
|
||||
await augmentPages(pages, nuxt.vfs, { pagesToSkip: augmentedPages, ...augmentCtx })
|
||||
augmentedPages?.clear()
|
||||
}
|
||||
|
||||
@ -158,13 +158,15 @@ export function generateRoutesFromFiles (files: ScannedFile[], options: Generate
|
||||
interface AugmentPagesContext {
|
||||
pagesToSkip?: Set<string>
|
||||
augmentedPages?: Set<string>
|
||||
extraExtractionKeys?: string[]
|
||||
}
|
||||
|
||||
export async function augmentPages (routes: NuxtPage[], vfs: Record<string, string>, ctx: AugmentPagesContext = {}) {
|
||||
ctx.augmentedPages ??= new Set()
|
||||
for (const route of routes) {
|
||||
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 routeMeta = await getRouteMeta(fileContent, route.file)
|
||||
const routeMeta = await getRouteMeta(fileContent, route.file, ctx.extraExtractionKeys)
|
||||
if (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
|
||||
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',
|
||||
@ -196,12 +198,12 @@ export function extractScriptContent (html: string) {
|
||||
}
|
||||
|
||||
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 pageContentsCache: Record<string, string> = {}
|
||||
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
|
||||
if (!(absolutePath in pageContentsCache) || pageContentsCache[absolutePath] !== contents) {
|
||||
pageContentsCache[absolutePath] = contents
|
||||
@ -219,7 +221,9 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
|
||||
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) {
|
||||
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 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) {
|
||||
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 }
|
||||
|
||||
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) {
|
||||
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.value.type === 'ObjectExpression') {
|
||||
@ -295,7 +294,7 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
|
||||
continue
|
||||
}
|
||||
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')
|
||||
break
|
||||
}
|
||||
@ -305,7 +304,6 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
|
||||
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)
|
||||
@ -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', () => {
|
||||
@ -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"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
* performance improvement when prerendering sites that use `useAsyncData` or `useFetch` and fetch the same
|
||||
|
449
pnpm-lock.yaml
449
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user