Merge branch 'main' into patch-21

This commit is contained in:
Michael Brevard 2024-06-14 18:39:18 +03:00 committed by GitHub
commit 0ead860117
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
71 changed files with 1326 additions and 503 deletions

View File

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:

View File

@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:

View File

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
- run: corepack enable

View File

@ -25,7 +25,7 @@ jobs:
restore-keys: cache-lychee-
# check links with Lychee
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Lychee link checker
uses: lycheeverse/lychee-action@25a231001d1723960a301b7d4c82884dc7ef857d # for v1.8.0

View File

@ -35,7 +35,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
@ -72,7 +72,7 @@ jobs:
- build
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
@ -83,7 +83,7 @@ jobs:
run: pnpm install
- name: Initialize CodeQL
uses: github/codeql-action/init@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
uses: github/codeql-action/init@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
with:
languages: javascript
queries: +security-and-quality
@ -95,7 +95,7 @@ jobs:
path: packages
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
uses: github/codeql-action/analyze@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
with:
category: "/language:javascript"
@ -111,7 +111,7 @@ jobs:
module: ["bundler", "node"]
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
@ -142,7 +142,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
@ -166,7 +166,7 @@ jobs:
needs:
- build
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
@ -218,7 +218,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
@ -248,7 +248,7 @@ jobs:
TEST_PAYLOAD: ${{ matrix.payload }}
SKIP_BUNDLE_SIZE: ${{ github.event_name != 'push' || matrix.env == 'dev' || matrix.builder == 'webpack' || matrix.context == 'default' || matrix.payload == 'js' || runner.os == 'Windows' }}
- uses: codecov/codecov-action@125fc84a9a348dbcf27191600683ec096ec9021c # v4.4.1
- uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
if: github.event_name != 'push' && matrix.env == 'built' && matrix.builder == 'vite' && matrix.context == 'default' && matrix.os == 'ubuntu-latest' && matrix.manifest == 'manifest-on'
with:
token: ${{ secrets.CODECOV_TOKEN }}
@ -270,7 +270,7 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
- run: corepack enable
@ -309,7 +309,7 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
- run: corepack enable

View File

@ -17,6 +17,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: 'Dependency Review'
uses: actions/dependency-review-action@72eb03d02c7872a771aacd928f3123ac62ad6d3a # v4.3.3

View File

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- run: corepack enable
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:

View File

@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
# From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
- name: Check workflow files
run: |

View File

@ -29,7 +29,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ github.event.issue.pull_request.head.sha }}
fetch-depth: 0

View File

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
- run: corepack enable

View File

@ -10,7 +10,7 @@ jobs:
reproduire:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: Hebilicious/reproduire@4b686ae9cbb72dad60f001d278b6e3b2ce40a9ac # v0.0.9-mp
with:
label: needs reproduction

View File

@ -32,7 +32,7 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
persist-credentials: false
@ -68,7 +68,7 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
uses: github/codeql-action/upload-sarif@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10
if: github.repository == 'nuxt/nuxt' && success()
with:
sarif_file: results.sarif

View File

