mirror of https://github.com/nuxt/nuxt.git
Merge branch 'main' into feat/shared-dir
This commit is contained in:
commit
35eae55d51
|
@ -36,7 +36,7 @@ body:
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: additonal
|
id: additional
|
||||||
attributes:
|
attributes:
|
||||||
label: Additional context
|
label: Additional context
|
||||||
description: If applicable, add any other context about the problem here
|
description: If applicable, add any other context about the problem here
|
||||||
|
|
|
@ -85,7 +85,7 @@ jobs:
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@f0f3afee809481da311ca3a6ff1ff51d81dbeb24 # v3.26.4
|
uses: github/codeql-action/init@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5
|
||||||
with:
|
with:
|
||||||
languages: javascript
|
languages: javascript
|
||||||
queries: +security-and-quality
|
queries: +security-and-quality
|
||||||
|
@ -97,7 +97,7 @@ jobs:
|
||||||
path: packages
|
path: packages
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@f0f3afee809481da311ca3a6ff1ff51d81dbeb24 # v3.26.4
|
uses: github/codeql-action/analyze@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5
|
||||||
with:
|
with:
|
||||||
category: "/language:javascript"
|
category: "/language:javascript"
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ jobs:
|
||||||
|
|
||||||
# Upload the results to GitHub's code scanning dashboard.
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@f0f3afee809481da311ca3a6ff1ff51d81dbeb24 # v3.26.4
|
uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5
|
||||||
if: github.repository == 'nuxt/nuxt' && success()
|
if: github.repository == 'nuxt/nuxt' && success()
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|
|
@ -55,7 +55,7 @@ This includes:
|
||||||
|
|
||||||
You cannot currently define a server-side handler for these errors, but can render an error page, see the [Render an Error Page](#error-page) section.
|
You cannot currently define a server-side handler for these errors, but can render an error page, see the [Render an Error Page](#error-page) section.
|
||||||
|
|
||||||
## Errors with JS chunks
|
## Errors with JS Chunks
|
||||||
|
|
||||||
You might encounter chunk loading errors due to a network connectivity failure or a new deployment (which invalidates your old, hashed JS chunk URLs). Nuxt provides built-in support for handling chunk loading errors by performing a hard reload when a chunk fails to load during route navigation.
|
You might encounter chunk loading errors due to a network connectivity failure or a new deployment (which invalidates your old, hashed JS chunk URLs). Nuxt provides built-in support for handling chunk loading errors by performing a hard reload when a chunk fails to load during route navigation.
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ While building Nuxt 3, we created a new server engine: [Nitro](https://nitro.unj
|
||||||
|
|
||||||
It is shipped with many features:
|
It is shipped with many features:
|
||||||
|
|
||||||
- Cross-platform support for Node.js, Browsers, service-workers and more.
|
- Cross-platform support for Node.js, browsers, service workers and more.
|
||||||
- Serverless support out-of-the-box.
|
- Serverless support out-of-the-box.
|
||||||
- API routes support.
|
- API routes support.
|
||||||
- Automatic code-splitting and async-loaded chunks.
|
- Automatic code-splitting and async-loaded chunks.
|
||||||
|
|
|
@ -359,3 +359,34 @@ export default defineNuxtConfig({
|
||||||
::read-more{icon="i-simple-icons-mdnwebdocs" color="gray" to="https://developer.mozilla.org/en-US/docs/Web/API/CookieStore" target="_blank"}
|
::read-more{icon="i-simple-icons-mdnwebdocs" color="gray" to="https://developer.mozilla.org/en-US/docs/Web/API/CookieStore" target="_blank"}
|
||||||
Read more about the **CookieStore**.
|
Read more about the **CookieStore**.
|
||||||
::
|
::
|
||||||
|
|
||||||
|
## buildCache
|
||||||
|
|
||||||
|
Caches Nuxt build artifacts based on a hash of the configuration and source files.
|
||||||
|
|
||||||
|
```ts twoslash [nuxt.config.ts]
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
experimental: {
|
||||||
|
buildCache: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
When enabled, changes to the following files will trigger a full rebuild:
|
||||||
|
|
||||||
|
```bash [Directory structure]
|
||||||
|
.nuxtrc
|
||||||
|
.npmrc
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
tsconfig.json
|
||||||
|
bun.lockb
|
||||||
|
```
|
||||||
|
|
||||||
|
In addition, any changes to files within `srcDir` will trigger a rebuild of the Vue client/server bundle. Nitro will always be rebuilt (though work is in progress to allow Nitro to announce its cacheable artifacts and their hashes).
|
||||||
|
|
||||||
|
::note
|
||||||
|
A maximum of 10 cache tarballs are kept.
|
||||||
|
::
|
||||||
|
|
|
@ -4,7 +4,7 @@ description: "Nuxt provides a <NuxtPicture> component to handle automatic image
|
||||||
links:
|
links:
|
||||||
- label: Source
|
- label: Source
|
||||||
icon: i-simple-icons-github
|
icon: i-simple-icons-github
|
||||||
to: https://github.com/nuxt/image/blob/main/src/runtime/components/nuxt-picture.ts
|
to: https://github.com/nuxt/image/blob/main/src/runtime/components/NuxtPicture.vue
|
||||||
size: xs
|
size: xs
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ description: "Nuxt provides a <NuxtImg> component to handle automatic image opti
|
||||||
links:
|
links:
|
||||||
- label: Source
|
- label: Source
|
||||||
icon: i-simple-icons-github
|
icon: i-simple-icons-github
|
||||||
to: https://github.com/nuxt/image/blob/main/src/runtime/components/nuxt-img.ts
|
to: https://github.com/nuxt/image/blob/main/src/runtime/components/NuxtImg.vue
|
||||||
size: xs
|
size: xs
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ Apart from dynamic parameters and query parameters, `useRoute()` also provides t
|
||||||
|
|
||||||
- `fullPath`: encoded URL associated with the current route that contains path, query and hash
|
- `fullPath`: encoded URL associated with the current route that contains path, query and hash
|
||||||
- `hash`: decoded hash section of the URL that starts with a #
|
- `hash`: decoded hash section of the URL that starts with a #
|
||||||
|
- `query`: access route query parameters
|
||||||
- `matched`: array of normalized matched routes with current route location
|
- `matched`: array of normalized matched routes with current route location
|
||||||
- `meta`: custom data attached to the record
|
- `meta`: custom data attached to the record
|
||||||
- `name`: unique name for the route record
|
- `name`: unique name for the route record
|
||||||
|
|
|
@ -34,7 +34,7 @@ Hook | Arguments | Environment | Description
|
||||||
|
|
||||||
## Nuxt Hooks (build time)
|
## Nuxt Hooks (build time)
|
||||||
|
|
||||||
Check the [schema source code](https://github.com/nuxt/nuxt/blob/main/packages/schema/src/types/hooks.ts#L53) for all available hooks.
|
Check the [schema source code](https://github.com/nuxt/nuxt/blob/main/packages/schema/src/types/hooks.ts#L83) for all available hooks.
|
||||||
|
|
||||||
Hook | Arguments | Description
|
Hook | Arguments | Description
|
||||||
-------------------------|----------------------------|-------------
|
-------------------------|----------------------------|-------------
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
// For pnpm typecheck:docs to generate correct types
|
// For pnpm typecheck:docs to generate correct types
|
||||||
|
|
||||||
import { addPluginTemplate } from 'nuxt/kit'
|
import { addPluginTemplate, addRouteMiddleware } from 'nuxt/kit'
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
typescript: { shim: process.env.DOCS_TYPECHECK === 'true' },
|
typescript: { shim: process.env.DOCS_TYPECHECK === 'true' },
|
||||||
pages: process.env.DOCS_TYPECHECK === 'true',
|
pages: process.env.DOCS_TYPECHECK === 'true',
|
||||||
modules: [
|
modules: [
|
||||||
function () {
|
function () {
|
||||||
|
if (!process.env.DOCS_TYPECHECK) { return }
|
||||||
addPluginTemplate({
|
addPluginTemplate({
|
||||||
filename: 'plugins/my-plugin.mjs',
|
filename: 'plugins/my-plugin.mjs',
|
||||||
getContents: () => 'export default defineNuxtPlugin({ name: \'my-plugin\' })',
|
getContents: () => 'export default defineNuxtPlugin({ name: \'my-plugin\' })',
|
||||||
})
|
})
|
||||||
|
addRouteMiddleware({
|
||||||
|
name: 'auth',
|
||||||
|
path: '#build/auth.js',
|
||||||
|
})
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
22
package.json
22
package.json
|
@ -39,28 +39,28 @@
|
||||||
"@nuxt/ui-templates": "workspace:*",
|
"@nuxt/ui-templates": "workspace:*",
|
||||||
"@nuxt/vite-builder": "workspace:*",
|
"@nuxt/vite-builder": "workspace:*",
|
||||||
"@nuxt/webpack-builder": "workspace:*",
|
"@nuxt/webpack-builder": "workspace:*",
|
||||||
"@types/node": "20.16.1",
|
"@types/node": "20.16.2",
|
||||||
"c12": "2.0.0-beta.1",
|
"c12": "2.0.0-beta.1",
|
||||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||||
"jiti": "2.0.0-beta.3",
|
"jiti": "2.0.0-beta.3",
|
||||||
"magic-string": "^0.30.11",
|
"magic-string": "^0.30.11",
|
||||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
|
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
|
||||||
"nuxt": "workspace:*",
|
"nuxt": "workspace:*",
|
||||||
"rollup": "^4.21.0",
|
"rollup": "^4.21.1",
|
||||||
"typescript": "5.5.4",
|
"typescript": "5.5.4",
|
||||||
"unbuild": "3.0.0-rc.7",
|
"unbuild": "3.0.0-rc.7",
|
||||||
"vite": "5.4.2",
|
"vite": "5.4.2",
|
||||||
"vue": "3.4.38"
|
"vue": "3.4.38"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.9.0",
|
"@eslint/js": "9.9.1",
|
||||||
"@nuxt/eslint-config": "0.5.2",
|
"@nuxt/eslint-config": "0.5.3",
|
||||||
"@nuxt/kit": "workspace:*",
|
"@nuxt/kit": "workspace:*",
|
||||||
"@nuxt/test-utils": "3.14.1",
|
"@nuxt/test-utils": "3.14.1",
|
||||||
"@nuxt/webpack-builder": "workspace:*",
|
"@nuxt/webpack-builder": "workspace:*",
|
||||||
"@testing-library/vue": "8.1.0",
|
"@testing-library/vue": "8.1.0",
|
||||||
"@types/eslint__js": "8.42.3",
|
"@types/eslint__js": "8.42.3",
|
||||||
"@types/node": "20.16.1",
|
"@types/node": "20.16.2",
|
||||||
"@types/semver": "7.5.8",
|
"@types/semver": "7.5.8",
|
||||||
"@unhead/schema": "1.10.0",
|
"@unhead/schema": "1.10.0",
|
||||||
"@unhead/vue": "1.10.0",
|
"@unhead/vue": "1.10.0",
|
||||||
|
@ -74,18 +74,16 @@
|
||||||
"cssnano": "7.0.5",
|
"cssnano": "7.0.5",
|
||||||
"destr": "2.0.3",
|
"destr": "2.0.3",
|
||||||
"devalue": "5.0.0",
|
"devalue": "5.0.0",
|
||||||
"eslint": "9.9.0",
|
"eslint": "9.9.1",
|
||||||
"eslint-plugin-no-only-tests": "3.3.0",
|
"eslint-plugin-no-only-tests": "3.3.0",
|
||||||
"eslint-plugin-perfectionist": "3.2.0",
|
"eslint-plugin-perfectionist": "3.3.0",
|
||||||
"eslint-typegen": "0.3.1",
|
"eslint-typegen": "0.3.1",
|
||||||
"execa": "9.3.1",
|
|
||||||
"globby": "14.0.2",
|
|
||||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||||
"happy-dom": "15.0.0",
|
"happy-dom": "15.0.0",
|
||||||
"jiti": "2.0.0-beta.3",
|
"jiti": "2.0.0-beta.3",
|
||||||
"markdownlint-cli": "0.41.0",
|
"markdownlint-cli": "0.41.0",
|
||||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
|
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
|
||||||
"nuxi": "3.12.0",
|
"nuxi": "3.13.1",
|
||||||
"nuxt": "workspace:*",
|
"nuxt": "workspace:*",
|
||||||
"nuxt-content-twoslash": "0.1.1",
|
"nuxt-content-twoslash": "0.1.1",
|
||||||
"ofetch": "1.3.4",
|
"ofetch": "1.3.4",
|
||||||
|
@ -94,6 +92,8 @@
|
||||||
"rimraf": "6.0.1",
|
"rimraf": "6.0.1",
|
||||||
"semver": "7.6.3",
|
"semver": "7.6.3",
|
||||||
"std-env": "3.7.0",
|
"std-env": "3.7.0",
|
||||||
|
"tinyexec": "0.3.0",
|
||||||
|
"tinyglobby": "0.2.5",
|
||||||
"typescript": "5.5.4",
|
"typescript": "5.5.4",
|
||||||
"ufo": "1.5.4",
|
"ufo": "1.5.4",
|
||||||
"vitest": "2.0.5",
|
"vitest": "2.0.5",
|
||||||
|
@ -102,7 +102,7 @@
|
||||||
"vue-router": "4.4.3",
|
"vue-router": "4.4.3",
|
||||||
"vue-tsc": "2.0.29"
|
"vue-tsc": "2.0.29"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.8.0",
|
"packageManager": "pnpm@9.9.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^16.10.0 || >=18.0.0"
|
"node": "^16.10.0 || >=18.0.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"ufo": "^1.5.4",
|
"ufo": "^1.5.4",
|
||||||
"unctx": "^2.3.1",
|
"unctx": "^2.3.1",
|
||||||
"unimport": "^3.11.0",
|
"unimport": "^3.11.1",
|
||||||
"untyped": "^1.4.2"
|
"untyped": "^1.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { existsSync } from 'node:fs'
|
||||||
import type { JSValue } from 'untyped'
|
import type { JSValue } from 'untyped'
|
||||||
import { applyDefaults } from 'untyped'
|
import { applyDefaults } from 'untyped'
|
||||||
import type { ConfigLayer, ConfigLayerMeta, LoadConfigOptions } from 'c12'
|
import type { ConfigLayer, ConfigLayerMeta, LoadConfigOptions } from 'c12'
|
||||||
|
@ -6,6 +7,7 @@ import type { NuxtConfig, NuxtOptions } from '@nuxt/schema'
|
||||||
import { NuxtConfigSchema } from '@nuxt/schema'
|
import { NuxtConfigSchema } from '@nuxt/schema'
|
||||||
import { globby } from 'globby'
|
import { globby } from 'globby'
|
||||||
import defu from 'defu'
|
import defu from 'defu'
|
||||||
|
import { join } from 'pathe'
|
||||||
|
|
||||||
export interface LoadNuxtConfigOptions extends Omit<LoadConfigOptions<NuxtConfig>, 'overrides'> {
|
export interface LoadNuxtConfigOptions extends Omit<LoadConfigOptions<NuxtConfig>, 'overrides'> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
|
@ -47,6 +49,11 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
|
||||||
nuxtConfig._nuxtConfigFile = configFile
|
nuxtConfig._nuxtConfigFile = configFile
|
||||||
nuxtConfig._nuxtConfigFiles = [configFile]
|
nuxtConfig._nuxtConfigFiles = [configFile]
|
||||||
|
|
||||||
|
const defaultBuildDir = join(nuxtConfig.rootDir!, '.nuxt')
|
||||||
|
if (!opts.overrides?._prepare && !nuxtConfig.dev && !nuxtConfig.buildDir && existsSync(defaultBuildDir)) {
|
||||||
|
nuxtConfig.buildDir = join(nuxtConfig.rootDir!, 'node_modules/.cache/nuxt/.nuxt')
|
||||||
|
}
|
||||||
|
|
||||||
const _layers: ConfigLayer<NuxtConfig, ConfigLayerMeta>[] = []
|
const _layers: ConfigLayer<NuxtConfig, ConfigLayerMeta>[] = []
|
||||||
const processedLayers = new Set<string>()
|
const processedLayers = new Set<string>()
|
||||||
for (const layer of layers) {
|
for (const layer of layers) {
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/devalue": "^2.0.2",
|
"@nuxt/devalue": "^2.0.2",
|
||||||
"@nuxt/devtools": "^1.3.14",
|
"@nuxt/devtools": "^1.4.1",
|
||||||
"@nuxt/kit": "workspace:*",
|
"@nuxt/kit": "workspace:*",
|
||||||
"@nuxt/schema": "workspace:*",
|
"@nuxt/schema": "workspace:*",
|
||||||
"@nuxt/telemetry": "^2.5.4",
|
"@nuxt/telemetry": "^2.5.4",
|
||||||
|
@ -75,6 +75,7 @@
|
||||||
"compatx": "^0.1.8",
|
"compatx": "^0.1.8",
|
||||||
"consola": "^3.2.3",
|
"consola": "^3.2.3",
|
||||||
"cookie-es": "^1.2.2",
|
"cookie-es": "^1.2.2",
|
||||||
|
"impound": "^0.1.0",
|
||||||
"defu": "^6.1.4",
|
"defu": "^6.1.4",
|
||||||
"destr": "^2.0.3",
|
"destr": "^2.0.3",
|
||||||
"devalue": "^5.0.0",
|
"devalue": "^5.0.0",
|
||||||
|
@ -91,9 +92,10 @@
|
||||||
"knitwork": "^1.1.0",
|
"knitwork": "^1.1.0",
|
||||||
"magic-string": "^0.30.11",
|
"magic-string": "^0.30.11",
|
||||||
"mlly": "^1.7.1",
|
"mlly": "^1.7.1",
|
||||||
|
"nanotar": "^0.1.1",
|
||||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
|
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
|
||||||
"nuxi": "^3.12.0",
|
"nuxi": "^3.13.1",
|
||||||
"nypm": "^0.3.9",
|
"nypm": "^0.3.11",
|
||||||
"ofetch": "^1.3.4",
|
"ofetch": "^1.3.4",
|
||||||
"ohash": "^1.1.3",
|
"ohash": "^1.1.3",
|
||||||
"pathe": "^1.1.2",
|
"pathe": "^1.1.2",
|
||||||
|
@ -104,12 +106,13 @@
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"std-env": "^3.7.0",
|
"std-env": "^3.7.0",
|
||||||
"strip-literal": "^2.1.0",
|
"strip-literal": "^2.1.0",
|
||||||
|
"tinyglobby": "0.2.5",
|
||||||
"ufo": "^1.5.4",
|
"ufo": "^1.5.4",
|
||||||
"ultrahtml": "^1.5.3",
|
"ultrahtml": "^1.5.3",
|
||||||
"uncrypto": "^0.1.3",
|
"uncrypto": "^0.1.3",
|
||||||
"unctx": "^2.3.1",
|
"unctx": "^2.3.1",
|
||||||
"unenv": "^1.10.0",
|
"unenv": "^1.10.0",
|
||||||
"unimport": "^3.11.0",
|
"unimport": "^3.11.1",
|
||||||
"unplugin": "^1.12.2",
|
"unplugin": "^1.12.2",
|
||||||
"unplugin-vue-router": "^0.10.7",
|
"unplugin-vue-router": "^0.10.7",
|
||||||
"unstorage": "^1.10.2",
|
"unstorage": "^1.10.2",
|
||||||
|
|
|
@ -381,13 +381,15 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||||
replace: props.replace,
|
replace: props.replace,
|
||||||
ariaCurrentValue: props.ariaCurrentValue,
|
ariaCurrentValue: props.ariaCurrentValue,
|
||||||
custom: props.custom,
|
custom: props.custom,
|
||||||
onPointerenter: shouldPrefetch('interaction') ? prefetch.bind(null, undefined) : undefined,
|
|
||||||
onFocus: shouldPrefetch('interaction') ? prefetch.bind(null, undefined) : undefined,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// `custom` API cannot support fallthrough attributes as the slot
|
// `custom` API cannot support fallthrough attributes as the slot
|
||||||
// may render fragment or text root nodes (#14897, #19375)
|
// may render fragment or text root nodes (#14897, #19375)
|
||||||
if (!props.custom) {
|
if (!props.custom) {
|
||||||
|
if (shouldPrefetch('interaction')) {
|
||||||
|
routerLinkProps.onPointerenter = prefetch.bind(null, undefined)
|
||||||
|
routerLinkProps.onFocus = prefetch.bind(null, undefined)
|
||||||
|
}
|
||||||
if (prefetched.value) {
|
if (prefetched.value) {
|
||||||
routerLinkProps.class = props.prefetchedClass || options.prefetchedClass
|
routerLinkProps.class = props.prefetchedClass || options.prefetchedClass
|
||||||
}
|
}
|
||||||
|
@ -427,6 +429,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||||
return slots.default({
|
return slots.default({
|
||||||
href: href.value,
|
href: href.value,
|
||||||
navigate,
|
navigate,
|
||||||
|
prefetch,
|
||||||
get route () {
|
get route () {
|
||||||
if (!href.value) { return undefined }
|
if (!href.value) { return undefined }
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import type { Nuxt, NuxtBuilder } from 'nuxt/schema'
|
||||||
|
|
||||||
import { generateApp as _generateApp, createApp } from './app'
|
import { generateApp as _generateApp, createApp } from './app'
|
||||||
import { checkForExternalConfigurationFiles } from './external-config-files'
|
import { checkForExternalConfigurationFiles } from './external-config-files'
|
||||||
|
import { cleanupCaches, getVueHash } from './cache'
|
||||||
|
|
||||||
export async function build (nuxt: Nuxt) {
|
export async function build (nuxt: Nuxt) {
|
||||||
const app = createApp(nuxt)
|
const app = createApp(nuxt)
|
||||||
|
@ -40,16 +41,32 @@ export async function build (nuxt: Nuxt) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await nuxt.callHook('build:before')
|
if (!nuxt.options._prepare && !nuxt.options.dev && nuxt.options.experimental.buildCache) {
|
||||||
if (!nuxt.options._prepare) {
|
const { restoreCache, collectCache } = await getVueHash(nuxt)
|
||||||
await Promise.all([checkForExternalConfigurationFiles(), bundle(nuxt)])
|
if (await restoreCache()) {
|
||||||
await nuxt.callHook('build:done')
|
await nuxt.callHook('build:done')
|
||||||
|
return await nuxt.callHook('close', nuxt)
|
||||||
if (!nuxt.options.dev) {
|
|
||||||
await nuxt.callHook('close', nuxt)
|
|
||||||
}
|
}
|
||||||
} else {
|
nuxt.hooks.hookOnce('nitro:build:before', () => collectCache())
|
||||||
|
nuxt.hooks.hookOnce('close', () => cleanupCaches(nuxt))
|
||||||
|
}
|
||||||
|
|
||||||
|
await nuxt.callHook('build:before')
|
||||||
|
if (nuxt.options._prepare) {
|
||||||
nuxt.hook('prepare:types', () => nuxt.close())
|
nuxt.hook('prepare:types', () => nuxt.close())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nuxt.options.dev) {
|
||||||
|
checkForExternalConfigurationFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
await bundle(nuxt)
|
||||||
|
|
||||||
|
await nuxt.callHook('build:done')
|
||||||
|
|
||||||
|
if (!nuxt.options.dev) {
|
||||||
|
await nuxt.callHook('close', nuxt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,275 @@
|
||||||
|
import { mkdir, open, readFile, stat, unlink, writeFile } from 'node:fs/promises'
|
||||||
|
import type { FileHandle } from 'node:fs/promises'
|
||||||
|
import { resolve } from 'node:path'
|
||||||
|
import { existsSync } from 'node:fs'
|
||||||
|
import { isIgnored } from '@nuxt/kit'
|
||||||
|
import type { Nuxt, NuxtConfig, NuxtConfigLayer } from '@nuxt/schema'
|
||||||
|
import { hash, murmurHash, objectHash } from 'ohash'
|
||||||
|
import { glob } from 'tinyglobby'
|
||||||
|
import _consola, { consola } from 'consola'
|
||||||
|
import { dirname, join, relative } from 'pathe'
|
||||||
|
import { createTar, parseTar } from 'nanotar'
|
||||||
|
import type { TarFileInput } from 'nanotar'
|
||||||
|
|
||||||
|
export async function getVueHash (nuxt: Nuxt) {
|
||||||
|
const id = 'vue'
|
||||||
|
|
||||||
|
const { hash } = await getHashes(nuxt, {
|
||||||
|
id,
|
||||||
|
cwd: layer => layer.config?.srcDir,
|
||||||
|
patterns: layer => [
|
||||||
|
join(relative(layer.cwd, layer.config.srcDir), '**'),
|
||||||
|
`!${relative(layer.cwd, layer.config.serverDir || join(layer.cwd, 'server'))}/**`,
|
||||||
|
`!${relative(layer.cwd, resolve(layer.config.srcDir || layer.cwd, layer.config.dir?.public || 'public'))}/**`,
|
||||||
|
`!${relative(layer.cwd, resolve(layer.config.srcDir || layer.cwd, layer.config.dir?.static || 'public'))}/**`,
|
||||||
|
'!node_modules/**',
|
||||||
|
'!nuxt.config.*',
|
||||||
|
],
|
||||||
|
configOverrides: {
|
||||||
|
buildId: undefined,
|
||||||
|
serverDir: undefined,
|
||||||
|
nitro: undefined,
|
||||||
|
devServer: undefined,
|
||||||
|
runtimeConfig: undefined,
|
||||||
|
logLevel: undefined,
|
||||||
|
devServerHandlers: undefined,
|
||||||
|
generate: undefined,
|
||||||
|
devtools: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const cacheFile = join(nuxt.options.workspaceDir, 'node_modules/.cache/nuxt/builds', id, hash + '.tar')
|
||||||
|
|
||||||
|
return {
|
||||||
|
hash,
|
||||||
|
async collectCache () {
|
||||||
|
const start = Date.now()
|
||||||
|
await writeCache(nuxt.options.buildDir, nuxt.options.buildDir, cacheFile)
|
||||||
|
const elapsed = Date.now() - start
|
||||||
|
consola.success(`Cached Vue client and server builds in \`${elapsed}ms\`.`)
|
||||||
|
},
|
||||||
|
async restoreCache () {
|
||||||
|
const start = Date.now()
|
||||||
|
const res = await restoreCache(nuxt.options.buildDir, cacheFile)
|
||||||
|
const elapsed = Date.now() - start
|
||||||
|
if (res) {
|
||||||
|
consola.success(`Restored Vue client and server builds from cache in \`${elapsed}ms\`.`)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cleanupCaches (nuxt: Nuxt) {
|
||||||
|
const start = Date.now()
|
||||||
|
const caches = await glob(['*/*.tar'], {
|
||||||
|
cwd: join(nuxt.options.workspaceDir, 'node_modules/.cache/nuxt/builds'),
|
||||||
|
absolute: true,
|
||||||
|
})
|
||||||
|
if (caches.length >= 10) {
|
||||||
|
const cachesWithMeta = await Promise.all(caches.map(async (cache) => {
|
||||||
|
return [cache, await stat(cache).then(r => r.mtime.getTime()).catch(() => 0)] as const
|
||||||
|
}))
|
||||||
|
cachesWithMeta.sort((a, b) => a[1] - b[1])
|
||||||
|
for (const [cache] of cachesWithMeta.slice(0, cachesWithMeta.length - 10)) {
|
||||||
|
await unlink(cache)
|
||||||
|
}
|
||||||
|
const elapsed = Date.now() - start
|
||||||
|
consola.success(`Cleaned up old build caches in \`${elapsed}ms\`.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal
|
||||||
|
|
||||||
|
type HashSource = { name: string, data: any }
|
||||||
|
type Hashes = { hash: string, sources: HashSource[] }
|
||||||
|
|
||||||
|
interface GetHashOptions {
|
||||||
|
id: string
|
||||||
|
cwd: (layer: NuxtConfigLayer) => string
|
||||||
|
patterns: (layer: NuxtConfigLayer) => string[]
|
||||||
|
configOverrides: Partial<Record<keyof NuxtConfig, unknown>>
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getHashes (nuxt: Nuxt, options: GetHashOptions): Promise<Hashes> {
|
||||||
|
if ((nuxt as any)[`_${options.id}BuildHash`]) {
|
||||||
|
return (nuxt as any)[`_${options.id}BuildHash`]
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = Date.now()
|
||||||
|
const hashSources: HashSource[] = []
|
||||||
|
|
||||||
|
// Layers
|
||||||
|
let layerCtr = 0
|
||||||
|
for (const layer of nuxt.options._layers) {
|
||||||
|
if (layer.cwd.includes('node_modules')) { continue }
|
||||||
|
|
||||||
|
const layerName = `layer#${layerCtr++}`
|
||||||
|
hashSources.push({
|
||||||
|
name: `${layerName}:config`,
|
||||||
|
data: objectHash({
|
||||||
|
...layer.config,
|
||||||
|
...options.configOverrides || {},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const normalizeFiles = (files: Awaited<ReturnType<typeof readFilesRecursive>>) => files.map(f => ({
|
||||||
|
name: f.name,
|
||||||
|
size: (f.attrs as any)?.size,
|
||||||
|
data: murmurHash(f.data as any /* ArrayBuffer */),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const sourceFiles = await readFilesRecursive(options.cwd(layer), {
|
||||||
|
shouldIgnore: isIgnored, // TODO: Validate if works with absolute paths
|
||||||
|
cwd: nuxt.options.rootDir,
|
||||||
|
patterns: options.patterns(layer),
|
||||||
|
})
|
||||||
|
|
||||||
|
hashSources.push({
|
||||||
|
name: `${layerName}:src`,
|
||||||
|
data: normalizeFiles(sourceFiles),
|
||||||
|
})
|
||||||
|
|
||||||
|
const rootFiles = await readFilesRecursive(layer.config?.rootDir || layer.cwd, {
|
||||||
|
shouldIgnore: isIgnored, // TODO: Validate if works with absolute paths
|
||||||
|
cwd: nuxt.options.rootDir,
|
||||||
|
patterns: [
|
||||||
|
'.nuxtrc',
|
||||||
|
'.npmrc',
|
||||||
|
'package.json',
|
||||||
|
'package-lock.json',
|
||||||
|
'yarn.lock',
|
||||||
|
'pnpm-lock.yaml',
|
||||||
|
'tsconfig.json',
|
||||||
|
'bun.lockb',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
hashSources.push({
|
||||||
|
name: `${layerName}:root`,
|
||||||
|
data: normalizeFiles(rootFiles),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = ((nuxt as any)[`_${options.id}BuildHash`] = {
|
||||||
|
hash: hash(hashSources),
|
||||||
|
sources: hashSources,
|
||||||
|
})
|
||||||
|
|
||||||
|
const elapsed = Date.now() - start
|
||||||
|
consola.debug(`Computed \`${options.id}\` build hash in \`${elapsed}ms\`.`)
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileWithMeta = TarFileInput & {
|
||||||
|
attrs: {
|
||||||
|
mtime: number
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReadFilesRecursiveOptions {
|
||||||
|
shouldIgnore?: (name: string) => boolean
|
||||||
|
patterns: string[]
|
||||||
|
cwd: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readFilesRecursive (dir: string | string[], opts: ReadFilesRecursiveOptions): Promise<FileWithMeta[]> {
|
||||||
|
if (Array.isArray(dir)) {
|
||||||
|
return (await Promise.all(dir.map(d => readFilesRecursive(d, opts)))).flat()
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await glob(opts.patterns, { cwd: dir })
|
||||||
|
|
||||||
|
const fileEntries = await Promise.all(files.map(async (fileName) => {
|
||||||
|
if (!opts.shouldIgnore?.(fileName)) {
|
||||||
|
const file = await readFileWithMeta(dir, fileName)
|
||||||
|
if (!file) { return }
|
||||||
|
return {
|
||||||
|
...file,
|
||||||
|
name: relative(opts.cwd, join(dir, file.name)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
return fileEntries.filter(Boolean) as FileWithMeta[]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readFileWithMeta (dir: string, fileName: string, count = 0): Promise<FileWithMeta | undefined> {
|
||||||
|
let fd: FileHandle | undefined = undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
fd = await open(resolve(dir, fileName))
|
||||||
|
const stats = await fd.stat()
|
||||||
|
|
||||||
|
if (!stats?.isFile()) { return }
|
||||||
|
|
||||||
|
const mtime = stats.mtime.getTime()
|
||||||
|
const data = await fd.readFile()
|
||||||
|
|
||||||
|
// retry if file has changed during read
|
||||||
|
if ((await fd.stat()).mtime.getTime() !== mtime) {
|
||||||
|
if (count < 5) {
|
||||||
|
return readFileWithMeta(dir, fileName, count + 1)
|
||||||
|
}
|
||||||
|
console.warn(`Failed to read file \`${fileName}\` as it changed during read.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: fileName,
|
||||||
|
data,
|
||||||
|
attrs: {
|
||||||
|
mtime,
|
||||||
|
size: stats.size,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Failed to read file \`${fileName}\`:`, err)
|
||||||
|
} finally {
|
||||||
|
await fd?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreCache (cwd: string, cacheFile: string) {
|
||||||
|
if (!existsSync(cacheFile)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = parseTar(await readFile(cacheFile))
|
||||||
|
for (const file of files) {
|
||||||
|
let fd: FileHandle | undefined = undefined
|
||||||
|
try {
|
||||||
|
const filePath = resolve(cwd, file.name)
|
||||||
|
await mkdir(dirname(filePath), { recursive: true })
|
||||||
|
|
||||||
|
fd = await open(filePath, 'w')
|
||||||
|
|
||||||
|
const stats = await fd.stat().catch(() => null)
|
||||||
|
if (stats?.isFile() && stats.size) {
|
||||||
|
const lastModified = Number.parseInt(file.attrs?.mtime?.toString().padEnd(13, '0') || '0')
|
||||||
|
if (stats.mtime.getTime() >= lastModified) {
|
||||||
|
consola.debug(`Skipping \`${file.name}\` (up to date or newer than cache)`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await fd.writeFile(file.data!)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
} finally {
|
||||||
|
await fd?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeCache (cwd: string, sources: string | string[], cacheFile: string) {
|
||||||
|
const fileEntries = await readFilesRecursive(sources, {
|
||||||
|
patterns: ['**/*', '!analyze/**'],
|
||||||
|
cwd,
|
||||||
|
})
|
||||||
|
const tarData = createTar(fileEntries)
|
||||||
|
await mkdir(dirname(cacheFile), { recursive: true })
|
||||||
|
await writeFile(cacheFile, tarData)
|
||||||
|
}
|
|
@ -11,12 +11,13 @@ import escapeRE from 'escape-string-regexp'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import { dynamicEventHandler } from 'h3'
|
import { dynamicEventHandler } from 'h3'
|
||||||
import { isWindows } from 'std-env'
|
import { isWindows } from 'std-env'
|
||||||
|
import { ImpoundPlugin } from 'impound'
|
||||||
import type { Nuxt, NuxtOptions } from 'nuxt/schema'
|
import type { Nuxt, NuxtOptions } from 'nuxt/schema'
|
||||||
import { version as nuxtVersion } from '../../package.json'
|
import { version as nuxtVersion } from '../../package.json'
|
||||||
import { distDir } from '../dirs'
|
import { distDir } from '../dirs'
|
||||||
import { toArray } from '../utils'
|
import { toArray } from '../utils'
|
||||||
import { template as defaultSpaLoadingTemplate } from '../../../ui-templates/dist/templates/spa-loading-icon'
|
import { template as defaultSpaLoadingTemplate } from '../../../ui-templates/dist/templates/spa-loading-icon'
|
||||||
import { ImportProtectionPlugin, nuxtImportProtections } from './plugins/import-protection'
|
import { nuxtImportProtections } from './plugins/import-protection'
|
||||||
|
|
||||||
const logLevelMapReverse = {
|
const logLevelMapReverse = {
|
||||||
silent: 0,
|
silent: 0,
|
||||||
|
@ -366,9 +367,8 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
||||||
nitroConfig.rollupConfig!.plugins = await nitroConfig.rollupConfig!.plugins || []
|
nitroConfig.rollupConfig!.plugins = await nitroConfig.rollupConfig!.plugins || []
|
||||||
nitroConfig.rollupConfig!.plugins = toArray(nitroConfig.rollupConfig!.plugins)
|
nitroConfig.rollupConfig!.plugins = toArray(nitroConfig.rollupConfig!.plugins)
|
||||||
nitroConfig.rollupConfig!.plugins!.push(
|
nitroConfig.rollupConfig!.plugins!.push(
|
||||||
ImportProtectionPlugin.rollup({
|
ImpoundPlugin.rollup({
|
||||||
rootDir: nuxt.options.rootDir,
|
cwd: nuxt.options.rootDir,
|
||||||
modulesDir: nuxt.options.modulesDir,
|
|
||||||
patterns: nuxtImportProtections(nuxt, { isNitro: true }),
|
patterns: nuxtImportProtections(nuxt, { isNitro: true }),
|
||||||
exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/],
|
exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/],
|
||||||
}),
|
}),
|
||||||
|
@ -525,26 +525,30 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function symlinkDist () {
|
||||||
|
if (nitro.options.static) {
|
||||||
|
const distDir = resolve(nuxt.options.rootDir, 'dist')
|
||||||
|
if (!existsSync(distDir)) {
|
||||||
|
await fsp.symlink(nitro.options.output.publicDir, distDir, 'junction').catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// nuxt build/dev
|
// nuxt build/dev
|
||||||
nuxt.hook('build:done', async () => {
|
nuxt.hook('build:done', async () => {
|
||||||
await nuxt.callHook('nitro:build:before', nitro)
|
await nuxt.callHook('nitro:build:before', nitro)
|
||||||
if (nuxt.options.dev) {
|
if (nuxt.options.dev) {
|
||||||
await build(nitro)
|
return build(nitro)
|
||||||
} else {
|
|
||||||
await prepare(nitro)
|
|
||||||
await prerender(nitro)
|
|
||||||
|
|
||||||
logger.restoreAll()
|
|
||||||
await build(nitro)
|
|
||||||
logger.wrapAll()
|
|
||||||
|
|
||||||
if (nitro.options.static) {
|
|
||||||
const distDir = resolve(nuxt.options.rootDir, 'dist')
|
|
||||||
if (!existsSync(distDir)) {
|
|
||||||
await fsp.symlink(nitro.options.output.publicDir, distDir, 'junction').catch(() => {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await prepare(nitro)
|
||||||
|
await prerender(nitro)
|
||||||
|
|
||||||
|
logger.restoreAll()
|
||||||
|
await build(nitro)
|
||||||
|
logger.wrapAll()
|
||||||
|
|
||||||
|
await symlinkDist()
|
||||||
})
|
})
|
||||||
|
|
||||||
// nuxt dev
|
// nuxt dev
|
||||||
|
|
|
@ -15,13 +15,14 @@ import { colorize } from 'consola/utils'
|
||||||
import { updateConfig } from 'c12/update'
|
import { updateConfig } from 'c12/update'
|
||||||
import { formatDate, resolveCompatibilityDatesFromEnv } from 'compatx'
|
import { formatDate, resolveCompatibilityDatesFromEnv } from 'compatx'
|
||||||
import type { DateString } from 'compatx'
|
import type { DateString } from 'compatx'
|
||||||
|
|
||||||
import escapeRE from 'escape-string-regexp'
|
import escapeRE from 'escape-string-regexp'
|
||||||
import { withTrailingSlash, withoutLeadingSlash } from 'ufo'
|
import { withTrailingSlash, withoutLeadingSlash } from 'ufo'
|
||||||
|
import { ImpoundPlugin } from 'impound'
|
||||||
|
import type { ImpoundOptions } from 'impound'
|
||||||
import defu from 'defu'
|
import defu from 'defu'
|
||||||
import { gt, satisfies } from 'semver'
|
import { gt, satisfies } from 'semver'
|
||||||
import { hasTTY, isCI } from 'std-env'
|
import { hasTTY, isCI } from 'std-env'
|
||||||
|
|
||||||
import pagesModule from '../pages/module'
|
import pagesModule from '../pages/module'
|
||||||
import metaModule from '../head/module'
|
import metaModule from '../head/module'
|
||||||
import componentsModule from '../components/module'
|
import componentsModule from '../components/module'
|
||||||
|
@ -31,7 +32,7 @@ import { distDir, pkgDir } from '../dirs'
|
||||||
import { version } from '../../package.json'
|
import { version } from '../../package.json'
|
||||||
import { scriptsStubsPreset } from '../imports/presets'
|
import { scriptsStubsPreset } from '../imports/presets'
|
||||||
import { resolveTypePath } from './utils/types'
|
import { resolveTypePath } from './utils/types'
|
||||||
import { ImportProtectionPlugin, nuxtImportProtections } from './plugins/import-protection'
|
import { nuxtImportProtections } from './plugins/import-protection'
|
||||||
import type { UnctxTransformPluginOptions } from './plugins/unctx'
|
import type { UnctxTransformPluginOptions } from './plugins/unctx'
|
||||||
import { UnctxTransformPlugin } from './plugins/unctx'
|
import { UnctxTransformPlugin } from './plugins/unctx'
|
||||||
import type { TreeShakeComposablesPluginOptions } from './plugins/tree-shake'
|
import type { TreeShakeComposablesPluginOptions } from './plugins/tree-shake'
|
||||||
|
@ -245,15 +246,15 @@ async function initNuxt (nuxt: Nuxt) {
|
||||||
addBuildPlugin(RemovePluginMetadataPlugin(nuxt))
|
addBuildPlugin(RemovePluginMetadataPlugin(nuxt))
|
||||||
|
|
||||||
// Add import protection
|
// Add import protection
|
||||||
const config = {
|
const config: ImpoundOptions = {
|
||||||
rootDir: nuxt.options.rootDir,
|
cwd: nuxt.options.rootDir,
|
||||||
// Exclude top-level resolutions by plugins
|
// Exclude top-level resolutions by plugins
|
||||||
exclude: [join(nuxt.options.srcDir, 'index.html')],
|
exclude: [join(nuxt.options.srcDir, 'index.html')],
|
||||||
patterns: nuxtImportProtections(nuxt),
|
patterns: nuxtImportProtections(nuxt),
|
||||||
modulesDir: nuxt.options.modulesDir,
|
|
||||||
}
|
}
|
||||||
addVitePlugin(() => ImportProtectionPlugin.vite(config))
|
addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...config, error: false }), { name: 'nuxt:import-protection' }), { client: false })
|
||||||
addWebpackPlugin(() => ImportProtectionPlugin.webpack(config))
|
addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...config, error: true }), { name: 'nuxt:import-protection' }), { server: false })
|
||||||
|
addWebpackPlugin(() => ImpoundPlugin.webpack(config))
|
||||||
|
|
||||||
// add resolver for modules used in virtual files
|
// add resolver for modules used in virtual files
|
||||||
addVitePlugin(() => resolveDeepImportsPlugin(nuxt))
|
addVitePlugin(() => resolveDeepImportsPlugin(nuxt))
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import { createUnplugin } from 'unplugin'
|
import { relative, resolve } from 'pathe'
|
||||||
import { logger } from '@nuxt/kit'
|
|
||||||
import { resolvePath } from 'mlly'
|
|
||||||
import { isAbsolute, join, relative, resolve } from 'pathe'
|
|
||||||
import escapeRE from 'escape-string-regexp'
|
import escapeRE from 'escape-string-regexp'
|
||||||
import type { NuxtOptions } from 'nuxt/schema'
|
import type { NuxtOptions } from 'nuxt/schema'
|
||||||
|
|
||||||
|
@ -53,41 +50,3 @@ export const nuxtImportProtections = (nuxt: { options: NuxtOptions }, options: {
|
||||||
|
|
||||||
return patterns
|
return patterns
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImportProtectionPlugin = createUnplugin(function (options: ImportProtectionOptions) {
|
|
||||||
const cache: Record<string, Map<string | RegExp, boolean>> = {}
|
|
||||||
const importersToExclude = options?.exclude || []
|
|
||||||
const proxy = resolvePath('unenv/runtime/mock/proxy', { url: options.modulesDir })
|
|
||||||
return {
|
|
||||||
name: 'nuxt:import-protection',
|
|
||||||
enforce: 'pre',
|
|
||||||
resolveId (id, importer) {
|
|
||||||
if (!importer) { return }
|
|
||||||
if (id[0] === '.') {
|
|
||||||
id = join(importer, '..', id)
|
|
||||||
}
|
|
||||||
if (isAbsolute(id)) {
|
|
||||||
id = relative(options.rootDir, id)
|
|
||||||
}
|
|
||||||
if (importersToExclude.some(p => typeof p === 'string' ? importer === p : p.test(importer))) { return }
|
|
||||||
|
|
||||||
const invalidImports = options.patterns.filter(([pattern]) => pattern instanceof RegExp ? pattern.test(id) : pattern === id)
|
|
||||||
let matched = false
|
|
||||||
for (const match of invalidImports) {
|
|
||||||
cache[id] = cache[id] || new Map()
|
|
||||||
const [pattern, warning] = match
|
|
||||||
// Skip if already warned
|
|
||||||
if (cache[id].has(pattern)) { continue }
|
|
||||||
|
|
||||||
const relativeImporter = isAbsolute(importer) ? relative(options.rootDir, importer) : importer
|
|
||||||
logger.error(warning || 'Invalid import', `[importing \`${id}\` from \`${relativeImporter}\`]`)
|
|
||||||
cache[id].set(pattern, true)
|
|
||||||
matched = true
|
|
||||||
}
|
|
||||||
if (matched) {
|
|
||||||
return proxy
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
|
@ -15,11 +15,13 @@ export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin {
|
||||||
if (!importer || isAbsolute(id) || (!isAbsolute(importer) && !importer.startsWith('virtual:')) || exclude.some(e => id.startsWith(e))) {
|
if (!importer || isAbsolute(id) || (!isAbsolute(importer) && !importer.startsWith('virtual:')) || exclude.some(e => id.startsWith(e))) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
id = normalize(id)
|
|
||||||
id = resolveAlias(id, nuxt.options.alias)
|
const normalisedId = resolveAlias(normalize(id), nuxt.options.alias)
|
||||||
const { dir } = parseNodeModulePath(importer)
|
const normalisedImporter = importer.replace(/^\0?virtual:(?:nuxt:)?/, '')
|
||||||
return await this.resolve?.(id, dir || pkgDir, { skipSelf: true }) ?? await resolvePath(id, {
|
const dir = parseNodeModulePath(normalisedImporter).dir || pkgDir
|
||||||
url: [dir || pkgDir, ...nuxt.options.modulesDir],
|
|
||||||
|
return await this.resolve?.(normalisedId, dir, { skipSelf: true }) ?? await resolvePath(id, {
|
||||||
|
url: [dir, ...nuxt.options.modulesDir],
|
||||||
// TODO: respect nitro runtime conditions
|
// TODO: respect nitro runtime conditions
|
||||||
conditions: options.ssr ? ['node', 'import', 'require'] : ['import', 'require'],
|
conditions: options.ssr ? ['node', 'import', 'require'] : ['import', 'require'],
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
|
|
@ -120,11 +120,21 @@ export const pluginsDeclaration: NuxtTemplate = {
|
||||||
const relativePath = relative(typesDir, pluginPath)
|
const relativePath = relative(typesDir, pluginPath)
|
||||||
|
|
||||||
const correspondingDeclaration = pluginPath.replace(/\.(?<letter>[cm])?jsx?$/, '.d.$<letter>ts')
|
const correspondingDeclaration = pluginPath.replace(/\.(?<letter>[cm])?jsx?$/, '.d.$<letter>ts')
|
||||||
|
// if `.d.ts` file exists alongside a `.js` plugin, or if `.d.mts` file exists alongside a `.mjs` plugin, we can use the entire path
|
||||||
if (correspondingDeclaration !== pluginPath && exists(correspondingDeclaration)) {
|
if (correspondingDeclaration !== pluginPath && exists(correspondingDeclaration)) {
|
||||||
tsImports.push(relativePath)
|
tsImports.push(relativePath)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const incorrectDeclaration = pluginPath.replace(/\.[cm]jsx?$/, '.d.ts')
|
||||||
|
// if `.d.ts` file exists, but plugin is `.mjs`, add `.js` extension to the import
|
||||||
|
// to hotfix issue until ecosystem updates to `@nuxt/module-builder@>=0.8.0`
|
||||||
|
if (incorrectDeclaration !== pluginPath && exists(incorrectDeclaration)) {
|
||||||
|
tsImports.push(relativePath.replace(/\.[cm](jsx?)$/, '.$1'))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is no declaration we only want to remove the extension if it's a TypeScript file
|
||||||
if (exists(pluginPath)) {
|
if (exists(pluginPath)) {
|
||||||
if (TS_RE.test(pluginPath)) {
|
if (TS_RE.test(pluginPath)) {
|
||||||
tsImports.push(relativePath.replace(EXTENSION_RE, ''))
|
tsImports.push(relativePath.replace(EXTENSION_RE, ''))
|
||||||
|
@ -181,7 +191,7 @@ export const schemaTemplate: NuxtTemplate = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const moduleOptionsInterface = (jsdocTags: boolean) => [
|
const moduleOptionsInterface = (options: { addJSDocTags: boolean, unresolved: boolean }) => [
|
||||||
...modules.flatMap(([configKey, importName, mod]) => {
|
...modules.flatMap(([configKey, importName, mod]) => {
|
||||||
let link: string | undefined
|
let link: string | undefined
|
||||||
|
|
||||||
|
@ -211,30 +221,32 @@ export const schemaTemplate: NuxtTemplate = {
|
||||||
return [
|
return [
|
||||||
` /**`,
|
` /**`,
|
||||||
` * Configuration for \`${importName}\``,
|
` * Configuration for \`${importName}\``,
|
||||||
...jsdocTags && link
|
...options.addJSDocTags && link ? [` * @see ${link}`] : [],
|
||||||
? [
|
|
||||||
` * @see ${link}`,
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
` */`,
|
` */`,
|
||||||
` [${configKey}]?: typeof ${genDynamicImport(importName, { wrapper: false })}.default extends NuxtModule<infer O> ? Partial<O> : Record<string, any>`,
|
` [${configKey}]${options.unresolved ? '?' : ''}: typeof ${genDynamicImport(importName, { wrapper: false })}.default extends NuxtModule<infer O> ? ${options.unresolved ? 'Partial<O>' : 'O'} : Record<string, any>`,
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
modules.length > 0 ? ` modules?: (undefined | null | false | NuxtModule | string | [NuxtModule | string, Record<string, any>] | ${modules.map(([configKey, importName, mod]) => `[${genString(mod.meta?.rawPath || importName)}, Exclude<NuxtConfig[${configKey}], boolean>]`).join(' | ')})[],` : '',
|
modules.length > 0 && options.unresolved ? ` modules?: (undefined | null | false | NuxtModule | string | [NuxtModule | string, Record<string, any>] | ${modules.map(([configKey, importName, mod]) => `[${genString(mod.meta?.rawPath || importName)}, Exclude<NuxtConfig[${configKey}], boolean>]`).join(' | ')})[],` : '',
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'import { NuxtModule, RuntimeConfig } from \'@nuxt/schema\'',
|
'import { NuxtModule, RuntimeConfig } from \'@nuxt/schema\'',
|
||||||
'declare module \'@nuxt/schema\' {',
|
'declare module \'@nuxt/schema\' {',
|
||||||
|
' interface NuxtOptions {',
|
||||||
|
...moduleOptionsInterface({ addJSDocTags: false, unresolved: false }),
|
||||||
|
' }',
|
||||||
' interface NuxtConfig {',
|
' interface NuxtConfig {',
|
||||||
// TypeScript will duplicate the jsdoc tags if we augment it twice
|
// TypeScript will duplicate the jsdoc tags if we augment it twice
|
||||||
// So here we only generate tags for `nuxt/schema`
|
// So here we only generate tags for `nuxt/schema`
|
||||||
...moduleOptionsInterface(false),
|
...moduleOptionsInterface({ addJSDocTags: false, unresolved: true }),
|
||||||
' }',
|
' }',
|
||||||
'}',
|
'}',
|
||||||
'declare module \'nuxt/schema\' {',
|
'declare module \'nuxt/schema\' {',
|
||||||
|
' interface NuxtOptions {',
|
||||||
|
...moduleOptionsInterface({ addJSDocTags: true, unresolved: false }),
|
||||||
|
' }',
|
||||||
' interface NuxtConfig {',
|
' interface NuxtConfig {',
|
||||||
...moduleOptionsInterface(true),
|
...moduleOptionsInterface({ addJSDocTags: true, unresolved: true }),
|
||||||
' }',
|
' }',
|
||||||
generateTypes(await resolveSchema(privateRuntimeConfig as Record<string, JSValue>),
|
generateTypes(await resolveSchema(privateRuntimeConfig as Record<string, JSValue>),
|
||||||
{
|
{
|
||||||
|
|
|
@ -515,7 +515,7 @@ export default defineNuxtModule({
|
||||||
const namedMiddleware = app.middleware.filter(mw => !mw.global)
|
const namedMiddleware = app.middleware.filter(mw => !mw.global)
|
||||||
return [
|
return [
|
||||||
'import type { NavigationGuard } from \'vue-router\'',
|
'import type { NavigationGuard } from \'vue-router\'',
|
||||||
`export type MiddlewareKey = ${namedMiddleware.map(mw => genString(mw.name)).join(' | ') || 'string'}`,
|
`export type MiddlewareKey = ${namedMiddleware.map(mw => genString(mw.name)).join(' | ') || 'never'}`,
|
||||||
`declare module ${genString(composablesFile)} {`,
|
`declare module ${genString(composablesFile)} {`,
|
||||||
' interface PageMeta {',
|
' interface PageMeta {',
|
||||||
' middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>',
|
' middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>',
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
import { normalize } from 'pathe'
|
import { normalize } from 'pathe'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { ImportProtectionPlugin, nuxtImportProtections } from '../src/core/plugins/import-protection'
|
import { ImpoundPlugin } from 'impound'
|
||||||
|
import { nuxtImportProtections } from '../src/core/plugins/import-protection'
|
||||||
import type { NuxtOptions } from '../schema'
|
import type { NuxtOptions } from '../schema'
|
||||||
|
|
||||||
const testsToTriggerOn = [
|
const testsToTriggerOn = [
|
||||||
|
@ -39,9 +39,8 @@ describe('import protection', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const transformWithImportProtection = (id: string, importer: string) => {
|
const transformWithImportProtection = (id: string, importer: string) => {
|
||||||
const plugin = ImportProtectionPlugin.rollup({
|
const plugin = ImpoundPlugin.rollup({
|
||||||
rootDir: '/root',
|
cwd: '/root',
|
||||||
modulesDir: [fileURLToPath(new URL('..', import.meta.url))],
|
|
||||||
patterns: nuxtImportProtections({
|
patterns: nuxtImportProtections({
|
||||||
options: {
|
options: {
|
||||||
modules: ['some-nuxt-module'],
|
modules: ['some-nuxt-module'],
|
||||||
|
@ -51,5 +50,5 @@ const transformWithImportProtection = (id: string, importer: string) => {
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
return (plugin as any).resolveId(id, importer)
|
return (plugin as any).resolveId.call({ error: () => {} }, id, importer)
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
"std-env": "^3.7.0",
|
"std-env": "^3.7.0",
|
||||||
"ufo": "^1.5.4",
|
"ufo": "^1.5.4",
|
||||||
"uncrypto": "^0.1.3",
|
"uncrypto": "^0.1.3",
|
||||||
"unimport": "^3.11.0",
|
"unimport": "^3.11.1",
|
||||||
"untyped": "^1.4.2"
|
"untyped": "^1.4.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { existsSync } from 'node:fs'
|
import { existsSync } from 'node:fs'
|
||||||
import { readdir } from 'node:fs/promises'
|
import { readdir } from 'node:fs/promises'
|
||||||
import { defineUntypedSchema } from 'untyped'
|
import { defineUntypedSchema } from 'untyped'
|
||||||
import { basename, relative, resolve } from 'pathe'
|
import { basename, join, relative, resolve } from 'pathe'
|
||||||
import { isDebug, isDevelopment, isTest } from 'std-env'
|
import { isDebug, isDevelopment, isTest } from 'std-env'
|
||||||
import { defu } from 'defu'
|
import { defu } from 'defu'
|
||||||
import { findWorkspaceDir } from 'pkg-types'
|
import { findWorkspaceDir } from 'pkg-types'
|
||||||
|
@ -156,9 +156,12 @@ export default defineUntypedSchema({
|
||||||
*/
|
*/
|
||||||
serverDir: {
|
serverDir: {
|
||||||
$resolve: async (val: string | undefined, get): Promise<string> => {
|
$resolve: async (val: string | undefined, get): Promise<string> => {
|
||||||
const isV4 = ((await get('future') as Record<string, unknown>).compatibilityVersion === 4)
|
if (val) {
|
||||||
|
const rootDir = await get('rootDir') as string
|
||||||
return resolve(isV4 ? await get('rootDir') as string : await get('srcDir') as string, val ?? 'server')
|
return resolve(rootDir, val)
|
||||||
|
}
|
||||||
|
const isV4 = (await get('future') as Record<string, unknown>).compatibilityVersion === 4
|
||||||
|
return join(isV4 ? await get('rootDir') as string : await get('srcDir') as string, 'server')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -175,28 +178,9 @@ export default defineUntypedSchema({
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
buildDir: {
|
buildDir: {
|
||||||
$resolve: async (val: string | undefined, get): Promise<string> => {
|
$resolve: async (val: string | undefined, get) => {
|
||||||
const rootDir = await get('rootDir') as string
|
const rootDir = await get('rootDir') as string
|
||||||
|
return resolve(rootDir, val ?? '.nuxt')
|
||||||
if (val) {
|
|
||||||
return resolve(rootDir, val)
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultBuildDir = resolve(rootDir, '.nuxt')
|
|
||||||
|
|
||||||
const isDev = await get('dev') as boolean
|
|
||||||
if (isDev) {
|
|
||||||
return defaultBuildDir
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: nuxi CLI should ensure .nuxt dir exists
|
|
||||||
if (!existsSync(defaultBuildDir)) {
|
|
||||||
// This is to ensure that types continue to work for CI builds
|
|
||||||
return defaultBuildDir
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: handle build caching + using buildId in directory
|
|
||||||
return resolve(rootDir, 'node_modules/.cache/nuxt/builds', 'production')
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -382,5 +382,12 @@ export default defineUntypedSchema({
|
||||||
* It can reduce INP when navigating on prerendered routes.
|
* It can reduce INP when navigating on prerendered routes.
|
||||||
*/
|
*/
|
||||||
navigationRepaint: true,
|
navigationRepaint: true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache Nuxt/Nitro build artifacts based on a hash of the configuration and source files.
|
||||||
|
*
|
||||||
|
* This only works for source files within `srcDir` and `serverDir` for the Vue/Nitro parts of your app.
|
||||||
|
*/
|
||||||
|
buildCache: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -48,7 +48,7 @@ export default defineUntypedSchema({
|
||||||
* Each handler accepts the following options:
|
* Each handler accepts the following options:
|
||||||
*
|
*
|
||||||
* - handler: The path to the file defining the handler.
|
* - handler: The path to the file defining the handler.
|
||||||
* - route: The route under which the handler is available. This follows the conventions of [radix3](https://github.com/unjs/radix3.)
|
* - route: The route under which the handler is available. This follows the conventions of [rou3](https://github.com/unjs/rou3.)
|
||||||
* - method: The HTTP method of requests that should be handled.
|
* - method: The HTTP method of requests that should be handled.
|
||||||
* - middleware: Specifies whether it is a middleware handler.
|
* - middleware: Specifies whether it is a middleware handler.
|
||||||
* - lazy: Specifies whether to use lazy loading to import the handler.
|
* - lazy: Specifies whether to use lazy loading to import the handler.
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import { join, resolve } from 'node:path'
|
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { promises as fsp } from 'node:fs'
|
import { promises as fsp } from 'node:fs'
|
||||||
import { globby } from 'globby'
|
import { glob } from 'tinyglobby'
|
||||||
|
|
||||||
const templatesRoot = fileURLToPath(new URL('..', import.meta.url))
|
const templatesRoot = fileURLToPath(new URL('..', import.meta.url))
|
||||||
|
|
||||||
const r = (...path: string[]) => resolve(join(templatesRoot, ...path))
|
|
||||||
|
|
||||||
async function main () {
|
async function main () {
|
||||||
const templates = await globby(r('dist/templates/*.js'))
|
const templates = await glob(['dist/templates/*.js'], { cwd: templatesRoot })
|
||||||
for (const file of templates) {
|
for (const file of templates) {
|
||||||
const { template } = await import(file)
|
const { template } = await import(file)
|
||||||
const updated = template({
|
const updated = template({
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { readFileSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs'
|
import { readFileSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs'
|
||||||
import { copyFile } from 'node:fs/promises'
|
import { copyFile } from 'node:fs/promises'
|
||||||
import { basename, dirname, join, resolve } from 'pathe'
|
import { basename, dirname, join } from 'pathe'
|
||||||
import type { Plugin } from 'vite'
|
import type { Plugin } from 'vite'
|
||||||
// @ts-expect-error https://github.com/GoogleChromeLabs/critters/pull/151
|
// @ts-expect-error https://github.com/GoogleChromeLabs/critters/pull/151
|
||||||
import Critters from 'critters'
|
import Critters from 'critters'
|
||||||
import { genObjectFromRawEntries } from 'knitwork'
|
import { genObjectFromRawEntries } from 'knitwork'
|
||||||
import htmlMinifier from 'html-minifier'
|
import htmlMinifier from 'html-minifier'
|
||||||
import { globby } from 'globby'
|
import { glob } from 'tinyglobby'
|
||||||
import { camelCase } from 'scule'
|
import { camelCase } from 'scule'
|
||||||
|
|
||||||
import { version } from '../../nuxt/package.json'
|
import { version } from '../../nuxt/package.json'
|
||||||
|
@ -26,7 +26,10 @@ export const RenderPlugin = () => {
|
||||||
enforce: 'post',
|
enforce: 'post',
|
||||||
async writeBundle () {
|
async writeBundle () {
|
||||||
const critters = new Critters({ path: outputDir })
|
const critters = new Critters({ path: outputDir })
|
||||||
const htmlFiles = await globby(resolve(outputDir, 'templates/**/*.html'), { absolute: true })
|
const htmlFiles = await glob(['templates/**/*.html'], {
|
||||||
|
cwd: outputDir,
|
||||||
|
absolute: true,
|
||||||
|
})
|
||||||
|
|
||||||
const templateExports: Array<{
|
const templateExports: Array<{
|
||||||
exportName: string
|
exportName: string
|
||||||
|
|
|
@ -19,10 +19,8 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/html-minifier": "4.0.5",
|
"@types/html-minifier": "4.0.5",
|
||||||
"@unocss/reset": "0.62.2",
|
"@unocss/reset": "0.62.3",
|
||||||
"critters": "0.0.24",
|
"critters": "0.0.24",
|
||||||
"execa": "9.3.1",
|
|
||||||
"globby": "14.0.2",
|
|
||||||
"html-minifier": "4.0.0",
|
"html-minifier": "4.0.0",
|
||||||
"html-validate": "8.21.0",
|
"html-validate": "8.21.0",
|
||||||
"jiti": "2.0.0-beta.3",
|
"jiti": "2.0.0-beta.3",
|
||||||
|
@ -30,7 +28,9 @@
|
||||||
"pathe": "1.1.2",
|
"pathe": "1.1.2",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.3.3",
|
||||||
"scule": "1.3.0",
|
"scule": "1.3.0",
|
||||||
"unocss": "0.62.2",
|
"tinyexec": "0.3.0",
|
||||||
|
"tinyglobby": "0.2.5",
|
||||||
|
"unocss": "0.62.3",
|
||||||
"vite": "5.4.2"
|
"vite": "5.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { fileURLToPath } from 'node:url'
|
||||||
import { readFileSync } from 'node:fs'
|
import { readFileSync } from 'node:fs'
|
||||||
import { rm } from 'node:fs/promises'
|
import { rm } from 'node:fs/promises'
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||||
import { execaCommand } from 'execa'
|
import { exec } from 'tinyexec'
|
||||||
import { format } from 'prettier'
|
import { format } from 'prettier'
|
||||||
import { createJiti } from 'jiti'
|
import { createJiti } from 'jiti'
|
||||||
// @ts-expect-error types not valid for bundler resolution
|
// @ts-expect-error types not valid for bundler resolution
|
||||||
|
@ -12,10 +12,12 @@ const distDir = fileURLToPath(new URL('../node_modules/.temp/dist/templates', im
|
||||||
|
|
||||||
describe('template', () => {
|
describe('template', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await execaCommand('pnpm build', {
|
await exec('pnpm', ['build'], {
|
||||||
cwd: fileURLToPath(new URL('..', import.meta.url)),
|
nodeOptions: {
|
||||||
env: {
|
cwd: fileURLToPath(new URL('..', import.meta.url)),
|
||||||
OUTPUT_DIR: './node_modules/.temp/dist',
|
env: {
|
||||||
|
OUTPUT_DIR: './node_modules/.temp/dist',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
"@nuxt/schema": "workspace:*",
|
"@nuxt/schema": "workspace:*",
|
||||||
"@types/clear": "0.1.4",
|
"@types/clear": "0.1.4",
|
||||||
"@types/estree": "1.0.5",
|
"@types/estree": "1.0.5",
|
||||||
"rollup": "4.21.0",
|
"rollup": "4.21.1",
|
||||||
"unbuild": "3.0.0-rc.7",
|
"unbuild": "3.0.0-rc.7",
|
||||||
"vue": "3.4.38"
|
"vue": "3.4.38"
|
||||||
},
|
},
|
||||||
|
|
|
@ -116,7 +116,7 @@ export async function buildServer (ctx: ViteBuildContext) {
|
||||||
if (Array.isArray(serverConfig.ssr!.external)) {
|
if (Array.isArray(serverConfig.ssr!.external)) {
|
||||||
serverConfig.ssr!.external.push(
|
serverConfig.ssr!.external.push(
|
||||||
// explicit dependencies we use in our ssr renderer - these can be inlined (if necessary) in the nitro build
|
// explicit dependencies we use in our ssr renderer - these can be inlined (if necessary) in the nitro build
|
||||||
'unhead', '@unhead/ssr', 'unctx', 'h3', 'devalue', '@nuxt/devalue', 'radix3', 'unstorage', 'hookable',
|
'unhead', '@unhead/ssr', 'unctx', 'h3', 'devalue', '@nuxt/devalue', 'radix3', 'rou3', 'unstorage', 'hookable',
|
||||||
// dependencies we might share with nitro - these can be inlined (if necessary) in the nitro build
|
// dependencies we might share with nitro - these can be inlined (if necessary) in the nitro build
|
||||||
...runtimeDependencies,
|
...runtimeDependencies,
|
||||||
)
|
)
|
||||||
|
|
|
@ -78,7 +78,7 @@
|
||||||
"@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",
|
||||||
"rollup": "4.21.0",
|
"rollup": "4.21.1",
|
||||||
"unbuild": "3.0.0-rc.7",
|
"unbuild": "3.0.0-rc.7",
|
||||||
"vue": "3.4.38"
|
"vue": "3.4.38"
|
||||||
},
|
},
|
||||||
|
|
1482
pnpm-lock.yaml
1482
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -1,9 +1,9 @@
|
||||||
import { execSync } from 'node:child_process'
|
|
||||||
import { promises as fsp } from 'node:fs'
|
import { promises as fsp } from 'node:fs'
|
||||||
import { $fetch } from 'ofetch'
|
import { $fetch } from 'ofetch'
|
||||||
import { resolve } from 'pathe'
|
import { resolve } from 'pathe'
|
||||||
import { globby } from 'globby'
|
import { compare } from 'semver'
|
||||||
import { execaSync } from 'execa'
|
import { glob } from 'tinyglobby'
|
||||||
|
import { exec } from 'tinyexec'
|
||||||
import { determineSemverChange, getGitDiff, loadChangelogConfig, parseCommits } from 'changelogen'
|
import { determineSemverChange, getGitDiff, loadChangelogConfig, parseCommits } from 'changelogen'
|
||||||
|
|
||||||
export interface Dep {
|
export interface Dep {
|
||||||
|
@ -43,7 +43,7 @@ export async function loadPackage (dir: string) {
|
||||||
|
|
||||||
export async function loadWorkspace (dir: string) {
|
export async function loadWorkspace (dir: string) {
|
||||||
const workspacePkg = await loadPackage(dir)
|
const workspacePkg = await loadPackage(dir)
|
||||||
const pkgDirs = (await globby(['packages/*'], { onlyDirectories: true })).sort()
|
const pkgDirs = (await glob(['packages/*'], { onlyDirectories: true })).sort()
|
||||||
|
|
||||||
const packages: Package[] = []
|
const packages: Package[] = []
|
||||||
|
|
||||||
|
@ -106,9 +106,27 @@ export async function determineBumpType () {
|
||||||
return determineSemverChange(commits, config)
|
return determineSemverChange(commits, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getLatestTag () {
|
||||||
|
const { stdout: latestTag } = await exec('git', ['describe', '--tags', '--abbrev=0'])
|
||||||
|
return latestTag.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLatestReleasedTag () {
|
||||||
|
const latestReleasedTag = await exec('git', ['tag', '-l']).then(r => r.stdout.trim().split('\n').filter(t => /v3\.\d+\.\d+/.test(t)).sort(compare)).then(r => r.pop()!.trim())
|
||||||
|
return latestReleasedTag
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPreviousReleasedCommits () {
|
||||||
|
const config = await loadChangelogConfig(process.cwd())
|
||||||
|
const latestTag = await getLatestTag()
|
||||||
|
const latestReleasedTag = await getLatestReleasedTag()
|
||||||
|
const commits = parseCommits(await getGitDiff(latestTag, latestReleasedTag), config)
|
||||||
|
return commits
|
||||||
|
}
|
||||||
|
|
||||||
export async function getLatestCommits () {
|
export async function getLatestCommits () {
|
||||||
const config = await loadChangelogConfig(process.cwd())
|
const config = await loadChangelogConfig(process.cwd())
|
||||||
const latestTag = execaSync('git', ['describe', '--tags', '--abbrev=0']).stdout
|
const latestTag = await getLatestTag()
|
||||||
|
|
||||||
return parseCommits(await getGitDiff(latestTag), config)
|
return parseCommits(await getGitDiff(latestTag), config)
|
||||||
}
|
}
|
||||||
|
@ -116,7 +134,7 @@ export async function getLatestCommits () {
|
||||||
export async function getContributors () {
|
export async function getContributors () {
|
||||||
const contributors = [] as Array<{ name: string, username: string }>
|
const contributors = [] as Array<{ name: string, username: string }>
|
||||||
const emails = new Set<string>()
|
const emails = new Set<string>()
|
||||||
const latestTag = execSync('git describe --tags --abbrev=0').toString().trim()
|
const latestTag = await getLatestTag()
|
||||||
const rawCommits = await getGitDiff(latestTag)
|
const rawCommits = await getGitDiff(latestTag)
|
||||||
for (const commit of rawCommits) {
|
for (const commit of rawCommits) {
|
||||||
if (emails.has(commit.author.email) || commit.author.name === 'renovate[bot]') { continue }
|
if (emails.has(commit.author.email) || commit.author.name === 'renovate[bot]') { continue }
|
||||||
|
|
|
@ -3,15 +3,19 @@ import { $fetch } from 'ofetch'
|
||||||
import { inc } from 'semver'
|
import { inc } from 'semver'
|
||||||
import { generateMarkDown, getCurrentGitBranch, loadChangelogConfig } from 'changelogen'
|
import { generateMarkDown, getCurrentGitBranch, loadChangelogConfig } from 'changelogen'
|
||||||
import { consola } from 'consola'
|
import { consola } from 'consola'
|
||||||
import { determineBumpType, getContributors, getLatestCommits, loadWorkspace } from './_utils'
|
import { determineBumpType, getContributors, getLatestCommits, getLatestReleasedTag, getLatestTag, getPreviousReleasedCommits, loadWorkspace } from './_utils'
|
||||||
|
|
||||||
|
const handleSeparateBranch = true
|
||||||
|
|
||||||
async function main () {
|
async function main () {
|
||||||
const releaseBranch = await getCurrentGitBranch()
|
const releaseBranch = await getCurrentGitBranch()
|
||||||
const workspace = await loadWorkspace(process.cwd())
|
const workspace = await loadWorkspace(process.cwd())
|
||||||
const config = await loadChangelogConfig(process.cwd(), {})
|
const config = await loadChangelogConfig(process.cwd(), {})
|
||||||
|
|
||||||
|
const prevMessages = new Set(handleSeparateBranch ? await getPreviousReleasedCommits().then(r => r.map(c => c.message)) : [])
|
||||||
|
|
||||||
const commits = await getLatestCommits().then(commits => commits.filter(
|
const commits = await getLatestCommits().then(commits => commits.filter(
|
||||||
c => config.types[c.type] && !(c.type === 'chore' && c.scope === 'deps'),
|
c => config.types[c.type] && !(c.type === 'chore' && c.scope === 'deps') && !prevMessages.has(c.message),
|
||||||
))
|
))
|
||||||
const bumpType = await determineBumpType() || 'patch'
|
const bumpType = await determineBumpType() || 'patch'
|
||||||
|
|
||||||
|
@ -38,6 +42,9 @@ async function main () {
|
||||||
const [currentPR] = await $fetch(`https://api.github.com/repos/nuxt/nuxt/pulls?head=nuxt:v${newVersion}`)
|
const [currentPR] = await $fetch(`https://api.github.com/repos/nuxt/nuxt/pulls?head=nuxt:v${newVersion}`)
|
||||||
const contributors = await getContributors()
|
const contributors = await getContributors()
|
||||||
|
|
||||||
|
const latestTag = await getLatestTag()
|
||||||
|
const previousReleasedTag = handleSeparateBranch ? await getLatestReleasedTag() : latestTag
|
||||||
|
|
||||||
const releaseNotes = [
|
const releaseNotes = [
|
||||||
currentPR?.body.replace(/## 👉 Changelog[\s\S]*$/, '') || `> ${newVersion} is the next ${bumpType} release.\n>\n> **Timetable**: to be announced.`,
|
currentPR?.body.replace(/## 👉 Changelog[\s\S]*$/, '') || `> ${newVersion} is the next ${bumpType} release.\n>\n> **Timetable**: to be announced.`,
|
||||||
'## 👉 Changelog',
|
'## 👉 Changelog',
|
||||||
|
@ -45,7 +52,8 @@ async function main () {
|
||||||
.replace(/^## v.*\n/, '')
|
.replace(/^## v.*\n/, '')
|
||||||
.replace(`...${releaseBranch}`, `...v${newVersion}`)
|
.replace(`...${releaseBranch}`, `...v${newVersion}`)
|
||||||
.replace(/### ❤️ Contributors[\s\S]*$/, '')
|
.replace(/### ❤️ Contributors[\s\S]*$/, '')
|
||||||
.replace(/[\n\r]+/g, '\n'),
|
.replace(/[\n\r]+/g, '\n')
|
||||||
|
.replace(latestTag, previousReleasedTag),
|
||||||
'### ❤️ Contributors',
|
'### ❤️ Contributors',
|
||||||
contributors.map(c => `- ${c.name} (@${c.username})`).join('\n'),
|
contributors.map(c => `- ${c.name} (@${c.username})`).join('\n'),
|
||||||
].join('\n')
|
].join('\n')
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import fsp from 'node:fs/promises'
|
import fsp from 'node:fs/promises'
|
||||||
import { beforeAll, describe, expect, it } from 'vitest'
|
import { beforeAll, describe, expect, it } from 'vitest'
|
||||||
import { execaCommand } from 'execa'
|
import { exec } from 'tinyexec'
|
||||||
import { globby } from 'globby'
|
import { glob } from 'tinyglobby'
|
||||||
import { join } from 'pathe'
|
import { join } from 'pathe'
|
||||||
|
|
||||||
describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM_CI)('minimal nuxt application', () => {
|
describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM_CI)('minimal nuxt application', () => {
|
||||||
|
@ -10,15 +10,15 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
execaCommand(`pnpm nuxi build ${rootDir}`, { env: { EXTERNAL_VUE: 'false' } }),
|
exec('pnpm', ['nuxi', 'build', rootDir], { nodeOptions: { env: { EXTERNAL_VUE: 'false' } } }),
|
||||||
execaCommand(`pnpm nuxi build ${rootDir}`, { env: { EXTERNAL_VUE: 'true' } }),
|
exec('pnpm', ['nuxi', 'build', rootDir], { nodeOptions: { env: { EXTERNAL_VUE: 'true' } } }),
|
||||||
])
|
])
|
||||||
}, 120 * 1000)
|
}, 120 * 1000)
|
||||||
|
|
||||||
// Identical behaviour between inline/external vue options as this should only affect the server build
|
// Identical behaviour between inline/external vue options as this should only affect the server build
|
||||||
for (const outputDir of ['.output', '.output-inline']) {
|
for (const outputDir of ['.output', '.output-inline']) {
|
||||||
it('default client bundle size', async () => {
|
it('default client bundle size', async () => {
|
||||||
const clientStats = await analyzeSizes('**/*.js', join(rootDir, outputDir, 'public'))
|
const clientStats = await analyzeSizes(['**/*.js'], join(rootDir, outputDir, 'public'))
|
||||||
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot(`"108k"`)
|
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot(`"108k"`)
|
||||||
expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
|
expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
|
@ -34,7 +34,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"205k"`)
|
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"205k"`)
|
||||||
|
|
||||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
|
||||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1355k"`)
|
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1355k"`)
|
||||||
|
|
||||||
const packages = modules.files
|
const packages = modules.files
|
||||||
|
@ -75,7 +75,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||||
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||||
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"529k"`)
|
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"529k"`)
|
||||||
|
|
||||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
|
||||||
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"86.1k"`)
|
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"86.1k"`)
|
||||||
|
|
||||||
const packages = modules.files
|
const packages = modules.files
|
||||||
|
@ -96,8 +96,8 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
async function analyzeSizes (pattern: string | string[], rootDir: string) {
|
async function analyzeSizes (pattern: string[], rootDir: string) {
|
||||||
const files: string[] = await globby(pattern, { cwd: rootDir })
|
const files: string[] = await glob(pattern, { cwd: rootDir })
|
||||||
let totalBytes = 0
|
let totalBytes = 0
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const path = join(rootDir, file)
|
const path = join(rootDir, file)
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { rm } from 'node:fs/promises'
|
import { rm } from 'node:fs/promises'
|
||||||
|
|
||||||
import { globby } from 'globby'
|
import { glob } from 'tinyglobby'
|
||||||
|
import { exec } from 'tinyexec'
|
||||||
import { execa } from 'execa'
|
|
||||||
|
|
||||||
async function initTesting () {
|
async function initTesting () {
|
||||||
const dirs = await globby('*', {
|
const dirs = await glob(['*'], {
|
||||||
onlyDirectories: true,
|
onlyDirectories: true,
|
||||||
cwd: fileURLToPath(new URL('./fixtures', import.meta.url)),
|
cwd: fileURLToPath(new URL('./fixtures', import.meta.url)),
|
||||||
absolute: true,
|
absolute: true,
|
||||||
|
@ -20,7 +19,7 @@ async function initTesting () {
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
dirs.map(dir => execa('pnpm', ['nuxi', 'prepare'], { cwd: dir })),
|
dirs.map(dir => exec('pnpm', ['nuxi', 'prepare'], { nodeOptions: { cwd: dir } })),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue