Merge branch 'main' into patch-21

This commit is contained in:
Michael Brevard 2025-01-13 17:59:26 +02:00 committed by GitHub
commit 8b21493917
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
91 changed files with 2489 additions and 1842 deletions

View File

@ -1,4 +1,4 @@
FROM node:lts@sha256:0e910f435308c36ea60b4cfd7b80208044d77a074d16b768a81901ce938a62dc
FROM node:lts@sha256:99981c3d1aac0d98cd9f03f74b92dddf30f30ffb0b34e6df8bd96283f62f12c6
RUN apt-get update && \
apt-get install -fy libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdbus-1-3 libdrm2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2 && \

View File

@ -21,7 +21,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 22
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@ -33,4 +33,4 @@ jobs:
- name: Lint (docs)
run: pnpm lint:docs:fix
- uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef

View File

@ -17,14 +17,14 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 22
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Check engine ranges, peer dependency ranges and installed versions
run: pnpm installed-check -d --fix
run: pnpm installed-check --no-include-workspace-root --ignore-dev --workspace-ignore='test/**,playground' --fix
- name: Build (stub)
run: pnpm dev:prepare
@ -55,4 +55,4 @@ jobs:
- name: Lint (code)
run: pnpm lint:fix
- uses: autofix-ci/action@ff86a557419858bb967097bfc916833f5647fa8c
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef

View File