@ -155,7 +155,7 @@ export default defineNuxtModule({
// Compatibility constraints
compatibility: {
// Semver version of supported nuxt versions
nuxt: '^3.0.0'
nuxt: '>=3.0.0'
}
},
// Default configuration options for your module, can also be a function returning those

View File

@ -18,7 +18,7 @@ Within your pages, components, and plugins you can use useAsyncData to get acces
```vue [pages/index.vue]
<script setup lang="ts">
const { data, pending, error, refresh } = await useAsyncData(
const { data, pending, error, refresh, clear } = await useAsyncData(
'mountains',
() => $fetch('https://api.nuxtjs.dev/mountains')
)
@ -26,7 +26,7 @@ const { data, pending, error, refresh } = await useAsyncData(
```
::note
`data`, `pending`, `status` and `error` are Vue refs and they should be accessed with `.value` when used within the `<script setup>`, while `refresh`/`execute` is a plain function for refetching data.
`data`, `pending`, `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.
::
### Watch Params
@ -92,6 +92,7 @@ Learn how to use `transform` and `getCachedData` to avoid superfluous calls to a
- `refresh`/`execute`: a function that can be used to refresh the data returned by the `handler` function.
- `error`: an error object if the data fetching failed.
- `status`: a string indicating the status of the data request (`"idle"`, `"pending"`, `"success"`, `"error"`).
- `clear`: a function which will set `data` to `undefined`, set `error` to `null`, set `pending` to `false`, set `status` to `'idle'`, and mark any currently pending requests as cancelled.
By default, Nuxt waits until a `refresh` is finished before it can be executed again.

View File

@ -19,14 +19,14 @@ It automatically generates a key based on URL and fetch options, provides type h
```vue [pages/modules.vue]
<script setup lang="ts">
const { data, pending, error, refresh } = await useFetch('/api/modules', {
const { data, pending, error, refresh, clear } = await useFetch('/api/modules', {
pick: ['title']
})
</script>
```
::note
`data`, `pending`, `status` and `error` are Vue refs and they should be accessed with `.value` when used within the `<script setup>`, while `refresh`/`execute` is a plain function for refetching data.
`data`, `pending`, `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.
@ -43,7 +43,7 @@ The above example results in `https://api.nuxt.com/modules?param1=value1&param2=
You can also use [interceptors](https://github.com/unjs/ofetch#%EF%B8%8F-interceptors):
```ts
const { data, pending, error, refresh } = await useFetch('/api/auth/login', {
const { data, pending, error, refresh, clear } = await useFetch('/api/auth/login', {
onRequest({ request, options }) {
// Set the request headers
options.headers = options.headers || {}
@ -128,6 +128,7 @@ Learn how to use `transform` and `getCachedData` to avoid superfluous calls to a
- `refresh`/`execute`: a function that can be used to refresh the data returned by the `handler` function.
- `error`: an error object if the data fetching failed.
- `status`: a string indicating the status of the data request (`"idle"`, `"pending"`, `"success"`, `"error"`).
- `clear`: a function which will set `data` to `undefined`, set `error` to `null`, set `pending` to `false`, set `status` to `'idle'`, and mark any currently pending requests as cancelled.
By default, Nuxt waits until a `refresh` is finished before it can be executed again.

View File

@ -98,6 +98,10 @@ export default defineNuxtComponent({
</script>
```
::warning
Possible breaking change: `head` receives the nuxt app but cannot access the component instance. If the code in your `head` tries to access the data object through `this` or `this.$data`, you will need to migrate to the `useHead` composable.
::
## Title Template
If you want to use a function (for full control), then this cannot be set in your nuxt.config, and it is recommended instead to set it within your `/layouts` directory.

View File

@ -42,7 +42,7 @@
"magic-string": "^0.30.10",
"nuxt": "workspace:*",
"rollup": "^4.18.0",
"vite": "5.2.13",
"vite": "5.3.0",
"vue": "3.4.27"
},
"devDependencies": {
@ -56,7 +56,7 @@
"@types/fs-extra": "11.0.4",
"@types/node": "20.14.2",
"@types/semver": "7.5.8",
"@unhead/schema": "1.9.12",
"@unhead/schema": "1.9.13",
"@vitejs/plugin-vue": "5.0.4",
"@vitest/coverage-v8": "1.6.0",
"@vue/test-utils": "2.4.6",
@ -66,7 +66,7 @@
"devalue": "5.0.0",
"eslint": "9.4.0",
"eslint-plugin-no-only-tests": "3.1.0",
"eslint-plugin-perfectionist": "2.10.0",
"eslint-plugin-perfectionist": "2.11.0",
"eslint-typegen": "0.2.4",
"execa": "9.2.0",
"fs-extra": "11.2.0",

View File

@ -27,7 +27,7 @@
},
"dependencies": {
"@nuxt/schema": "workspace:*",
"c12": "^1.10.0",
"c12": "^1.11.1",
"consola": "^3.2.3",
"defu": "^6.1.4",
"destr": "^2.0.3",
@ -54,7 +54,7 @@
"lodash-es": "4.17.21",
"nitropack": "2.9.6",
"unbuild": "latest",
"vite": "5.2.13",
"vite": "5.3.0",
"vitest": "1.6.0",
"webpack": "5.92.0"
},

View File

@ -61,7 +61,7 @@ function getRequireCacheItem (id: string) {
}
}
export function getModulePaths (paths?: string[] | string) {
export function getNodeModulesPaths (paths?: string[] | string) {
return ([] as Array<string | undefined>).concat(
global.__NUXT_PREPATHS__,
paths || [],
@ -73,7 +73,7 @@ export function getModulePaths (paths?: string[] | string) {
/** @deprecated Do not use CJS utils */
export function resolveModule (id: string, opts: ResolveModuleOptions = {}) {
return normalize(_require.resolve(id, {
paths: getModulePaths(opts.paths),
paths: getNodeModulesPaths(opts.paths),
}))
}

View File

@ -32,10 +32,11 @@ const serialize = (data: any) => JSON.stringify(data, null, 2).replace(/"\{(.+)\
/** @deprecated */
const importSources = (sources: string | string[], { lazy = false } = {}) => {
return toArray(sources).map((src) => {
const safeVariableName = genSafeVariableName(src)
if (lazy) {
return `const ${genSafeVariableName(src)} = ${genDynamicImport(src, { comment: `webpackChunkName: ${JSON.stringify(src)}` })}`
return `const ${safeVariableName} = ${genDynamicImport(src, { comment: `webpackChunkName: ${JSON.stringify(src)}` })}`
}
return genImport(src, genSafeVariableName(src))
return genImport(src, safeVariableName)
}).join('\n')
}

View File

@ -7,10 +7,17 @@ import { NuxtConfigSchema } from '@nuxt/schema'
import { globby } from 'globby'
import defu from 'defu'
export interface LoadNuxtConfigOptions extends LoadConfigOptions<NuxtConfig> {}
export interface LoadNuxtConfigOptions extends Omit<LoadConfigOptions<NuxtConfig>, 'overrides'> {
overrides?: Exclude<LoadConfigOptions<NuxtConfig>['overrides'], Promise<any> | Function>
}
const layerSchemaKeys = ['future', 'srcDir', 'rootDir', 'dir']
const layerSchema = Object.fromEntries(Object.entries(NuxtConfigSchema).filter(([key]) => layerSchemaKeys.includes(key)))
const layerSchema = Object.create(null)
for (const key of layerSchemaKeys) {
if (key in NuxtConfigSchema) {
layerSchema[key] = NuxtConfigSchema[key]
}
}
export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<NuxtOptions> {
// Automatically detect and import layers from `~~/layers/` directory
@ -40,10 +47,15 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
nuxtConfig._nuxtConfigFiles = [configFile]
const _layers: ConfigLayer<NuxtConfig, ConfigLayerMeta>[] = []
const processedLayers = new Set<string>()
for (const layer of layers) {
// Resolve `rootDir` & `srcDir` of layers
layer.config = layer.config || {}
layer.config.rootDir = layer.config.rootDir ?? layer.cwd
layer.config.rootDir = layer.config.rootDir ?? layer.cwd!
// Only process/resolve layers once
if (processedLayers.has(layer.config.rootDir)) { continue }
processedLayers.add(layer.config.rootDir)
// Normalise layer directories
layer.config = await applyDefaults(layerSchema, layer.config as NuxtConfig & Record<string, JSValue>) as unknown as NuxtConfig

View File

@ -51,11 +51,10 @@ export async function getNuxtModuleVersion (module: string | NuxtModule, nuxt: N
// need a name from here
if (!moduleMeta.name) { return false }
// maybe the version got attached within the installed module instance?
const version = nuxt.options._installedModules
// @ts-expect-error _installedModules is not typed
.filter(m => m.meta.name === moduleMeta.name).map(m => m.meta.version)?.[0]
if (version) {
return version
for (const m of nuxt.options._installedModules) {
if (m.meta.name === moduleMeta.name && m.meta.version) {
return m.meta.version
}
}
// it's possible that the module will be installed, it just hasn't been done yet, preemptively load the instance
if (hasNuxtModule(moduleMeta.name)) {

View File

@ -51,11 +51,12 @@ export function addRouteMiddleware (input: NuxtMiddleware | NuxtMiddleware[], op
for (const middleware of middlewares) {
const find = app.middleware.findIndex(item => item.name === middleware.name)
if (find >= 0) {
if (app.middleware[find].path === middleware.path) { continue }
const foundPath = app.middleware[find].path
if (foundPath === middleware.path) { continue }
if (options.override === true) {
app.middleware[find] = { ...middleware }
} else {
logger.warn(`'${middleware.name}' middleware already exists at '${app.middleware[find].path}'. You can set \`override: true\` to replace it.`)
logger.warn(`'${middleware.name}' middleware already exists at '${foundPath}'. You can set \`override: true\` to replace it.`)
}
} else {
app.middleware.push({ ...middleware })

View File

@ -168,8 +168,8 @@ export function createResolver (base: string | URL): Resolver {
}
}
export async function resolveNuxtModule (base: string, paths: string[]) {
const resolved = []
export async function resolveNuxtModule (base: string, paths: string[]): Promise<string[]> {
const resolved: string[] = []
const resolver = createResolver(base)
for (const path of paths) {
@ -209,6 +209,12 @@ function existsInVFS (path: string, nuxt = tryUseNuxt()) {
}
export async function resolveFiles (path: string, pattern: string | string[], opts: { followSymbolicLinks?: boolean } = {}) {
const files = await globby(pattern, { cwd: path, followSymbolicLinks: opts.followSymbolicLinks ?? true })
return files.map(p => resolve(path, p)).filter(p => !isIgnored(p)).sort()
const files: string[] = []
for (const file of await globby(pattern, { cwd: path, followSymbolicLinks: opts.followSymbolicLinks ?? true })) {
const p = resolve(path, file)
if (!isIgnored(p)) {
files.push(p)
}
}
return files.sort()
}

View File

@ -11,7 +11,7 @@ import { readPackageJSON } from 'pkg-types'
import { tryResolveModule } from './internal/esm'
import { getDirectory } from './module/install'
import { tryUseNuxt, useNuxt } from './context'
import { getModulePaths } from './internal/cjs'
import { getNodeModulesPaths } from './internal/cjs'
import { resolveNuxtModule } from './resolve'
/**
@ -113,18 +113,55 @@ export async function updateTemplates (options?: { filter?: (template: ResolvedN
}
export async function _generateTypes (nuxt: Nuxt) {
const nodeModulePaths = getModulePaths(nuxt.options.modulesDir)
const rootDirWithSlash = withTrailingSlash(nuxt.options.rootDir)
const relativeRootDir = relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir)
const modulePaths = await resolveNuxtModule(rootDirWithSlash,
nuxt.options._installedModules
.filter(m => m.entryPath)
.map(m => getDirectory(m.entryPath)),
)
const include = new Set<string>([
'./nuxt.d.ts',
join(relativeRootDir, '.config/nuxt.*'),
join(relativeRootDir, '**/*'),
])
if (nuxt.options.srcDir !== nuxt.options.rootDir) {
include.add(join(relative(nuxt.options.buildDir, nuxt.options.srcDir), '**/*'))
}
if (nuxt.options.typescript.includeWorkspace && nuxt.options.workspaceDir !== nuxt.options.rootDir) {
include.add(join(relative(nuxt.options.buildDir, nuxt.options.workspaceDir), '**/*'))
}
for (const layer of nuxt.options._layers) {
const srcOrCwd = layer.config.srcDir ?? layer.cwd
if (!srcOrCwd.startsWith(rootDirWithSlash) || srcOrCwd.includes('node_modules')) {
include.add(join(relative(nuxt.options.buildDir, srcOrCwd), '**/*'))
}
}
const exclude = new Set<string>([
// nitro generate output: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/nitro.ts#L186
relativeWithDot(nuxt.options.buildDir, resolve(nuxt.options.rootDir, 'dist')),
])
for (const dir of nuxt.options.modulesDir) {
exclude.add(relativeWithDot(nuxt.options.buildDir, dir))
}
const moduleEntryPaths: string[] = []
for (const m of nuxt.options._installedModules) {
if (m.entryPath) {
moduleEntryPaths.push(getDirectory(m.entryPath))
}
}
const modulePaths = await resolveNuxtModule(rootDirWithSlash, moduleEntryPaths)
for (const path of modulePaths) {
const relative = relativeWithDot(nuxt.options.buildDir, path)
include.add(join(relative, 'runtime'))
exclude.add(join(relative, 'runtime/server'))
}
const isV4 = nuxt.options.future?.compatibilityVersion === 4
const hasTypescriptVersionWithModulePreserve = await readPackageJSON('typescript', { url: nuxt.options.modulesDir })
.then(r => r?.version && gte(r.version, '5.4.0'))
.catch(() => isV4)
@ -168,23 +205,8 @@ export async function _generateTypes (nuxt: Nuxt) {
noImplicitThis: true, /* enabled with `strict` */
allowSyntheticDefaultImports: true,
},
include: [
'./nuxt.d.ts',
join(relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir), '.config/nuxt.*'),
join(relativeWithDot(nuxt.options.buildDir, nuxt.options.rootDir), '**/*'),
...nuxt.options.srcDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.srcDir), '**/*')] : [],
...nuxt.options._layers.map(layer => layer.config.srcDir ?? layer.cwd)
.filter(srcOrCwd => !srcOrCwd.startsWith(rootDirWithSlash) || srcOrCwd.includes('node_modules'))
.map(srcOrCwd => join(relative(nuxt.options.buildDir, srcOrCwd), '**/*')),
...nuxt.options.typescript.includeWorkspace && nuxt.options.workspaceDir !== nuxt.options.rootDir ? [join(relative(nuxt.options.buildDir, nuxt.options.workspaceDir), '**/*')] : [],
...modulePaths.map(m => join(relativeWithDot(nuxt.options.buildDir, m), 'runtime')),
],
exclude: [
...nuxt.options.modulesDir.map(m => relativeWithDot(nuxt.options.buildDir, m)),
...modulePaths.map(m => join(relativeWithDot(nuxt.options.buildDir, m), 'runtime/server')),
// nitro generate output: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/core/nitro.ts#L186
relativeWithDot(nuxt.options.buildDir, resolve(nuxt.options.rootDir, 'dist')),
],
include: [...include],
exclude: [...exclude],
} satisfies TSConfig)
const aliases: Record<string, string> = {
@ -195,7 +217,9 @@ export async function _generateTypes (nuxt: Nuxt) {
// Exclude bridge alias types to support Volar
const excludedAlias = [/^@vue\/.*$/]
const basePath = tsConfig.compilerOptions!.baseUrl ? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl) : nuxt.options.buildDir
const basePath = tsConfig.compilerOptions!.baseUrl
? resolve(nuxt.options.buildDir, tsConfig.compilerOptions!.baseUrl)
: nuxt.options.buildDir
tsConfig.compilerOptions = tsConfig.compilerOptions || {}
tsConfig.include = tsConfig.include || []
@ -237,12 +261,13 @@ export async function _generateTypes (nuxt: Nuxt) {
}
}
const references: TSReference[] = await Promise.all([
...nuxt.options.modules,
...nuxt.options._modules,
]
.filter(f => typeof f === 'string')
.map(async id => ({ types: (await readPackageJSON(id, { url: nodeModulePaths }).catch(() => null))?.name || id })))
const references: TSReference[] = []
await Promise.all([...nuxt.options.modules, ...nuxt.options._modules].map(async (id) => {
if (typeof id !== 'string') { return }
const pkg = await readPackageJSON(id, { url: getNodeModulesPaths(nuxt.options.modulesDir) }).catch(() => null)
references.push(({ types: pkg?.name || id }))
}))
const declarations: string[] = []
@ -302,7 +327,11 @@ export async function writeTypes (nuxt: Nuxt) {
}
function renderAttrs (obj: Record<string, string>) {
return Object.entries(obj).map(e => renderAttr(e[0], e[1])).join(' ')
const attrs: string[] = []
for (const key in obj) {
attrs.push(renderAttr(key, obj[key]))
}
return attrs.join(' ')
}
function renderAttr (key: string, value: string) {

View File

@ -53,12 +53,12 @@ describe('tsConfig generation', () => {
}))
expect(tsConfig.exclude).toMatchInlineSnapshot(`
[
"../dist",
"../modules/test/node_modules",
"../modules/node_modules",
"../node_modules/@some/module/node_modules",
"../node_modules",
"../../node_modules",
"../dist",
]
`)
})

