mirror of
https://github.com/nuxt/nuxt.git
synced 2025-02-02 08:40:31 +00:00
Merge branch 'main' into feat/unhead-v2
This commit is contained in:
commit
0ce81cba96
27
.github/workflows/ci.yml
vendored
27
.github/workflows/ci.yml
vendored
@ -188,6 +188,31 @@ jobs:
|
|||||||
- name: Test (runtime unit)
|
- name: Test (runtime unit)
|
||||||
run: pnpm test:runtime
|
run: pnpm test:runtime
|
||||||
|
|
||||||
|
test-size:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
- run: corepack enable
|
||||||
|
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Restore dist cache
|
||||||
|
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||||
|
with:
|
||||||
|
name: dist
|
||||||
|
path: packages
|
||||||
|
|
||||||
|
- name: Check bundle size
|
||||||
|
run: pnpm vitest run bundle
|
||||||
|
|
||||||
test-fixtures:
|
test-fixtures:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
needs:
|
needs:
|
||||||
@ -253,7 +278,7 @@ jobs:
|
|||||||
TEST_MANIFEST: ${{ matrix.manifest }}
|
TEST_MANIFEST: ${{ matrix.manifest }}
|
||||||
TEST_CONTEXT: ${{ matrix.context }}
|
TEST_CONTEXT: ${{ matrix.context }}
|
||||||
TEST_PAYLOAD: ${{ matrix.payload }}
|
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' }}
|
SKIP_BUNDLE_SIZE: true
|
||||||
|
|
||||||
- uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
|
- uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
|
||||||
if: github.event_name != 'push' && matrix.env == 'built' && matrix.builder == 'vite' && matrix.context == 'default' && matrix.os == 'ubuntu-latest' && matrix.manifest == 'manifest-on'
|
if: github.event_name != 'push' && matrix.env == 'built' && matrix.builder == 'vite' && matrix.context == 'default' && matrix.os == 'ubuntu-latest' && matrix.manifest == 'manifest-on'
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
import { declare } from '@babel/helper-plugin-utils'
|
import { declare } from '@babel/helper-plugin-utils'
|
||||||
import { types as t } from '@babel/core'
|
import { types as t } from '@babel/core'
|
||||||
|
|
||||||
|
const metricsPath = fileURLToPath(new URL('../../debug-timings.json', import.meta.url))
|
||||||
|
|
||||||
// inlined from https://github.com/danielroe/errx
|
// inlined from https://github.com/danielroe/errx
|
||||||
function captureStackTrace () {
|
function captureStackTrace () {
|
||||||
const IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[a-z]:[/\\]/i
|
const IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[a-z]:[/\\]/i
|
||||||
@ -46,6 +50,7 @@ function captureStackTrace () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const leading = `
|
export const leading = `
|
||||||
|
import { writeFileSync as ____writeFileSync } from 'node:fs'
|
||||||
const ___captureStackTrace = ${captureStackTrace.toString()};
|
const ___captureStackTrace = ${captureStackTrace.toString()};
|
||||||
globalThis.___calls ||= {};
|
globalThis.___calls ||= {};
|
||||||
globalThis.___timings ||= {};
|
globalThis.___timings ||= {};
|
||||||
@ -55,6 +60,16 @@ function onExit () {
|
|||||||
if (globalThis.___logged) { return }
|
if (globalThis.___logged) { return }
|
||||||
globalThis.___logged = true
|
globalThis.___logged = true
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
____writeFileSync(metricsPath, JSON.stringify(Object.fromEntries(Object.entries(globalThis.___timings).map(([name, time]) => [
|
||||||
|
name,
|
||||||
|
{
|
||||||
|
time: Number(Number(time).toFixed(2)),
|
||||||
|
calls: globalThis.___calls[name],
|
||||||
|
callers: globalThis.___callers[name] ? Object.fromEntries(Object.entries(globalThis.___callers[name]).map(([name, count]) => [name.trim(), count]).sort((a, b) => typeof b[0] === 'string' && typeof a[0] === 'string' ? a[0].localeCompare(b[0]) : 0)) : undefined,
|
||||||
|
},
|
||||||
|
]).sort((a, b) => typeof b[0] === 'string' && typeof a[0] === 'string' ? a[0].localeCompare(b[0]) : 0)), null, 2))
|
||||||
|
|
||||||
// worst by total time
|
// worst by total time
|
||||||
const timings = Object.entries(globalThis.___timings)
|
const timings = Object.entries(globalThis.___timings)
|
||||||
|
|
||||||
@ -93,7 +108,7 @@ function onExit () {
|
|||||||
console.table(topFunctionsAverageTime)
|
console.table(topFunctionsAverageTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const trailing = `process.on("exit", ${onExit.toString()})`
|
export const trailing = `process.on("exit", ${onExit.toString().replace('metricsPath', JSON.stringify(metricsPath))})`
|
||||||
|
|
||||||
/** @param {string} functionName */
|
/** @param {string} functionName */
|
||||||
export function generateInitCode (functionName) {
|
export function generateInitCode (functionName) {
|
||||||
|
@ -15,6 +15,8 @@ declare global {
|
|||||||
var ___calls: Record<string, number>
|
var ___calls: Record<string, number>
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
var ___callers: Record<string, number>
|
var ___callers: Record<string, number>
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var ____writeFileSync: typeof import('fs').writeFileSync
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AnnotateFunctionTimingsPlugin () {
|
export function AnnotateFunctionTimingsPlugin () {
|
||||||
@ -29,7 +31,7 @@ export function AnnotateFunctionTimingsPlugin () {
|
|||||||
walk(ast as Node, {
|
walk(ast as Node, {
|
||||||
enter (node) {
|
enter (node) {
|
||||||
if (node.type === 'FunctionDeclaration' && node.id && node.id.name) {
|
if (node.type === 'FunctionDeclaration' && node.id && node.id.name) {
|
||||||
const functionName = node.id.name ? `${node.id.name} (${id.match(/\/packages\/([^/]+)\//)?.[1]})` : ''
|
const functionName = node.id.name ? `${node.id.name} (${id.match(/[\\/]packages[\\/]([^/]+)[\\/]/)?.[1]})` : ''
|
||||||
const start = (node.body as Node & { start: number, end: number }).start
|
const start = (node.body as Node & { start: number, end: number }).start
|
||||||
const end = (node.body as Node & { start: number, end: number }).end
|
const end = (node.body as Node & { start: number, end: number }).end
|
||||||
|
|
||||||
|
@ -51,3 +51,26 @@ The content of the default slot will be tree-shaken out of the server build. (Th
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Accessing HTML Elements
|
||||||
|
|
||||||
|
Components inside `<ClientOnly>` are rendered only after being mounted. To access the rendered elements in the DOM, you can watch a template ref:
|
||||||
|
|
||||||
|
```vue [pages/example.vue]
|
||||||
|
<script setup lang="ts">
|
||||||
|
const nuxtWelcomeRef = ref()
|
||||||
|
|
||||||
|
// The watch will be triggered when the component is available
|
||||||
|
watch(nuxtWelcomeRef, () => {
|
||||||
|
console.log('<NuxtWelcome /> mounted')
|
||||||
|
}, { once: true })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ClientOnly>
|
||||||
|
<NuxtWelcome ref="nuxtWelcomeRef" />
|
||||||
|
</ClientOnly>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
@ -24,6 +24,8 @@ This is useful for code that should be executed only once, such as logging an ev
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
The default mode of `callOnce` is to run code only once. For example, if the code runs on the server it won't run again on the client. It also won't run again if you `callOnce` more than once on the client, for example by navigating back to this page.
|
||||||
|
|
||||||
```vue [app.vue]
|
```vue [app.vue]
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const websiteConfig = useState('config')
|
const websiteConfig = useState('config')
|
||||||
@ -35,6 +37,23 @@ await callOnce(async () => {
|
|||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
It is also possible to run on every navigation while still avoiding the initial server/client double load. For this, it is possible to use the `navigation` mode:
|
||||||
|
|
||||||
|
```vue [app.vue]
|
||||||
|
<script setup lang="ts">
|
||||||
|
const websiteConfig = useState('config')
|
||||||
|
|
||||||
|
await callOnce(async () => {
|
||||||
|
console.log('This will only be logged once and then on every client side navigation')
|
||||||
|
websiteConfig.value = await $fetch('https://my-cms.com/api/website-config')
|
||||||
|
}, { mode: 'navigation' })
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
::important
|
||||||
|
`navigation` mode is available since [Nuxt v3.15](/blog/v3-15).
|
||||||
|
::
|
||||||
|
|
||||||
::tip{to="/docs/getting-started/state-management#usage-with-pinia"}
|
::tip{to="/docs/getting-started/state-management#usage-with-pinia"}
|
||||||
`callOnce` is useful in combination with the [Pinia module](/modules/pinia) to call store actions.
|
`callOnce` is useful in combination with the [Pinia module](/modules/pinia) to call store actions.
|
||||||
::
|
::
|
||||||
@ -52,9 +71,22 @@ Note that `callOnce` doesn't return anything. You should use [`useAsyncData`](/d
|
|||||||
## Type
|
## Type
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
callOnce(fn?: () => any | Promise<any>): Promise<void>
|
callOnce (key?: string, fn?: (() => any | Promise<any>), options?: CallOnceOptions): Promise<void>
|
||||||
callOnce(key: string, fn?: () => any | Promise<any>): Promise<void>
|
callOnce(fn?: (() => any | Promise<any>), options?: CallOnceOptions): Promise<void>
|
||||||
|
|
||||||
|
type CallOnceOptions = {
|
||||||
|
/**
|
||||||
|
* Execution mode for the callOnce function
|
||||||
|
* @default 'render'
|
||||||
|
*/
|
||||||
|
mode?: 'navigation' | 'render'
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
- `key`: A unique key ensuring that the code is run once. If you do not provide a key, then a key that is unique to the file and line number of the instance of `callOnce` will be generated for you.
|
- `key`: A unique key ensuring that the code is run once. If you do not provide a key, then a key that is unique to the file and line number of the instance of `callOnce` will be generated for you.
|
||||||
- `fn`: The function to run once. This function can also return a `Promise` and a value.
|
- `fn`: The function to run once. This function can also return a `Promise` and a value.
|
||||||
|
- `options`: Setup the mode, either to re-execute on navigation (`navigation`) or just once for the lifetime of the app (`render`). Defaults to `render`.
|
||||||
|
- `render`: Executes once during initial render (either SSR or CSR) - Default mode
|
||||||
|
- `navigation`: Executes once during initial render and once per subsequent client-side navigation
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
"unbuild": "3.3.1",
|
"unbuild": "3.3.1",
|
||||||
"unhead": "2.0.0-alpha.7",
|
"unhead": "2.0.0-alpha.7",
|
||||||
"unimport": "3.14.6",
|
"unimport": "3.14.6",
|
||||||
"vite": "6.0.10",
|
"vite": "6.0.11",
|
||||||
"vue": "3.5.13"
|
"vue": "3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -50,11 +50,11 @@
|
|||||||
"untyped": "^1.5.2"
|
"untyped": "^1.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rspack/core": "1.1.8",
|
"@rspack/core": "1.2.0",
|
||||||
"@types/semver": "7.5.8",
|
"@types/semver": "7.5.8",
|
||||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28938837.19ec5395",
|
"nitro": "npm:nitro-nightly@3.0.0-beta-28938837.19ec5395",
|
||||||
"unbuild": "3.3.1",
|
"unbuild": "3.3.1",
|
||||||
"vite": "6.0.10",
|
"vite": "6.0.11",
|
||||||
"vitest": "3.0.2",
|
"vitest": "3.0.2",
|
||||||
"webpack": "5.97.1"
|
"webpack": "5.97.1"
|
||||||
},
|
},
|
||||||
|
@ -14,6 +14,11 @@ const builderMap = {
|
|||||||
'@nuxt/webpack-builder': 'webpack',
|
'@nuxt/webpack-builder': 'webpack',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkNuxtVersion (version: string, nuxt: Nuxt = useNuxt()) {
|
||||||
|
const nuxtVersion = getNuxtVersion(nuxt)
|
||||||
|
return satisfies(normalizeSemanticVersion(nuxtVersion), version, { includePrerelease: true })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check version constraints and return incompatibility issues as an array
|
* Check version constraints and return incompatibility issues as an array
|
||||||
*/
|
*/
|
||||||
@ -23,7 +28,7 @@ export async function checkNuxtCompatibility (constraints: NuxtCompatibility, nu
|
|||||||
// Nuxt version check
|
// Nuxt version check
|
||||||
if (constraints.nuxt) {
|
if (constraints.nuxt) {
|
||||||
const nuxtVersion = getNuxtVersion(nuxt)
|
const nuxtVersion = getNuxtVersion(nuxt)
|
||||||
if (!satisfies(normalizeSemanticVersion(nuxtVersion), constraints.nuxt, { includePrerelease: true })) {
|
if (!checkNuxtVersion(constraints.nuxt, nuxt)) {
|
||||||
issues.push({
|
issues.push({
|
||||||
name: 'nuxt',
|
name: 'nuxt',
|
||||||
message: `Nuxt version \`${constraints.nuxt}\` is required but currently using \`${nuxtVersion}\``,
|
message: `Nuxt version \`${constraints.nuxt}\` is required but currently using \`${nuxtVersion}\``,
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import { kebabCase, pascalCase } from 'scule'
|
import { kebabCase, pascalCase } from 'scule'
|
||||||
import type { Component, ComponentsDir } from '@nuxt/schema'
|
import type { Component, ComponentsDir } from '@nuxt/schema'
|
||||||
import { useNuxt } from './context'
|
import { useNuxt } from './context'
|
||||||
import { assertNuxtCompatibility } from './compatibility'
|
import { checkNuxtVersion } from './compatibility'
|
||||||
import { logger } from './logger'
|
import { logger } from './logger'
|
||||||
import { MODE_RE } from './utils'
|
import { MODE_RE } from './utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a directory to be scanned for components and imported only when used.
|
* Register a directory to be scanned for components and imported only when used.
|
||||||
*/
|
*/
|
||||||
export async function addComponentsDir (dir: ComponentsDir, opts: { prepend?: boolean } = {}) {
|
export function addComponentsDir (dir: ComponentsDir, opts: { prepend?: boolean } = {}) {
|
||||||
const nuxt = useNuxt()
|
const nuxt = useNuxt()
|
||||||
await assertNuxtCompatibility({ nuxt: '>=2.13' }, nuxt)
|
if (!checkNuxtVersion('>=2.13', nuxt)) {
|
||||||
|
throw new Error(`\`addComponentsDir\` requires Nuxt 2.13 or higher.`)
|
||||||
|
}
|
||||||
nuxt.options.components ||= []
|
nuxt.options.components ||= []
|
||||||
dir.priority ||= 0
|
dir.priority ||= 0
|
||||||
nuxt.hook('components:dirs', (dirs) => { dirs[opts.prepend ? 'unshift' : 'push'](dir) })
|
nuxt.hook('components:dirs', (dirs) => { dirs[opts.prepend ? 'unshift' : 'push'](dir) })
|
||||||
@ -23,9 +25,12 @@ export type AddComponentOptions = { name: string, filePath: string } & Partial<E
|
|||||||
/**
|
/**
|
||||||
* Register a component by its name and filePath.
|
* Register a component by its name and filePath.
|
||||||
*/
|
*/
|
||||||
export async function addComponent (opts: AddComponentOptions) {
|
export function addComponent (opts: AddComponentOptions) {
|
||||||
const nuxt = useNuxt()
|
const nuxt = useNuxt()
|
||||||
await assertNuxtCompatibility({ nuxt: '>=2.13' }, nuxt)
|
if (!checkNuxtVersion('>=2.13', nuxt)) {
|
||||||
|
throw new Error(`\`addComponent\` requires Nuxt 2.13 or higher.`)
|
||||||
|
}
|
||||||
|
|
||||||
nuxt.options.components ||= []
|
nuxt.options.components ||= []
|
||||||
|
|
||||||
if (!opts.mode) {
|
if (!opts.mode) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { existsSync, promises as fsp } from 'node:fs'
|
import { promises as fsp } from 'node:fs'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { basename, dirname, isAbsolute, join, normalize, resolve } from 'pathe'
|
import { basename, dirname, isAbsolute, join, normalize, resolve } from 'pathe'
|
||||||
import { globby } from 'globby'
|
import { globby } from 'globby'
|
||||||
@ -38,97 +38,30 @@ export interface ResolvePathOptions {
|
|||||||
* If path could not be resolved, normalized input path will be returned
|
* If path could not be resolved, normalized input path will be returned
|
||||||
*/
|
*/
|
||||||
export async function resolvePath (path: string, opts: ResolvePathOptions = {}): Promise<string> {
|
export async function resolvePath (path: string, opts: ResolvePathOptions = {}): Promise<string> {
|
||||||
// Always normalize input
|
const res = await _resolvePathGranularly(path, opts)
|
||||||
const _path = path
|
|
||||||
path = normalize(path)
|
|
||||||
|
|
||||||
// Fast return if the path exists
|
if (res.type === 'file') {
|
||||||
if (isAbsolute(path)) {
|
return res.path
|
||||||
if (opts?.virtual && existsInVFS(path)) {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
if (existsSync(path) && !(await isDirectory(path))) {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use current nuxt options
|
|
||||||
const nuxt = tryUseNuxt()
|
|
||||||
const cwd = opts.cwd || (nuxt ? nuxt.options.rootDir : process.cwd())
|
|
||||||
const extensions = opts.extensions || (nuxt ? nuxt.options.extensions : ['.ts', '.mjs', '.cjs', '.json'])
|
|
||||||
const modulesDir = nuxt ? nuxt.options.modulesDir : []
|
|
||||||
|
|
||||||
// Resolve aliases
|
|
||||||
path = resolveAlias(path)
|
|
||||||
|
|
||||||
// Resolve relative to cwd
|
|
||||||
if (!isAbsolute(path)) {
|
|
||||||
path = resolve(cwd, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if resolvedPath is a file
|
|
||||||
if (opts?.virtual && existsInVFS(path, nuxt)) {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
let _isDir = false
|
|
||||||
if (existsSync(path)) {
|
|
||||||
_isDir = await isDirectory(path)
|
|
||||||
if (!_isDir) {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check possible extensions
|
|
||||||
for (const ext of extensions) {
|
|
||||||
// path.[ext]
|
|
||||||
const pathWithExt = path + ext
|
|
||||||
if (opts?.virtual && existsInVFS(pathWithExt, nuxt)) {
|
|
||||||
return pathWithExt
|
|
||||||
}
|
|
||||||
if (existsSync(pathWithExt)) {
|
|
||||||
return pathWithExt
|
|
||||||
}
|
|
||||||
// path/index.[ext]
|
|
||||||
const pathWithIndex = join(path, 'index' + ext)
|
|
||||||
if (opts?.virtual && existsInVFS(pathWithIndex, nuxt)) {
|
|
||||||
return pathWithIndex
|
|
||||||
}
|
|
||||||
if (_isDir && existsSync(pathWithIndex)) {
|
|
||||||
return pathWithIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to resolve as module id
|
|
||||||
const resolveModulePath = await _resolvePath(_path, { url: [cwd, ...modulesDir] }).catch(() => null)
|
|
||||||
if (resolveModulePath) {
|
|
||||||
return resolveModulePath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return normalized input
|
// Return normalized input
|
||||||
return opts.fallbackToOriginal ? _path : path
|
return opts.fallbackToOriginal ? path : res.path
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to resolve first existing file in paths
|
* Try to resolve first existing file in paths
|
||||||
*/
|
*/
|
||||||
export async function findPath (paths: string | string[], opts?: ResolvePathOptions, pathType: 'file' | 'dir' = 'file'): Promise<string | null> {
|
export async function findPath (paths: string | string[], opts?: ResolvePathOptions, pathType: 'file' | 'dir' = 'file'): Promise<string | null> {
|
||||||
const nuxt = opts?.virtual ? tryUseNuxt() : undefined
|
|
||||||
|
|
||||||
for (const path of toArray(paths)) {
|
for (const path of toArray(paths)) {
|
||||||
const rPath = await resolvePath(path, opts)
|
const res = await _resolvePathGranularly(path, opts)
|
||||||
|
|
||||||
// Check VFS
|
if (!res.type || (pathType && res.type !== pathType)) {
|
||||||
if (opts?.virtual && existsInVFS(rPath, nuxt)) {
|
continue
|
||||||
return rPath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check file system
|
// Check file system
|
||||||
if (await existsSensitive(rPath)) {
|
if (res.virtual || await existsSensitive(res.path)) {
|
||||||
const _isDir = await isDirectory(rPath)
|
return res.path
|
||||||
if (!pathType || (pathType === 'file' && !_isDir) || (pathType === 'dir' && _isDir)) {
|
|
||||||
return rPath
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@ -186,15 +119,106 @@ export async function resolveNuxtModule (base: string, paths: string[]): Promise
|
|||||||
|
|
||||||
// --- Internal ---
|
// --- Internal ---
|
||||||
|
|
||||||
async function existsSensitive (path: string) {
|
interface PathResolution {
|
||||||
if (!existsSync(path)) { return false }
|
path: string
|
||||||
const dirFiles = await fsp.readdir(dirname(path))
|
type?: 'file' | 'dir'
|
||||||
return dirFiles.includes(basename(path))
|
virtual?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usage note: We assume path existence is already ensured
|
async function _resolvePathType (path: string, opts: ResolvePathOptions = {}, skipFs = false): Promise<PathResolution | undefined> {
|
||||||
async function isDirectory (path: string) {
|
if (opts?.virtual && existsInVFS(path)) {
|
||||||
return (await fsp.lstat(path)).isDirectory()
|
return {
|
||||||
|
path,
|
||||||
|
type: 'file',
|
||||||
|
virtual: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipFs) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fd = await fsp.open(path, 'r').catch(() => null)
|
||||||
|
try {
|
||||||
|
const stats = await fd?.stat()
|
||||||
|
if (stats) {
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
type: stats.isFile() ? 'file' : 'dir',
|
||||||
|
virtual: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
fd?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _resolvePathGranularly (path: string, opts: ResolvePathOptions = {}): Promise<PathResolution> {
|
||||||
|
// Always normalize input
|
||||||
|
const _path = path
|
||||||
|
path = normalize(path)
|
||||||
|
|
||||||
|
// Fast return if the path exists
|
||||||
|
if (isAbsolute(path)) {
|
||||||
|
const res = await _resolvePathType(path, opts)
|
||||||
|
if (res && res.type === 'file') {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use current nuxt options
|
||||||
|
const nuxt = tryUseNuxt()
|
||||||
|
const cwd = opts.cwd || (nuxt ? nuxt.options.rootDir : process.cwd())
|
||||||
|
const extensions = opts.extensions || (nuxt ? nuxt.options.extensions : ['.ts', '.mjs', '.cjs', '.json'])
|
||||||
|
const modulesDir = nuxt ? nuxt.options.modulesDir : []
|
||||||
|
|
||||||
|
// Resolve aliases
|
||||||
|
path = resolveAlias(path)
|
||||||
|
|
||||||
|
// Resolve relative to cwd
|
||||||
|
if (!isAbsolute(path)) {
|
||||||
|
path = resolve(cwd, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await _resolvePathType(path, opts)
|
||||||
|
if (res && res.type === 'file') {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check possible extensions
|
||||||
|
for (const ext of extensions) {
|
||||||
|
// path.[ext]
|
||||||
|
const extPath = await _resolvePathType(path + ext, opts)
|
||||||
|
if (extPath && extPath.type === 'file') {
|
||||||
|
return extPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// path/index.[ext]
|
||||||
|
const indexPath = await _resolvePathType(join(path, 'index' + ext), opts, res?.type !== 'dir' /* skip checking if parent is not a directory */)
|
||||||
|
if (indexPath && indexPath.type === 'file') {
|
||||||
|
return indexPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to resolve as module id
|
||||||
|
const resolvedModulePath = await _resolvePath(_path, { url: [cwd, ...modulesDir] }).catch(() => null)
|
||||||
|
if (resolvedModulePath) {
|
||||||
|
return {
|
||||||
|
path: resolvedModulePath,
|
||||||
|
type: 'file',
|
||||||
|
virtual: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return normalized input
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function existsSensitive (path: string) {
|
||||||
|
const dirFiles = await fsp.readdir(dirname(path)).catch(() => null)
|
||||||
|
return dirFiles && dirFiles.includes(basename(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
function existsInVFS (path: string, nuxt = tryUseNuxt()) {
|
function existsInVFS (path: string, nuxt = tryUseNuxt()) {
|
||||||
|
@ -117,7 +117,7 @@
|
|||||||
"unenv": "^1.10.0",
|
"unenv": "^1.10.0",
|
||||||
"unimport": "^3.14.6",
|
"unimport": "^3.14.6",
|
||||||
"unplugin": "^2.1.2",
|
"unplugin": "^2.1.2",
|
||||||
"unplugin-vue-router": "^0.10.9",
|
"unplugin-vue-router": "^0.11.0",
|
||||||
"unstorage": "^1.14.4",
|
"unstorage": "^1.14.4",
|
||||||
"untyped": "^1.5.2",
|
"untyped": "^1.5.2",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
@ -132,7 +132,7 @@
|
|||||||
"@vitejs/plugin-vue": "5.2.1",
|
"@vitejs/plugin-vue": "5.2.1",
|
||||||
"@vue/compiler-sfc": "3.5.13",
|
"@vue/compiler-sfc": "3.5.13",
|
||||||
"unbuild": "3.3.1",
|
"unbuild": "3.3.1",
|
||||||
"vite": "6.0.10",
|
"vite": "6.0.11",
|
||||||
"vitest": "3.0.2"
|
"vitest": "3.0.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { existsSync, statSync, writeFileSync } from 'node:fs'
|
import { existsSync, statSync, writeFileSync } from 'node:fs'
|
||||||
import { isAbsolute, join, normalize, relative, resolve } from 'pathe'
|
import { isAbsolute, join, normalize, relative, resolve } from 'pathe'
|
||||||
import { addBuildPlugin, addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, defineNuxtModule, findPath, resolveAlias, resolvePath, updateTemplates } from '@nuxt/kit'
|
import { addBuildPlugin, addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, defineNuxtModule, findPath, resolveAlias, resolvePath } from '@nuxt/kit'
|
||||||
import type { Component, ComponentsDir, ComponentsOptions } from 'nuxt/schema'
|
import type { Component, ComponentsDir, ComponentsOptions } from 'nuxt/schema'
|
||||||
|
|
||||||
import { distDir } from '../dirs'
|
import { distDir } from '../dirs'
|
||||||
@ -198,24 +198,6 @@ export default defineNuxtModule<ComponentsOptions>({
|
|||||||
tsConfig.compilerOptions!.paths['#components'] = [resolve(nuxt.options.buildDir, 'components')]
|
tsConfig.compilerOptions!.paths['#components'] = [resolve(nuxt.options.buildDir, 'components')]
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch for changes
|
|
||||||
nuxt.hook('builder:watch', async (event, relativePath) => {
|
|
||||||
if (!['add', 'unlink'].includes(event)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const path = resolve(nuxt.options.srcDir, relativePath)
|
|
||||||
if (componentDirs.some(dir => path.startsWith(dir.path + '/'))) {
|
|
||||||
await updateTemplates({
|
|
||||||
filter: template => [
|
|
||||||
'components.plugin.mjs',
|
|
||||||
'components.d.ts',
|
|
||||||
'components.server.mjs',
|
|
||||||
'components.client.mjs',
|
|
||||||
].includes(template.filename),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
addBuildPlugin(TreeShakeTemplatePlugin({ sourcemap: !!nuxt.options.sourcemap.server, getComponents }), { client: false })
|
addBuildPlugin(TreeShakeTemplatePlugin({ sourcemap: !!nuxt.options.sourcemap.server, getComponents }), { client: false })
|
||||||
|
|
||||||
const sharedLoaderOptions = {
|
const sharedLoaderOptions = {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { existsSync, readdirSync } from 'node:fs'
|
import { existsSync, readdirSync } from 'node:fs'
|
||||||
import { mkdir, readFile } from 'node:fs/promises'
|
import { mkdir, readFile } from 'node:fs/promises'
|
||||||
import { addBuildPlugin, addComponent, addPlugin, addTemplate, addTypeTemplate, defineNuxtModule, findPath, resolvePath, updateTemplates, useNitro } from '@nuxt/kit'
|
import { addBuildPlugin, addComponent, addPlugin, addTemplate, addTypeTemplate, defineNuxtModule, findPath, resolvePath, useNitro } from '@nuxt/kit'
|
||||||
import { dirname, join, relative, resolve } from 'pathe'
|
import { dirname, join, relative, resolve } from 'pathe'
|
||||||
import { genImport, genObjectFromRawEntries, genString } from 'knitwork'
|
import { genImport, genObjectFromRawEntries, genString } from 'knitwork'
|
||||||
import { joinURL } from 'ufo'
|
import { joinURL } from 'ufo'
|
||||||
@ -93,10 +93,8 @@ export default defineNuxtModule({
|
|||||||
addPlugin(resolve(runtimeDir, 'plugins/check-if-page-unused'))
|
addPlugin(resolve(runtimeDir, 'plugins/check-if-page-unused'))
|
||||||
}
|
}
|
||||||
|
|
||||||
nuxt.hook('app:templates', async (app) => {
|
nuxt.hook('app:templates', (app) => {
|
||||||
app.pages = await resolvePagesRoutes(nuxt)
|
if (!nuxt.options.ssr && app.pages?.some(p => p.mode === 'server')) {
|
||||||
|
|
||||||
if (!nuxt.options.ssr && app.pages.some(p => p.mode === 'server')) {
|
|
||||||
logger.warn('Using server pages with `ssr: false` is not supported with auto-detected component islands. Set `experimental.componentIslands` to `true`.')
|
logger.warn('Using server pages with `ssr: false` is not supported with auto-detected component islands. Set `experimental.componentIslands` to `true`.')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -269,6 +267,13 @@ export default defineNuxtModule({
|
|||||||
if (!pages) { return false }
|
if (!pages) { return false }
|
||||||
return pages.some(page => page.file === file) || pages.some(page => page.children && isPage(file, page.children))
|
return pages.some(page => page.file === file) || pages.some(page => page.children && isPage(file, page.children))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nuxt.hooks.hookOnce('app:templates', async (app) => {
|
||||||
|
if (!app.pages) {
|
||||||
|
app.pages = await resolvePagesRoutes(nuxt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
nuxt.hook('builder:watch', async (event, relativePath) => {
|
nuxt.hook('builder:watch', async (event, relativePath) => {
|
||||||
const path = resolve(nuxt.options.srcDir, relativePath)
|
const path = resolve(nuxt.options.srcDir, relativePath)
|
||||||
const shouldAlwaysRegenerate = nuxt.options.experimental.scanPageMeta && isPage(path)
|
const shouldAlwaysRegenerate = nuxt.options.experimental.scanPageMeta && isPage(path)
|
||||||
@ -276,9 +281,7 @@ export default defineNuxtModule({
|
|||||||
if (event === 'change' && !shouldAlwaysRegenerate) { return }
|
if (event === 'change' && !shouldAlwaysRegenerate) { return }
|
||||||
|
|
||||||
if (shouldAlwaysRegenerate || updateTemplatePaths.some(dir => path.startsWith(dir))) {
|
if (shouldAlwaysRegenerate || updateTemplatePaths.some(dir => path.startsWith(dir))) {
|
||||||
await updateTemplates({
|
nuxt.apps.default!.pages = await resolvePagesRoutes(nuxt)
|
||||||
filter: template => template.filename === 'routes.mjs',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/friendly-errors-webpack-plugin": "^2.6.0",
|
"@nuxt/friendly-errors-webpack-plugin": "^2.6.0",
|
||||||
"@nuxt/kit": "workspace:*",
|
"@nuxt/kit": "workspace:*",
|
||||||
"@rspack/core": "^1.1.8",
|
"@rspack/core": "^1.2.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||||
|
@ -60,7 +60,7 @@
|
|||||||
"unctx": "2.4.1",
|
"unctx": "2.4.1",
|
||||||
"unimport": "3.14.6",
|
"unimport": "3.14.6",
|
||||||
"untyped": "1.5.2",
|
"untyped": "1.5.2",
|
||||||
"vite": "6.0.10",
|
"vite": "6.0.11",
|
||||||
"vue": "3.5.13",
|
"vue": "3.5.13",
|
||||||
"vue-bundle-renderer": "2.1.1",
|
"vue-bundle-renderer": "2.1.1",
|
||||||
"vue-loader": "17.4.2",
|
"vue-loader": "17.4.2",
|
||||||
|
@ -30,6 +30,6 @@
|
|||||||
"tinyexec": "0.3.2",
|
"tinyexec": "0.3.2",
|
||||||
"tinyglobby": "0.2.10",
|
"tinyglobby": "0.2.10",
|
||||||
"unocss": "65.4.2",
|
"unocss": "65.4.2",
|
||||||
"vite": "6.0.10"
|
"vite": "6.0.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
"ufo": "^1.5.4",
|
"ufo": "^1.5.4",
|
||||||
"unenv": "^1.10.0",
|
"unenv": "^1.10.0",
|
||||||
"unplugin": "^2.1.2",
|
"unplugin": "^2.1.2",
|
||||||
"vite": "^6.0.10",
|
"vite": "^6.0.11",
|
||||||
"vite-node": "^3.0.2",
|
"vite-node": "^3.0.2",
|
||||||
"vite-plugin-checker": "^0.8.0",
|
"vite-plugin-checker": "^0.8.0",
|
||||||
"vue-bundle-renderer": "^2.1.1"
|
"vue-bundle-renderer": "^2.1.1"
|
||||||
|
@ -6,9 +6,8 @@ import { isAbsolute, join, normalize, resolve } from 'pathe'
|
|||||||
// import { addDevServerHandler } from '@nuxt/kit'
|
// import { addDevServerHandler } from '@nuxt/kit'
|
||||||
import { isFileServingAllowed } from 'vite'
|
import { isFileServingAllowed } from 'vite'
|
||||||
import type { ModuleNode, Plugin as VitePlugin } from 'vite'
|
import type { ModuleNode, Plugin as VitePlugin } from 'vite'
|
||||||
import { getQuery, withTrailingSlash } from 'ufo'
|
import { getQuery } from 'ufo'
|
||||||
import { normalizeViteManifest } from 'vue-bundle-renderer'
|
import { normalizeViteManifest } from 'vue-bundle-renderer'
|
||||||
import escapeStringRegexp from 'escape-string-regexp'
|
|
||||||
import { distDir } from './dirs'
|
import { distDir } from './dirs'
|
||||||
import type { ViteBuildContext } from './vite'
|
import type { ViteBuildContext } from './vite'
|
||||||
import { isCSS } from './utils'
|
import { isCSS } from './utils'
|
||||||
@ -120,10 +119,6 @@ function createViteNodeApp (ctx: ViteBuildContext, invalidates: Set<string> = ne
|
|||||||
/^#/,
|
/^#/,
|
||||||
/\?/,
|
/\?/,
|
||||||
],
|
],
|
||||||
external: [
|
|
||||||
'#shared',
|
|
||||||
new RegExp('^' + escapeStringRegexp(withTrailingSlash(resolve(ctx.nuxt.options.rootDir, ctx.nuxt.options.dir.shared)))),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
transformMode: {
|
transformMode: {
|
||||||
ssr: [/.*/],
|
ssr: [/.*/],
|
||||||
|
@ -212,13 +212,13 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
|
|||||||
|
|
||||||
nuxt.hook('vite:serverCreated', (server: vite.ViteDevServer, env) => {
|
nuxt.hook('vite:serverCreated', (server: vite.ViteDevServer, env) => {
|
||||||
// Invalidate virtual modules when templates are re-generated
|
// Invalidate virtual modules when templates are re-generated
|
||||||
ctx.nuxt.hook('app:templatesGenerated', (_app, changedTemplates) => {
|
ctx.nuxt.hook('app:templatesGenerated', async (_app, changedTemplates) => {
|
||||||
for (const template of changedTemplates) {
|
await Promise.all(changedTemplates.map(async (template) => {
|
||||||
for (const mod of server.moduleGraph.getModulesByFile(`virtual:nuxt:${encodeURIComponent(template.dst)}`) || []) {
|
for (const mod of server.moduleGraph.getModulesByFile(`virtual:nuxt:${encodeURIComponent(template.dst)}`) || []) {
|
||||||
server.moduleGraph.invalidateModule(mod)
|
server.moduleGraph.invalidateModule(mod)
|
||||||
server.reloadModule(mod)
|
await server.reloadModule(mod)
|
||||||
}
|
}
|
||||||
}
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
if (nuxt.options.vite.warmupEntry !== false) {
|
if (nuxt.options.vite.warmupEntry !== false) {
|
||||||
|
@ -73,7 +73,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt/schema": "workspace:*",
|
"@nuxt/schema": "workspace:*",
|
||||||
"@rspack/core": "1.1.8",
|
"@rspack/core": "1.2.0",
|
||||||
"@types/pify": "5.0.4",
|
"@types/pify": "5.0.4",
|
||||||
"@types/webpack-bundle-analyzer": "4.7.0",
|
"@types/webpack-bundle-analyzer": "4.7.0",
|
||||||
"@types/webpack-hot-middleware": "2.25.9",
|
"@types/webpack-hot-middleware": "2.25.9",
|
||||||
|
549
pnpm-lock.yaml
549
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -123,7 +123,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
|||||||
const serverDir = join(pagesRootDir, '.output/server')
|
const serverDir = join(pagesRootDir, '.output/server')
|
||||||
|
|
||||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"302k"`)
|
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"303k"`)
|
||||||
|
|
||||||
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
|
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
|
||||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1382k"`)
|
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1382k"`)
|
||||||
|
Loading…
Reference in New Issue
Block a user