@ -33,7 +33,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 22
node-version: lts/*
cache: "pnpm"
- name: Install dependencies

View File

@ -28,7 +28,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 22
node-version: lts/*
cache: "pnpm"
- name: Install dependencies

View File

@ -41,7 +41,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@ -56,11 +56,8 @@ jobs:
- name: Build
run: pnpm build
- name: Check types
run: pnpm test:attw
- name: Cache dist
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
retention-days: 3
name: dist
@ -81,7 +78,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
uses: github/codeql-action/init@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
with:
config: |
paths:
@ -98,7 +95,7 @@ jobs:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
uses: github/codeql-action/analyze@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
with:
category: "/language:${{ matrix.language }}"
@ -118,7 +115,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 22
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@ -149,7 +146,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 22
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@ -161,6 +158,9 @@ jobs:
- name: Lint
run: pnpm lint
- name: Check built types
run: pnpm test:attw
test-unit:
# autofix workflow will be triggered instead for PRs
if: github.event_name == 'push'
@ -173,7 +173,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 22
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@ -260,20 +260,18 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
build-release:
release-nightly:
concurrency:
group: release
permissions:
id-token: write
if: |
github.event_name == 'push' &&
github.repository == 'nuxt/nuxt' &&
github.repository_owner == 'nuxt' &&
!contains(github.event.head_commit.message, '[skip-release]') &&
!startsWith(github.event.head_commit.message, 'docs')
needs:
- lint
- build
- test-fixtures
runs-on: ubuntu-latest
timeout-minutes: 20
@ -284,7 +282,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 22
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@ -299,22 +297,15 @@ jobs:
- name: Release Edge
run: ./scripts/release-edge.sh ${{ github.ref == 'refs/heads/main' && 'latest' || '3x' }}
env:
NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}}
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
NPM_CONFIG_PROVENANCE: true
release-pr:
concurrency:
group: release
permissions:
id-token: write
pull-requests: write
if: |
github.event_name == 'pull_request' &&
contains(github.event.pull_request.labels.*.name, '🧷 edge release')
if: github.repository_owner == 'nuxt' && github.event_name != 'push'
needs:
- lint
- build
- test-fixtures
runs-on: ubuntu-latest
timeout-minutes: 20
@ -325,7 +316,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 22
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@ -337,8 +328,4 @@ jobs:
name: dist
path: packages
- name: Release Edge
run: ./scripts/release-edge.sh pr-${{ github.event.issue.number }}
env:
NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}}
NPM_CONFIG_PROVENANCE: true
- run: pnpm pkg-pr-new publish --compact './packages/kit' './packages/nuxt' './packages/rspack' './packages/schema' './packages/vite' './packages/webpack'

View File

@ -25,7 +25,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 22
node-version: lts/*
cache: "pnpm"
- name: Install dependencies

View File

@ -1,4 +1,4 @@
name: CI
name: ci
on:
push:
@ -29,7 +29,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 22
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
@ -39,4 +39,4 @@ jobs:
run: pnpm sherif -r multiple-dependency-versions
- name: Check engine ranges, peer dependency ranges and installed versions
run: pnpm installed-check -d
run: pnpm installed-check --no-include-workspace-root --ignore-dev --workspace-ignore='test/**,playground'

View File

@ -26,6 +26,6 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
- name: Check workflow files
uses: docker://rhysd/actionlint:1.7.4@sha256:82244e1db1c60d82c7792180a48dd0bcb838370bb589d53ff132503fc9485868
uses: docker://rhysd/actionlint:1.7.6@sha256:e3856d413f923accc4120884ff79f6bdba3dd53fd42884d325f21af61cc15ce0
with:
args: -color

View File

@ -25,7 +25,7 @@ jobs:
- run: corepack enable
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 20
node-version: lts/*
registry-url: "https://registry.npmjs.org/"
cache: "pnpm"

View File

@ -59,7 +59,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
if: github.repository == 'nuxt/nuxt' && success()
with:
name: SARIF file
@ -68,7 +68,7 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
uses: github/codeql-action/upload-sarif@b6a472f63d85b9c78a3ac5e89422239fc15e9b3c # v3.28.1
if: github.repository == 'nuxt/nuxt' && success()
with:
sarif_file: results.sarif

View File

@ -17,7 +17,7 @@ jobs:
statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR
if: github.repository == 'nuxt/nuxt' && !startsWith(github.head_ref, 'v')
runs-on: ubuntu-latest
name: Semantic pull request
name: semantic-pr
steps:
- name: Validate PR title
uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3

View File

@ -638,11 +638,11 @@ We have raised PRs to update modules using EJS syntax, but if you need to do thi
* Moving your string interpolation logic directly into `getContents()`.
* Using a custom function to handle the replacement, such as in https://github.com/nuxt-modules/color-mode/pull/240.
* Continuing to use `lodash`, as a dependency of _your_ project rather than Nuxt:
* Use `es-toolkit/compat` (a drop-in replacement for lodash template), as a dependency of _your_ project rather than Nuxt:
```diff
+ import { readFileSync } from 'node:fs'
+ import { template } from 'lodash-es'
+ import { template } from 'es-toolkit/compat'
// ...
addTemplate({
fileName: 'appinsights-vue.js'

View File

@ -100,6 +100,10 @@ Watch a video from Alexander Lichter about **Building a plain SPA with Nuxt!?**.
If you deploy your app to [static hosting](/docs/getting-started/deployment#static-hosting) with the `nuxi generate` or `nuxi build --prerender` commands, then by default, Nuxt will render every page as a separate static HTML file.
::warning
If you prerender your app with the `nuxi generate` or `nuxi build --prerender` commands, then you will not be able to use any server endpoints as no server will be included in your output folder. If you need server functionality, use `nuxi build` instead.
::
If you are using purely client-side rendering, then this might be unnecessary. You might only need a single `index.html` file, plus `200.html` and `404.html` fallbacks, which you can tell your static web host to serve up for all requests.
In order to achieve this we can change how the routes are prerendered. Just add this to [your hooks](/docs/api/advanced/hooks#nuxt-hooks-build-time) in your `nuxt.config.ts`:

View File

@ -71,7 +71,7 @@ export const useFoo = () => {
### Access plugin injections
You can access [plugin injections](/docs/guide/directory-structure/plugins#automatically-providing-helpers) from composables:
You can access [plugin injections](/docs/guide/directory-structure/plugins#providing-helpers) from composables:
```js [composables/test.ts]
export const useHello = () => {

View File

@ -61,7 +61,7 @@ This feature will likely be removed in a near future.
Emits `app:chunkError` hook when there is an error loading vite/webpack chunks. Default behavior is to perform a reload of the new route on navigation to a new route when a chunk fails to load.
If you set this to `'automatic-immediate'` Nuxt will reload the current route immediatly, instead of waiting for a navigation. This is useful for chunk errors that are not triggered by navigation, e.g., when your Nuxt app fails to load a [lazy component](/docs/guide/directory-structure/components#dynamic-imports). A potential downside of this behavior is undesired reloads, e.g., when your app does not need the chunk that caused the error.
If you set this to `'automatic-immediate'` Nuxt will reload the current route immediately, instead of waiting for a navigation. This is useful for chunk errors that are not triggered by navigation, e.g., when your Nuxt app fails to load a [lazy component](/docs/guide/directory-structure/components#dynamic-imports). A potential downside of this behavior is undesired reloads, e.g., when your app does not need the chunk that caused the error.
You can disable automatic handling by setting this to `false`, or handle chunk errors manually by setting it to `manual`.

View File

@ -162,7 +162,7 @@ export default defineNuxtRouteMiddleware(() => {
## Home Page
Now that we have our app middleware to protect our routes, we can use it on our home page that display our authenticated user informations. If the user is not authenticated, they will be redirected to the login page.
Now that we have our app middleware to protect our routes, we can use it on our home page that display our authenticated user information. If the user is not authenticated, they will be redirected to the login page.
We'll use [`definePageMeta`](/docs/api/utils/define-page-meta) to apply the middleware to the route that we want to protect.

View File

@ -69,7 +69,13 @@ const { data: posts } = await useAsyncData(
- `immediate`: when set to `false`, will prevent the request from firing immediately. (defaults to `true`)
- `default`: a factory function to set the default value of the `data`, before the async function resolves - useful with the `lazy: true` or `immediate: false` option
- `transform`: a function that can be used to alter `handler` function result after resolving
- `getCachedData`: Provide a function which returns cached data. A _null_ or _undefined_ return value will trigger a fetch. By default, this is: `key => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]`, which only caches data when `payloadExtraction` is enabled.
- `getCachedData`: Provide a function which returns cached data. A `null` or `undefined` return value will trigger a fetch. By default, this is:
```ts
const getDefaultCachedData = (key) => nuxtApp.isHydrating
? nuxtApp.payload.data[key]
: nuxtApp.static.data[key]
```
Which only caches data when `experimental.payloadExtraction` of `nuxt.config` is enabled.
- `pick`: only pick specified keys in this array from the `handler` function result
- `watch`: watch reactive sources to auto-refresh
- `deep`: return data in a deep ref object. It is `false` by default to return data in a shallow ref object for performance.
@ -94,7 +100,13 @@ Learn how to use `transform` and `getCachedData` to avoid superfluous calls to a
- `data`: the result of the asynchronous function that is passed in.
- `refresh`/`execute`: a function that can be used to refresh the data returned by the `handler` function.
- `error`: an error object if the data fetching failed.
- `status`: a string indicating the status of the data request (`"idle"`, `"pending"`, `"success"`, `"error"`).
- `status`: a string indicating the status of the data request:
- `idle`: when the request has not started, such as:
- when `execute` has not yet been called and `{ immediate: false }` is set
- when rendering HTML on the server and `{ server: false }` is set
- `pending`: the request is in progress
- `success`: the request has completed successfully
- `error`: the request has failed
- `clear`: a function which will set `data` to `undefined`, set `error` to `null`, set `status` to `'idle'`, and mark any currently pending requests as cancelled.
By default, Nuxt waits until a `refresh` is finished before it can be executed again.

View File

@ -109,7 +109,13 @@ All fetch options can be given a `computed` or `ref` value. These will be watche
- `immediate`: when set to `false`, will prevent the request from firing immediately. (defaults to `true`)
- `default`: a factory function to set the default value of the `data`, before the async function resolves - useful with the `lazy: true` or `immediate: false` option
- `transform`: a function that can be used to alter `handler` function result after resolving
- `getCachedData`: Provide a function which returns cached data. A _null_ or _undefined_ return value will trigger a fetch. By default, this is: `key => nuxt.isHydrating ? nuxt.payload.data[key] : nuxt.static.data[key]`, which only caches data when `payloadExtraction` is enabled.
- `getCachedData`: Provide a function which returns cached data. A `null` or `undefined` return value will trigger a fetch. By default, this is:
```ts
const getDefaultCachedData = (key) => nuxtApp.isHydrating
? nuxtApp.payload.data[key]
: nuxtApp.static.data[key]
```
Which only caches data when `experimental.payloadExtraction` of `nuxt.config` is enabled.
- `pick`: only pick specified keys in this array from the `handler` function result
- `watch`: watch an array of reactive sources and auto-refresh the fetch result when they change. Fetch options and URL are watched by default. You can completely ignore reactive sources by using `watch: false`. Together with `immediate: false`, this allows for a fully-manual `useFetch`. (You can [see an example here](/docs/getting-started/data-fetching#watch) of using `watch`.)
- `deep`: return data in a deep ref object. It is `false` by default to return data in a shallow ref object for performance.
@ -134,7 +140,13 @@ Learn how to use `transform` and `getCachedData` to avoid superfluous calls to a
- `data`: the result of the asynchronous function that is passed in.
- `refresh`/`execute`: a function that can be used to refresh the data returned by the `handler` function.
- `error`: an error object if the data fetching failed.
- `status`: a string indicating the status of the data request (`"idle"`, `"pending"`, `"success"`, `"error"`).
- `status`: a string indicating the status of the data request:
- `idle`: when the request has not started, such as:
- when `execute` has not yet been called and `{ immediate: false }` is set
- when rendering HTML on the server and `{ server: false }` is set
- `pending`: the request is in progress
- `success`: the request has completed successfully
- `error`: the request has failed
- `clear`: a function which will set `data` to `undefined`, set `error` to `null`, set `status` to `'idle'`, and mark any currently pending requests as cancelled.
By default, Nuxt waits until a `refresh` is finished before it can be executed again.
@ -147,7 +159,7 @@ If you have not fetched data on the server (for example, with `server: false`),
```ts [Signature]
function useFetch<DataT, ErrorT>(
url: string | Request | Ref<string | Request> | (() => string) | Request,
url: string | Request | Ref<string | Request> | (() => string | Request),
options?: UseFetchOptions<DataT>
): Promise<AsyncData<DataT, ErrorT>>

View File

@ -19,7 +19,7 @@ npx nuxi add <TEMPLATE> <NAME> [--cwd=<directory>] [--logLevel=<silent|info|verb
<!--add-args-->
Argument | Description
--- | ---
`TEMPLATE` | Specify which template to generate (options: <api\|plugin\|component\|composable\|middleware\|layout\|page>)
`TEMPLATE` | Specify which template to generate (options: <api\|plugin\|component\|composable\|middleware\|layout\|page\|layer>)
`NAME` | Specify name of the generated file
<!--/add-args-->
@ -103,3 +103,10 @@ npx nuxi add middleware auth
# Generates `server/api/hello.ts`
npx nuxi add api hello
```
## `nuxi add layer`
```bash [Terminal]
# Generates `layers/subscribe/nuxt.config.ts`
npx nuxi add layer subscribe
```

View File

@ -4,7 +4,7 @@ description: 'Nuxt command to build your Nuxt module before publishing.'
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/cli/blob/main/src/commands/build-module.ts
to: https://github.com/nuxt/module-builder/blob/main/src/cli.ts
size: xs
---

View File

@ -14,7 +14,7 @@ Nuxi provides a few utilities to work with [Nuxt modules](/modules) seamlessly.
<!--module-add-cmd-->
```bash [Terminal]
npx nuxi module add <MODULENAME> [--cwd=<directory>] [--logLevel=<silent|info|verbose>] [--skipInstall] [--skipConfig]
npx nuxi module add <MODULENAME> [--cwd=<directory>] [--logLevel=<silent|info|verbose>] [--skipInstall] [--skipConfig] [--dev]
```
<!--/module-add-cmd-->
@ -31,6 +31,7 @@ Option | Default | Description
`--logLevel=<silent\|info\|verbose>` | | Specify build-time log level
`--skipInstall` | | Skip npm install
`--skipConfig` | | Skip nuxt.config.ts update
`--dev` | | Install module as dev dependency
<!--/module-add-opts-->
The command lets you install [Nuxt modules](/modules) in your application with no manual work.

View File

@ -63,7 +63,7 @@ Each active version has its own nightly releases which are generated automatical
Release | | Initial release | End Of Life | Docs
----------------------------------------|---------------------------------------------------------------------------------------------------|-----------------|--------------|-------
**4.x** (scheduled) | | 2024 Q3 | | &nbsp;
**4.x** (scheduled) | | approximately 1 month after release of nitro v3 | | &nbsp;
**3.x** (stable) | <a href="https://npmjs.com/package/nuxt"><img alt="Nuxt latest 3.x version" src="https://flat.badgen.net/npm/v/nuxt?label=" class="not-prose"></a> | 2022-11-16 | TBA | [nuxt.com](/docs)
**2.x** (unsupported) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 2.x version" src="https://flat.badgen.net/npm/v/nuxt/2x?label=" class="not-prose"></a> | 2018-09-21 | 2024-06-30 | [v2.nuxt.com](https://v2.nuxt.com/docs)
**1.x** (unsupported) | <a href="https://www.npmjs.com/package/nuxt?activeTab=versions"><img alt="Nuxt 1.x version" src="https://flat.badgen.net/npm/v/nuxt/1x?label=" class="not-prose"></a> | 2018-01-08 | 2019-09-21 | &nbsp;

View File

@ -166,6 +166,55 @@ export default createConfigForNuxt({
'no-console': 'off',
},
},
// manually specify dependencies for nuxt browser app
{
files: ['packages/nuxt/src/app/**', 'packages/nuxt/src/(components,head,imports,pages)/runtime/**'],
name: 'local/client-packages',
rules: {
'@typescript-eslint/no-restricted-imports': ['error', {
'patterns': [
{
allowTypeImports: true,
group: [
// disallow everything
'[@a-z]*',
// except certain dependencies
...[
// vue ecosystem
'@unhead',
'@vue',
'@vue/shared',
'vue/server-renderer',
'vue',
'vue-router',
// other deps
'devalue',
'klona',
// unjs ecosystem
'defu',
'ufo',
'h3',
'destr',
'consola',
'hookable',
'unctx',
'cookie-es',
'perfect-debounce',
'radix3',
'ohash',
'pathe',
'uncrypto',
// internal deps
'nuxt/app',
].map(r => `!${r}`),
'!#[a-z]*/**', // aliases
'!.*/**', // relative imports
],
},
],
}],
},
},
{
files: ['**/fixtures/**', '**/fixture/**'],
name: 'local/disables/fixtures',

View File

@ -26,6 +26,7 @@ exclude = [
"https://awesome-lib.js/",
"https://myawesome-lib.css/",
"https://awesome-lib.css/",
"https://mycdn.org/",
'https://www.npmjs.com/package/(.*)importName(.*)',
# TODO: address 404s (non-prerendered files?) from nuxt.com
"https://nuxt.com/docs/guide/going-further/modules",

View File

@ -39,12 +39,12 @@
"@nuxt/schema": "workspace:*",
"@nuxt/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*",
"@types/node": "22.10.2",
"@unhead/dom": "1.11.14",
"@unhead/schema": "1.11.14",
"@unhead/shared": "1.11.14",
"@unhead/ssr": "1.11.14",
"@unhead/vue": "1.11.14",
"@types/node": "22.10.5",
"@unhead/dom": "1.11.16",
"@unhead/schema": "1.11.16",
"@unhead/shared": "1.11.16",
"@unhead/ssr": "1.11.16",
"@unhead/vue": "1.11.16",
"@vue/compiler-core": "3.5.13",
"@vue/compiler-dom": "3.5.13",
"@vue/shared": "3.5.13",
@ -56,28 +56,29 @@
"nuxt": "workspace:*",
"ohash": "1.1.4",
"postcss": "8.4.49",
"rollup": "4.29.1",
"rollup": "4.30.1",
"send": ">=1.1.0",
"typescript": "5.7.2",
"typescript": "5.7.3",
"ufo": "1.5.4",
"unbuild": "3.0.1",
"unhead": "1.11.14",
"unbuild": "3.3.0",
"unhead": "1.11.16",
"unimport": "3.14.5",
"vite": "6.0.6",
"vite": "6.0.7",
"vue": "3.5.13"
},
"devDependencies": {
"@arethetypeswrong/cli": "0.17.2",
"@nuxt/eslint-config": "0.7.4",
"@arethetypeswrong/cli": "0.17.3",
"@nuxt/cli": "3.20.0",
"@nuxt/eslint-config": "0.7.5",
"@nuxt/kit": "workspace:*",
"@nuxt/rspack-builder": "workspace:*",
"@nuxt/test-utils": "3.15.1",
"@nuxt/test-utils": "3.15.4",
"@nuxt/webpack-builder": "workspace:*",
"@testing-library/vue": "8.1.0",
"@types/node": "22.10.2",
"@types/node": "22.10.5",
"@types/semver": "7.5.8",
"@unhead/schema": "1.11.14",
"@unhead/vue": "1.11.14",
"@unhead/schema": "1.11.16",
"@unhead/vue": "1.11.16",
"@vitest/coverage-v8": "2.1.8",
"@vue/test-utils": "2.4.6",
"autoprefixer": "10.4.20",
@ -87,30 +88,30 @@
"cssnano": "7.0.6",
"destr": "2.0.3",
"devalue": "5.1.1",
"eslint": "9.17.0",
"eslint": "9.18.0",
"eslint-plugin-no-only-tests": "3.3.0",
"eslint-plugin-perfectionist": "4.4.0",
"eslint-typegen": "0.3.2",
"eslint-plugin-perfectionist": "4.6.0",
"eslint-typegen": "1.0.0",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"happy-dom": "16.0.1",
"happy-dom": "16.5.3",
"installed-check": "9.3.0",
"jiti": "2.4.2",
"knip": "5.41.1",
"knip": "5.42.0",
"markdownlint-cli": "0.43.0",
"memfs": "4.15.1",
"memfs": "4.17.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxi": "3.17.2",
"nuxt": "workspace:*",
"nuxt-content-twoslash": "0.1.2",
"ofetch": "1.4.1",
"pathe": "1.1.2",
"pathe": "2.0.1",
"pkg-pr-new": "0.0.39",
"playwright-core": "1.49.1",
"semver": "7.6.3",
"sherif": "1.1.1",
"std-env": "3.8.0",
"tinyexec": "0.3.1",
"tinyexec": "0.3.2",
"tinyglobby": "0.2.10",
"typescript": "5.7.2",
"typescript": "5.7.3",
"ufo": "1.5.4",
"vitest": "2.1.8",
"vitest-environment-nuxt": "1.0.1",
@ -118,9 +119,6 @@
"vue-tsc": "2.2.0",
"webpack": "5.97.1"
},
"packageManager": "pnpm@9.15.1",
"engines": {
"node": "^18.20.4 || ^20.9.0 || ^22.0.0 || >=23.0.0"
},
"packageManager": "pnpm@9.15.3",
"version": ""
}

View File

@ -37,9 +37,9 @@
"ignore": "^7.0.0",
"jiti": "^2.4.2",
"klona": "^2.0.6",
"mlly": "^1.7.3",
"mlly": "^1.7.4",
"ohash": "^1.1.4",
"pathe": "^1.1.2",
"pathe": "^2.0.1",
"pkg-types": "^1.3.0",
"scule": "^1.3.0",
"semver": "^7.6.3",
@ -52,12 +52,12 @@
"@rspack/core": "1.1.8",
"@types/semver": "7.5.8",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"unbuild": "3.0.1",
"vite": "6.0.6",
"unbuild": "3.3.0",
"vite": "6.0.7",
"vitest": "2.1.8",
"webpack": "5.97.1"
},
"engines": {
"node": ">=18.20.5"
"node": ">=18.0.0"
}
}