View File

@ -65,12 +65,12 @@
"@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.5.4",
"@nuxt/vite-builder": "workspace:*",
"@unhead/dom": "^1.9.12",
"@unhead/ssr": "^1.9.12",
"@unhead/vue": "^1.9.12",
"@unhead/dom": "^1.9.13",
"@unhead/ssr": "^1.9.13",
"@unhead/vue": "^1.9.13",
"@vue/shared": "^3.4.27",
"acorn": "8.11.3",
"c12": "^1.10.0",
"c12": "^1.11.1",
"chokidar": "^3.6.0",
"cookie-es": "^1.1.0",
"defu": "^6.1.4",
@ -118,7 +118,7 @@
"vue-router": "^4.3.3"
},
"devDependencies": {
"@nuxt/scripts": "0.4.7",
"@nuxt/scripts": "0.5.1",
"@nuxt/ui-templates": "1.3.4",
"@parcel/watcher": "2.4.1",
"@types/estree": "1.0.5",
@ -126,7 +126,7 @@
"@vitejs/plugin-vue": "5.0.4",
"@vue/compiler-sfc": "3.4.27",
"unbuild": "latest",
"vite": "5.2.13",
"vite": "5.3.0",
"vitest": "1.6.0"
},
"peerDependencies": {

View File

@ -1,4 +1,4 @@
import type { Component, PropType } from 'vue'
import type { Component, PropType, VNode } from 'vue'
import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onMounted, ref, toRaw, watch, withMemo } from 'vue'
import { debounce } from 'perfect-debounce'
import { hash } from 'ohash'
@ -29,7 +29,7 @@ const getId = import.meta.client ? () => (id++).toString() : randomUUID
const components = import.meta.client ? new Map<string, Component>() : undefined
async function loadComponents (source = appBaseURL, paths: NuxtIslandResponse['components']) {
const promises = []
const promises: Array<Promise<void>> = []
for (const component in paths) {
if (!(components!.has(component))) {
@ -259,7 +259,7 @@ export default defineComponent({
// should away be triggered ONE tick after re-rendering the static node
withMemo([teleportKey.value], () => {
const teleports = []
const teleports: Array<VNode> = []
// this is used to force trigger Teleport when vue makes the diff between old and new node
const isKeyOdd = teleportKey.value === 0 || !!(teleportKey.value && !(teleportKey.value % 2))

View File

@ -8,7 +8,7 @@ import type {
} from 'vue'
import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref, resolveComponent } from 'vue'
import type { RouteLocation, RouteLocationRaw, Router, RouterLink, RouterLinkProps, useLink } from '#vue-router'
import { hasProtocol, joinURL, parseQuery, withTrailingSlash, withoutTrailingSlash } from 'ufo'
import { hasProtocol, joinURL, parseQuery, withQuery, withTrailingSlash, withoutTrailingSlash } from 'ufo'
import { preloadRouteComponents } from '../composables/preload'
import { onNuxtReady } from '../composables/ready'
import { navigateTo, useRouter } from '../composables/router'
@ -71,8 +71,8 @@ export interface NuxtLinkProps extends Omit<RouterLinkProps, 'to'> {
* @see https://nuxt.com/docs/api/components/nuxt-link
*/
export interface NuxtLinkOptions extends
Pick<RouterLinkProps, 'activeClass' | 'exactActiveClass'>,
Pick<NuxtLinkProps, 'prefetchedClass'> {
Partial<Pick<RouterLinkProps, 'activeClass' | 'exactActiveClass'>>,
Partial<Pick<NuxtLinkProps, 'prefetchedClass'>> {
/**
* The name of the component.
* @default "NuxtLink"
@ -125,34 +125,17 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
const router = useRouter()
const config = useRuntimeConfig()
// Resolving `to` value from `to` and `href` props
const to: ComputedRef<string | RouteLocationRaw> = computed(() => {
checkPropConflicts(props, 'to', 'href')
const path = props.to || props.href || '' // Defaults to empty string (won't render any `href` attribute)
return resolveTrailingSlashBehavior(path, router.resolve)
})
const hasTarget = computed(() => !!props.target && props.target !== '_self')
// Lazily check whether to.value has a protocol
const isAbsoluteUrl = computed(() => typeof to.value === 'string' && hasProtocol(to.value, { acceptRelative: true }))
// Resolves `to` value if it's a route location object
const href = computed(() => (typeof to.value === 'object'
? router.resolve(to.value)?.href ?? null
: (to.value && !props.external && !isAbsoluteUrl.value)
? resolveTrailingSlashBehavior(joinURL(config.app.baseURL, to.value), router.resolve) as string
: to.value
))
const isAbsoluteUrl = computed(() => {
const path = props.to || props.href || ''
return typeof path === 'string' && hasProtocol(path, { acceptRelative: true })
})
const builtinRouterLink = resolveComponent('RouterLink') as string | typeof RouterLink
const useBuiltinLink = builtinRouterLink && typeof builtinRouterLink !== 'string' ? builtinRouterLink.useLink : undefined
const link = useBuiltinLink?.({
...props,
to: to.value,
})
const hasTarget = computed(() => props.target && props.target !== '_self')
// Resolving link type
const isExternal = computed<boolean>(() => {
// External prop is explicitly set
@ -160,17 +143,40 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
return true
}
// When `target` prop is set, link is external
if (hasTarget.value) {
return true
}
const path = props.to || props.href || ''
// When `to` is a route object then it's an internal link
if (typeof to.value === 'object') {
if (typeof path === 'object') {
return false
}
return to.value === '' || isAbsoluteUrl.value
return path === '' || isAbsoluteUrl.value
})
// Resolving `to` value from `to` and `href` props
const to: ComputedRef<RouteLocationRaw> = computed(() => {
checkPropConflicts(props, 'to', 'href')
const path = props.to || props.href || '' // Defaults to empty string (won't render any `href` attribute)
if (isExternal.value) { return path }
return resolveTrailingSlashBehavior(path, router.resolve)
})
const link = isExternal.value ? undefined : useBuiltinLink?.({ ...props, to })
// Resolves `to` value if it's a route location object
const href = computed(() => {
if (!to.value || isAbsoluteUrl.value) { return to.value as string }
if (isExternal.value) {
const path = typeof to.value === 'object' ? resolveRouteObject(to.value) : to.value
return resolveTrailingSlashBehavior(path, router.resolve /* will not be called */) as string
}
if (typeof to.value === 'object') {
return router.resolve(to.value)?.href ?? null
}
return resolveTrailingSlashBehavior(joinURL(config.app.baseURL, to.value), router.resolve /* will not be called */)
})
return {
@ -184,10 +190,10 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
isExactActive: link?.isExactActive ?? computed(() => to.value === router.currentRoute.value.path),
route: link?.route ?? computed(() => router.resolve(to.value)),
async navigate () {
await navigateTo(href.value, { replace: props.replace, external: props.external })
await navigateTo(href.value, { replace: props.replace, external: isExternal.value || hasTarget.value })
},
} satisfies ReturnType<typeof useLink> & {
to: ComputedRef<string | RouteLocationRaw>
to: ComputedRef<RouteLocationRaw>
hasTarget: ComputedRef<boolean | null | undefined>
isAbsoluteUrl: ComputedRef<boolean>
isExternal: ComputedRef<boolean>
@ -308,10 +314,12 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
unobserve?.()
unobserve = null
const path = typeof to.value === 'string' ? to.value : router.resolve(to.value).fullPath
const path = typeof to.value === 'string'
? to.value
: isExternal.value ? resolveRouteObject(to.value) : router.resolve(to.value).fullPath
await Promise.all([
nuxtApp.hooks.callHook('link:prefetch', path).catch(() => {}),
!isExternal.value && preloadRouteComponents(to.value as string, router).catch(() => {}),
!isExternal.value && !hasTarget.value && preloadRouteComponents(to.value as string, router).catch(() => {}),
])
prefetched.value = true
})
@ -337,7 +345,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
}
return () => {
if (!isExternal.value) {
if (!isExternal.value && !hasTarget.value) {
const routerLinkProps: RouterLinkProps & VNodeProps & AllowedComponentProps & AnchorHTMLAttributes = {
ref: elRef,
to: to.value,
@ -409,7 +417,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
},
rel,
target,
isExternal: isExternal.value,
isExternal: isExternal.value || hasTarget.value,
isActive: false,
isExactActive: false,
})
@ -486,3 +494,7 @@ function isSlowConnection () {
if (cn && (cn.saveData || /2g/.test(cn.effectiveType))) { return true }
return false
}
function resolveRouteObject (to: Exclude<RouteLocationRaw, string>) {
return withQuery(to.path || '', to.query || {}) + (to.hash ? '#' + to.hash : '')
}

View File

@ -86,7 +86,7 @@ export function vforToArray (source: any): any[] {
if (import.meta.dev && !Number.isInteger(source)) {
console.warn(`The v-for range expect an integer value but got ${source}.`)
}
const array = []
const array: number[] = []
for (let i = 0; i < source; i++) {
array[i] = i
}

View File

@ -37,3 +37,4 @@ export { reloadNuxtApp } from './chunk'
export { useRequestURL } from './url'
export { usePreviewMode } from './preview'
export { useId } from './id'
export { useRouteAnnouncer } from './route-announcer'

View File

@ -568,7 +568,7 @@ function wrappedConfig (runtimeConfig: Record<string, unknown>) {
if (typeof p === 'string' && p !== 'public' && !(p in target) && !p.startsWith('__v') /* vue check for reactivity, e.g. `__v_isRef` */) {
if (!loggedKeys.has(p)) {
loggedKeys.add(p)
console.warn(`[nuxt] Could not access \`${p}\`. The only available runtime config keys on the client side are ${keys.join(', ')} and ${lastKey}. See \`https://nuxt.com/docs/guide/going-further/runtime-config\` for more information.`)
console.warn(`[nuxt] Could not access \`${p}\`. The only available runtime config keys on the client side are ${keys.join(', ')} and ${lastKey}. See https://nuxt.com/docs/guide/going-further/runtime-config for more information.`)
}
}
return Reflect.get(target, p, receiver)

View File

@ -136,6 +136,7 @@ function createGranularWatcher () {
console.timeEnd('[nuxt] builder:chokidar:watch')
}
})
nuxt.hook('close', () => watcher?.close())
}
}

View File

@ -43,7 +43,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
const modules = await resolveNuxtModule(rootDirWithSlash,
nuxt.options._installedModules
.filter(m => m.entryPath)
.map(m => m.entryPath),
.map(m => m.entryPath!),
)
const nitroConfig: NitroConfig = defu(nuxt.options.nitro, {

View File

@ -4,7 +4,7 @@ import ignore from 'ignore'
import type { LoadNuxtOptions } from '@nuxt/kit'
import { addBuildPlugin, addComponent, addPlugin, addRouteMiddleware, addServerPlugin, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolveIgnorePatterns, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit'
import { resolvePath as _resolvePath } from 'mlly'
import type { Nuxt, NuxtHooks, NuxtModule, NuxtOptions, RuntimeConfig } from 'nuxt/schema'
import type { Nuxt, NuxtHooks, NuxtModule, NuxtOptions } from 'nuxt/schema'
import type { PackageJson } from 'pkg-types'
import { readPackageJSON, resolvePackageJSON } from 'pkg-types'
import { hash } from 'ohash'
@ -49,7 +49,10 @@ export function createNuxt (options: NuxtOptions): Nuxt {
addHooks: hooks.addHooks,
hook: hooks.hook,
ready: () => initNuxt(nuxt),
close: () => Promise.resolve(hooks.callHook('close', nuxt)),
close: async () => {
await hooks.callHook('close', nuxt)
hooks.removeAllHooks()
},
vfs: {},
apps: {},
}
@ -125,6 +128,7 @@ async function initNuxt (nuxt: Nuxt) {
// Add nuxt types
nuxt.hook('prepare:types', (opts) => {
opts.references.push({ types: 'nuxt' })
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/app-defaults.d.ts') })
opts.references.push({ path: resolve(nuxt.options.buildDir, 'types/plugins.d.ts') })
// Add vue shim
if (nuxt.options.typescript.shim) {
@ -641,8 +645,22 @@ export async function loadNuxt (opts: LoadNuxtOptions): Promise<Nuxt> {
options._modules.push('@nuxt/telemetry')
}
// Ensure we share runtime config between Nuxt and Nitro
options.runtimeConfig = options.nitro.runtimeConfig as RuntimeConfig
// Ensure we share key config between Nuxt and Nitro
createPortalProperties(options.nitro.runtimeConfig, options, ['nitro.runtimeConfig', 'runtimeConfig'])
createPortalProperties(options.nitro.routeRules, options, ['nitro.routeRules', 'routeRules'])
// prevent replacement of options.nitro
const nitroOptions = options.nitro
Object.defineProperties(options, {
nitro: {
configurable: false,
enumerable: true,
get: () => nitroOptions,
set (value) {
Object.assign(nitroOptions, value)
},
},
})
const nuxt = createNuxt(options)
@ -680,7 +698,7 @@ const RESTART_RE = /^(?:app|error|app\.config)\.(?:js|ts|mjs|jsx|tsx|vue)$/i
function deduplicateArray<T = unknown> (maybeArray: T): T {
if (!Array.isArray(maybeArray)) { return maybeArray }
const fresh = []
const fresh: any[] = []
const hashes = new Set<string>()
for (const item of maybeArray) {
const _hash = hash(item)
@ -691,3 +709,31 @@ function deduplicateArray<T = unknown> (maybeArray: T): T {
}
return fresh as T
}
function createPortalProperties (sourceValue: any, options: NuxtOptions, paths: string[]) {
let sharedValue = sourceValue
for (const path of paths) {
const segments = path.split('.')
const key = segments.pop()!
let parent: Record<string, any> = options
while (segments.length) {
const key = segments.shift()!
parent = parent[key] || (parent[key] = {})
}
delete parent[key]
Object.defineProperties(parent, {
[key]: {
configurable: false,
enumerable: true,
get: () => sharedValue,
set (value) {
sharedValue = value
},
},
})
}
}

View File

@ -400,7 +400,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
// 2. Styles
head.push({ style: inlinedStyles })
if (!isRenderingIsland || import.meta.dev) {
const link = []
const link: Link[] = []
for (const style in styles) {
const resource = styles[style]
// Do not add links to resources that are inlined (vite v5+)

View File

@ -7,7 +7,7 @@ import escapeRE from 'escape-string-regexp'
import { hash } from 'ohash'
import { camelCase } from 'scule'
import { filename } from 'pathe/utils'
import type { NuxtTemplate } from 'nuxt/schema'
import type { NuxtTemplate, NuxtTypeTemplate } from 'nuxt/schema'
import { annotatePlugins, checkForCircularDependencies } from './app'
@ -96,6 +96,20 @@ export const serverPluginTemplate: NuxtTemplate = {
},
}
export const appDefaults: NuxtTypeTemplate = {
filename: 'types/app-defaults.d.ts',
getContents: (ctx) => {
const isV4 = ctx.nuxt.options.future.compatibilityVersion === 4
return `
declare module '#app/defaults' {
type DefaultAsyncDataErrorValue = ${isV4 ? 'undefined' : 'null'}
type DefaultAsyncDataValue = ${isV4 ? 'undefined' : 'null'}
type DefaultErrorValue = ${isV4 ? 'undefined' : 'null'}
type DedupeOption = ${isV4 ? '\'cancel\' | \'defer\'' : 'boolean | \'cancel\' | \'defer\''}
}`
},
}
export const pluginsDeclaration: NuxtTemplate = {
filename: 'types/plugins.d.ts',
getContents: async (ctx) => {
@ -112,8 +126,6 @@ export const pluginsDeclaration: NuxtTemplate = {
const pluginsName = (await annotatePlugins(ctx.nuxt, ctx.app.plugins)).filter(p => p.name).map(p => `'${p.name}'`)
const isV4 = ctx.nuxt.options.future.compatibilityVersion === 4
return `// Generated by Nuxt'
import type { Plugin } from '#app'
@ -132,13 +144,6 @@ declare module '#app' {
}
}
declare module '#app/defaults' {
type DefaultAsyncDataErrorValue = ${isV4 ? 'undefined' : 'null'}
type DefaultAsyncDataValue = ${isV4 ? 'undefined' : 'null'}
type DefaultErrorValue = ${isV4 ? 'undefined' : 'null'}
type DedupeOption = ${isV4 ? '\'cancel\' | \'defer\'' : 'boolean | \'cancel\' | \'defer\''}
}
declare module 'vue' {
interface ComponentCustomProperties extends NuxtAppInjections { }
}

View File

@ -109,6 +109,10 @@ const granularAppPresets: InlinePreset[] = [
imports: ['useId'],
from: '#app/composables/id',
},
{
imports: ['useRouteAnnouncer'],
from: '#app/composables/route-announcer',
},
]
export const scriptsStubsPreset = {
@ -119,6 +123,7 @@ export const scriptsStubsPreset = {
'useScript',
'useScriptGoogleAnalytics',
'useScriptPlausibleAnalytics',
'useScriptClarity',
'useScriptCloudflareWebAnalytics',
'useScriptFathomAnalytics',
'useScriptMatomoAnalytics',

View File

@ -275,6 +275,20 @@ export default defineNuxtModule({
}
})
// TODO: inject routes in `200.html` in next nitro upgrade (2.9.7+) via https://github.com/unjs/nitro/pull/2517
if (!nuxt.options.dev && !nuxt.options._prepare) {
nuxt.hook('app:templatesGenerated', (app) => {
const nitro = useNitro()
if (nitro.options.prerender.crawlLinks) {
for (const page of app.pages!) {
if (page.path && !page.path.includes(':')) {
nitro.options.prerender.routes.push(page.path)
}
}
}
})
}
nuxt.hook('imports:extend', (imports) => {
imports.push(
{ name: 'definePageMeta', as: 'definePageMeta', from: resolve(runtimeDir, 'composables') },

View File

@ -18,11 +18,12 @@ export async function extractRouteRules (code: string): Promise<NitroRouteConfig
}
if (!ROUTE_RULE_RE.test(code)) { return null }
code = extractScriptContent(code) || code
const script = extractScriptContent(code)
code = script?.code || code
let rule: NitroRouteConfig | null = null
const js = await transform(code, { loader: 'ts' })
const js = await transform(code, { loader: script?.loader || 'ts' })
walk(parse(js.code, {
sourceType: 'module',
ecmaVersion: 'latest',

View File

@ -9,6 +9,7 @@ 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 { NuxtPage } from 'nuxt/schema'
@ -154,12 +155,15 @@ export async function augmentPages (routes: NuxtPage[], vfs: Record<string, stri
return augmentedPages
}
const SFC_SCRIPT_RE = /<script[^>]*>([\s\S]*?)<\/script[^>]*>/i
const SFC_SCRIPT_RE = /<script(?<attrs>[^>]*)>(?<content>[\s\S]*?)<\/script[^>]*>/i
export function extractScriptContent (html: string) {
const match = html.match(SFC_SCRIPT_RE)
const groups = html.match(SFC_SCRIPT_RE)?.groups || {}
if (match && match[1]) {
return match[1].trim()
if (groups.content) {
return {
loader: groups.attrs.includes('tsx') ? 'tsx' : 'ts',
code: groups.content.trim(),
} as const
}
return null
@ -170,7 +174,7 @@ const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
const pageContentsCache: Record<string, string> = {}
const metaCache: Record<string, Partial<Record<keyof NuxtPage, any>>> = {}
async function getRouteMeta (contents: string, absolutePath: string): Promise<Partial<Record<keyof NuxtPage, any>>> {
export async function getRouteMeta (contents: string, absolutePath: 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
@ -185,28 +189,33 @@ async function getRouteMeta (contents: string, absolutePath: string): Promise<Pa
return {}
}
if (!PAGE_META_RE.test(script)) {
if (!PAGE_META_RE.test(script.code)) {
metaCache[absolutePath] = {}
return {}
}
const js = await transform(script, { loader: 'ts' })
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 pageMetaAST = ast.body.find(node => node.type === 'ExpressionStatement' && node.expression.type === 'CallExpression' && node.expression.callee.type === 'Identifier' && node.expression.callee.name === 'definePageMeta')
if (!pageMetaAST) {
metaCache[absolutePath] = {}
return {}
}
const pageMetaArgument = ((pageMetaAST as ExpressionStatement).expression as CallExpression).arguments[0] as ObjectExpression
const extractedMeta = {} as Partial<Record<keyof NuxtPage, any>>
const extractionKeys = ['name', 'path', 'alias', 'redirect'] as const
const dynamicProperties = new Set<keyof NuxtPage>()
let foundMeta = false
walk(ast, {
enter (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
for (const key of extractionKeys) {
const property = pageMetaArgument.properties.find(property => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === key) as Property
if (!property) { continue }
@ -223,7 +232,7 @@ async function getRouteMeta (contents: string, absolutePath: string): Promise<Pa
}
if (property.value.type === 'ArrayExpression') {
const values = []
const values: string[] = []
for (const element of property.value.elements) {
if (!element) {
continue
@ -260,6 +269,8 @@ async function getRouteMeta (contents: string, absolutePath: string): Promise<Pa
extractedMeta.meta ??= {}
extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties
}
},
})
metaCache[absolutePath] = extractedMeta
return extractedMeta
@ -502,19 +513,20 @@ async function createClientPage(loader) {
}`)
}
if (route.children != null) {
if (route.children) {
metaRoute.children = route.children
}
if (overrideMeta) {
metaRoute.name = `${metaImportName}?.name`
metaRoute.path = `${metaImportName}?.path ?? ''`
if (route.meta) {
metaRoute.meta = `{ ...(${metaImportName} || {}), ...${route.meta} }`
}
if (overrideMeta) {
// skip and retain fallback if marked dynamic
// set to extracted value or fallback if none extracted
for (const key of ['name', 'path'] satisfies NormalizedRouteKeys) {
if (markedDynamic.has(key)) { continue }
metaRoute[key] = route[key] ?? metaRoute[key]
metaRoute[key] = route[key] ?? `${metaImportName}?.${key}`
}
// set to extracted value or delete if none extracted
@ -529,10 +541,6 @@ async function createClientPage(loader) {
metaRoute[key] = route[key]
}
} else {
if (route.meta != null) {
metaRoute.meta = `{ ...(${metaImportName} || {}), ...${route.meta} }`
}
if (route.alias != null) {
metaRoute.alias = `${route.alias}.concat(${metaImportName}?.alias || [])`
}

View File

@ -303,7 +303,7 @@
"alias": "mockMeta?.alias || []",
"component": "() => import("pages/index.vue").then(m => m.default || m)",
"meta": "mockMeta || {}",
"name": "mockMeta?.name",
"name": "mockMeta?.name ?? "index"",
"path": ""/"",
"redirect": "mockMeta?.redirect",
},

View File

@ -202,6 +202,9 @@ describe('imports:nuxt/scripts', () => {
'useAnalyticsPageEvent',
'useElementScriptTrigger',
'useConsentScriptTrigger',
// registered separately
'useScriptGoogleTagManager',
'useScriptGoogleAnalytics',
])
it.each(scriptsStubsPreset.imports)(`should register %s from @nuxt/scripts`, (name) => {
if (globalScripts.has(name)) { return }

View File

@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import type { RouteLocation, RouteLocationRaw } from 'vue-router'
import type { RouteLocation } from 'vue-router'
import type { NuxtLinkOptions, NuxtLinkProps } from '../src/app/components/nuxt-link'
import { defineNuxtLink } from '../src/app/components/nuxt-link'
import { useRuntimeConfig } from '../src/app/nuxt'
@ -99,7 +99,11 @@ describe('nuxt-link:isExternal', () => {
})
it('returns `false` when `to` is a route location object', () => {
expect(nuxtLink({ to: { to: '/to' } as RouteLocationRaw }).type).toBe(INTERNAL)
expect(nuxtLink({ to: { path: '/to' } }).type).toBe(INTERNAL)
})
it('returns `true` when `to` has a `target`', () => {
expect(nuxtLink({ to: { path: '/to' }, target: '_blank' }).type).toBe(EXTERNAL)
})
it('honors `external` prop', () => {
@ -122,7 +126,12 @@ describe('nuxt-link:propsOrAttributes', () => {
})
it('resolves route location object', () => {
expect(nuxtLink({ to: { to: '/to' } as RouteLocationRaw, external: true }).props.href).toBe('/to')
expect(nuxtLink({ to: { path: '/to' }, external: true }).props.href).toBe('/to')
})
it('applies trailing slash behaviour', () => {
expect(nuxtLink({ to: { path: '/to' }, external: true }, { trailingSlash: 'append' }).props.href).toBe('/to/')
expect(nuxtLink({ to: '/to', external: true }, { trailingSlash: 'append' }).props.href).toBe('/to/')
})
})
@ -167,6 +176,8 @@ describe('nuxt-link:propsOrAttributes', () => {
}, () => {
expect(nuxtLink({ to: 'http://nuxtjs.org/app/about', target: '_blank' }).props.href).toBe('http://nuxtjs.org/app/about')
expect(nuxtLink({ to: '//nuxtjs.org/app/about', target: '_blank' }).props.href).toBe('//nuxtjs.org/app/about')
expect(nuxtLink({ to: { path: '/' }, external: true }).props.href).toBe('/')
expect(nuxtLink({ to: '/', external: true }).props.href).toBe('/')
})
})
})
@ -209,7 +220,7 @@ describe('nuxt-link:propsOrAttributes', () => {
describe('to', () => {
it('forwards `to` prop', () => {
expect(nuxtLink({ to: '/to' }).props.to).toBe('/to')
expect(nuxtLink({ to: { to: '/to' } as RouteLocationRaw }).props.to).toEqual({ to: '/to' })
expect(nuxtLink({ to: { path: '/to' } }).props.to).toEqual({ path: '/to' })
})
})

View File

@ -0,0 +1,148 @@
import { describe, expect, it } from 'vitest'
import { getRouteMeta, normalizeRoutes } from '../src/pages/utils'
import type { NuxtPage } from '../schema'
const filePath = '/app/pages/index.vue'
describe('page metadata', () => {
it('should not extract metadata from empty files', async () => {
expect(await getRouteMeta('', filePath)).toEqual({})
expect(await getRouteMeta('<template><div>Hi</div></template>', filePath)).toEqual({})
})
it('should use and invalidate cache', async () => {
const fileContents = `<script setup>definePageMeta({ foo: 'bar' })</script>`
const meta = await getRouteMeta(fileContents, filePath)
expect(meta === await getRouteMeta(fileContents, filePath)).toBeTruthy()
expect(meta === await getRouteMeta(fileContents, '/app/pages/other.vue')).toBeFalsy()
expect(meta === await getRouteMeta('<template><div>Hi</div></template>' + fileContents, filePath)).toBeFalsy()
})
it('should extract serialisable metadata', async () => {
const meta = await getRouteMeta(`
<script setup>
definePageMeta({
name: 'some-custom-name',
path: '/some-custom-path',
validate: () => true,
middleware: [
function () {},
],
otherValue: {
foo: 'bar',
},
})
</script>
`, filePath)
expect(meta).toMatchInlineSnapshot(`
{
"meta": {
"__nuxt_dynamic_meta_key": Set {
"meta",
},
},
"name": "some-custom-name",
"path": "/some-custom-path",
}
`)
})
it('should extract serialisable metadata in options api', async () => {
const meta = await getRouteMeta(`
<script>
export default {
setup() {
definePageMeta({
name: 'some-custom-name',
path: '/some-custom-path',
middleware: (from, to) => console.warn('middleware'),
})
},
};
</script>
`, filePath)
expect(meta).toMatchInlineSnapshot(`
{
"meta": {
"__nuxt_dynamic_meta_key": Set {
"meta",
},
},
"name": "some-custom-name",
"path": "/some-custom-path",
}
`)
})
})
describe('normalizeRoutes', () => {
it('should produce valid route objects when used with extracted meta', async () => {
const page: NuxtPage = { path: '/', file: filePath }
Object.assign(page, await getRouteMeta(`
<script setup>
definePageMeta({
name: 'some-custom-name',
path: ref('/some-custom-path'), /* dynamic */
validate: () => true,
redirect: '/',
middleware: [
function () {},
],
otherValue: {
foo: 'bar',
},
})
</script>
`, filePath))
page.meta ||= {}
page.meta.layout = 'test'
page.meta.foo = 'bar'
const { routes, imports } = normalizeRoutes([page], new Set(), true)
expect({ routes, imports }).toMatchInlineSnapshot(`
{
"imports": Set {
"import { default as indexN6pT4Un8hYMeta } from "/app/pages/index.vue?macro=true";",
},
"routes": "[
{
name: "some-custom-name",
path: indexN6pT4Un8hYMeta?.path ?? "/",
meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} },
redirect: "/",
component: () => import("/app/pages/index.vue").then(m => m.default || m)
}
]",
}
`)
})
it('should produce valid route objects when used without extracted meta', () => {
const page: NuxtPage = { path: '/', file: filePath }
page.meta ||= {}
page.meta.layout = 'test'
page.meta.foo = 'bar'
const { routes, imports } = normalizeRoutes([page], new Set())
expect({ routes, imports }).toMatchInlineSnapshot(`
{
"imports": Set {
"import { default as indexN6pT4Un8hYMeta } from "/app/pages/index.vue?macro=true";",
},
"routes": "[
{
name: indexN6pT4Un8hYMeta?.name ?? undefined,
path: indexN6pT4Un8hYMeta?.path ?? "/",
meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} },
alias: indexN6pT4Un8hYMeta?.alias || [],
redirect: indexN6pT4Un8hYMeta?.redirect,
component: () => import("/app/pages/index.vue").then(m => m.default || m)
}
]",
}
`)
})
})

View File

@ -39,13 +39,13 @@
"@types/file-loader": "5.0.4",
"@types/pug": "2.0.10",
"@types/sass-loader": "8.0.8",
"@unhead/schema": "1.9.12",
"@unhead/schema": "1.9.13",
"@vitejs/plugin-vue": "5.0.4",
"@vitejs/plugin-vue-jsx": "4.0.0",
"@vue/compiler-core": "3.4.27",
"@vue/compiler-sfc": "3.4.27",
"@vue/language-core": "2.0.21",
"c12": "1.10.0",
"c12": "1.11.1",
"esbuild-loader": "4.1.0",
"h3": "1.11.1",
"ignore": "5.3.1",
@ -54,7 +54,7 @@
"unbuild": "latest",
"unctx": "2.3.1",
"unenv": "1.9.0",
"vite": "5.2.13",
"vite": "5.3.0",
"vue": "3.4.27",
"vue-bundle-renderer": "2.1.0",
"vue-loader": "17.4.2",
@ -63,7 +63,7 @@
"webpack-dev-middleware": "7.2.1"
},
"dependencies": {
"compatx": "^0.1.3",
"compatx": "^0.1.8",
"consola": "^3.2.3",
"defu": "^6.1.4",
"hookable": "^5.5.3",

View File

@ -23,7 +23,10 @@ export default defineUntypedSchema({
_nuxtConfigFiles: [],
/** @private */
appDir: '',
/** @private */
/**
* @private
* @type {Array<{ meta: ModuleMeta; timings?: Record<string, number | undefined>; entryPath?: string }>}
*/
_installedModules: [],
/** @private */
_modules: [],

View File

@ -21,7 +21,7 @@
"devDependencies": {
"@types/html-minifier": "4.0.5",
"@types/lodash-es": "4.17.12",
"@unocss/reset": "0.60.4",
"@unocss/reset": "0.61.0",
"critters": "0.0.22",
"execa": "9.2.0",
"globby": "14.0.1",
@ -32,7 +32,7 @@
"pathe": "1.1.2",
"prettier": "3.3.2",
"scule": "1.3.0",
"unocss": "0.60.4",
"vite": "5.2.13"
"unocss": "0.61.0",
"vite": "5.3.0"
}
}

View File

@ -28,6 +28,7 @@
"@types/clear": "0.1.4",
"@types/estree": "1.0.5",
"@types/fs-extra": "11.0.4",
"rollup": "4.18.0",
"unbuild": "latest",
"vue": "3.4.27"
},
@ -62,7 +63,7 @@
"ufo": "^1.5.3",
"unenv": "^1.9.0",
"unplugin": "^1.10.1",
"vite": "^5.2.13",
"vite": "^5.3.0",
"vite-node": "^1.6.0",
"vite-plugin-checker": "^0.6.4",
"vue-bundle-renderer": "^2.1.0"

View File

@ -163,10 +163,11 @@ export async function buildClient (ctx: ViteBuildContext) {
}
// We want to respect users' own rollup output options
const fileNames = withoutLeadingSlash(join(ctx.nuxt.options.app.buildAssetsDir, '[hash].js'))
clientConfig.build!.rollupOptions = defu(clientConfig.build!.rollupOptions!, {
output: {
chunkFileNames: ctx.nuxt.options.dev ? undefined : withoutLeadingSlash(join(ctx.nuxt.options.app.buildAssetsDir, '[hash].js')),
entryFileNames: ctx.nuxt.options.dev ? 'entry.js' : withoutLeadingSlash(join(ctx.nuxt.options.app.buildAssetsDir, '[hash].js')),
chunkFileNames: ctx.nuxt.options.dev ? undefined : fileNames,
entryFileNames: ctx.nuxt.options.dev ? 'entry.js' : fileNames,
} satisfies NonNullable<BuildOptions['rollupOptions']>['output'],
}) as any
@ -228,7 +229,13 @@ export async function buildClient (ctx: ViteBuildContext) {
})
const viteMiddleware = defineEventHandler(async (event) => {
const viteRoutes = viteServer.middlewares.stack.map(m => m.route).filter(r => r.length > 1)
const viteRoutes: string[] = []
for (const viteRoute of viteServer.middlewares.stack) {
const m = viteRoute.route
if (m.length > 1) {
viteRoutes.push(m)
}
}
if (!event.path.startsWith(clientConfig.base!) && !viteRoutes.some(route => event.path.startsWith(route))) {
// @ts-expect-error _skip_transform is a private property
event.node.req._skip_transform = true

View File

@ -3,6 +3,8 @@ import type { Nuxt } from '@nuxt/schema'
import type { InlineConfig as ViteConfig } from 'vite'
import { distDir } from './dirs'
const lastPlugins = ['autoprefixer', 'cssnano']
export function resolveCSSOptions (nuxt: Nuxt): ViteConfig['css'] {
const css: ViteConfig['css'] & { postcss: NonNullable<Exclude<NonNullable<ViteConfig['css']>['postcss'], string>> } = {
postcss: {
@ -10,19 +12,22 @@ export function resolveCSSOptions (nuxt: Nuxt): ViteConfig['css'] {
},
}
const lastPlugins = ['autoprefixer', 'cssnano']
css.postcss.plugins = Object.entries(nuxt.options.postcss.plugins)
css.postcss.plugins = []
const plugins = Object.entries(nuxt.options.postcss.plugins)
.sort((a, b) => lastPlugins.indexOf(a[0]) - lastPlugins.indexOf(b[0]))
.filter(([, opts]) => opts)
.map(([name, opts]) => {
for (const [name, opts] of plugins) {
if (opts) {
const plugin = requireModule(name, {
paths: [
...nuxt.options.modulesDir,
distDir,
],
})
return plugin(opts)
})
css.postcss.plugins.push(plugin(opts))
}
}
return css
}

View File

@ -238,7 +238,13 @@ export async function initViteDevBundler (ctx: ViteBuildContext, onBuild: () =>
const { code, ids } = await bundleRequest(options, ctx.entry)
await fse.writeFile(resolve(ctx.nuxt.options.buildDir, 'dist/server/server.mjs'), code, 'utf-8')
// Have CSS in the manifest to prevent FOUC on dev SSR
await writeManifest(ctx, ids.filter(isCSS).map(i => i.slice(1)))
const manifestIds: string[] = []
for (const i of ids) {
if (isCSS(i)) {
manifestIds.push(i.slice(1))
}
}
await writeManifest(ctx, manifestIds)
const time = (Date.now() - start)
logger.success(`Vite server built in ${time}ms`)
await onBuild()

View File

@ -50,11 +50,14 @@ export async function writeManifest (ctx: ViteBuildContext, css: string[] = [])
await fse.mkdirp(serverDist)
if (ctx.config.build?.cssCodeSplit === false) {
const entryCSS = Object.values(clientManifest as Record<string, { file?: string }>).find(val => (val).file?.endsWith('.css'))?.file
if (entryCSS) {
for (const key in clientManifest as Record<string, { file?: string }>) {
const val = clientManifest[key]
if (val.file?.endsWith('.css')) {
const key = relative(ctx.config.root!, ctx.entry)
clientManifest[key].css ||= []
clientManifest[key].css!.push(entryCSS)
clientManifest[key].css!.push(val.file)
break
}
}
}

View File

@ -3,6 +3,7 @@ import { transform } from 'esbuild'
import { visualizer } from 'rollup-plugin-visualizer'
import defu from 'defu'
import type { NuxtOptions } from 'nuxt/schema'
import type { RenderedModule } from 'rollup'
import type { ViteBuildContext } from '../vite'
export function analyzePlugin (ctx: ViteBuildContext): Plugin[] {
@ -13,14 +14,18 @@ export function analyzePlugin (ctx: ViteBuildContext): Plugin[] {
{
name: 'nuxt:analyze-minify',
async generateBundle (_opts, outputBundle) {
for (const [_bundleId, bundle] of Object.entries(outputBundle)) {
for (const _bundleId in outputBundle) {
const bundle = outputBundle[_bundleId]
if (bundle.type !== 'chunk') { continue }
const originalEntries = Object.entries(bundle.modules)
const minifiedEntries = await Promise.all(originalEntries.map(async ([moduleId, module]) => {
const { code } = await transform(module.code || '', { minify: true })
return [moduleId, { ...module, code }]
}))
bundle.modules = Object.fromEntries(minifiedEntries)
const minifiedModuleEntryPromises: Array<Promise<[string, RenderedModule]>> = []
for (const moduleId in bundle.modules) {
const module = bundle.modules[moduleId]
minifiedModuleEntryPromises.push(
transform(module.code || '', { minify: true })
.then(result => [moduleId, { ...module, code: result.code }]),
)
}
bundle.modules = Object.fromEntries(await Promise.all(minifiedModuleEntryPromises))
}
},
},

View File

@ -23,12 +23,15 @@ const SUPPORTED_EXT_RE = /\.(?:m?[jt]sx?|vue)/
export const composableKeysPlugin = createUnplugin((options: ComposableKeysOptions) => {
const composableMeta: Record<string, any> = {}
const composableLengths = new Set<number>()
const keyedFunctions = new Set<string>()
for (const { name, ...meta } of options.composables) {
composableMeta[name] = meta
keyedFunctions.add(name)
composableLengths.add(meta.argumentLength)
}
const maxLength = Math.max(...options.composables.map(({ argumentLength }) => argumentLength))
const keyedFunctions = new Set(options.composables.map(({ name }) => name))
const maxLength = Math.max(...composableLengths)
const KEYED_FUNCTIONS_RE = new RegExp(`\\b(${[...keyedFunctions].map(f => escapeRE(f)).join('|')})\\b`)
return {

View File

@ -25,7 +25,7 @@ export const VitePublicDirsPlugin = createUnplugin((options: { sourcemap?: boole
resolveId: {
enforce: 'post',
handler (id) {
if (id === '/__skip_vite' || !id.startsWith('/') || id.startsWith('/@fs')) { return }
if (id === '/__skip_vite' || id[0] !== '/' || id.startsWith('/@fs')) { return }
if (resolveFromPublicAssets(id)) {
return PREFIX + encodeURIComponent(id)

View File

@ -66,12 +66,12 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
const { files, inBundle } = cssMap[file]
// File has been tree-shaken out of build (or there are no styles to inline)
if (!files.length || !inBundle) { continue }
const fileName = filename(file)
const base = typeof outputOptions.assetFileNames === 'string'
? outputOptions.assetFileNames
: outputOptions.assetFileNames({
type: 'asset',
name: `${filename(file)}-styles.mjs`,
name: `${fileName}-styles.mjs`,
source: '',
})
@ -79,7 +79,7 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
emitted[file] = this.emitFile({
type: 'asset',
name: `${filename(file)}-styles.mjs`,
name: `${fileName}-styles.mjs`,
source: [
...files.map((css, i) => `import style_${i} from './${relative(baseDir, this.getFileName(css))}';`),
`export default [${files.map((_, i) => `style_${i}`).join(', ')}]`,

View File

@ -105,7 +105,7 @@ export async function buildServer (ctx: ViteBuildContext) {
if (!ctx.nuxt.options.dev) {
const nitroDependencies = await tryResolveModule('nitropack/package.json', ctx.nuxt.options.modulesDir)
.then(r => import(r!)).then(r => Object.keys(r.dependencies || {})).catch(() => [])
.then(r => import(r!)).then(r => r.dependencies ? Object.keys(r.dependencies) : []).catch(() => [])
if (Array.isArray(serverConfig.ssr!.external)) {
serverConfig.ssr!.external.push(
// explicit dependencies we use in our ssr renderer - these can be inlined (if necessary) in the nitro build

View File

@ -10,7 +10,7 @@ interface Envs {
export function transpile (envs: Envs): Array<string | RegExp> {
const nuxt = useNuxt()
const transpile = []
const transpile: Array<string | RegExp> = []
for (let pattern of nuxt.options.build.transpile) {
if (typeof pattern === 'function') {

View File

@ -4,6 +4,7 @@ import { dirname, join, normalize, resolve } from 'pathe'
import type { Nuxt, NuxtBuilder, ViteConfig } from '@nuxt/schema'
import { addVitePlugin, isIgnored, logger, resolvePath } from '@nuxt/kit'
import replace from '@rollup/plugin-replace'
import type { RollupReplaceOptions } from '@rollup/plugin-replace'
import { sanitizeFilePath } from 'mlly'
import { withoutLeadingSlash } from 'ufo'
import { filename } from 'pathe/utils'
@ -102,10 +103,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
rootDir: nuxt.options.rootDir,
composables: nuxt.options.optimization.keyedComposables,
}),
replace({
...Object.fromEntries([';', '(', '{', '}', ' ', '\t', '\n'].map(d => [`${d}global.`, `${d}globalThis.`])),
preventAssignment: true,
}),
replace({ preventAssignment: true, ...globalThisReplacements }),
virtual(nuxt.vfs),
],
server: {
@ -164,10 +162,16 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
await nuxt.callHook('vite:extend', ctx)
nuxt.hook('vite:extendConfig', (config) => {
config.plugins!.push(replace({
preventAssignment: true,
...Object.fromEntries(Object.entries(config.define!).filter(([key]) => key.startsWith('import.meta.'))),
}))
const replaceOptions: RollupReplaceOptions = Object.create(null)
replaceOptions.preventAssignment = true
for (const key in config.define!) {
if (key.startsWith('import.meta.')) {
replaceOptions[key] = config.define![key]
}
}
config.plugins!.push(replace(replaceOptions))
})
if (!ctx.nuxt.options.dev) {
@ -224,3 +228,5 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
await buildClient(ctx)
await buildServer(ctx)
}
const globalThisReplacements = Object.fromEntries([';', '(', '{', '}', ' ', '\t', '\n'].map(d => [`${d}global.`, `${d}globalThis.`]))

View File

@ -77,7 +77,6 @@
"@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9",
"@types/webpack-virtual-modules": "0.4.2",
"unbuild": "latest",
"vue": "3.4.27"
},

View File

@ -43,24 +43,19 @@ export default class VueSSRClientPlugin {
const allFiles = new Set<string>()
const asyncFiles = new Set<string>()
const assetsMapping: Record<string, string[]> = {}
for (const asset of stats.assets!) {
const file = asset.name
if (!isHotUpdate(file)) {
for (const { name: file, chunkNames = [] } of stats.assets!) {
if (isHotUpdate(file)) { continue }
allFiles.add(file)
if (initialFiles.has(file)) { continue }
if (isJS(file) || isCSS(file)) {
const isFileJS = isJS(file)
if (!initialFiles.has(file) && (isFileJS || isCSS(file))) {
asyncFiles.add(file)
}
}
}
const assetsMapping: Record<string, string[]> = {}
for (const { name, chunkNames = [] } of stats.assets!) {
if (isJS(name) && !isHotUpdate(name)) {
if (isFileJS) {
const componentHash = hash(chunkNames.join('|'))
const map = assetsMapping[componentHash] ||= []
map.push(name)
map.push(file)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -13,9 +13,9 @@ async function main () {
const commits = await getLatestCommits().then(commits => commits.filter(
c => config.types[c.type] && !(c.type === 'chore' && c.scope === 'deps' && !c.isBreaking),
))
const bumpType = await determineBumpType()
const bumpType = await determineBumpType() || 'patch'
const newVersion = inc(workspace.find('nuxt').data.version, bumpType || 'patch')
const newVersion = inc(workspace.find('nuxt').data.version, bumpType)
const changelog = await generateMarkDown(commits, config)
// Create and push a branch with bumped versions if it has not already been created
@ -44,7 +44,8 @@ async function main () {
changelog
.replace(/^## v.*\n/, '')
.replace(`...${releaseBranch}`, `...v${newVersion}`)
.replace(/### ❤️ Contributors[\s\S]*$/, ''),
.replace(/### ❤️ Contributors[\s\S]*$/, '')
.replace(/[\n\r]+/g, '\n'),
'### ❤️ Contributors',
contributors.map(c => `- ${c.name} (@${c.username})`).join('\n'),
].join('\n')

View File

@ -166,6 +166,21 @@ describe('pages', () => {
expect(res.headers.get('x-extend')).toEqual('added in pages:extend')
})
it('preserves page metadata added in pages:extend hook', async () => {
const html = await $fetch<string>('/some-custom-path')
expect (html.match(/<pre>([^<]*)<\/pre>/)?.[1]?.trim().replace(/&quot;/g, '"').replace(/&gt;/g, '>')).toMatchInlineSnapshot(`
"{
"name": "some-custom-name",
"path": "/some-custom-path",
"validate": "() => true",
"middleware": [
"() => true"
],
"otherValue": "{\\"foo\\":\\"bar\\"}"
}"
`)
})
it('validates routes', async () => {
const { status, headers } = await fetch('/forbidden')
expect(status).toEqual(404)
@ -745,6 +760,24 @@ describe('nuxt links', () => {
`)
})
it('respects external links in edge cases', async () => {
const html = await $fetch<string>('/nuxt-link/custom-external')
const hrefs = html.match(/<a[^>]*href="([^"]+)"/g)
expect(hrefs).toMatchInlineSnapshot(`
[
"<a href="https://thehackernews.com/2024/01/urgent-upgrade-gitlab-critical.html"",
"<a href="https://thehackernews.com/2024/01/urgent-upgrade-gitlab-critical.html"",
"<a href="/missing-page/"",
"<a href="/missing-page/"",
]
`)
const { page, consoleLogs } = await renderPage('/nuxt-link/custom-external')
const warnings = consoleLogs.filter(c => c.text.includes('No match found for location'))
expect(warnings).toMatchInlineSnapshot(`[]`)
await page.close()
})
it('preserves route state', async () => {
const { page } = await renderPage('/nuxt-link/trailing-slash')

View File

@ -1,5 +1,6 @@
import { addBuildPlugin, addComponent } from 'nuxt/kit'
import type { NuxtPage } from 'nuxt/schema'
import { defu } from 'defu'
import { createUnplugin } from 'unplugin'
import { withoutLeadingSlash } from 'ufo'
@ -88,10 +89,17 @@ export default defineNuxtConfig({
runtimeConfig: {
public: {
needsFallback: undefined,
testConfig: 123,
},
},
modules: [
function (_options, nuxt) {
// ensure setting `runtimeConfig` also sets `nitro.runtimeConfig`
nuxt.options.runtimeConfig = defu(nuxt.options.runtimeConfig, {
public: {
testConfig: 123,
},
})
},
function (_options, nuxt) {
nuxt.hook('modules:done', () => {
// @ts-expect-error not valid nuxt option
@ -151,6 +159,17 @@ export default defineNuxtConfig({
internalParent!.children = newPages
})
},
function (_options, nuxt) {
// to check that page metadata is preserved
nuxt.hook('pages:extend', (pages) => {
const customName = pages.find(page => page.name === 'some-custom-name')
if (!customName) { throw new Error('Page with custom name not found') }
if (customName.path !== '/some-custom-path') { throw new Error('Page path not extracted') }
customName.meta ||= {}
customName.meta.someProp = true
})
},
// To test falsy module values
undefined,
],

36
test/fixtures/basic/pages/meta.vue vendored Normal file
View File

@ -0,0 +1,36 @@
<script setup lang="ts">
definePageMeta({
name: 'some-custom-name',
path: '/some-custom-path',
validate: () => true,
middleware: [() => true],
otherValue: {
foo: 'bar',
},
})
const serialisedMeta: Record<string, string> = {}
const meta = useRoute().meta
for (const key in meta) {
if (Array.isArray(meta[key])) {
serialisedMeta[key] = meta[key].map((fn: Function) => fn.toString())
continue
}
if (typeof meta[key] === 'string') {
serialisedMeta[key] = meta[key]
continue
}
if (typeof meta[key] === 'object') {
serialisedMeta[key] = JSON.stringify(meta[key])
continue
}
if (typeof meta[key] === 'function') {
serialisedMeta[key] = meta[key].toString()
continue
}
}
</script>
<template>
<pre>{{ serialisedMeta }}</pre>
</template>

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
const MyLink = defineNuxtLink({
componentName: 'MyLink',
trailingSlash: 'append',
})
</script>
<template>
<div>
<div>
<MyLink to="https://thehackernews.com/2024/01/urgent-upgrade-gitlab-critical.html">
Trailing slashes should not be applied to implicit external links
</MyLink>
</div>
<div>
<NuxtLink
:to="{ path: 'https://thehackernews.com/2024/01/urgent-upgrade-gitlab-critical.html' }"
external
>
External links within route objects should be respected and not have trailing slashes applied
</NuxtLink>
</div>
<div>
<MyLink
:to="{ path: '/missing-page' }"
external
>
External links within route objects should be respected and have trailing slashes applied
</MyLink>
</div>
<div>
<MyLink
to="/missing-page"
external
>
External links should be respected and have trailing slashes applied
</MyLink>
</div>
<div>
<NuxtLink
custom
to="https://google.com"
external
>
<template #default="{ navigate }">
<button @click="navigate()">
Using navigate() with external link should work
</button>
</template>
</NuxtLink>
</div>
</div>
</template>

View File

@ -0,0 +1,9 @@
<template>
<PageContent />
</template>
<script setup lang="tsx">
definePageMeta({})
defineRouteRules({})
const PageContent = () => (<div>Home Page</div>)
</script>

View File

@ -53,6 +53,7 @@ describe('app config', () => {
describe('composables', () => {
it('are all tested', () => {
const testedComposables: string[] = [
'useRouteAnnouncer',
'clearNuxtData',
'refreshNuxtData',
'useAsyncData',