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
|
::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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
::
|
|
||||||
|
@ -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
|
||||||
---
|
---
|
||||||
|
@ -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({
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
12
package.json
12
package.json
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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()) {
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
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,
|
type: Boolean,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
style: String,
|
style: [String, Object, Array],
|
||||||
tabindex: String,
|
tabindex: String,
|
||||||
title: String,
|
title: String,
|
||||||
translate: String,
|
translate: String,
|
||||||
|
@ -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)
|
||||||
|
@ -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')) {
|
||||||
|
@ -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.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 { 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`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -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"
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -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: () => {},
|
||||||
|
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 { 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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
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