View File

@ -95,31 +95,28 @@ export async function loadNuxtModuleInstance (nuxtModule: string | NuxtModule, n
paths.add(nuxtModule)
for (const path of paths) {
for (const parentURL of nuxt.options.modulesDir) {
try {
const src = isAbsolute(path)
? pathToFileURL(await resolvePath(path, { cwd: parentURL, fallbackToOriginal: false, extensions: nuxt.options.extensions })).href
: await resolveModule(path, { url: pathToFileURL(parentURL.replace(/\/node_modules\/?$/, '')), extensions: nuxt.options.extensions })
try {
const src = isAbsolute(path)
? pathToFileURL(await resolvePath(path, { fallbackToOriginal: false, extensions: nuxt.options.extensions })).href
: await resolveModule(path, { url: nuxt.options.modulesDir.map(m => pathToFileURL(m.replace(/\/node_modules\/?$/, ''))), extensions: nuxt.options.extensions })
nuxtModule = await jiti.import(src, { default: true }) as NuxtModule
resolvedModulePath = fileURLToPath(new URL(src))
nuxtModule = await jiti.import(src, { default: true }) as NuxtModule
resolvedModulePath = fileURLToPath(new URL(src))
// nuxt-module-builder generates a module.json with metadata including the version
const moduleMetadataPath = new URL('module.json', src)
if (existsSync(moduleMetadataPath)) {
buildTimeModuleMeta = JSON.parse(await fsp.readFile(moduleMetadataPath, 'utf-8'))
}
break
} catch (error: unknown) {
const code = (error as Error & { code?: string }).code
if (code === 'MODULE_NOT_FOUND' || code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || code === 'ERR_MODULE_NOT_FOUND' || code === 'ERR_UNSUPPORTED_DIR_IMPORT' || code === 'ENOTDIR') {
continue
}
logger.error(`Error while importing module \`${nuxtModule}\`: ${error}`)
throw error
// nuxt-module-builder generates a module.json with metadata including the version
const moduleMetadataPath = new URL('module.json', src)
if (existsSync(moduleMetadataPath)) {
buildTimeModuleMeta = JSON.parse(await fsp.readFile(moduleMetadataPath, 'utf-8'))
}
break
} catch (error: unknown) {
const code = (error as Error & { code?: string }).code
if (code === 'MODULE_NOT_FOUND' || code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || code === 'ERR_MODULE_NOT_FOUND' || code === 'ERR_UNSUPPORTED_DIR_IMPORT' || code === 'ENOTDIR') {
continue
}
logger.error(`Error while importing module \`${nuxtModule}\`: ${error}`)
throw error
}
if (typeof nuxtModule !== 'string') { break }
}
}

View File

@ -14,7 +14,7 @@ import { tryUseNuxt, useNuxt } from './context'
import { resolveNuxtModule } from './resolve'
/**
* Renders given template using lodash template during build into the project buildDir
* Renders given template during build into the virtual file system (and optionally to disk in the project `buildDir`)
*/
export function addTemplate<T> (_template: NuxtTemplate<T> | string) {
const nuxt = useNuxt()
@ -44,7 +44,7 @@ export function addServerTemplate (template: NuxtServerTemplate) {
}
/**
* Renders given types using lodash template during build into the project buildDir
* Renders given types during build to disk in the project `buildDir`
* and register them as types.
*/
export function addTypeTemplate<T> (_template: NuxtTypeTemplate<T>) {
@ -291,6 +291,10 @@ export async function _generateTypes (nuxt: Nuxt) {
}))
}
// Ensure `#build` is placed at the end of the paths object.
// https://github.com/nuxt/nuxt/issues/30325
sortTsPaths(tsConfig.compilerOptions.paths)
tsConfig.include = [...new Set(tsConfig.include.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))]
tsConfig.exclude = [...new Set(tsConfig.exclude!.map(p => isAbsolute(p) ? relativeWithDot(nuxt.options.buildDir, p) : p))]
@ -330,6 +334,17 @@ export async function writeTypes (nuxt: Nuxt) {
await writeFile()
}
function sortTsPaths (paths: Record<string, string[]>) {
for (const pathKey in paths) {
if (pathKey.startsWith('#build')) {
const pathValue = paths[pathKey]!
// Delete & Reassign to ensure key is inserted at the end of object.
delete paths[pathKey]
paths[pathKey] = pathValue
}
}
}
function renderAttrs (obj: Record<string, string>) {
const attrs: string[] = []
for (const key in obj) {

View File

@ -59,4 +59,42 @@ describe('tsConfig generation', () => {
]
`)
})
it('should add #build after #components to paths', async () => {
const { tsConfig } = await _generateTypes(mockNuxtWithOptions({
alias: {
'~': '/my-app',
'@': '/my-app',
'some-custom-alias': '/my-app/some-alias',
'#build': './build-dir',
'#build/*': './build-dir/*',
'#imports': './imports',
'#components': './components',
},
}))
expect(tsConfig.compilerOptions?.paths).toMatchObject({
'~': [
'..',
],
'some-custom-alias': [
'../some-alias',
],
'@': [
'..',
],
'#imports': [
'./imports',
],
'#components': [
'./components',
],
'#build': [
'./build-dir',
],
'#build/*': [
'./build-dir/*',
],
})
})
})

View File

@ -1,2 +1,2 @@
#!/usr/bin/env node
import 'nuxi/cli'
import '@nuxt/cli/cli'

View File

@ -22,7 +22,7 @@ export default defineBuildConfig({
},
},
dependencies: [
'nuxi',
'@nuxt/cli',
'vue-router',
'ofetch',
],

View File

@ -64,16 +64,17 @@
"test:attw": "attw --pack"
},
"dependencies": {
"@nuxt/cli": "^3.20.0",
"@nuxt/devalue": "^2.0.2",
"@nuxt/devtools": "^1.7.0",
"@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.6.2",
"@nuxt/telemetry": "^2.6.4",
"@nuxt/vite-builder": "workspace:*",
"@unhead/dom": "^1.11.14",
"@unhead/shared": "^1.11.14",
"@unhead/ssr": "^1.11.14",
"@unhead/vue": "^1.11.14",
"@unhead/dom": "^1.11.16",
"@unhead/shared": "^1.11.16",
"@unhead/ssr": "^1.11.16",
"@unhead/vue": "^1.11.16",
"@vue/shared": "^3.5.13",
"acorn": "8.14.0",
"c12": "^2.0.1",
@ -97,14 +98,13 @@
"klona": "^2.0.6",
"knitwork": "^1.2.0",
"magic-string": "^0.30.17",
"mlly": "^1.7.3",
"mlly": "^1.7.4",
"nanotar": "^0.1.1",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"nuxi": "^3.17.2",
"nypm": "^0.4.1",
"ofetch": "^1.4.1",
"ohash": "^1.1.4",
"pathe": "^1.1.2",
"pathe": "^2.0.1",
"perfect-debounce": "^1.0.0",
"pkg-types": "^1.3.0",
"radix3": "^1.1.2",
@ -118,9 +118,9 @@
"uncrypto": "^0.1.3",
"unctx": "^2.4.1",
"unenv": "^1.10.0",
"unhead": "^1.11.14",
"unhead": "^1.11.16",
"unimport": "^3.14.5",
"unplugin": "^2.1.0",
"unplugin": "^2.1.2",
"unplugin-vue-router": "^0.10.9",
"unstorage": "^1.14.4",
"untyped": "^1.5.2",
@ -135,8 +135,8 @@
"@types/estree": "1.0.6",
"@vitejs/plugin-vue": "5.2.1",
"@vue/compiler-sfc": "3.5.13",
"unbuild": "3.0.1",
"vite": "6.0.6",
"unbuild": "3.3.0",
"vite": "6.0.7",
"vitest": "2.1.8"
},
"peerDependencies": {

View File

@ -1,12 +1,14 @@
import { cloneVNode, createElementBlock, createStaticVNode, defineComponent, getCurrentInstance, h, onMounted, provide, ref } from 'vue'
import { cloneVNode, createElementBlock, defineComponent, getCurrentInstance, h, onMounted, provide, ref } from 'vue'
import type { ComponentInternalInstance, ComponentOptions, InjectionKey } from 'vue'
import { isPromise } from '@vue/shared'
import { useNuxtApp } from '../nuxt'
import { getFragmentHTML } from './utils'
import ServerPlaceholder from './server-placeholder'
import { elToStaticVNode } from './utils'
export const clientOnlySymbol: InjectionKey<boolean> = Symbol.for('nuxt:client-only')
const STATIC_DIV = '<div></div>'
export default defineComponent({
name: 'ClientOnly',
inheritAttrs: false,
@ -54,16 +56,14 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
return (res.children === null || typeof res.children === 'string')
? cloneVNode(res)
: h(res)
} else {
const fragment = getFragmentHTML(ctx._.vnode.el ?? null) ?? ['<div></div>']
return createStaticVNode(fragment.join(''), fragment.length)
}
return elToStaticVNode(ctx._.vnode.el, STATIC_DIV)
}
} else if (clone.template) {
// handle runtime-compiler template
clone.template = `
<template v-if="mounted$">${component.template}</template>
<template v-else><div></div></template>
<template v-else>${STATIC_DIV}</template>
`
}
@ -105,10 +105,8 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
return (res.children === null || typeof res.children === 'string')
? cloneVNode(res)
: h(res)
} else {
const fragment = getFragmentHTML(instance?.vnode.el ?? null) ?? ['<div></div>']
return createStaticVNode(fragment.join(''), fragment.length)
}
return elToStaticVNode(instance?.vnode.el, STATIC_DIV)
}
})
} else {
@ -117,8 +115,7 @@ export function createClientOnly<T extends ComponentOptions> (component: T) {
if (mounted$.value) {
return h(setupState(...args), ctx.attrs)
}
const fragment = getFragmentHTML(instance?.vnode.el ?? null) ?? ['<div></div>']
return createStaticVNode(fragment.join(''), fragment.length)
return elToStaticVNode(instance?.vnode.el, STATIC_DIV)
}
}
return Object.assign(setupState, { mounted$ })

View File

@ -7,7 +7,6 @@ import { type ActiveHeadEntry, type Head, injectHead } from '@unhead/vue'
import { randomUUID } from 'uncrypto'
import { joinURL, withQuery } from 'ufo'
import type { FetchResponse } from 'ofetch'
import { join } from 'pathe'
import type { NuxtIslandResponse } from '../types'
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
@ -37,7 +36,7 @@ async function loadComponents (source = appBaseURL, paths: NuxtIslandResponse['c
for (const [component, item] of Object.entries(paths)) {
if (!(components!.has(component))) {
promises.push((async () => {
const chunkSource = join(source, item.chunk)
const chunkSource = joinURL(source, item.chunk)
const c = await import(/* @vite-ignore */ chunkSource).then(m => m.default || m)
components!.set(component, c)
})())

View File

@ -3,6 +3,8 @@ import { Teleport, defineComponent, h, inject, provide, useId } from 'vue'
import { useNuxtApp } from '../nuxt'
// @ts-expect-error virtual file
import { paths } from '#build/components-chunk'
// @ts-expect-error virtual file
import { buildAssetsURL } from '#internal/nuxt/paths'
type ExtendedComponent = Component & {
__file: string
@ -41,7 +43,7 @@ export default defineComponent({
const name = (slotType.__name || slotType.name) as string
islandContext.components[to] = {
chunk: import.meta.dev ? nuxtApp.$config.app.buildAssetsDir + paths[name] : paths[name],
chunk: import.meta.dev ? buildAssetsURL(paths[name]) : paths[name],
props: slot.props || {},
}

View File

@ -1,5 +1,5 @@
import { h } from 'vue'
import type { Component, RendererNode } from 'vue'
import { createStaticVNode, h } from 'vue'
import type { Component, RendererNode, VNode } from 'vue'
// eslint-disable-next-line
import { isString, isPromise, isArray, isObject } from '@vue/shared'
import type { RouteLocationNormalized } from 'vue-router'
@ -117,9 +117,9 @@ export function vforToArray (source: any): any[] {
* Handles `<!--[-->` Fragment elements
* @param element the element to retrieve the HTML
* @param withoutSlots purge all slots from the HTML string retrieved
* @returns {string[]} An array of string which represent the content of each element. Use `.join('')` to retrieve a component vnode.el HTML
* @returns {string[]|undefined} An array of string which represent the content of each element. Use `.join('')` to retrieve a component vnode.el HTML
*/
export function getFragmentHTML (element: RendererNode | null, withoutSlots = false): string[] | null {
export function getFragmentHTML (element: RendererNode | null, withoutSlots = false): string[] | undefined {
if (element) {
if (element.nodeName === '#comment' && element.nodeValue === '[') {
return getFragmentChildren(element, [], withoutSlots)
@ -131,7 +131,6 @@ export function getFragmentHTML (element: RendererNode | null, withoutSlots = fa
}
return [element.outerHTML]
}
return null
}
function getFragmentChildren (element: RendererNode | null, blocks: string[] = [], withoutSlots = false) {
@ -151,6 +150,20 @@ function getFragmentChildren (element: RendererNode | null, blocks: string[] = [
return blocks
}
/**
* Return a static vnode from an element
* Default to a div if the element is not found and if a fallback is not provided
* @param el renderer node retrieved from the component internal instance
* @param staticNodeFallback fallback string to use if the element is not found. Must be a valid HTML string
*/
export function elToStaticVNode (el: RendererNode | null, staticNodeFallback?: string): VNode {
const fragment: string[] | undefined = el ? getFragmentHTML(el) : staticNodeFallback ? [staticNodeFallback] : undefined
if (fragment) {
return createStaticVNode(fragment.join(''), fragment.length)
}
return h('div')
}
function isStartFragment (element: RendererNode) {
return element.nodeName === '#comment' && element.nodeValue === '['
}

View File

@ -1,9 +1,10 @@
import { existsSync, statSync, writeFileSync } from 'node:fs'
import { isAbsolute, join, normalize, relative, resolve } from 'pathe'
import { addBuildPlugin, addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, defineNuxtModule, findPath, logger, resolveAlias, resolvePath, updateTemplates } from '@nuxt/kit'
import { addBuildPlugin, addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, defineNuxtModule, findPath, resolveAlias, resolvePath, updateTemplates } from '@nuxt/kit'
import type { Component, ComponentsDir, ComponentsOptions } from 'nuxt/schema'
import { distDir } from '../dirs'
import { logger } from '../utils'
import { componentNamesTemplate, componentsIslandsTemplate, componentsMetadataTemplate, componentsPluginTemplate, componentsTypeTemplate } from './templates'
import { scanComponents } from './scan'

View File

@ -5,9 +5,10 @@ import { pascalCase } from 'scule'
import { relative } from 'pathe'
import type { Component, ComponentsOptions } from 'nuxt/schema'
import { logger, tryUseNuxt } from '@nuxt/kit'
import { tryUseNuxt } from '@nuxt/kit'
import { QUOTE_RE, SX_RE, isVue } from '../../core/utils'
import { installNuxtModule } from '../../core/features'
import { logger } from '../../utils'
interface LoaderOptions {
getComponents (): Component[]

View File

@ -2,11 +2,12 @@ import { readdir } from 'node:fs/promises'
import { basename, dirname, extname, join, relative } from 'pathe'
import { globby } from 'globby'
import { kebabCase, pascalCase, splitByCase } from 'scule'
import { isIgnored, logger, useNuxt } from '@nuxt/kit'
import { isIgnored, useNuxt } from '@nuxt/kit'
import { withTrailingSlash } from 'ufo'
import type { Component, ComponentsDir } from 'nuxt/schema'
import { QUOTE_RE, resolveComponentNameSegments } from '../core/utils'
import { logger } from '../utils'
const ISLAND_RE = /\.island(?:\.global)?$/
const GLOBAL_RE = /\.global(?:\.island)?$/

View File

@ -1,11 +1,12 @@
import { promises as fsp, mkdirSync, writeFileSync } from 'node:fs'
import { dirname, join, relative, resolve } from 'pathe'
import { defu } from 'defu'
import { findPath, logger, normalizePlugin, normalizeTemplate, resolveAlias, resolveFiles, resolvePath } from '@nuxt/kit'
import { findPath, normalizePlugin, normalizeTemplate, resolveAlias, resolveFiles, resolvePath } from '@nuxt/kit'
import type { Nuxt, NuxtApp, NuxtPlugin, NuxtTemplate, ResolvedNuxtTemplate } from 'nuxt/schema'
import type { PluginMeta } from 'nuxt/app'
import { logger } from '../utils'
import * as defaultTemplates from './templates'
import { getNameFromPath, hasSuffix, uniqueBy } from './utils'
import { extractMetadata, orderMap } from './plugins/plugin-metadata'

View File

@ -1,11 +1,12 @@
import type { EventType } from '@parcel/watcher'
import type { FSWatcher } from 'chokidar'
import { watch as chokidarWatch } from 'chokidar'
import { importModule, isIgnored, logger, tryResolveModule, useNuxt } from '@nuxt/kit'
import { importModule, isIgnored, tryResolveModule, useNuxt } from '@nuxt/kit'
import { debounce } from 'perfect-debounce'
import { normalize, relative, resolve } from 'pathe'
import type { Nuxt, NuxtBuilder } from 'nuxt/schema'
import { logger } from '../utils'
import { generateApp as _generateApp, createApp } from './app'
import { checkForExternalConfigurationFiles } from './external-config-files'
import { cleanupCaches, getVueHash } from './cache'

View File

@ -1,5 +1,6 @@
import { findPath, logger } from '@nuxt/kit'
import { findPath } from '@nuxt/kit'
import { basename } from 'pathe'
import { logger } from '../utils'
/**
* Check for those external configuration files that are not compatible with Nuxt,

View File

@ -1,7 +1,8 @@
import { addDependency } from 'nypm'
import { resolvePackageJSON } from 'pkg-types'
import { logger, useNuxt } from '@nuxt/kit'
import { useNuxt } from '@nuxt/kit'
import { isCI, provider } from 'std-env'
import { logger } from '../utils'
const isStackblitz = provider === 'stackblitz'
@ -52,7 +53,7 @@ export function installNuxtModule (name: string, options?: EnsurePackageInstalle
installPrompts.add(name)
const nuxt = useNuxt()
return promptToInstall(name, async () => {
const { runCommand } = await import('nuxi')
const { runCommand } = await import('@nuxt/cli')
await runCommand('module', ['add', name, '--cwd', nuxt.options.rootDir])
}, { rootDir: nuxt.options.rootDir, searchPaths: nuxt.options.modulesDir, ...options })
}

View File

@ -254,7 +254,7 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
nitroConfig.prerender ||= {}
nitroConfig.prerender.ignore ||= []
nitroConfig.prerender.ignore.push(manifestPrefix)
nitroConfig.prerender.ignore.push(joinURL(nuxt.options.app.baseURL, manifestPrefix))
nitroConfig.publicAssets!.unshift(
// build manifest

View File

@ -4,7 +4,7 @@ import { join, normalize, relative, resolve } from 'pathe'
import { createDebugger, createHooks } from 'hookable'
import ignore from 'ignore'
import type { LoadNuxtOptions } from '@nuxt/kit'
import { addBuildPlugin, addComponent, addPlugin, addPluginTemplate, addRouteMiddleware, addServerPlugin, addTypeTemplate, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, logger, nuxtCtx, resolveAlias, resolveFiles, resolveIgnorePatterns, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit'
import { addBuildPlugin, addComponent, addPlugin, addPluginTemplate, addRouteMiddleware, addServerPlugin, addTypeTemplate, addVitePlugin, addWebpackPlugin, installModule, loadNuxtConfig, nuxtCtx, resolveAlias, resolveFiles, resolveIgnorePatterns, resolvePath, tryResolveModule, useNitro } from '@nuxt/kit'
import type { Nuxt, NuxtHooks, NuxtModule, NuxtOptions } from 'nuxt/schema'
import type { PackageJson } from 'pkg-types'
import { readPackageJSON } from 'pkg-types'
@ -31,6 +31,7 @@ import importsModule from '../imports/module'
import { distDir, pkgDir } from '../dirs'
import { version } from '../../package.json'
import { scriptsStubsPreset } from '../imports/presets'
import { logger } from '../utils'
import { resolveTypePath } from './utils/types'
import { createImportProtectionPatterns } from './plugins/import-protection'
import { UnctxTransformPlugin } from './plugins/unctx'

View File

@ -36,7 +36,7 @@ export const createImportProtectionPatterns = (nuxt: { options: NuxtOptions }, o
])
}
for (const i of [/(^|node_modules\/)@nuxt\/(kit|test-utils)/, /(^|node_modules\/)nuxi/, /(^|node_modules\/)nitro(?:pack)?(?:-nightly)?(?:$|\/)(?!(?:dist\/)?(?:node_modules|presets|runtime|types))/, /(^|node_modules\/)nuxt\/(config|kit|schema)/]) {
for (const i of [/(^|node_modules\/)@nuxt\/(cli|kit|test-utils)/, /(^|node_modules\/)nuxi/, /(^|node_modules\/)nitro(?:pack)?(?:-nightly)?(?:$|\/)(?!(?:dist\/)?(?:node_modules|presets|runtime|types))/, /(^|node_modules\/)nuxt\/(config|kit|schema)/]) {
patterns.push([i, `This module cannot be imported in ${context}.`])
}

View File

@ -6,10 +6,10 @@ import type { Nuxt } from '@nuxt/schema'
import { createUnplugin } from 'unplugin'
import MagicString from 'magic-string'
import { normalize } from 'pathe'
import { logger } from '@nuxt/kit'
import type { ObjectPlugin, PluginMeta } from 'nuxt/app'
import { parseAndWalk, withLocations } from '../../core/utils/parse'
import { logger } from '../../utils'
const internalOrderMap = {
// -50: pre-all (nuxt)

View File

@ -1,10 +1,11 @@
import { parseNodeModulePath, resolvePath } from 'mlly'
import { isAbsolute, normalize } from 'pathe'
import type { Plugin } from 'vite'
import { logger, resolveAlias } from '@nuxt/kit'
import { resolveAlias } from '@nuxt/kit'
import type { Nuxt } from '@nuxt/schema'
import { pkgDir } from '../../dirs'
import { logger } from '../../utils'
export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin {
const exclude: string[] = ['virtual:', '\0virtual:', '/__skip_vite', '@vitest/']

View File

@ -5,7 +5,7 @@ import { resolve } from 'pathe'
import { watch } from 'chokidar'
import { defu } from 'defu'
import { debounce } from 'perfect-debounce'
import { createResolver, defineNuxtModule, importModule, logger, tryResolveModule } from '@nuxt/kit'
import { createResolver, defineNuxtModule, importModule, tryResolveModule } from '@nuxt/kit'
import {
generateTypes,
resolveSchema as resolveUntypedSchema,
@ -13,6 +13,7 @@ import {
import type { Schema, SchemaDefinition } from 'untyped'
import untypedPlugin from 'untyped/babel-plugin'
import { createJiti } from 'jiti'
import { logger } from '../utils'
export default defineNuxtModule({
meta: {

View File

@ -53,12 +53,30 @@ export function withLocations<T> (node: T): WithLocations<T> {
return node as WithLocations<T>
}
/**
* A function to check whether scope A is a child of scope B.
* @example
* ```ts
* isChildScope('0-1-2', '0-1') // true
* isChildScope('0-1', '0-1') // false
* ```
*
* @param a the child scope
* @param b the parent scope
* @returns true if scope A is a child of scope B, false otherwise (also when they are the same)
*/
function isChildScope (a: string, b: string) {
return a.startsWith(b) && a.length > b.length
}
abstract class BaseNode<T extends Node = Node> {
abstract type: string
readonly scope: string
node: WithLocations<T>
constructor (node: WithLocations<T>) {
constructor (node: WithLocations<T>, scope: string) {
this.node = node
this.scope = scope
}
/**
@ -72,6 +90,14 @@ abstract class BaseNode<T extends Node = Node> {
* For instance, for a function parameter, this would be the end of the function declaration.
*/
abstract get end (): number
/**
* Check if the node is defined under a specific scope.
* @param scope
*/
isUnderScope (scope: string) {
return isChildScope(this.scope, scope)
}
}
class IdentifierNode extends BaseNode<Identifier> {
@ -90,8 +116,8 @@ class FunctionParamNode extends BaseNode {
type = 'FunctionParam' as const
fnNode: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression>
constructor (node: WithLocations<Node>, fnNode: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression>) {
super(node)
constructor (node: WithLocations<Node>, scope: string, fnNode: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression>) {
super(node, scope)
this.fnNode = fnNode
}
@ -120,8 +146,8 @@ class VariableNode extends BaseNode<Identifier> {
type = 'Variable' as const
variableNode: WithLocations<VariableDeclaration>
constructor (node: WithLocations<Identifier>, variableNode: WithLocations<VariableDeclaration>) {
super(node)
constructor (node: WithLocations<Identifier>, scope: string, variableNode: WithLocations<VariableDeclaration>) {
super(node, scope)
this.variableNode = variableNode
}
@ -138,8 +164,8 @@ class ImportNode extends BaseNode<ImportSpecifier | ImportDefaultSpecifier | Imp
type = 'Import' as const
importNode: WithLocations<Node>
constructor (node: WithLocations<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier>, importNode: WithLocations<Node>) {
super(node)
constructor (node: WithLocations<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier>, scope: string, importNode: WithLocations<Node>) {
super(node, scope)
this.importNode = importNode
}
@ -156,8 +182,8 @@ class CatchParamNode extends BaseNode {
type = 'CatchParam' as const
catchNode: WithLocations<CatchClause>
constructor (node: WithLocations<Node>, catchNode: WithLocations<CatchClause>) {
super(node)
constructor (node: WithLocations<Node>, scope: string, catchNode: WithLocations<CatchClause>) {
super(node, scope)
this.catchNode = catchNode
}
@ -264,7 +290,7 @@ export class ScopeTracker {
const identifiers = getPatternIdentifiers(param)
for (const identifier of identifiers) {
this.declareIdentifier(identifier.name, new FunctionParamNode(identifier, fn))
this.declareIdentifier(identifier.name, new FunctionParamNode(identifier, this.scopeIndexKey, fn))
}
}
@ -276,10 +302,10 @@ export class ScopeTracker {
this.declareIdentifier(
identifier.name,
parent.type === 'VariableDeclaration'
? new VariableNode(identifier, parent)
? new VariableNode(identifier, this.scopeIndexKey, parent)
: parent.type === 'CatchClause'
? new CatchParamNode(identifier, parent)
: new FunctionParamNode(identifier, parent),
? new CatchParamNode(identifier, this.scopeIndexKey, parent)
: new FunctionParamNode(identifier, this.scopeIndexKey, parent),
)
}
}
@ -295,7 +321,7 @@ export class ScopeTracker {
case 'FunctionDeclaration':
// declare function name for named functions, skip for `export default`
if (node.id?.name) {
this.declareIdentifier(node.id.name, new FunctionNode(node))
this.declareIdentifier(node.id.name, new FunctionNode(node, this.scopeIndexKey))
}
this.pushScope()
for (const param of node.params) {
@ -309,7 +335,7 @@ export class ScopeTracker {
this.pushScope()
// can be undefined, for example in class method definitions
if (node.id?.name) {
this.declareIdentifier(node.id.name, new FunctionNode(node))
this.declareIdentifier(node.id.name, new FunctionNode(node, this.scopeIndexKey))
}
this.pushScope()
@ -333,7 +359,7 @@ export class ScopeTracker {
case 'ClassDeclaration':
// declare class name for named classes, skip for `export default`
if (node.id?.name) {
this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id)))
this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id), this.scopeIndexKey))
}
break
@ -342,13 +368,13 @@ export class ScopeTracker {
// e.g. const MyClass = class InternalClassName { // InternalClassName is only available within the class body
this.pushScope()
if (node.id?.name) {
this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id)))
this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id), this.scopeIndexKey))
}
break
case 'ImportDeclaration':
for (const specifier of node.specifiers) {
this.declareIdentifier(specifier.local.name, new ImportNode(withLocations(specifier), node))
this.declareIdentifier(specifier.local.name, new ImportNode(withLocations(specifier), this.scopeIndexKey, node))
}
break
@ -429,6 +455,26 @@ export class ScopeTracker {
return null
}
getCurrentScope () {
return this.scopeIndexKey
}
/**
* Check if the current scope is a child of a specific scope.
* @example
* ```ts
* // current scope is 0-1
* isCurrentScopeUnder('0') // true
* isCurrentScopeUnder('0-1') // false
* ```
*
* @param scope the parent scope
* @returns `true` if the current scope is a child of the specified scope, `false` otherwise (also when they are the same)
*/
isCurrentScopeUnder (scope: string) {
return isChildScope(this.scopeIndexKey, scope)
}
/**
* Freezes the scope tracker, preventing further declarations.
* It also resets the scope index stack to its initial state, so that the scope tracker can be reused.

View File

@ -1,5 +1,5 @@
import { existsSync } from 'node:fs'
import { addBuildPlugin, addTemplate, addTypeTemplate, defineNuxtModule, isIgnored, logger, resolveAlias, tryResolveModule, updateTemplates, useNuxt } from '@nuxt/kit'
import { addBuildPlugin, addTemplate, addTypeTemplate, defineNuxtModule, isIgnored, resolveAlias, tryResolveModule, updateTemplates, useNuxt } from '@nuxt/kit'
import { isAbsolute, join, normalize, relative, resolve } from 'pathe'
import type { Import, Unimport } from 'unimport'
import { createUnimport, scanDirExports, toExports } from 'unimport'
@ -7,7 +7,7 @@ import type { ImportPresetWithDeprecation, ImportsOptions, ResolvedNuxtTemplate
import escapeRE from 'escape-string-regexp'
import { lookupNodeModuleSubpath, parseNodeModulePath } from 'mlly'
import { isDirectory } from '../utils'
import { isDirectory, logger } from '../utils'
import { TransformPlugin } from './transform'
import { defaultPresets } from './presets'

View File

@ -5,3 +5,13 @@ declare module '#build/router.options' {
const _default: RouterOptions
export default _default
}
declare module '#build/routes' {
import type { RouterOptions } from '@nuxt/schema'
import type { Router, RouterOptions as VueRouterOptions } from 'vue-router'
export const handleHotUpdate: (_router: Router, _generateRoutes: RouterOptions['routes']) => void
const _default: VueRouterOptions['routes']
export default _default
}

View File

@ -1,6 +1,6 @@
import { existsSync, readdirSync } from 'node:fs'
import { mkdir, readFile } from 'node:fs/promises'
import { addBuildPlugin, addComponent, addPlugin, addTemplate, addTypeTemplate, defineNuxtModule, findPath, logger, resolvePath, updateTemplates, useNitro } from '@nuxt/kit'
import { addBuildPlugin, addComponent, addPlugin, addTemplate, addTypeTemplate, defineNuxtModule, findPath, resolvePath, updateTemplates, useNitro } from '@nuxt/kit'
import { dirname, join, relative, resolve } from 'pathe'
import { genImport, genObjectFromRawEntries, genString } from 'knitwork'
import { joinURL } from 'ufo'
@ -14,6 +14,7 @@ import type { NitroRouteConfig } from 'nitro/types'
import { defu } from 'defu'
import { distDir } from '../dirs'
import { resolveTypePath } from '../core/utils/types'
import { logger } from '../utils'
import { normalizeRoutes, resolvePagesRoutes, resolveRoutePaths } from './utils'
import { extractRouteRules, getMappedPages } from './route-rules'
import { PageMetaPlugin } from './plugins/page-meta'
@ -75,7 +76,7 @@ export default defineNuxtModule({
return true
}
const pages = await resolvePagesRoutes()
const pages = await resolvePagesRoutes(nuxt)
if (pages.length) {
if (nuxt.apps.default) {
nuxt.apps.default.pages = pages
@ -93,7 +94,7 @@ export default defineNuxtModule({
}
nuxt.hook('app:templates', async (app) => {
app.pages = await resolvePagesRoutes()
app.pages = await resolvePagesRoutes(nuxt)
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`.')
@ -178,7 +179,7 @@ export default defineNuxtModule({
logs: nuxt.options.debug,
async beforeWriteFiles (rootPage) {
rootPage.children.forEach(child => child.delete())
const pages = nuxt.apps.default?.pages || await resolvePagesRoutes()
const pages = nuxt.apps.default?.pages || await resolvePagesRoutes(nuxt)
if (nuxt.apps.default) {
nuxt.apps.default.pages = pages
}
@ -630,22 +631,32 @@ const ROUTES_HMR_CODE = /* js */`
if (import.meta.hot) {
import.meta.hot.accept((mod) => {
const router = import.meta.hot.data.router
if (!router) {
const generateRoutes = import.meta.hot.data.generateRoutes
if (!router || !generateRoutes) {
import.meta.hot.invalidate('[nuxt] Cannot replace routes because there is no active router. Reloading.')
return
}
router.clearRoutes()
for (const route of mod.default || mod) {
router.addRoute(route)
const routes = generateRoutes(mod.default || mod)
function addRoutes (routes) {
for (const route of routes) {
router.addRoute(route)
}
router.replace(router.currentRoute.value.fullPath)
}
if (routes && 'then' in routes) {
routes.then(addRoutes)
} else {
addRoutes(routes)
}
router.replace('')
})
}
export function handleHotUpdate(_router) {
export function handleHotUpdate(_router, _generateRoutes) {
if (import.meta.hot) {
import.meta.hot.data ||= {}
import.meta.hot.data.router = _router
import.meta.hot.data.generateRoutes = _generateRoutes
}
}
`

View File

@ -5,7 +5,6 @@ import type { StaticImport } from 'mlly'
import { findExports, findStaticImports, parseStaticImport } from 'mlly'
import MagicString from 'magic-string'
import { isAbsolute } from 'pathe'
import { logger } from '@nuxt/kit'
import {
ScopeTracker,
@ -16,6 +15,7 @@ import {
walk,
withLocations,
} from '../../core/utils/parse'
import { logger } from '../../utils'
interface PageMetaPluginOptions {
dev?: boolean
@ -173,7 +173,9 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
}
}
const scopeTracker = new ScopeTracker()
const scopeTracker = new ScopeTracker({
keepExitedScopes: true,
})
function processDeclaration (scopeTrackerNode: ScopeTrackerNode | null) {
if (scopeTrackerNode?.type === 'Variable') {
@ -184,7 +186,7 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
walk(decl.init, {
enter: (node, parent) => {
if (node.type === 'AwaitExpression') {
logger.error(`[nuxt] Await expressions are not supported in definePageMeta. File: '${id}'`)
logger.error(`Await expressions are not supported in definePageMeta. File: '${id}'`)
throw new Error('await in definePageMeta')
}
if (
@ -210,7 +212,13 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
}
}
parseAndWalk(code, id, {
const ast = parseAndWalk(code, id, {
scopeTracker,
})
scopeTracker.freeze()
walk(ast, {
scopeTracker,
enter: (node) => {
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
@ -220,17 +228,34 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
if (!meta) { return }
const definePageMetaScope = scopeTracker.getCurrentScope()
walk(meta, {
scopeTracker,
enter (node, parent) {
if (
isNotReferencePosition(node, parent)
|| node.type !== 'Identifier' // checking for `node.type` to narrow down the type
) { return }
const declaration = scopeTracker.getDeclaration(node.name)
if (declaration) {
// check if the declaration was made inside `definePageMeta` and if so, do not process it
// (ensures that we don't hoist local variables in inline middleware, for example)
if (
declaration.isUnderScope(definePageMetaScope)
// ensures that we compare the correct declaration to the reference
// (when in the same scope, the declaration must come before the reference, otherwise it must be in a parent scope)
&& (scopeTracker.isCurrentScopeUnder(declaration.scope) || declaration.start < node.start)
) {
return
}
}
if (isStaticIdentifier(node.name)) {
addImport(node.name)
} else {
processDeclaration(scopeTracker.getDeclaration(node.name))
} else if (declaration) {
processDeclaration(declaration)
}
},
})

View File

@ -5,7 +5,6 @@ import defu from 'defu'
import { defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt'
import { prerenderRoutes } from '#app/composables/ssr'
// @ts-expect-error virtual file
import _routes from '#build/routes'
import routerOptions, { hashMode } from '#build/router.options'
// @ts-expect-error virtual file
@ -39,7 +38,7 @@ function shouldPrerender (path: string) {
return !_routeRulesMatcher || defu({} as Record<string, any>, ..._routeRulesMatcher.matchAll(path).reverse()).prerender
}
function processRoutes (routes: RouteRecordRaw[], currentPath = '/', routesToPrerender = new Set<string>()) {
function processRoutes (routes: readonly RouteRecordRaw[], currentPath = '/', routesToPrerender = new Set<string>()) {
for (const route of routes) {
// Add root of optional dynamic paths and catchalls
if (OPTIONAL_PARAM_RE.test(route.path) && !route.children?.length && shouldPrerender(currentPath)) {

View File

@ -17,7 +17,6 @@ import { navigateTo } from '#app/composables/router'
// @ts-expect-error virtual file
import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs'
// @ts-expect-error virtual file
import _routes, { handleHotUpdate } from '#build/routes'
import routerOptions, { hashMode } from '#build/router.options'
// @ts-expect-error virtual file
@ -88,7 +87,7 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
routes,
})
handleHotUpdate(router)
handleHotUpdate(router, routerOptions.routes ? routerOptions.routes : routes => routes)
if (import.meta.client && 'scrollRestoration' in window.history) {
window.history.scrollRestoration = 'auto'

View File

@ -2,7 +2,7 @@ import { runInNewContext } from 'node:vm'
import fs from 'node:fs'
import { extname, normalize, relative, resolve } from 'pathe'
import { encodePath, joinURL, withLeadingSlash } from 'ufo'
import { logger, resolveFiles, resolvePath, useNuxt } from '@nuxt/kit'
import { resolveFiles, resolvePath, useNuxt } from '@nuxt/kit'
import { genArrayFromRaw, genDynamicImport, genImport, genSafeVariableName } from 'knitwork'
import escapeRE from 'escape-string-regexp'
import { filename } from 'pathe/utils'
@ -11,9 +11,9 @@ import { transform } from 'esbuild'
import type { Property } from 'estree'
import type { NuxtPage } from 'nuxt/schema'
import { parseAndWalk } from '../core/utils/parse'
import { parseAndWalk, withLocations } from '../core/utils/parse'
import { getLoader, uniqueBy } from '../core/utils'
import { toArray } from '../utils'
import { logger, toArray } from '../utils'
enum SegmentParserState {
initial,
@ -42,9 +42,7 @@ interface ScannedFile {
absolutePath: string
}
export async function resolvePagesRoutes (): Promise<NuxtPage[]> {
const nuxt = useNuxt()
export async function resolvePagesRoutes (nuxt = useNuxt()): Promise<NuxtPage[]> {
const pagesDirs = nuxt.options._layers.map(
layer => resolve(layer.config.srcDir, (layer.config.rootDir === nuxt.options.rootDir ? nuxt.options : layer.config).dir?.pages || 'pages'),
)
@ -249,25 +247,27 @@ export async function getRouteMeta (contents: string, absolutePath: string, extr
const property = pageMetaArgument.properties.find((property): property is Property => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === key)
if (!property) { continue }
if (property.value.type === 'ObjectExpression') {
const valueString = js.code.slice(property.value.range![0], property.value.range![1])
const propertyValue = withLocations(property.value)
if (propertyValue.type === 'ObjectExpression') {
const valueString = js.code.slice(propertyValue.start, propertyValue.end)
try {
extractedMeta[key] = JSON.parse(runInNewContext(`JSON.stringify(${valueString})`, {}))
} catch {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not JSON-serializable (reading \`${absolutePath}\`).`)
logger.debug(`Skipping extraction of \`${key}\` metadata as it is not JSON-serializable (reading \`${absolutePath}\`).`)
dynamicProperties.add(key)
continue
}
}
if (property.value.type === 'ArrayExpression') {
if (propertyValue.type === 'ArrayExpression') {
const values: string[] = []
for (const element of property.value.elements) {
for (const element of propertyValue.elements) {
if (!element) {
continue
}
if (element.type !== 'Literal' || typeof element.value !== 'string') {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not an array of string literals (reading \`${absolutePath}\`).`)
logger.debug(`Skipping extraction of \`${key}\` metadata as it is not an array of string literals (reading \`${absolutePath}\`).`)
dynamicProperties.add(key)
continue
}
@ -277,12 +277,12 @@ export async function getRouteMeta (contents: string, absolutePath: string, extr
continue
}
if (property.value.type !== 'Literal' || (typeof property.value.value !== 'string' && typeof property.value.value !== 'boolean')) {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`)
if (propertyValue.type !== 'Literal' || (typeof propertyValue.value !== 'string' && typeof propertyValue.value !== 'boolean')) {
logger.debug(`Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`)
dynamicProperties.add(key)
continue
}
extractedMeta[key] = property.value.value
extractedMeta[key] = propertyValue.value
}
for (const property of pageMetaArgument.properties) {

View File

@ -1,4 +1,5 @@
import { promises as fsp } from 'node:fs'
import { useLogger } from '@nuxt/kit'
/** @since 3.9.0 */
export function toArray<T> (value: T | T[]): T[] {
@ -8,3 +9,5 @@ export function toArray<T> (value: T | T[]): T[] {
export async function isDirectory (path: string) {
return (await fsp.lstat(path)).isDirectory()
}
export const logger = useLogger('nuxt')

View File

@ -73,7 +73,6 @@ definePageMeta({ name: 'bar' })
const meta = await getRouteMeta(`
<script setup>
definePageMeta({
name: 'some-custom-name',
path: '/some-custom-path',
validate: () => true,
middleware: [
@ -82,19 +81,32 @@ definePageMeta({ name: 'bar' })
otherValue: {
foo: 'bar',
},
// 'name', 'props' and 'alias' are part of 'defaultExtractionKeys'; they're extracted from the component, so we should test the AST walking for different value types
name: 'some-custom-name',
props: {
foo: 'bar',
},
alias: ['/alias'],
})
</script>
`, filePath)
expect(meta).toMatchInlineSnapshot(`
{
"alias": [
"/alias",
],
"meta": {
"__nuxt_dynamic_meta_key": Set {
"props",
"meta",
},
},
"name": "some-custom-name",
"path": "/some-custom-path",
"props": {
"foo": "bar",
},
}
`)
})
@ -381,6 +393,188 @@ definePageMeta({
`)
})
it('should not import static identifiers when shadowed in the same scope', () => {
const sfc = `
<script setup lang="ts">
import { useState } from '#app/composables/state'
definePageMeta({
middleware: () => {
const useState = (key) => ({ value: { isLoggedIn: false } })
const auth = useState('auth')
if (!auth.value.isLoggedIn) {
return navigateTo('/login')
}
},
})
</script>
`
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
"const __nuxt_page_meta = {
middleware: () => {
const useState = (key) => ({ value: { isLoggedIn: false } })
const auth = useState('auth')
if (!auth.value.isLoggedIn) {
return navigateTo('/login')
}
},
}
export default __nuxt_page_meta"
`)
})
it('should not import static identifiers when shadowed in parent scope', () => {
const sfc = `
<script setup lang="ts">
import { useState } from '#app/composables/state'
definePageMeta({
middleware: () => {
function isLoggedIn() {
const auth = useState('auth')
return auth.value.isLoggedIn
}
const useState = (key) => ({ value: { isLoggedIn: false } })
if (!isLoggedIn()) {
return navigateTo('/login')
}
},
})
</script>
`
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
"const __nuxt_page_meta = {
middleware: () => {
function isLoggedIn() {
const auth = useState('auth')
return auth.value.isLoggedIn
}
const useState = (key) => ({ value: { isLoggedIn: false } })
if (!isLoggedIn()) {
return navigateTo('/login')
}
},
}
export default __nuxt_page_meta"
`)
})
it('should import static identifiers when a shadowed and a non-shadowed one is used', () => {
const sfc = `
<script setup lang="ts">
import { useState } from '#app/composables/state'
definePageMeta({
middleware: [
() => {
const useState = (key) => ({ value: { isLoggedIn: false } })
const auth = useState('auth')
if (!auth.value.isLoggedIn) {
return navigateTo('/login')
}
},
() => {
const auth = useState('auth')
if (!auth.value.isLoggedIn) {
return navigateTo('/login')
}
}
]
})
</script>
`
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
"import { useState } from '#app/composables/state'
const __nuxt_page_meta = {
middleware: [
() => {
const useState = (key) => ({ value: { isLoggedIn: false } })
const auth = useState('auth')
if (!auth.value.isLoggedIn) {
return navigateTo('/login')
}
},
() => {
const auth = useState('auth')
if (!auth.value.isLoggedIn) {
return navigateTo('/login')
}
}
]
}
export default __nuxt_page_meta"
`)
})
it('should import static identifiers when a shadowed and a non-shadowed one is used in the same scope', () => {
const sfc = `
<script setup lang="ts">
import { useState } from '#app/composables/state'
definePageMeta({
middleware: () => {
const auth1 = useState('auth')
const useState = (key) => ({ value: { isLoggedIn: false } })
const auth2 = useState('auth')
if (!auth1.value.isLoggedIn || !auth2.value.isLoggedIn) {
return navigateTo('/login')
}
},
})
</script>
`
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
expect(transformPlugin.transform.call({
parse: (code: string, opts: any = {}) => Parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts,
}),
}, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
"import { useState } from '#app/composables/state'
const __nuxt_page_meta = {
middleware: () => {
const auth1 = useState('auth')
const useState = (key) => ({ value: { isLoggedIn: false } })
const auth2 = useState('auth')
if (!auth1.value.isLoggedIn || !auth2.value.isLoggedIn) {
return navigateTo('/login')
}
},
}
export default __nuxt_page_meta"
`)
})
it('should work with esbuild.keepNames = true', async () => {
const sfc = `
<script setup lang="ts">
@ -485,6 +679,8 @@ function recursive () {
recursive()
}
const route = useRoute()
definePageMeta({
middleware: [
() => {
@ -501,9 +697,24 @@ definePageMeta({
prop = 'prop'
test () {}
}
const someFunction = () => {
const someValue = 'someValue'
console.log(someValue)
}
console.log(hoisted.value, val)
},
],
validate: (route) => {
return route.params.id === 'test'
}
})
// the order of a ref relative to the 'definePageMeta' call should be preserved (in contrast to a simple const)
// this tests whether the extraction handles all variables in the upper scope
const hoisted = ref('hoisted')
</script>
`
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
@ -522,6 +733,7 @@ definePageMeta({
function recursive () {
recursive()
}
const hoisted = ref('hoisted')
const __nuxt_page_meta = {
middleware: [
() => {
@ -538,8 +750,18 @@ definePageMeta({
prop = 'prop'
test () {}
}
const someFunction = () => {
const someValue = 'someValue'
console.log(someValue)
}
console.log(hoisted.value, val)
},
],
validate: (route) => {
return route.params.id === 'test'
}
}
export default __nuxt_page_meta"
`)

View File

@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest'
import { assert, describe, expect, it } from 'vitest'
import { getUndeclaredIdentifiersInFunction, parseAndWalk } from '../src/core/utils/parse'
import { TestScopeTracker } from './fixture/scope-tracker'
@ -667,4 +667,87 @@ describe('parsing', () => {
expect(processedFunctions).toBe(5)
})
it ('should correctly compare identifiers defined in different scopes', () => {
const code = `
// ""
const a = 1
// ""
const func = () => {
// "0-0"
const b = 2
// "0-0"
function foo() {
// "0-0-0-0"
const c = 3
}
}
// ""
const func2 = () => {
// "1-0"
const d = 2
// "1-0"
function bar() {
// "1-0-0-0"
const e = 3
}
}
// ""
const f = 4
`
const scopeTracker = new TestScopeTracker({
keepExitedScopes: true,
})
parseAndWalk(code, filename, {
scopeTracker,
})
const a = scopeTracker.getDeclarationFromScope('a', '')
const func = scopeTracker.getDeclarationFromScope('func', '')
const foo = scopeTracker.getDeclarationFromScope('foo', '0-0')
const b = scopeTracker.getDeclarationFromScope('b', '0-0')
const c = scopeTracker.getDeclarationFromScope('c', '0-0-0-0')
const func2 = scopeTracker.getDeclarationFromScope('func2', '')
const bar = scopeTracker.getDeclarationFromScope('bar', '1-0')
const d = scopeTracker.getDeclarationFromScope('d', '1-0')
const e = scopeTracker.getDeclarationFromScope('e', '1-0-0-0')
const f = scopeTracker.getDeclarationFromScope('f', '')
assert(a && func && foo && b && c && func2 && bar && d && e && f, 'All declarations should be found')
// identifiers in the same scope should be equal
expect(f.isUnderScope(a.scope)).toBe(false)
expect(func.isUnderScope(a.scope)).toBe(false)
expect(d.isUnderScope(bar.scope)).toBe(false)
// identifiers in deeper scopes should be under the scope of the parent scope
expect(b.isUnderScope(a.scope)).toBe(true)
expect(b.isUnderScope(func.scope)).toBe(true)
expect(c.isUnderScope(a.scope)).toBe(true)
expect(c.isUnderScope(b.scope)).toBe(true)
expect(d.isUnderScope(a.scope)).toBe(true)
expect(d.isUnderScope(func2.scope)).toBe(true)
expect(e.isUnderScope(a.scope)).toBe(true)
expect(e.isUnderScope(d.scope)).toBe(true)
// identifiers in parent scope should not be under the scope of the children
expect(a.isUnderScope(b.scope)).toBe(false)
expect(a.isUnderScope(c.scope)).toBe(false)
expect(a.isUnderScope(d.scope)).toBe(false)
expect(a.isUnderScope(e.scope)).toBe(false)
expect(b.isUnderScope(c.scope)).toBe(false)
// identifiers in parallel scopes should not influence each other
expect(d.isUnderScope(b.scope)).toBe(false)
expect(e.isUnderScope(b.scope)).toBe(false)
expect(b.isUnderScope(d.scope)).toBe(false)
expect(c.isUnderScope(e.scope)).toBe(false)
})
})

View File

@ -1,5 +1,6 @@
import { fileURLToPath } from 'node:url'
import { resolve } from 'pathe'
import { consola } from 'consola'
import { expect, it, vi } from 'vitest'
import type { ComponentsDir } from 'nuxt/schema'
@ -10,6 +11,7 @@ const rFixture = (...p: string[]) => resolve(fixtureDir, ...p)
vi.mock('@nuxt/kit', () => ({
isIgnored: () => false,
useLogger: () => consola.create({}).withTag('nuxt'),
}))
const dirs: ComponentsDir[] = [

View File

@ -46,11 +46,10 @@
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"jiti": "^2.4.2",
"knitwork": "^1.2.0",
"lodash-es": "4.17.21",
"magic-string": "^0.30.17",
"memfs": "^4.15.1",
"memfs": "^4.17.0",
"ohash": "^1.1.4",
"pathe": "^1.1.2",
"pathe": "^2.0.1",
"pify": "^6.1.0",
"postcss": "^8.4.49",
"postcss-import": "^16.1.0",
@ -62,7 +61,7 @@
"time-fix-plugin": "^2.0.7",
"ufo": "^1.5.4",
"unenv": "^1.10.0",
"unplugin": "^2.1.0",
"unplugin": "^2.1.2",
"url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.1.1",
"vue-loader": "^17.4.2",
@ -73,18 +72,17 @@
},
"devDependencies": {
"@nuxt/schema": "workspace:*",
"@types/lodash-es": "4.17.12",
"@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.29.1",
"unbuild": "3.0.1",
"rollup": "4.30.1",
"unbuild": "3.3.0",
"vue": "3.5.13"
},
"peerDependencies": {
"vue": "^3.3.4"
},
"engines": {
"node": "^18.20.5 || ^20.9.0 || >=22.0.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
}
}

View File

@ -20,45 +20,60 @@ export default defineBuildConfig({
'src/index',
'src/builder-env',
],
hooks: {
'rollup:options' (ctx, options) {
ctx.options.rollup.dts.respectExternal = false
const isExternal = options.external! as (id: string, importer?: string, isResolved?: boolean) => boolean
options.external = (source, importer, isResolved) => {
if (source === 'untyped' || source === 'knitwork') {
return false
}
return isExternal(source, importer, isResolved)
}
},
},
externals: [
// Type imports
'nuxt/app',
'cssnano',
'autoprefixer',
'ofetch',
'vue-router',
'vue-bundle-renderer',
'@unhead/schema',
'vue',
'unctx',
'hookable',
'nitro',
'nitropack',
'webpack',
'webpack-bundle-analyzer',
'rollup-plugin-visualizer',
'vite',
'@vitejs/plugin-vue',
'@vitejs/plugin-vue-jsx',
'mini-css-extract-plugin',
'css-minimizer-webpack-plugin',
'webpack-dev-middleware',
'h3',
'webpack-hot-middleware',
'postcss',
'@vue/language-core',
'autoprefixer',
'c12',
'compatx',
'consola',
'ignore',
'vue-loader',
'css-minimizer-webpack-plugin',
'cssnano',
'esbuild-loader',
'file-loader',
'h3',
'hookable',
'ignore',
'mini-css-extract-plugin',
'nitro',
'nitropack',
'nuxt/app',
'ofetch',
'pkg-types',
'postcss',
'pug',
'rollup-plugin-visualizer',
'sass-loader',
'c12',
'@vue/language-core',
'scule',
'unctx',
'unimport',
'vite',
'vue',
'vue-bundle-renderer',
'vue-loader',
'vue-router',
'webpack',
'webpack-bundle-analyzer',
'webpack-dev-middleware',
'webpack-hot-middleware',
// Implicit
'@vue/compiler-core',
'@vue/compiler-sfc',
'@vue/shared',
'untyped',
],
})

View File

@ -37,22 +37,29 @@
},
"devDependencies": {
"@types/pug": "2.0.10",
"@unhead/schema": "1.11.14",
"@unhead/schema": "1.11.16",
"@vitejs/plugin-vue": "5.2.1",
"@vitejs/plugin-vue-jsx": "4.1.1",
"@vue/compiler-core": "3.5.13",
"@vue/compiler-sfc": "3.5.13",
"@vue/language-core": "2.2.0",
"c12": "2.0.1",
"compatx": "0.1.8",
"esbuild-loader": "4.2.2",
"file-loader": "6.2.0",
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"hookable": "5.5.3",
"ignore": "7.0.0",
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
"ofetch": "1.4.1",
"pkg-types": "1.3.0",
"sass-loader": "16.0.4",
"unbuild": "3.0.1",
"scule": "1.3.0",
"unbuild": "3.3.0",
"unctx": "2.4.1",
"vite": "6.0.6",
"unimport": "3.14.5",
"untyped": "1.5.2",
"vite": "6.0.7",
"vue": "3.5.13",
"vue-bundle-renderer": "2.1.1",
"vue-loader": "17.4.2",
@ -61,19 +68,10 @@
"webpack-dev-middleware": "7.4.2"
},
"dependencies": {
"c12": "^2.0.1",
"compatx": "^0.1.8",
"consola": "^3.3.3",
"defu": "^6.1.4",
"hookable": "^5.5.3",
"pathe": "^1.1.2",
"pkg-types": "^1.3.0",
"scule": "^1.3.0",
"std-env": "^3.8.0",
"ufo": "^1.5.4",
"uncrypto": "^0.1.3",
"unimport": "^3.14.5",
"untyped": "^1.5.2"
"pathe": "^2.0.1",
"std-env": "^3.8.0"
},
"engines": {
"node": "^14.18.0 || >=16.10.0"

View File

@ -85,20 +85,14 @@ export default defineUntypedSchema({
},
/**
* You can provide your own templates which will be rendered based
* on Nuxt configuration. This feature is specially useful for using with modules.
* It is recommended to use `addTemplate` from `@nuxt/kit` instead of this option.
*
* Templates are rendered using [`lodash/template`](https://lodash.com/docs/4.17.15#template).
* @example
* ```js
* templates: [
* {
* src: '~/modules/support/plugin.js', // `src` can be absolute or relative
* dst: 'support.js', // `dst` is relative to project `.nuxt` dir
* options: {
* // Options are provided to template as `options` key
* live_chat: false
* }
* }
* ]
* ```

View File

@ -1,11 +1,12 @@
import { existsSync } from 'node:fs'
import { readdir } from 'node:fs/promises'
import { randomUUID } from 'node:crypto'
import { defineUntypedSchema } from 'untyped'
import { basename, join, relative, resolve } from 'pathe'
import { isDebug, isDevelopment, isTest } from 'std-env'
import { defu } from 'defu'
import { findWorkspaceDir } from 'pkg-types'
import { randomUUID } from 'uncrypto'
import type { RuntimeConfig } from '../types/config'
export default defineUntypedSchema({

View File

@ -16,7 +16,7 @@ export default defineUntypedSchema({
* }
* })
* ```
* @type {boolean | { key: string; cert: string }}
* @type {boolean | { key: string; cert: string } | { pfx: string; passphrase: string }}
*/
https: false,

View File

@ -73,7 +73,7 @@ export default defineUntypedSchema({
/**
* You can extend generated `.nuxt/tsconfig.json` using this option.
* @type {0 extends 1 & VueCompilerOptions ? typeof import('pkg-types')['TSConfig'] : typeof import('pkg-types')['TSConfig'] & { vueCompilerOptions?: typeof import('@vue/language-core')['VueCompilerOptions']}}
* @type {0 extends 1 & VueCompilerOptions ? typeof import('pkg-types')['TSConfig'] : typeof import('pkg-types')['TSConfig'] & { vueCompilerOptions?: Omit<typeof import('@vue/language-core')['VueCompilerOptions'], 'plugins'> & { plugins?: string[] } }}
*/
tsConfig: {},

View File

@ -1,7 +1,6 @@
import { consola } from 'consola'
import { resolve } from 'pathe'
import { isTest } from 'std-env'
import { withoutLeadingSlash } from 'ufo'
import { defineUntypedSchema } from 'untyped'
export default defineUntypedSchema({
@ -97,7 +96,7 @@ export default defineUntypedSchema({
clearScreen: true,
build: {
assetsDir: {
$resolve: async (val, get) => val ?? withoutLeadingSlash((await get('app') as Record<string, string>).buildAssetsDir),
$resolve: async (val, get) => val ?? (await get('app') as Record<string, string>).buildAssetsDir?.replace(/^\/+/, ''),
},
emptyOutDir: false,
},

View File

@ -23,7 +23,7 @@ export type WatchEvent = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir'
// If the user does not have `@vue/language-core` installed, VueCompilerOptions will be typed as `any`,
// thus making the whole `VueTSConfig` type `any`. We only augment TSConfig if VueCompilerOptions is available.
export type VueTSConfig = 0 extends 1 & VueCompilerOptions ? TSConfig : TSConfig & { vueCompilerOptions?: VueCompilerOptions }
export type VueTSConfig = 0 extends 1 & VueCompilerOptions ? TSConfig : TSConfig & { vueCompilerOptions?: Omit<VueCompilerOptions, 'plugins'> & { plugins?: string[] } }
export type NuxtPage = {
name?: string

View File

@ -17,22 +17,19 @@
"prerender": "pnpm build && jiti ./lib/prerender"
},
"devDependencies": {
"@unocss/reset": "0.65.3",
"@unocss/reset": "65.4.0",
"beasties": "0.2.0",
"html-validate": "9.1.0",
"html-validate": "9.1.3",
"htmlnano": "2.1.1",
"jiti": "2.4.2",
"knitwork": "1.2.0",
"pathe": "1.1.2",
"pathe": "2.0.1",
"prettier": "3.4.2",
"scule": "1.3.0",
"svgo": "3.3.2",
"tinyexec": "0.3.1",
"tinyexec": "0.3.2",
"tinyglobby": "0.2.10",
"unocss": "0.65.3",
"vite": "6.0.6"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
"unocss": "65.4.0",
"vite": "6.0.7"
}
}

View File

@ -26,8 +26,8 @@
},
"devDependencies": {
"@nuxt/schema": "workspace:*",
"rollup": "4.29.1",
"unbuild": "3.0.1",
"rollup": "4.30.1",
"unbuild": "3.3.0",
"vue": "3.5.13"
},
"dependencies": {
@ -47,16 +47,16 @@
"jiti": "^2.4.2",
"knitwork": "^1.2.0",
"magic-string": "^0.30.17",
"mlly": "^1.7.3",
"pathe": "^1.1.2",
"mlly": "^1.7.4",
"pathe": "^2.0.1",
"pkg-types": "^1.3.0",
"postcss": "^8.4.49",
"rollup-plugin-visualizer": "^5.13.1",
"std-env": "^3.8.0",
"ufo": "^1.5.4",
"unenv": "^1.10.0",
"unplugin": "^2.1.0",
"vite": "^6.0.6",
"unplugin": "^2.1.2",
"vite": "^6.0.7",
"vite-node": "^2.1.8",
"vite-plugin-checker": "^0.8.0",
"vue-bundle-renderer": "^2.1.1"
@ -65,6 +65,6 @@
"vue": "^3.3.4"
},
"engines": {
"node": "^18.20.5 || ^20.9.0 || >=22.0.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
}
}

View File

@ -45,12 +45,11 @@
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
"jiti": "^2.4.2",
"knitwork": "^1.2.0",
"lodash-es": "4.17.21",
"magic-string": "^0.30.17",
"memfs": "^4.15.1",
"memfs": "^4.17.0",
"mini-css-extract-plugin": "^2.9.2",
"ohash": "^1.1.4",
"pathe": "^1.1.2",
"pathe": "^2.0.1",
"pify": "^6.1.0",
"postcss": "^8.4.49",
"postcss-import": "^16.1.0",
@ -62,7 +61,7 @@
"time-fix-plugin": "^2.0.7",
"ufo": "^1.5.4",
"unenv": "^1.10.0",
"unplugin": "^2.1.0",
"unplugin": "^2.1.2",
"url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.1.1",
"vue-loader": "^17.4.2",
@ -75,18 +74,17 @@
"devDependencies": {
"@nuxt/schema": "workspace:*",
"@rspack/core": "1.1.8",
"@types/lodash-es": "4.17.12",
"@types/pify": "5.0.4",
"@types/webpack-bundle-analyzer": "4.7.0",
"@types/webpack-hot-middleware": "2.25.9",
"rollup": "4.29.1",
"unbuild": "3.0.1",
"rollup": "4.30.1",
"unbuild": "3.3.0",
"vue": "3.5.13"
},
"peerDependencies": {
"vue": "^3.3.4"
},
"engines": {
"node": "^18.20.5 || ^20.9.0 || >=22.0.0"
"node": "^18.12.0 || ^20.9.0 || >=22.0.0"
}
}

View File

@ -9,6 +9,7 @@ import escapeRegExp from 'escape-string-regexp'
import { joinURL } from 'ufo'
import type { NuxtOptions } from '@nuxt/schema'
import { isTest } from 'std-env'
import { defu } from 'defu'
import type { WarningFilter } from '../plugins/warning-ignore'
import WarningIgnorePlugin from '../plugins/warning-ignore'
import type { WebpackConfigContext } from '../utils/config'
@ -27,7 +28,7 @@ export async function base (ctx: WebpackConfigContext) {
}
function baseConfig (ctx: WebpackConfigContext) {
ctx.config = {
ctx.config = defu({}, {
name: ctx.name,
entry: { app: [resolve(ctx.options.appDir, ctx.options.experimental.asyncEntry ? 'entry.async' : 'entry')] },
module: { rules: [] },
@ -45,7 +46,7 @@ function baseConfig (ctx: WebpackConfigContext) {
output: getOutput(ctx),
stats: statsMap[ctx.nuxt.options.logLevel] ?? statsMap.info,
...ctx.config,
}
} satisfies Configuration)
}
function basePlugins (ctx: WebpackConfigContext) {

View File

@ -1,7 +1,6 @@
import type { Configuration } from 'webpack'
import type { Nuxt, NuxtOptions } from '@nuxt/schema'
import { logger } from '@nuxt/kit'
import { cloneDeep } from 'lodash-es'
import { toArray } from './index'
export interface WebpackConfigContext {
@ -63,9 +62,3 @@ export function fileName (ctx: WebpackConfigContext, key: string) {
return fileName
}
export function getWebpackConfig (ctx: WebpackConfigContext): Configuration {
// Clone to avoid leaking config between Client and Server
// TODO: rewrite webpack implementation to avoid necessity for this
return cloneDeep(ctx.config)
}

View File

@ -15,7 +15,7 @@ import { DynamicBasePlugin } from './plugins/dynamic-base'
import { ChunkErrorPlugin } from './plugins/chunk'
import { createMFS } from './utils/mfs'
import { client, server } from './configs'
import { applyPresets, createWebpackConfigContext, getWebpackConfig } from './utils/config'
import { applyPresets, createWebpackConfigContext } from './utils/config'
import { dynamicRequire } from './nitro/plugins/dynamic-require'
import { builder, webpack } from '#builder'
@ -28,7 +28,7 @@ export const bundle: NuxtBuilder['bundle'] = async (nuxt) => {
const ctx = createWebpackConfigContext(nuxt)
ctx.userConfig = defu(nuxt.options.webpack[`$${preset.name as 'client' | 'server'}`], ctx.userConfig)
await applyPresets(ctx, preset)
return getWebpackConfig(ctx)
return ctx.config
}))
/** Inject rollup plugin for Nitro to handle dynamic imports from webpack chunks */

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,10 @@
"main",
"3.x"
],
"ignoreDeps": [
"node",
"npm"
],
"packageRules": [
{
"groupName": "vitest",

View File

@ -7,11 +7,13 @@ import { join } from 'pathe'
describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM_CI)('minimal nuxt application', () => {
const rootDir = fileURLToPath(new URL('./fixtures/minimal', import.meta.url))
const pagesRootDir = fileURLToPath(new URL('./fixtures/minimal-pages', import.meta.url))
beforeAll(async () => {
await Promise.all([
exec('pnpm', ['nuxi', 'build', rootDir], { nodeOptions: { env: { EXTERNAL_VUE: 'false' } } }),
exec('pnpm', ['nuxi', 'build', rootDir], { nodeOptions: { env: { EXTERNAL_VUE: 'true' } } }),
exec('pnpm', ['nuxi', 'build', pagesRootDir]),
])
}, 120 * 1000)
@ -33,14 +35,33 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
`)
})
it('default client bundle size (pages)', async () => {
const clientStats = await analyzeSizes(['**/*.js'], join(pagesRootDir, '.output/public'))
expect.soft(roundToKilobytes(clientStats!.totalBytes)).toMatchInlineSnapshot(`"175k"`)
const files = clientStats!.files.map(f => f.replace(/\..*\.js/, '.js'))
expect([...files]).toMatchInlineSnapshot(`
[
"_nuxt/a.js",
"_nuxt/client-component.js",
"_nuxt/default.js",
"_nuxt/entry.js",
"_nuxt/index.js",
"_nuxt/server-component.js",
]
`)
})
it('default server bundle size', async () => {
const serverDir = join(rootDir, '.output/server')
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"209k"`)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"210k"`)
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1396k"`)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1398k"`)
const packages = modules.files
.filter(m => m.endsWith('package.json'))
@ -65,6 +86,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
"entities",
"estree-walker",
"hookable",
"packrup",
"source-map-js",
"ufo",
"unhead",
@ -81,7 +103,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"560k"`)
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"94.4k"`)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"96.4k"`)
const packages = modules.files
.filter(m => m.endsWith('package.json'))
@ -95,10 +117,53 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
"db0",
"devalue",
"hookable",
"packrup",
"unhead",
]
`)
})
it('default server bundle size (pages)', async () => {
const serverDir = join(pagesRootDir, '.output/server')
const serverStats = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot(`"303k"`)
const modules = await analyzeSizes(['node_modules/**/*'], serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot(`"1398k"`)
const packages = modules.files
.filter(m => m.endsWith('package.json'))
.map(m => m.replace('/package.json', '').replace('node_modules/', ''))
.sort()
expect(packages).toMatchInlineSnapshot(`
[
"@babel/parser",
"@unhead/dom",
"@unhead/shared",
"@unhead/ssr",
"@vue/compiler-core",
"@vue/compiler-dom",
"@vue/compiler-ssr",
"@vue/reactivity",
"@vue/runtime-core",
"@vue/runtime-dom",
"@vue/server-renderer",
"@vue/shared",
"db0",
"devalue",
"entities",
"estree-walker",
"hookable",
"packrup",
"source-map-js",
"ufo",
"unhead",
"vue",
"vue-bundle-renderer",
]
`)
})
})
async function analyzeSizes (pattern: string[], rootDir: string) {

3
test/fixtures/minimal-pages/app.vue vendored Normal file
View File

@ -0,0 +1,3 @@
<template>
<NuxtLayout><NuxtPage /></NuxtLayout>
</template>

3
test/fixtures/minimal-pages/error.vue vendored Normal file
View File

@ -0,0 +1,3 @@
<template>
<div>Error page</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
<slot />
</template>

View File

@ -0,0 +1 @@
export default defineNuxtRouteMiddleware(() => {})

View File

@ -0,0 +1,27 @@
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
const nuxtEntry = fileURLToPath(new URL('../../../packages/nuxt/dist/index.mjs', import.meta.url))
const isStubbed = readFileSync(nuxtEntry, 'utf-8').includes('const _module = await jiti')
export default defineNuxtConfig({
$production: {
vite: {
$client: {
build: {
rollupOptions: {
output: {
chunkFileNames: '_nuxt/[name].js',
entryFileNames: '_nuxt/[name].js',
},
},
},
},
},
},
sourcemap: false,
compatibilityDate: '2024-06-28',
typescript: {
typeCheck: isStubbed ? false : 'build',
},
})

View File

@ -0,0 +1,13 @@
{
"private": true,
"name": "fixture-minimal-pages",
"scripts": {
"build": "nuxi build"
},
"dependencies": {
"nuxt": "workspace:*"
},
"engines": {
"node": "^18.20.5 || ^20.9.0 || >=22.0.0"
}
}

View File

@ -0,0 +1,3 @@
<template>
<div>Client-only page</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
<div>Server-only page</div>
</template>

View File

@ -0,0 +1,3 @@
<template>
<div>Hello World!</div>
</template>

View File

@ -0,0 +1 @@
export default defineNuxtPlugin(() => {})

View File

@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}