mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-26 07:32:01 +00:00
Merge remote-tracking branch 'origin/main' into patch-21
This commit is contained in:
commit
889a5642fb
@ -1,4 +1,4 @@
|
||||
FROM node:lts
|
||||
FROM node:lts@sha256:48db4f6ea21d134be744207225753a1730c4bc1b4cdf836d44511c36bf0e34d7
|
||||
|
||||
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 && \
|
||||
|
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@ -36,7 +36,7 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additonal
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: If applicable, add any other context about the problem here
|
||||
|
10
.github/codeql/codeql-config.yml
vendored
Normal file
10
.github/codeql/codeql-config.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
paths:
|
||||
- 'packages/*/dist/**'
|
||||
- 'packages/nuxt/bin/**'
|
||||
- 'packages/schema/schema/**'
|
||||
paths-ignore:
|
||||
- 'test/**'
|
||||
- '**/*.test.js'
|
||||
- '**/*.test.ts'
|
||||
- '**/*.test.tsx'
|
||||
- '**/__tests__/**'
|
6
.github/workflows/cache-cleanup.yml
vendored
6
.github/workflows/cache-cleanup.yml
vendored
@ -6,6 +6,8 @@ on:
|
||||
types:
|
||||
- closed
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
@ -20,14 +22,14 @@ jobs:
|
||||
gh extension install actions/gh-actions-cache
|
||||
|
||||
echo "Fetching list of cache keys"
|
||||
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 )
|
||||
cacheKeysForPR=$(gh actions-cache list -R "$REPO" -B "$BRANCH" -L 100 | cut -f 1 )
|
||||
|
||||
## Setting this to not fail the workflow while deleting cache keys.
|
||||
set +e
|
||||
echo "Deleting caches..."
|
||||
for cacheKey in $cacheKeysForPR
|
||||
do
|
||||
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
|
||||
gh actions-cache delete "$cacheKey" -R "$REPO" -B "$BRANCH" --confirm
|
||||
done
|
||||
echo "Done"
|
||||
env:
|
||||
|
8
.github/workflows/changelog.yml
vendored
8
.github/workflows/changelog.yml
vendored
@ -6,9 +6,7 @@ on:
|
||||
- main
|
||||
- 3.x
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
|
||||
@ -19,6 +17,10 @@ jobs:
|
||||
if: github.repository_owner == 'nuxt' && !contains(github.event.head_commit.message, 'v3.') && !contains(github.event.head_commit.message, 'v4.')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
|
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@ -57,7 +57,7 @@ jobs:
|
||||
run: pnpm build
|
||||
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||
with:
|
||||
retention-days: 3
|
||||
name: dist
|
||||
@ -70,8 +70,6 @@ jobs:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
needs:
|
||||
- build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
@ -81,25 +79,26 @@ jobs:
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@883d8588e56d1753a8a58c1c86e88976f0c23449 # v3.26.3
|
||||
uses: github/codeql-action/init@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
|
||||
with:
|
||||
languages: javascript
|
||||
config: |
|
||||
paths:
|
||||
- 'packages/*/src/**'
|
||||
- 'packages/nuxt/bin/**'
|
||||
- 'packages/schema/schema/**'
|
||||
paths-ignore:
|
||||
- 'test/**'
|
||||
- '**/*.spec.ts'
|
||||
- '**/*.test.ts'
|
||||
- '**/__snapshots__/**'
|
||||
languages: javascript-typescript
|
||||
queries: +security-and-quality
|
||||
|
||||
- name: Restore dist cache
|
||||
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
with:
|
||||
name: dist
|
||||
path: packages
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@883d8588e56d1753a8a58c1c86e88976f0c23449 # v3.26.3
|
||||
uses: github/codeql-action/analyze@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
|
||||
with:
|
||||
category: "/language:javascript"
|
||||
category: "/language:javascript-typescript"
|
||||
|
||||
typecheck:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
@ -1,4 +1,4 @@
|
||||
name: Check links with Lychee
|
||||
name: docs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@ -29,7 +29,7 @@ jobs:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
|
||||
- name: Lychee link checker
|
||||
uses: lycheeverse/lychee-action@25a231001d1723960a301b7d4c82884dc7ef857d # for v1.8.0
|
||||
uses: lycheeverse/lychee-action@c38ba4f281730ee0d64e6963f49b708e01567b86 # for v1.8.0
|
||||
with:
|
||||
# arguments with file types to check
|
||||
args: >-
|
4
.github/workflows/docs-deploy.yml
vendored
4
.github/workflows/docs-deploy.yml
vendored
@ -1,11 +1,11 @@
|
||||
name: Deploy docs
|
||||
name: docs
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "docs/**"
|
||||
branches:
|
||||
- main
|
||||
- 3.x
|
||||
|
||||
# Remove default permissions of GITHUB_TOKEN for security
|
||||
# https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
|
||||
|
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Docs
|
||||
name: docs
|
||||
|
||||
on:
|
||||
push:
|
||||
|
28
.github/workflows/label-issue.yml
vendored
Normal file
28
.github/workflows/label-issue.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: chore
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
add-issue-labels:
|
||||
name: Add labels
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'nuxt/nuxt'
|
||||
steps:
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
// add 'pending triage' label if issue is created with no labels
|
||||
if (context.payload.issue.labels.length === 0) {
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.payload.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ['pending triage']
|
||||
})
|
||||
}
|
4
.github/workflows/label-pr.yml
vendored
4
.github/workflows/label-pr.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Label PR
|
||||
name: chore
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@ -8,6 +8,8 @@ on:
|
||||
- main
|
||||
- 3.x
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
add-pr-labels:
|
||||
name: Add PR labels
|
||||
|
36
.github/workflows/lint-sherif.yml
vendored
Normal file
36
.github/workflows/lint-sherif.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "**/package.json"
|
||||
branches:
|
||||
- main
|
||||
- 3.x
|
||||
pull_request:
|
||||
paths:
|
||||
- "**/package.json"
|
||||
branches:
|
||||
- main
|
||||
- 3.x
|
||||
- "!v[0-9]*"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint-monorepo:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Lint monorepo
|
||||
run: pnpm sherif -r multiple-dependency-versions
|
@ -26,6 +26,6 @@ jobs:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
# From https://github.com/rhysd/actionlint/blob/main/docs/usage.md#use-actionlint-on-github-actions
|
||||
- name: Check workflow files
|
||||
run: |
|
||||
bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/590d3bd9dde0c91f7a66071d40eb84716526e5a6/scripts/download-actionlint.bash) 1.6.25
|
||||
./actionlint -color -shellcheck=""
|
||||
uses: docker://rhysd/actionlint:1.7.1@sha256:435ecdb63b1169e80ca3e136290072548c07fc4d76a044cf5541021712f8f344
|
||||
with:
|
||||
args: -color
|
3
.github/workflows/notify-nuxt-bridge.yml
vendored
3
.github/workflows/notify-nuxt-bridge.yml
vendored
@ -4,6 +4,9 @@ on:
|
||||
types: [closed]
|
||||
paths:
|
||||
- "packages/nuxt/src/app/composables/**"
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
if: github.event.pull_request.merged == true
|
||||
|
4
.github/workflows/release-pr.yml
vendored
4
.github/workflows/release-pr.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
COMMENT_AT: ${{ github.event.comment.created_at }}
|
||||
run: |
|
||||
pr="$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/${GH_REPO}/pulls/${PR_NUMBER})"
|
||||
pr="$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/"${GH_REPO}"/pulls/"${PR_NUMBER}")"
|
||||
head_sha="$(echo "$pr" | jq -r .head.sha)"
|
||||
updated_at="$(echo "$pr" | jq -r .updated_at)"
|
||||
|
||||
@ -47,7 +47,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "head_sha=$head_sha" >> $GITHUB_OUTPUT
|
||||
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
with:
|
||||
ref: ${{ steps.pr.outputs.head_sha }}
|
||||
|
17
.github/workflows/reproduire-sur-stackblitz.yml
vendored
17
.github/workflows/reproduire-sur-stackblitz.yml
vendored
@ -1,17 +0,0 @@
|
||||
name: reproduire-sur-stackblitz
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
opened
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
reproduire-sur-stackblitz:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: huang-julien/reproduire-sur-stackblitz@v1.0.0
|
||||
with:
|
||||
reproduction-heading: '### Reproduction'
|
2
.github/workflows/reproduire.yml
vendored
2
.github/workflows/reproduire.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Reproduire
|
||||
name: chore
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
6
.github/workflows/scorecards.yml
vendored
6
.github/workflows/scorecards.yml
vendored
@ -2,7 +2,7 @@
|
||||
# by a third-party and are governed by separate terms of service, privacy
|
||||
# policy, and support documentation.
|
||||
|
||||
name: Scorecard supply-chain security
|
||||
name: ossf
|
||||
on:
|
||||
# For Branch-Protection check. Only the default branch is supported. See
|
||||
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
|
||||
@ -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@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
|
||||
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.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@883d8588e56d1753a8a58c1c86e88976f0c23449 # v3.26.3
|
||||
uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6
|
||||
if: github.repository == 'nuxt/nuxt' && success()
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
8
.github/workflows/semantic-pull-requests.yml
vendored
8
.github/workflows/semantic-pull-requests.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Semantic pull request
|
||||
name: chore
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@ -7,12 +7,12 @@ on:
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
main:
|
||||
semantic-pr:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read # for amannn/action-semantic-pull-request to analyze PRs
|
||||
statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR
|
||||
if: github.repository == 'nuxt/nuxt' && !startsWith(github.head_ref, 'v')
|
||||
|
17
.github/workflows/stackblitz-link.yml
vendored
Normal file
17
.github/workflows/stackblitz-link.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
name: chore
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
opened
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
stackblitz:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||
- uses: huang-julien/reproduire-sur-stackblitz@9ceccbfbb0f2f9a9a8db2d1f0dd909cf5cfe67aa # v1.0.2
|
||||
with:
|
||||
reproduction-heading: '### Reproduction'
|
@ -1,4 +1,4 @@
|
||||
name: Close incomplete issues
|
||||
name: chore
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
@ -427,8 +427,8 @@ Nuxt comes with postcss built-in. You can configure it in your `nuxt.config` fil
|
||||
export default defineNuxtConfig({
|
||||
postcss: {
|
||||
plugins: {
|
||||
'postcss-nested': {}
|
||||
"postcss-custom-media": {}
|
||||
'postcss-nested': {},
|
||||
'postcss-custom-media': {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -55,7 +55,7 @@ This includes:
|
||||
|
||||
You cannot currently define a server-side handler for these errors, but can render an error page, see the [Render an Error Page](#error-page) section.
|
||||
|
||||
## Errors with JS chunks
|
||||
## Errors with JS Chunks
|
||||
|
||||
You might encounter chunk loading errors due to a network connectivity failure or a new deployment (which invalidates your old, hashed JS chunk URLs). Nuxt provides built-in support for handling chunk loading errors by performing a hard reload when a chunk fails to load during route navigation.
|
||||
|
||||
|
@ -7,7 +7,7 @@ While building Nuxt 3, we created a new server engine: [Nitro](https://nitro.unj
|
||||
|
||||
It is shipped with many features:
|
||||
|
||||
- Cross-platform support for Node.js, Browsers, service-workers and more.
|
||||
- Cross-platform support for Node.js, browsers, service workers and more.
|
||||
- Serverless support out-of-the-box.
|
||||
- API routes support.
|
||||
- Automatic code-splitting and async-loaded chunks.
|
||||
|
@ -121,3 +121,37 @@ export default defineAppConfig({
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
## Known Limitations
|
||||
|
||||
As of Nuxt v3.3, the `app.config.ts` file is shared with Nitro, which results in the following limitations:
|
||||
|
||||
1. You cannot import Vue components directly in `app.config.ts`.
|
||||
2. Some auto-imports are not available in the Nitro context.
|
||||
|
||||
These limitations occur because Nitro processes the app config without full Vue component support.
|
||||
|
||||
While it's possible to use Vite plugins in the Nitro config as a workaround, this approach is not recommended:
|
||||
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
nitro: {
|
||||
vite: {
|
||||
plugins: [vue()]
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
::warning
|
||||
Using this workaround may lead to unexpected behavior and bugs. The Vue plugin is one of many that are not available in the Nitro context.
|
||||
::
|
||||
|
||||
Related issues:
|
||||
- [Issue #19858](https://github.com/nuxt/nuxt/issues/19858)
|
||||
- [Issue #19854](https://github.com/nuxt/nuxt/issues/19854)
|
||||
|
||||
::info
|
||||
Nitro v3 will resolve these limitations by removing support for the app config.
|
||||
You can track the progress in [this pull request](https://github.com/unjs/nitro/pull/2521).
|
||||
::
|
||||
|
@ -359,3 +359,34 @@ export default defineNuxtConfig({
|
||||
::read-more{icon="i-simple-icons-mdnwebdocs" color="gray" to="https://developer.mozilla.org/en-US/docs/Web/API/CookieStore" target="_blank"}
|
||||
Read more about the **CookieStore**.
|
||||
::
|
||||
|
||||
## buildCache
|
||||
|
||||
Caches Nuxt build artifacts based on a hash of the configuration and source files.
|
||||
|
||||
```ts twoslash [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
experimental: {
|
||||
buildCache: true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
When enabled, changes to the following files will trigger a full rebuild:
|
||||
|
||||
```bash [Directory structure]
|
||||
.nuxtrc
|
||||
.npmrc
|
||||
package.json
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
tsconfig.json
|
||||
bun.lockb
|
||||
```
|
||||
|
||||
In addition, any changes to files within `srcDir` will trigger a rebuild of the Vue client/server bundle. Nitro will always be rebuilt (though work is in progress to allow Nitro to announce its cacheable artifacts and their hashes).
|
||||
|
||||
::note
|
||||
A maximum of 10 cache tarballs are kept.
|
||||
::
|
||||
|
@ -22,7 +22,7 @@ export default {
|
||||
{
|
||||
name: 'home',
|
||||
path: '/',
|
||||
component: () => import('~/pages/home.vue').then(r => r.default || r)
|
||||
component: () => import('~/pages/home.vue')
|
||||
}
|
||||
],
|
||||
} satisfies RouterConfig
|
||||
|
@ -4,7 +4,7 @@ description: "Nuxt provides a <NuxtPicture> component to handle automatic image
|
||||
links:
|
||||
- label: Source
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxt/image/blob/main/src/runtime/components/nuxt-picture.ts
|
||||
to: https://github.com/nuxt/image/blob/main/src/runtime/components/NuxtPicture.vue
|
||||
size: xs
|
||||
---
|
||||
|
||||
|
@ -4,7 +4,7 @@ description: "Nuxt provides a <NuxtImg> component to handle automatic image opti
|
||||
links:
|
||||
- label: Source
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxt/image/blob/main/src/runtime/components/nuxt-img.ts
|
||||
to: https://github.com/nuxt/image/blob/main/src/runtime/components/NuxtImg.vue
|
||||
size: xs
|
||||
---
|
||||
|
||||
|
@ -114,7 +114,7 @@ function useAsyncData<DataT, DataE>(
|
||||
key: string,
|
||||
handler: (nuxtApp?: NuxtApp) => Promise<DataT>,
|
||||
options?: AsyncDataOptions<DataT>
|
||||
): Promise<AsyncData<DataT, DataE>
|
||||
): Promise<AsyncData<DataT, DataE>>
|
||||
|
||||
type AsyncDataOptions<DataT> = {
|
||||
server?: boolean
|
||||
|
@ -70,6 +70,10 @@ const { data, status, error, refresh, clear } = await useFetch('/api/auth/login'
|
||||
`useFetch` is a reserved function name transformed by the compiler, so you should not name your own function `useFetch`.
|
||||
::
|
||||
|
||||
::warning
|
||||
If you encounter the `data` variable destructured from a `useFetch` returns a string and not a JSON parsed object then make sure your component doesn't include an import statement like `import { useFetch } from '@vueuse/core`.
|
||||
::
|
||||
|
||||
::tip{icon="i-ph-video-duotone" to="https://www.youtube.com/watch?v=njsGVmcWviY" target="_blank"}
|
||||
Watch the video from Alexander Lichter to avoid using `useFetch` the wrong way!
|
||||
::
|
||||
|
@ -38,6 +38,7 @@ Apart from dynamic parameters and query parameters, `useRoute()` also provides t
|
||||
|
||||
- `fullPath`: encoded URL associated with the current route that contains path, query and hash
|
||||
- `hash`: decoded hash section of the URL that starts with a #
|
||||
- `query`: access route query parameters
|
||||
- `matched`: array of normalized matched routes with current route location
|
||||
- `meta`: custom data attached to the record
|
||||
- `name`: unique name for the route record
|
||||
|
@ -34,7 +34,7 @@ Hook | Arguments | Environment | Description
|
||||
|
||||
## Nuxt Hooks (build time)
|
||||
|
||||
Check the [schema source code](https://github.com/nuxt/nuxt/blob/main/packages/schema/src/types/hooks.ts#L53) for all available hooks.
|
||||
Check the [schema source code](https://github.com/nuxt/nuxt/blob/main/packages/schema/src/types/hooks.ts#L83) for all available hooks.
|
||||
|
||||
Hook | Arguments | Description
|
||||
-------------------------|----------------------------|-------------
|
||||
|
@ -25,9 +25,9 @@ To contribute to Nuxt, you need to set up a local environment.
|
||||
```bash [Terminal]
|
||||
corepack enable
|
||||
```
|
||||
4. Run `pnpm install` to Install the dependencies with pnpm:
|
||||
4. Run `pnpm install --frozen-lockfile` to Install the dependencies with pnpm:
|
||||
```bash [Terminal]
|
||||
pnpm install
|
||||
pnpm install --frozen-lockfile
|
||||
```
|
||||
::note
|
||||
If you are adding a dependency, please use `pnpm add`. :br
|
||||
|
@ -40,7 +40,7 @@ In addition to the Nuxt framework, there are modules that are vital for the ecos
|
||||
|
||||
Module | Status | Nuxt Support | Repository | Description
|
||||
------------------------------------|---------------------|--------------|------------|-------------------
|
||||
[Scripts](https://scripts.nuxt.com) | Public Preview | 3.x | [nuxt/scripts](https://github.com/nuxt/scripts) | Easy 3rd party script management.
|
||||
[Scripts](https://scripts.nuxt.com) | Public Beta | 3.x | [nuxt/scripts](https://github.com/nuxt/scripts) | Easy 3rd party script management.
|
||||
A11y | Planned | 3.x | `nuxt/a11y` to be announced | Accessibility hinting and utilities [nuxt/nuxt#23255](https://github.com/nuxt/nuxt/issues/23255)
|
||||
Auth | Planned | 3.x | `nuxt/auth` to be announced | Support is planned after session support.
|
||||
Hints | Planned | 3.x | `nuxt/hints` to be announced | Guidance and suggestions for enhancing development practices.
|
||||
|
@ -73,7 +73,7 @@ navigation.icon: i-ph-notification-duotone
|
||||
target: _blank
|
||||
ui.icon.base: text-black dark:text-white
|
||||
---
|
||||
Nuxt Scripts releases. (Public Preview)
|
||||
Nuxt Scripts releases.
|
||||
::
|
||||
::card
|
||||
---
|
||||
|
@ -1,16 +1,21 @@
|
||||
// For pnpm typecheck:docs to generate correct types
|
||||
|
||||
import { addPluginTemplate } from 'nuxt/kit'
|
||||
import { addPluginTemplate, addRouteMiddleware } from 'nuxt/kit'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
typescript: { shim: process.env.DOCS_TYPECHECK === 'true' },
|
||||
pages: process.env.DOCS_TYPECHECK === 'true',
|
||||
modules: [
|
||||
function () {
|
||||
if (!process.env.DOCS_TYPECHECK) { return }
|
||||
addPluginTemplate({
|
||||
filename: 'plugins/my-plugin.mjs',
|
||||
getContents: () => 'export default defineNuxtPlugin({ name: \'my-plugin\' })',
|
||||
})
|
||||
addRouteMiddleware({
|
||||
name: 'auth',
|
||||
path: '#build/auth.js',
|
||||
})
|
||||
},
|
||||
],
|
||||
})
|
||||
|
62
package.json
62
package.json
@ -39,74 +39,74 @@
|
||||
"@nuxt/ui-templates": "workspace:*",
|
||||
"@nuxt/vite-builder": "workspace:*",
|
||||
"@nuxt/webpack-builder": "workspace:*",
|
||||
"@types/node": "20.16.1",
|
||||
"c12": "2.0.0-beta.1",
|
||||
"@types/node": "20.16.5",
|
||||
"c12": "2.0.0-beta.2",
|
||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||
"jiti": "2.0.0-beta.3",
|
||||
"magic-string": "^0.30.11",
|
||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
|
||||
"nuxt": "workspace:*",
|
||||
"rollup": "^4.21.0",
|
||||
"typescript": "5.5.4",
|
||||
"postcss": "8.4.45",
|
||||
"rollup": "4.21.2",
|
||||
"send": ">=0.19.0",
|
||||
"typescript": "5.6.2",
|
||||
"ufo": "1.5.4",
|
||||
"unbuild": "3.0.0-rc.7",
|
||||
"vite": "5.4.1",
|
||||
"vue": "3.5.0-beta.1",
|
||||
"@vue/compiler-core": "3.5.0-beta.1",
|
||||
"@vue/compiler-dom": "3.5.0-beta.1",
|
||||
"@vue/compiler-sfc": "3.5.0-beta.1",
|
||||
"@vue/compiler-ssr": "3.5.0-beta.1",
|
||||
"@vue/shared": "3.5.0-beta.1"
|
||||
"vite": "5.4.4",
|
||||
"vue": "3.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.9.0",
|
||||
"@nuxt/eslint-config": "0.5.1",
|
||||
"@eslint/js": "9.10.0",
|
||||
"@nuxt/eslint-config": "0.5.7",
|
||||
"@nuxt/kit": "workspace:*",
|
||||
"@nuxt/test-utils": "3.14.1",
|
||||
"@nuxt/test-utils": "3.14.2",
|
||||
"@nuxt/webpack-builder": "workspace:*",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/eslint__js": "8.42.3",
|
||||
"@types/node": "20.16.1",
|
||||
"@types/node": "20.16.5",
|
||||
"@types/semver": "7.5.8",
|
||||
"@unhead/schema": "1.9.16",
|
||||
"@vitejs/plugin-vue": "5.1.2",
|
||||
"@unhead/schema": "1.11.2",
|
||||
"@unhead/vue": "1.11.2",
|
||||
"@vitejs/plugin-vue": "5.1.3",
|
||||
"@vitest/coverage-v8": "2.0.5",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
"autoprefixer": "10.4.20",
|
||||
"case-police": "0.7.0",
|
||||
"changelogen": "0.5.5",
|
||||
"consola": "3.2.3",
|
||||
"cssnano": "7.0.5",
|
||||
"cssnano": "7.0.6",
|
||||
"destr": "2.0.3",
|
||||
"devalue": "5.0.0",
|
||||
"eslint": "9.9.0",
|
||||
"eslint": "9.10.0",
|
||||
"eslint-plugin-no-only-tests": "3.3.0",
|
||||
"eslint-plugin-perfectionist": "3.2.0",
|
||||
"eslint-typegen": "0.3.1",
|
||||
"execa": "9.3.1",
|
||||
"globby": "14.0.2",
|
||||
"eslint-plugin-perfectionist": "3.5.0",
|
||||
"eslint-typegen": "0.3.2",
|
||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||
"happy-dom": "14.12.3",
|
||||
"happy-dom": "15.7.3",
|
||||
"jiti": "2.0.0-beta.3",
|
||||
"markdownlint-cli": "0.41.0",
|
||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
|
||||
"nuxi": "3.12.0",
|
||||
"nuxi": "3.13.1",
|
||||
"nuxt": "workspace:*",
|
||||
"nuxt-content-twoslash": "0.1.1",
|
||||
"ofetch": "1.3.4",
|
||||
"pathe": "1.1.2",
|
||||
"playwright-core": "1.46.1",
|
||||
"playwright-core": "1.47.0",
|
||||
"rimraf": "6.0.1",
|
||||
"semver": "7.6.3",
|
||||
"sherif": "1.0.0",
|
||||
"std-env": "3.7.0",
|
||||
"typescript": "5.5.4",
|
||||
"tinyexec": "0.3.0",
|
||||
"tinyglobby": "0.2.6",
|
||||
"typescript": "5.6.2",
|
||||
"ufo": "1.5.4",
|
||||
"vitest": "2.0.5",
|
||||
"vitest-environment-nuxt": "1.0.1",
|
||||
"vue": "3.4.38",
|
||||
"vue-router": "4.4.3",
|
||||
"vue-tsc": "2.0.29"
|
||||
"vue": "3.5.4",
|
||||
"vue-router": "4.4.4",
|
||||
"vue-tsc": "2.1.6"
|
||||
},
|
||||
"packageManager": "pnpm@9.7.1",
|
||||
"packageManager": "pnpm@9.10.0",
|
||||
"engines": {
|
||||
"node": "^16.10.0 || >=18.0.0"
|
||||
},
|
||||
|
@ -27,7 +27,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/schema": "workspace:*",
|
||||
"c12": "^2.0.0-beta.1",
|
||||
"c12": "^2.0.0-beta.2",
|
||||
"consola": "^3.2.3",
|
||||
"defu": "^6.1.4",
|
||||
"destr": "^2.0.3",
|
||||
@ -39,12 +39,12 @@
|
||||
"klona": "^2.0.6",
|
||||
"mlly": "^1.7.1",
|
||||
"pathe": "^1.1.2",
|
||||
"pkg-types": "^1.1.3",
|
||||
"pkg-types": "^1.2.0",
|
||||
"scule": "^1.3.0",
|
||||
"semver": "^7.6.3",
|
||||
"ufo": "^1.5.4",
|
||||
"unctx": "^2.3.1",
|
||||
"unimport": "^3.10.0",
|
||||
"unimport": "^3.11.1",
|
||||
"untyped": "^1.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -52,9 +52,9 @@
|
||||
"@types/semver": "7.5.8",
|
||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
|
||||
"unbuild": "3.0.0-rc.7",
|
||||
"vite": "5.4.1",
|
||||
"vite": "5.4.4",
|
||||
"vitest": "2.0.5",
|
||||
"webpack": "5.93.0"
|
||||
"webpack": "5.94.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.10.0"
|
||||
|
@ -6,8 +6,6 @@ import { logger } from './logger'
|
||||
|
||||
/**
|
||||
* Register a directory to be scanned for components and imported only when used.
|
||||
*
|
||||
* Requires Nuxt 2.13+
|
||||
*/
|
||||
export async function addComponentsDir (dir: ComponentsDir, opts: { prepend?: boolean } = {}) {
|
||||
const nuxt = useNuxt()
|
||||
@ -23,8 +21,6 @@ export type AddComponentOptions = { name: string, filePath: string } & Partial<E
|
||||
|
||||
/**
|
||||
* Register a component by its name and filePath.
|
||||
*
|
||||
* Requires Nuxt 2.13+
|
||||
*/
|
||||
export async function addComponent (opts: AddComponentOptions) {
|
||||
const nuxt = useNuxt()
|
||||
|
@ -1,20 +1,15 @@
|
||||
import type { Import } from 'unimport'
|
||||
import type { ImportPresetWithDeprecation } from '@nuxt/schema'
|
||||
import { useNuxt } from './context'
|
||||
import { assertNuxtCompatibility } from './compatibility'
|
||||
import { toArray } from './utils'
|
||||
|
||||
export function addImports (imports: Import | Import[]) {
|
||||
assertNuxtCompatibility({ bridge: true })
|
||||
|
||||
useNuxt().hook('imports:extend', (_imports) => {
|
||||
_imports.push(...toArray(imports))
|
||||
})
|
||||
}
|
||||
|
||||
export function addImportsDir (dirs: string | string[], opts: { prepend?: boolean } = {}) {
|
||||
assertNuxtCompatibility({ bridge: true })
|
||||
|
||||
useNuxt().hook('imports:dirs', (_dirs: string[]) => {
|
||||
for (const dir of toArray(dirs)) {
|
||||
_dirs[opts.prepend ? 'unshift' : 'push'](dir)
|
||||
@ -22,8 +17,6 @@ export function addImportsDir (dirs: string | string[], opts: { prepend?: boolea
|
||||
})
|
||||
}
|
||||
export function addImportsSources (presets: ImportPresetWithDeprecation | ImportPresetWithDeprecation[]) {
|
||||
assertNuxtCompatibility({ bridge: true })
|
||||
|
||||
useNuxt().hook('imports:sources', (_presets: ImportPresetWithDeprecation[]) => {
|
||||
for (const preset of toArray(presets)) {
|
||||
_presets.push(preset)
|
||||
|
@ -5,7 +5,7 @@ import { useNuxt } from './context'
|
||||
import { logger } from './logger'
|
||||
import { addTemplate } from './template'
|
||||
|
||||
export function addLayout (this: any, template: NuxtTemplate | string, name?: string) {
|
||||
export function addLayout (template: NuxtTemplate | string, name?: string) {
|
||||
const nuxt = useNuxt()
|
||||
const { filename, src } = addTemplate(template)
|
||||
const layoutName = kebabCase(name || parse(filename).name).replace(/["']/g, '')
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import type { JSValue } from 'untyped'
|
||||
import { applyDefaults } from 'untyped'
|
||||
import type { ConfigLayer, ConfigLayerMeta, LoadConfigOptions } from 'c12'
|
||||
@ -6,6 +7,7 @@ import type { NuxtConfig, NuxtOptions } from '@nuxt/schema'
|
||||
import { NuxtConfigSchema } from '@nuxt/schema'
|
||||
import { globby } from 'globby'
|
||||
import defu from 'defu'
|
||||
import { join } from 'pathe'
|
||||
|
||||
export interface LoadNuxtConfigOptions extends Omit<LoadConfigOptions<NuxtConfig>, 'overrides'> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
@ -47,6 +49,11 @@ export async function loadNuxtConfig (opts: LoadNuxtConfigOptions): Promise<Nuxt
|
||||
nuxtConfig._nuxtConfigFile = configFile
|
||||
nuxtConfig._nuxtConfigFiles = [configFile]
|
||||
|
||||
const defaultBuildDir = join(nuxtConfig.rootDir!, '.nuxt')
|
||||
if (!opts.overrides?._prepare && !nuxtConfig.dev && !nuxtConfig.buildDir && existsSync(defaultBuildDir)) {
|
||||
nuxtConfig.buildDir = join(nuxtConfig.rootDir!, 'node_modules/.cache/nuxt/.nuxt')
|
||||
}
|
||||
|
||||
const _layers: ConfigLayer<NuxtConfig, ConfigLayerMeta>[] = []
|
||||
const processedLayers = new Set<string>()
|
||||
for (const layer of layers) {
|
||||
|
@ -79,9 +79,9 @@ function _defineNuxtModule<
|
||||
}
|
||||
|
||||
// Module format is always a simple function
|
||||
async function normalizedModule (this: any, inlineOptions: Partial<TOptions>, nuxt: Nuxt): Promise<ModuleSetupReturn> {
|
||||
async function normalizedModule (inlineOptions: Partial<TOptions>, nuxt = tryUseNuxt()!): Promise<ModuleSetupReturn> {
|
||||
if (!nuxt) {
|
||||
nuxt = tryUseNuxt() || this.nuxt /* invoked by nuxt 2 */
|
||||
throw new TypeError('Cannot use module outside of Nuxt context')
|
||||
}
|
||||
|
||||
// Avoid duplicate installs
|
||||
|
@ -320,11 +320,6 @@ export async function writeTypes (nuxt: Nuxt) {
|
||||
await fsp.writeFile(declarationPath, GeneratedBy + '\n' + declaration)
|
||||
}
|
||||
|
||||
// This is needed for Nuxt 2 which clears the build directory again before building
|
||||
// https://github.com/nuxt/nuxt/blob/2.x/packages/builder/src/builder.js#L144
|
||||
// @ts-expect-error TODO: Nuxt 2 hook
|
||||
nuxt.hook('builder:prepared', writeFile)
|
||||
|
||||
await writeFile()
|
||||
}
|
||||
|
||||
|
@ -60,17 +60,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/devalue": "^2.0.2",
|
||||
"@nuxt/devtools": "^1.3.9",
|
||||
"@nuxt/devtools": "^1.4.2",
|
||||
"@nuxt/kit": "workspace:*",
|
||||
"@nuxt/schema": "workspace:*",
|
||||
"@nuxt/telemetry": "^2.5.4",
|
||||
"@nuxt/telemetry": "^2.6.0",
|
||||
"@nuxt/vite-builder": "workspace:*",
|
||||
"@unhead/dom": "^1.9.16",
|
||||
"@unhead/ssr": "^1.9.16",
|
||||
"@unhead/vue": "^1.9.16",
|
||||
"@vue/shared": "^3.4.38",
|
||||
"@unhead/dom": "^1.11.2",
|
||||
"@unhead/shared": "^1.11.2",
|
||||
"@unhead/ssr": "^1.11.2",
|
||||
"@unhead/vue": "^1.11.2",
|
||||
"@vue/shared": "^3.5.4",
|
||||
"acorn": "8.12.1",
|
||||
"c12": "^2.0.0-beta.1",
|
||||
"c12": "^2.0.0-beta.2",
|
||||
"chokidar": "^3.6.0",
|
||||
"compatx": "^0.1.8",
|
||||
"consola": "^3.2.3",
|
||||
@ -86,48 +87,52 @@
|
||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||
"hookable": "^5.5.3",
|
||||
"ignore": "^5.3.2",
|
||||
"impound": "^0.1.0",
|
||||
"jiti": "^2.0.0-beta.3",
|
||||
"klona": "^2.0.6",
|
||||
"knitwork": "^1.1.0",
|
||||
"magic-string": "^0.30.11",
|
||||
"mlly": "^1.7.1",
|
||||
"nanotar": "^0.1.1",
|
||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28665895.e727afda",
|
||||
"nuxi": "^3.12.0",
|
||||
"nypm": "^0.3.9",
|
||||
"nuxi": "^3.13.1",
|
||||
"nypm": "^0.3.11",
|
||||
"ofetch": "^1.3.4",
|
||||
"ohash": "^1.1.3",
|
||||
"pathe": "^1.1.2",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"pkg-types": "^1.1.3",
|
||||
"pkg-types": "^1.2.0",
|
||||
"radix3": "^1.1.2",
|
||||
"scule": "^1.3.0",
|
||||
"semver": "^7.6.3",
|
||||
"std-env": "^3.7.0",
|
||||
"strip-literal": "^2.1.0",
|
||||
"tinyglobby": "0.2.6",
|
||||
"ufo": "^1.5.4",
|
||||
"ultrahtml": "^1.5.3",
|
||||
"uncrypto": "^0.1.3",
|
||||
"unctx": "^2.3.1",
|
||||
"unenv": "^1.10.0",
|
||||
"unimport": "^3.10.0",
|
||||
"unplugin": "^1.12.2",
|
||||
"unplugin-vue-router": "^0.10.7",
|
||||
"unstorage": "^1.10.2",
|
||||
"unhead": "^1.11.2",
|
||||
"unimport": "^3.11.1",
|
||||
"unplugin": "^1.14.1",
|
||||
"unplugin-vue-router": "^0.10.8",
|
||||
"unstorage": "^1.12.0",
|
||||
"untyped": "^1.4.2",
|
||||
"vue": "^3.4.38",
|
||||
"vue": "^3.5.4",
|
||||
"vue-bundle-renderer": "^2.1.0",
|
||||
"vue-devtools-stub": "^0.1.0",
|
||||
"vue-router": "^4.4.3"
|
||||
"vue-router": "^4.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/scripts": "0.6.6",
|
||||
"@nuxt/scripts": "0.8.5",
|
||||
"@nuxt/ui-templates": "1.3.4",
|
||||
"@parcel/watcher": "2.4.1",
|
||||
"@types/estree": "1.0.5",
|
||||
"@vitejs/plugin-vue": "5.1.2",
|
||||
"@vue/compiler-sfc": "3.4.38",
|
||||
"@vitejs/plugin-vue": "5.1.3",
|
||||
"@vue/compiler-sfc": "3.5.4",
|
||||
"unbuild": "3.0.0-rc.7",
|
||||
"vite": "5.4.1",
|
||||
"vite": "5.4.4",
|
||||
"vitest": "2.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@ -2,6 +2,7 @@ import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DevOnly',
|
||||
inheritAttrs: false,
|
||||
setup (_, props) {
|
||||
if (import.meta.dev) {
|
||||
return () => props.slots.default?.()
|
||||
|
@ -1,12 +1,14 @@
|
||||
import type { defineAsyncComponent } from 'vue'
|
||||
import { createVNode, defineComponent, onErrorCaptured } from 'vue'
|
||||
|
||||
import { injectHead } from '@unhead/vue'
|
||||
import { createError } from '../composables/error'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { islandComponents } from '#build/components.islands.mjs'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'IslandRenderer',
|
||||
props: {
|
||||
context: {
|
||||
type: Object as () => { name: string, props?: Record<string, any> },
|
||||
@ -14,6 +16,10 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup (props) {
|
||||
// reset head - we don't want to have any head tags from plugin or anywhere else.
|
||||
const head = injectHead()
|
||||
head.headEntries().splice(0, head.headEntries().length)
|
||||
|
||||
const component = islandComponents[props.context.name] as ReturnType<typeof defineAsyncComponent>
|
||||
|
||||
if (!component) {
|
||||
|
@ -2,6 +2,8 @@ import { defineComponent, onErrorCaptured, ref } from 'vue'
|
||||
import { useNuxtApp } from '../nuxt'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NuxtErrorBoundary',
|
||||
inheritAttrs: false,
|
||||
emits: {
|
||||
error (_error: unknown) {
|
||||
return true
|
||||
@ -11,14 +13,16 @@ export default defineComponent({
|
||||
const error = ref<Error | null>(null)
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
onErrorCaptured((err, target, info) => {
|
||||
if (import.meta.client && (!nuxtApp.isHydrating || !nuxtApp.payload.serverRendered)) {
|
||||
emit('error', err)
|
||||
nuxtApp.hooks.callHook('vue:error', err, target, info)
|
||||
error.value = err
|
||||
return false
|
||||
}
|
||||
})
|
||||
if (import.meta.client) {
|
||||
onErrorCaptured((err, target, info) => {
|
||||
if (!nuxtApp.isHydrating || !nuxtApp.payload.serverRendered) {
|
||||
emit('error', err)
|
||||
nuxtApp.hooks.callHook('vue:error', err, target, info)
|
||||
error.value = err
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function clearError () {
|
||||
error.value = null
|
||||
|
@ -40,10 +40,10 @@ const description = _error.message || _error.toString()
|
||||
const stack = import.meta.dev && !is404 ? _error.description || `<pre>${stacktrace}</pre>` : undefined
|
||||
|
||||
// TODO: Investigate side-effect issue with imports
|
||||
const _Error404 = defineAsyncComponent(() => import('./error-404.vue').then(r => r.default || r))
|
||||
const _Error404 = defineAsyncComponent(() => import('./error-404.vue'))
|
||||
const _Error = import.meta.dev
|
||||
? defineAsyncComponent(() => import('./error-dev.vue').then(r => r.default || r))
|
||||
: defineAsyncComponent(() => import('./error-500.vue').then(r => r.default || r))
|
||||
? defineAsyncComponent(() => import('./error-dev.vue'))
|
||||
: defineAsyncComponent(() => import('./error-500.vue'))
|
||||
|
||||
const ErrorTemplate = is404 ? _Error404 : _Error
|
||||
</script>
|
||||
|
@ -3,7 +3,7 @@ import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineCom
|
||||
import { debounce } from 'perfect-debounce'
|
||||
import { hash } from 'ohash'
|
||||
import { appendResponseHeader } from 'h3'
|
||||
import { useHead } from '@unhead/vue'
|
||||
import { injectHead } from '@unhead/vue'
|
||||
import { randomUUID } from 'uncrypto'
|
||||
import { joinURL, withQuery } from 'ufo'
|
||||
import type { FetchResponse } from 'ofetch'
|
||||
@ -45,6 +45,7 @@ async function loadComponents (source = appBaseURL, paths: NuxtIslandResponse['c
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NuxtIsland',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
@ -96,7 +97,7 @@ export default defineComponent({
|
||||
if (result.props) { toRevive.props = result.props }
|
||||
if (result.slots) { toRevive.slots = result.slots }
|
||||
if (result.components) { toRevive.components = result.components }
|
||||
|
||||
if (result.head) { toRevive.head = result.head }
|
||||
nuxtApp.payload.data[key] = {
|
||||
__nuxt_island: {
|
||||
key,
|
||||
@ -158,8 +159,7 @@ export default defineComponent({
|
||||
return html
|
||||
})
|
||||
|
||||
const cHead = ref<Record<'link' | 'style', Array<Record<string, string>>>>({ link: [], style: [] })
|
||||
useHead(cHead)
|
||||
const head = injectHead()
|
||||
|
||||
async function _fetchComponent (force = false) {
|
||||
const key = `${props.name}_${hashId.value}`
|
||||
@ -199,8 +199,7 @@ export default defineComponent({
|
||||
}
|
||||
try {
|
||||
const res: NuxtIslandResponse = await nuxtApp[pKey][uid.value]
|
||||
cHead.value.link = res.head.link
|
||||
cHead.value.style = res.head.style
|
||||
|
||||
ssrHTML.value = res.html.replaceAll(DATA_ISLAND_UID_RE, `data-island-uid="${uid.value}"`)
|
||||
key.value++
|
||||
error.value = null
|
||||
@ -248,6 +247,14 @@ export default defineComponent({
|
||||
await loadComponents(props.source, payloads.components)
|
||||
}
|
||||
|
||||
if (import.meta.server || nuxtApp.isHydrating) {
|
||||
// re-push head into active head instance
|
||||
const responseHead = (nuxtApp.payload.data[`${props.name}_${hashId.value}`] as NuxtIslandResponse)?.head
|
||||
if (responseHead) {
|
||||
head.push(responseHead)
|
||||
}
|
||||
}
|
||||
|
||||
return (_ctx: any, _cache: any) => {
|
||||
if (!html.value || error.value) {
|
||||
return [slots.fallback?.({ error: error.value }) ?? createVNode('div')]
|
||||
|
@ -253,10 +253,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||
},
|
||||
prefetchOn: {
|
||||
type: [String, Object] as PropType<NuxtLinkProps['prefetchOn']>,
|
||||
default: options.prefetchOn || {
|
||||
visibility: true,
|
||||
interaction: false,
|
||||
} satisfies NuxtLinkProps['prefetchOn'],
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
noPrefetch: {
|
||||
@ -384,13 +381,15 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||
replace: props.replace,
|
||||
ariaCurrentValue: props.ariaCurrentValue,
|
||||
custom: props.custom,
|
||||
onPointerenter: shouldPrefetch('interaction') ? prefetch.bind(null, undefined) : undefined,
|
||||
onFocus: shouldPrefetch('interaction') ? prefetch.bind(null, undefined) : undefined,
|
||||
}
|
||||
|
||||
// `custom` API cannot support fallthrough attributes as the slot
|
||||
// may render fragment or text root nodes (#14897, #19375)
|
||||
if (!props.custom) {
|
||||
if (shouldPrefetch('interaction')) {
|
||||
routerLinkProps.onPointerenter = prefetch.bind(null, undefined)
|
||||
routerLinkProps.onFocus = prefetch.bind(null, undefined)
|
||||
}
|
||||
if (prefetched.value) {
|
||||
routerLinkProps.class = props.prefetchedClass || options.prefetchedClass
|
||||
}
|
||||
@ -430,6 +429,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||
return slots.default({
|
||||
href: href.value,
|
||||
navigate,
|
||||
prefetch,
|
||||
get route () {
|
||||
if (!href.value) { return undefined }
|
||||
|
||||
|
@ -18,6 +18,7 @@ export const NuxtTeleportIslandSymbol = Symbol('NuxtTeleportIslandComponent') as
|
||||
/* @__PURE__ */
|
||||
export default defineComponent({
|
||||
name: 'NuxtTeleportIslandComponent',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
|
@ -9,6 +9,7 @@ import { NuxtTeleportIslandSymbol } from './nuxt-teleport-island-component'
|
||||
/* @__PURE__ */
|
||||
export default defineComponent({
|
||||
name: 'NuxtTeleportIslandSlot',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
|
@ -27,6 +27,7 @@ export const RouteProvider = defineComponent({
|
||||
for (const key in props.route) {
|
||||
Object.defineProperty(route, key, {
|
||||
get: () => previousKey === props.renderKey ? props.route[key as keyof RouteLocationNormalizedLoaded] : previousRoute[key as keyof RouteLocationNormalizedLoaded],
|
||||
enumerable: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ import { devRootDir } from '#build/nuxt.config.mjs'
|
||||
|
||||
export default (url: string) => defineComponent({
|
||||
name: 'NuxtTestComponentWrapper',
|
||||
|
||||
inheritAttrs: false,
|
||||
async setup (props, { attrs }) {
|
||||
const query = parseQuery(new URL(url, 'http://localhost').search)
|
||||
const urlProps = query.props ? destr<Record<string, any>>(query.props as string) : {}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { computed, getCurrentInstance, getCurrentScope, onBeforeMount, onScopeDispose, onServerPrefetch, onUnmounted, ref, shallowRef, toRef, unref, watch } from 'vue'
|
||||
import type { Ref, WatchSource } from 'vue'
|
||||
import type { MultiWatchSources, Ref } from 'vue'
|
||||
import type { NuxtApp } from '../nuxt'
|
||||
import { useNuxtApp } from '../nuxt'
|
||||
import { toArray } from '../utils'
|
||||
@ -34,7 +34,7 @@ export type KeysOf<T> = Array<
|
||||
|
||||
export type KeyOfRes<Transform extends _Transform> = KeysOf<ReturnType<Transform>>
|
||||
|
||||
export type MultiWatchSources = (WatchSource<unknown> | object)[]
|
||||
export type { MultiWatchSources }
|
||||
|
||||
export type NoInfer<T> = [T][T extends any ? 0 : never]
|
||||
|
||||
|
@ -16,7 +16,7 @@ async function runLegacyAsyncData (res: Record<string, any> | Promise<Record<str
|
||||
const { fetchKey, _fetchKeyBase } = vm.proxy!.$options
|
||||
const key = (typeof fetchKey === 'function' ? fetchKey(() => '') : fetchKey) ||
|
||||
([_fetchKeyBase, route.fullPath, route.matched.findIndex(r => Object.values(r.components || {}).includes(vm.type))].join(':'))
|
||||
const { data, error } = await useAsyncData(`options:asyncdata:${key}`, () => nuxtApp.runWithContext(() => fn(nuxtApp)))
|
||||
const { data, error } = await useAsyncData(`options:asyncdata:${key}`, () => import.meta.server ? nuxtApp.runWithContext(() => fn(nuxtApp)) : fn(nuxtApp))
|
||||
if (error.value) {
|
||||
throw createError(error.value)
|
||||
}
|
||||
|
@ -103,9 +103,18 @@ export function useCookie<T = string | null | undefined> (name: string, _opts?:
|
||||
}
|
||||
|
||||
if (store) {
|
||||
/* event is of type CookieChangeEvent */
|
||||
const changeHandler = (event: any) => {
|
||||
const cookie = event.changed.find((c: any) => c.name === name)
|
||||
if (cookie) { handleChange({ value: cookie.value }) }
|
||||
const changedCookie = event.changed.find((c: any) => c.name === name)
|
||||
const removedCookie = event.deleted.find((c: any) => c.name === name)
|
||||
|
||||
if (changedCookie) {
|
||||
handleChange({ value: changedCookie.value })
|
||||
}
|
||||
|
||||
if (removedCookie) {
|
||||
handleChange({ value: null })
|
||||
}
|
||||
}
|
||||
store.addEventListener('change', changeHandler)
|
||||
if (hasScope) {
|
||||
|
@ -53,7 +53,7 @@ export const clearError = async (options: { redirect?: string } = {}) => {
|
||||
|
||||
/** @since 3.0.0 */
|
||||
export const isNuxtError = <DataT = unknown>(
|
||||
error?: string | object,
|
||||
error: unknown,
|
||||
): error is NuxtError<DataT> => !!error && typeof error === 'object' && NUXT_ERROR_SIGNATURE in error
|
||||
|
||||
/** @since 3.0.0 */
|
||||
|
@ -11,6 +11,15 @@ type DeepPartial<T> = T extends Function ? T : T extends Record<string, any> ? {
|
||||
// Workaround for vite HMR with virtual modules
|
||||
export const _getAppConfig = () => __appConfig as AppConfig
|
||||
|
||||
function isPojoOrArray (val: unknown): val is object {
|
||||
return (
|
||||
Array.isArray(val) ||
|
||||
(!!val &&
|
||||
typeof val === 'object' &&
|
||||
val.constructor?.name === 'Object')
|
||||
)
|
||||
}
|
||||
|
||||
function deepDelete (obj: any, newObj: any) {
|
||||
for (const key in obj) {
|
||||
const val = newObj[key]
|
||||
@ -18,7 +27,7 @@ function deepDelete (obj: any, newObj: any) {
|
||||
delete (obj as any)[key]
|
||||
}
|
||||
|
||||
if (val !== null && typeof val === 'object') {
|
||||
if (isPojoOrArray(val)) {
|
||||
deepDelete(obj[key], newObj[key])
|
||||
}
|
||||
}
|
||||
@ -27,7 +36,7 @@ function deepDelete (obj: any, newObj: any) {
|
||||
function deepAssign (obj: any, newObj: any) {
|
||||
for (const key in newObj) {
|
||||
const val = newObj[key]
|
||||
if (val !== null && typeof val === 'object') {
|
||||
if (isPojoOrArray(val)) {
|
||||
const defaultVal = Array.isArray(val) ? [] : {}
|
||||
obj[key] = obj[key] || defaultVal
|
||||
deepAssign(obj[key], val)
|
||||
|
@ -2,6 +2,7 @@ import { createApp, createSSRApp, nextTick } from 'vue'
|
||||
import type { App } from 'vue'
|
||||
|
||||
// This file must be imported first as we set globalThis.$fetch via this import
|
||||
// @ts-expect-error virtual file
|
||||
import '#build/fetch.mjs'
|
||||
|
||||
import { applyPlugins, createNuxtApp } from './nuxt'
|
||||
@ -9,6 +10,7 @@ import type { CreateOptions } from './nuxt'
|
||||
|
||||
import { createError } from './composables/error'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import '#build/css'
|
||||
// @ts-expect-error virtual file
|
||||
import plugins from '#build/plugins'
|
||||
|
@ -18,10 +18,9 @@ import type { AsyncDataRequestStatus } from '../app/composables/asyncData'
|
||||
import type { NuxtAppManifestMeta } from '../app/composables/manifest'
|
||||
import type { LoadingIndicator } from '../app/composables/loading-indicator'
|
||||
import type { RouteAnnouncer } from '../app/composables/route-announcer'
|
||||
import type { ViewTransition } from './plugins/view-transitions.client'
|
||||
|
||||
// @ts-expect-error virtual file
|
||||
import { appId, multiApp } from '#build/nuxt.config.mjs'
|
||||
import { appId, chunkErrorEvent, multiApp } from '#build/nuxt.config.mjs'
|
||||
|
||||
import type { NuxtAppLiterals } from '#app'
|
||||
|
||||
@ -267,6 +266,7 @@ export function createNuxtApp (options: CreateOptions) {
|
||||
get vue () { return nuxtApp.vueApp.version },
|
||||
},
|
||||
payload: shallowReactive({
|
||||
...options.ssrContext?.payload || {},
|
||||
data: shallowReactive({}),
|
||||
state: reactive({}),
|
||||
once: new Set<string>(),
|
||||
@ -275,7 +275,7 @@ export function createNuxtApp (options: CreateOptions) {
|
||||
static: {
|
||||
data: {},
|
||||
},
|
||||
runWithContext (fn: any) {
|
||||
runWithContext <T>(fn: () => T) {
|
||||
if (nuxtApp._scope.active && !getCurrentScope()) {
|
||||
return nuxtApp._scope.run(() => callWithNuxt(nuxtApp, fn))
|
||||
}
|
||||
@ -310,6 +310,20 @@ export function createNuxtApp (options: CreateOptions) {
|
||||
nuxtApp.payload.serverRendered = true
|
||||
}
|
||||
|
||||
if (import.meta.server && nuxtApp.ssrContext) {
|
||||
nuxtApp.payload.path = nuxtApp.ssrContext.url
|
||||
|
||||
// Expose nuxt to the renderContext
|
||||
nuxtApp.ssrContext.nuxt = nuxtApp
|
||||
nuxtApp.ssrContext.payload = nuxtApp.payload
|
||||
|
||||
// Expose client runtime-config to the payload
|
||||
nuxtApp.ssrContext.config = {
|
||||
public: nuxtApp.ssrContext.runtimeConfig.public,
|
||||
app: nuxtApp.ssrContext.runtimeConfig.app,
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.client) {
|
||||
const __NUXT__ = multiApp ? window.__NUXT__?.[nuxtApp._id] : window.__NUXT__
|
||||
// TODO: remove/refactor in https://github.com/nuxt/nuxt/issues/25336
|
||||
@ -356,35 +370,14 @@ export function createNuxtApp (options: CreateOptions) {
|
||||
defineGetter(nuxtApp.vueApp, '$nuxt', nuxtApp)
|
||||
defineGetter(nuxtApp.vueApp.config.globalProperties, '$nuxt', nuxtApp)
|
||||
|
||||
if (import.meta.server) {
|
||||
if (nuxtApp.ssrContext) {
|
||||
// Expose nuxt to the renderContext
|
||||
nuxtApp.ssrContext.nuxt = nuxtApp
|
||||
// Expose payload types
|
||||
nuxtApp.ssrContext._payloadReducers = {}
|
||||
// Expose current path
|
||||
nuxtApp.payload.path = nuxtApp.ssrContext.url
|
||||
}
|
||||
// Expose to server renderer to create payload
|
||||
nuxtApp.ssrContext = nuxtApp.ssrContext || {} as any
|
||||
if (nuxtApp.ssrContext!.payload) {
|
||||
Object.assign(nuxtApp.payload, nuxtApp.ssrContext!.payload)
|
||||
}
|
||||
nuxtApp.ssrContext!.payload = nuxtApp.payload
|
||||
|
||||
// Expose client runtime-config to the payload
|
||||
nuxtApp.ssrContext!.config = {
|
||||
public: options.ssrContext!.runtimeConfig.public,
|
||||
app: options.ssrContext!.runtimeConfig.app,
|
||||
}
|
||||
}
|
||||
|
||||
// Listen to chunk load errors
|
||||
if (import.meta.client) {
|
||||
window.addEventListener('nuxt.preloadError', (event) => {
|
||||
nuxtApp.callHook('app:chunkError', { error: (event as Event & { payload: Error }).payload })
|
||||
})
|
||||
|
||||
// Listen to chunk load errors
|
||||
if (chunkErrorEvent) {
|
||||
window.addEventListener(chunkErrorEvent, (event) => {
|
||||
nuxtApp.callHook('app:chunkError', { error: (event as Event & { payload: Error }).payload })
|
||||
event.preventDefault()
|
||||
})
|
||||
}
|
||||
window.useNuxtApp = window.useNuxtApp || useNuxtApp
|
||||
|
||||
// Log errors captured when running plugins, in the `app:created` and `app:beforeMount` hooks
|
||||
|
@ -26,7 +26,7 @@ export default defineNuxtPlugin({
|
||||
})
|
||||
|
||||
router.onError((error, to) => {
|
||||
if (chunkErrors.has(error)) {
|
||||
if (chunkErrors.has(error) || error.message.includes('Failed to fetch dynamically imported module')) {
|
||||
reloadAppAtPath(to)
|
||||
}
|
||||
})
|
||||
|
@ -40,7 +40,7 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const nuxtLogsElement = document.querySelector(`[data-nuxt-logs="${nuxtApp._name}"]`)
|
||||
const nuxtLogsElement = document.querySelector(`[data-nuxt-logs="${nuxtApp._id}"]`)
|
||||
const content = nuxtLogsElement?.textContent
|
||||
const logs = content ? parse(content, { ...devRevivers, ...nuxtApp._payloadRevivers }) as LogObject[] : []
|
||||
await nuxtApp.hooks.callHook('dev:ssr-logs', logs)
|
||||
|
@ -31,11 +31,6 @@ if (componentIslands) {
|
||||
}
|
||||
return {
|
||||
html: '',
|
||||
state: {},
|
||||
head: {
|
||||
link: [],
|
||||
style: [],
|
||||
},
|
||||
...result,
|
||||
}
|
||||
}
|
||||
|
@ -6,25 +6,25 @@ import { defineNuxtPlugin } from '../nuxt'
|
||||
// @ts-expect-error Virtual file.
|
||||
import { componentIslands } from '#build/nuxt.config.mjs'
|
||||
|
||||
const reducers: Record<string, (data: any) => any> = {
|
||||
NuxtError: data => isNuxtError(data) && data.toJSON(),
|
||||
EmptyShallowRef: data => isRef(data) && isShallow(data) && !data.value && (typeof data.value === 'bigint' ? '0n' : (JSON.stringify(data.value) || '_')),
|
||||
EmptyRef: data => isRef(data) && !data.value && (typeof data.value === 'bigint' ? '0n' : (JSON.stringify(data.value) || '_')),
|
||||
ShallowRef: data => isRef(data) && isShallow(data) && data.value,
|
||||
ShallowReactive: data => isReactive(data) && isShallow(data) && toRaw(data),
|
||||
Ref: data => isRef(data) && data.value,
|
||||
Reactive: data => isReactive(data) && toRaw(data),
|
||||
}
|
||||
const reducers: [string, (data: any) => any][] = [
|
||||
['NuxtError', data => isNuxtError(data) && data.toJSON()],
|
||||
['EmptyShallowRef', data => isRef(data) && isShallow(data) && !data.value && (typeof data.value === 'bigint' ? '0n' : (JSON.stringify(data.value) || '_'))],
|
||||
['EmptyRef', data => isRef(data) && !data.value && (typeof data.value === 'bigint' ? '0n' : (JSON.stringify(data.value) || '_'))],
|
||||
['ShallowRef', data => isRef(data) && isShallow(data) && data.value],
|
||||
['ShallowReactive', data => isReactive(data) && isShallow(data) && toRaw(data)],
|
||||
['Ref', data => isRef(data) && data.value],
|
||||
['Reactive', data => isReactive(data) && toRaw(data)],
|
||||
]
|
||||
|
||||
if (componentIslands) {
|
||||
reducers.Island = data => data && data?.__nuxt_island
|
||||
reducers.push(['Island', data => data && data?.__nuxt_island])
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: 'nuxt:revive-payload:server',
|
||||
setup () {
|
||||
for (const reducer in reducers) {
|
||||
definePayloadReducer(reducer, reducers[reducer as keyof typeof reducers])
|
||||
for (const [reducer, fn] of reducers) {
|
||||
definePayloadReducer(reducer, fn)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -56,15 +56,3 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
finishTransition = undefined
|
||||
})
|
||||
})
|
||||
|
||||
export interface ViewTransition {
|
||||
ready: Promise<void>
|
||||
finished: Promise<void>
|
||||
updateCallbackDone: Promise<void>
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Document {
|
||||
startViewTransition?: (callback: () => Promise<void> | void) => ViewTransition
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ interface ComponentChunkOptions {
|
||||
buildDir: string
|
||||
}
|
||||
|
||||
const SCRIPT_RE = /<script[^>]*>/g
|
||||
const SCRIPT_RE = /<script[^>]*>/gi
|
||||
const HAS_SLOT_OR_CLIENT_RE = /<slot[^>]*>|nuxt-client/
|
||||
const TEMPLATE_RE = /<template>([\s\S]*)<\/template>/
|
||||
const NUXTCLIENT_ATTR_RE = /\s:?nuxt-client(="[^"]*")?/g
|
||||
|
@ -1,6 +1,6 @@
|
||||
import fs, { statSync } from 'node:fs'
|
||||
import { existsSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { join, normalize, relative, resolve } from 'pathe'
|
||||
import { addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, logger, resolveAlias, updateTemplates } from '@nuxt/kit'
|
||||
import { addPluginTemplate, addTemplate, addTypeTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, logger, resolveAlias, resolvePath, updateTemplates } from '@nuxt/kit'
|
||||
import type { Component, ComponentsDir, ComponentsOptions } from 'nuxt/schema'
|
||||
|
||||
import { distDir } from '../dirs'
|
||||
@ -169,6 +169,10 @@ export default defineNuxtModule<ComponentsOptions>({
|
||||
await nuxt.callHook('components:extend', newComponents)
|
||||
// add server placeholder for .client components server side. issue: #7085
|
||||
for (const component of newComponents) {
|
||||
if (!(component as any /* untyped internal property */)._scanned && !(component.filePath in nuxt.vfs) && !existsSync(component.filePath)) {
|
||||
// attempt to resolve component path
|
||||
component.filePath = await resolvePath(component.filePath, { fallbackToOriginal: true })
|
||||
}
|
||||
if (component.mode === 'client' && !newComponents.some(c => c.pascalName === component.pascalName && c.mode === 'server')) {
|
||||
newComponents.push({
|
||||
...component,
|
||||
@ -236,17 +240,17 @@ export default defineNuxtModule<ComponentsOptions>({
|
||||
const selectiveClient = typeof nuxt.options.experimental.componentIslands === 'object' && nuxt.options.experimental.componentIslands.selectiveClient
|
||||
|
||||
if (isClient && selectiveClient) {
|
||||
fs.writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
|
||||
writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
|
||||
if (!nuxt.options.dev) {
|
||||
config.plugins.push(componentsChunkPlugin.vite({
|
||||
getComponents,
|
||||
buildDir: nuxt.options.buildDir,
|
||||
}))
|
||||
} else {
|
||||
fs.writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), `export const paths = ${JSON.stringify(
|
||||
writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), `export const paths = ${JSON.stringify(
|
||||
getComponents().filter(c => c.mode === 'client' || c.mode === 'all').reduce((acc, c) => {
|
||||
if (c.filePath.endsWith('.vue') || c.filePath.endsWith('.js') || c.filePath.endsWith('.ts')) { return Object.assign(acc, { [c.pascalName]: `/@fs/${c.filePath}` }) }
|
||||
const filePath = fs.existsSync(`${c.filePath}.vue`) ? `${c.filePath}.vue` : fs.existsSync(`${c.filePath}.js`) ? `${c.filePath}.js` : `${c.filePath}.ts`
|
||||
const filePath = existsSync(`${c.filePath}.vue`) ? `${c.filePath}.vue` : existsSync(`${c.filePath}.js`) ? `${c.filePath}.js` : `${c.filePath}.ts`
|
||||
return Object.assign(acc, { [c.pascalName]: `/@fs/${filePath}` })
|
||||
}, {} as Record<string, string>),
|
||||
)}`)
|
||||
@ -307,7 +311,7 @@ export default defineNuxtModule<ComponentsOptions>({
|
||||
getComponents,
|
||||
}))
|
||||
} else {
|
||||
fs.writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
|
||||
writeFileSync(join(nuxt.options.buildDir, 'components-chunk.mjs'), 'export const paths = {}')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -126,6 +126,8 @@ export async function scanComponents (dirs: ComponentsDir[], srcDir: string): Pr
|
||||
export: 'default',
|
||||
// by default, give priority to scanned components
|
||||
priority: dir.priority ?? 1,
|
||||
// @ts-expect-error untyped property
|
||||
_scanned: true,
|
||||
}
|
||||
|
||||
if (typeof dir.extendComponent === 'function') {
|
||||
|
@ -22,6 +22,7 @@ export function createTransformPlugin (nuxt: Nuxt, getComponents: getComponentsT
|
||||
},
|
||||
],
|
||||
virtualImports: ['#components'],
|
||||
injectAtEnd: true,
|
||||
})
|
||||
|
||||
function getComponentsImports (): Import[] {
|
||||
@ -50,6 +51,7 @@ export function createTransformPlugin (nuxt: Nuxt, getComponents: getComponentsT
|
||||
|
||||
return createUnplugin(() => ({
|
||||
name: 'nuxt:components:imports',
|
||||
enforce: 'post',
|
||||
transformInclude (id) {
|
||||
id = normalize(id)
|
||||
return id.startsWith('virtual:') || id.startsWith('\0virtual:') || id.startsWith(nuxt.options.buildDir) || !isIgnored(id)
|
||||
|
@ -8,6 +8,7 @@ import type { Nuxt, NuxtBuilder } from 'nuxt/schema'
|
||||
|
||||
import { generateApp as _generateApp, createApp } from './app'
|
||||
import { checkForExternalConfigurationFiles } from './external-config-files'
|
||||
import { cleanupCaches, getVueHash } from './cache'
|
||||
|
||||
export async function build (nuxt: Nuxt) {
|
||||
const app = createApp(nuxt)
|
||||
@ -40,16 +41,32 @@ export async function build (nuxt: Nuxt) {
|
||||
})
|
||||
}
|
||||
|
||||
await nuxt.callHook('build:before')
|
||||
if (!nuxt.options._prepare) {
|
||||
await Promise.all([checkForExternalConfigurationFiles(), bundle(nuxt)])
|
||||
await nuxt.callHook('build:done')
|
||||
|
||||
if (!nuxt.options.dev) {
|
||||
await nuxt.callHook('close', nuxt)
|
||||
if (!nuxt.options._prepare && !nuxt.options.dev && nuxt.options.experimental.buildCache) {
|
||||
const { restoreCache, collectCache } = await getVueHash(nuxt)
|
||||
if (await restoreCache()) {
|
||||
await nuxt.callHook('build:done')
|
||||
return await nuxt.callHook('close', nuxt)
|
||||
}
|
||||
} else {
|
||||
nuxt.hooks.hookOnce('nitro:build:before', () => collectCache())
|
||||
nuxt.hooks.hookOnce('close', () => cleanupCaches(nuxt))
|
||||
}
|
||||
|
||||
await nuxt.callHook('build:before')
|
||||
if (nuxt.options._prepare) {
|
||||
nuxt.hook('prepare:types', () => nuxt.close())
|
||||
return
|
||||
}
|
||||
|
||||
if (nuxt.options.dev) {
|
||||
checkForExternalConfigurationFiles()
|
||||
}
|
||||
|
||||
await bundle(nuxt)
|
||||
|
||||
await nuxt.callHook('build:done')
|
||||
|
||||
if (!nuxt.options.dev) {
|
||||
await nuxt.callHook('close', nuxt)
|
||||
}
|
||||
}
|
||||
|
||||
|
275
packages/nuxt/src/core/cache.ts
Normal file
275
packages/nuxt/src/core/cache.ts
Normal file
@ -0,0 +1,275 @@
|
||||
import { mkdir, open, readFile, stat, unlink, writeFile } from 'node:fs/promises'
|
||||
import type { FileHandle } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { isIgnored } from '@nuxt/kit'
|
||||
import type { Nuxt, NuxtConfig, NuxtConfigLayer } from '@nuxt/schema'
|
||||
import { hash, murmurHash, objectHash } from 'ohash'
|
||||
import { glob } from 'tinyglobby'
|
||||
import _consola, { consola } from 'consola'
|
||||
import { dirname, join, relative } from 'pathe'
|
||||
import { createTar, parseTar } from 'nanotar'
|
||||
import type { TarFileInput } from 'nanotar'
|
||||
|
||||
export async function getVueHash (nuxt: Nuxt) {
|
||||
const id = 'vue'
|
||||
|
||||
const { hash } = await getHashes(nuxt, {
|
||||
id,
|
||||
cwd: layer => layer.config?.srcDir,
|
||||
patterns: layer => [
|
||||
join(relative(layer.cwd, layer.config.srcDir), '**'),
|
||||
`!${relative(layer.cwd, layer.config.serverDir || join(layer.cwd, 'server'))}/**`,
|
||||
`!${relative(layer.cwd, resolve(layer.config.srcDir || layer.cwd, layer.config.dir?.public || 'public'))}/**`,
|
||||
`!${relative(layer.cwd, resolve(layer.config.srcDir || layer.cwd, layer.config.dir?.static || 'public'))}/**`,
|
||||
'!node_modules/**',
|
||||
'!nuxt.config.*',
|
||||
],
|
||||
configOverrides: {
|
||||
buildId: undefined,
|
||||
serverDir: undefined,
|
||||
nitro: undefined,
|
||||
devServer: undefined,
|
||||
runtimeConfig: undefined,
|
||||
logLevel: undefined,
|
||||
devServerHandlers: undefined,
|
||||
generate: undefined,
|
||||
devtools: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const cacheFile = join(nuxt.options.workspaceDir, 'node_modules/.cache/nuxt/builds', id, hash + '.tar')
|
||||
|
||||
return {
|
||||
hash,
|
||||
async collectCache () {
|
||||
const start = Date.now()
|
||||
await writeCache(nuxt.options.buildDir, nuxt.options.buildDir, cacheFile)
|
||||
const elapsed = Date.now() - start
|
||||
consola.success(`Cached Vue client and server builds in \`${elapsed}ms\`.`)
|
||||
},
|
||||
async restoreCache () {
|
||||
const start = Date.now()
|
||||
const res = await restoreCache(nuxt.options.buildDir, cacheFile)
|
||||
const elapsed = Date.now() - start
|
||||
if (res) {
|
||||
consola.success(`Restored Vue client and server builds from cache in \`${elapsed}ms\`.`)
|
||||
}
|
||||
return res
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupCaches (nuxt: Nuxt) {
|
||||
const start = Date.now()
|
||||
const caches = await glob(['*/*.tar'], {
|
||||
cwd: join(nuxt.options.workspaceDir, 'node_modules/.cache/nuxt/builds'),
|
||||
absolute: true,
|
||||
})
|
||||
if (caches.length >= 10) {
|
||||
const cachesWithMeta = await Promise.all(caches.map(async (cache) => {
|
||||
return [cache, await stat(cache).then(r => r.mtime.getTime()).catch(() => 0)] as const
|
||||
}))
|
||||
cachesWithMeta.sort((a, b) => a[1] - b[1])
|
||||
for (const [cache] of cachesWithMeta.slice(0, cachesWithMeta.length - 10)) {
|
||||
await unlink(cache)
|
||||
}
|
||||
const elapsed = Date.now() - start
|
||||
consola.success(`Cleaned up old build caches in \`${elapsed}ms\`.`)
|
||||
}
|
||||
}
|
||||
|
||||
// internal
|
||||
|
||||
type HashSource = { name: string, data: any }
|
||||
type Hashes = { hash: string, sources: HashSource[] }
|
||||
|
||||
interface GetHashOptions {
|
||||
id: string
|
||||
cwd: (layer: NuxtConfigLayer) => string
|
||||
patterns: (layer: NuxtConfigLayer) => string[]
|
||||
configOverrides: Partial<Record<keyof NuxtConfig, unknown>>
|
||||
}
|
||||
|
||||
async function getHashes (nuxt: Nuxt, options: GetHashOptions): Promise<Hashes> {
|
||||
if ((nuxt as any)[`_${options.id}BuildHash`]) {
|
||||
return (nuxt as any)[`_${options.id}BuildHash`]
|
||||
}
|
||||
|
||||
const start = Date.now()
|
||||
const hashSources: HashSource[] = []
|
||||
|
||||
// Layers
|
||||
let layerCtr = 0
|
||||
for (const layer of nuxt.options._layers) {
|
||||
if (layer.cwd.includes('node_modules')) { continue }
|
||||
|
||||
const layerName = `layer#${layerCtr++}`
|
||||
hashSources.push({
|
||||
name: `${layerName}:config`,
|
||||
data: objectHash({
|
||||
...layer.config,
|
||||
...options.configOverrides || {},
|
||||
}),
|
||||
})
|
||||
|
||||
const normalizeFiles = (files: Awaited<ReturnType<typeof readFilesRecursive>>) => files.map(f => ({
|
||||
name: f.name,
|
||||
size: (f.attrs as any)?.size,
|
||||
data: murmurHash(f.data as any /* ArrayBuffer */),
|
||||
}))
|
||||
|
||||
const sourceFiles = await readFilesRecursive(options.cwd(layer), {
|
||||
shouldIgnore: isIgnored, // TODO: Validate if works with absolute paths
|
||||
cwd: nuxt.options.rootDir,
|
||||
patterns: options.patterns(layer),
|
||||
})
|
||||
|
||||
hashSources.push({
|
||||
name: `${layerName}:src`,
|
||||
data: normalizeFiles(sourceFiles),
|
||||
})
|
||||
|
||||
const rootFiles = await readFilesRecursive(layer.config?.rootDir || layer.cwd, {
|
||||
shouldIgnore: isIgnored, // TODO: Validate if works with absolute paths
|
||||
cwd: nuxt.options.rootDir,
|
||||
patterns: [
|
||||
'.nuxtrc',
|
||||
'.npmrc',
|
||||
'package.json',
|
||||
'package-lock.json',
|
||||
'yarn.lock',
|
||||
'pnpm-lock.yaml',
|
||||
'tsconfig.json',
|
||||
'bun.lockb',
|
||||
],
|
||||
})
|
||||
|
||||
hashSources.push({
|
||||
name: `${layerName}:root`,
|
||||
data: normalizeFiles(rootFiles),
|
||||
})
|
||||
}
|
||||
|
||||
const res = ((nuxt as any)[`_${options.id}BuildHash`] = {
|
||||
hash: hash(hashSources),
|
||||
sources: hashSources,
|
||||
})
|
||||
|
||||
const elapsed = Date.now() - start
|
||||
consola.debug(`Computed \`${options.id}\` build hash in \`${elapsed}ms\`.`)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
type FileWithMeta = TarFileInput & {
|
||||
attrs: {
|
||||
mtime: number
|
||||
size: number
|
||||
}
|
||||
}
|
||||
|
||||
interface ReadFilesRecursiveOptions {
|
||||
shouldIgnore?: (name: string) => boolean
|
||||
patterns: string[]
|
||||
cwd: string
|
||||
}
|
||||
|
||||
async function readFilesRecursive (dir: string | string[], opts: ReadFilesRecursiveOptions): Promise<FileWithMeta[]> {
|
||||
if (Array.isArray(dir)) {
|
||||
return (await Promise.all(dir.map(d => readFilesRecursive(d, opts)))).flat()
|
||||
}
|
||||
|
||||
const files = await glob(opts.patterns, { cwd: dir })
|
||||
|
||||
const fileEntries = await Promise.all(files.map(async (fileName) => {
|
||||
if (!opts.shouldIgnore?.(fileName)) {
|
||||
const file = await readFileWithMeta(dir, fileName)
|
||||
if (!file) { return }
|
||||
return {
|
||||
...file,
|
||||
name: relative(opts.cwd, join(dir, file.name)),
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
return fileEntries.filter(Boolean) as FileWithMeta[]
|
||||
}
|
||||
|
||||
async function readFileWithMeta (dir: string, fileName: string, count = 0): Promise<FileWithMeta | undefined> {
|
||||
let fd: FileHandle | undefined = undefined
|
||||
|
||||
try {
|
||||
fd = await open(resolve(dir, fileName))
|
||||
const stats = await fd.stat()
|
||||
|
||||
if (!stats?.isFile()) { return }
|
||||
|
||||
const mtime = stats.mtime.getTime()
|
||||
const data = await fd.readFile()
|
||||
|
||||
// retry if file has changed during read
|
||||
if ((await fd.stat()).mtime.getTime() !== mtime) {
|
||||
if (count < 5) {
|
||||
return readFileWithMeta(dir, fileName, count + 1)
|
||||
}
|
||||
console.warn(`Failed to read file \`${fileName}\` as it changed during read.`)
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
name: fileName,
|
||||
data,
|
||||
attrs: {
|
||||
mtime,
|
||||
size: stats.size,
|
||||
},
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to read file \`${fileName}\`:`, err)
|
||||
} finally {
|
||||
await fd?.close()
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreCache (cwd: string, cacheFile: string) {
|
||||
if (!existsSync(cacheFile)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const files = parseTar(await readFile(cacheFile))
|
||||
for (const file of files) {
|
||||
let fd: FileHandle | undefined = undefined
|
||||
try {
|
||||
const filePath = resolve(cwd, file.name)
|
||||
await mkdir(dirname(filePath), { recursive: true })
|
||||
|
||||
fd = await open(filePath, 'w')
|
||||
|
||||
const stats = await fd.stat().catch(() => null)
|
||||
if (stats?.isFile() && stats.size) {
|
||||
const lastModified = Number.parseInt(file.attrs?.mtime?.toString().padEnd(13, '0') || '0')
|
||||
if (stats.mtime.getTime() >= lastModified) {
|
||||
consola.debug(`Skipping \`${file.name}\` (up to date or newer than cache)`)
|
||||
continue
|
||||
}
|
||||
}
|
||||
await fd.writeFile(file.data!)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
await fd?.close()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function writeCache (cwd: string, sources: string | string[], cacheFile: string) {
|
||||
const fileEntries = await readFilesRecursive(sources, {
|
||||
patterns: ['**/*', '!analyze/**'],
|
||||
cwd,
|
||||
})
|
||||
const tarData = createTar(fileEntries)
|
||||
await mkdir(dirname(cacheFile), { recursive: true })
|
||||
await writeFile(cacheFile, tarData)
|
||||
}
|
@ -11,12 +11,13 @@ import escapeRE from 'escape-string-regexp'
|
||||
import { defu } from 'defu'
|
||||
import { dynamicEventHandler } from 'h3'
|
||||
import { isWindows } from 'std-env'
|
||||
import { ImpoundPlugin } from 'impound'
|
||||
import type { Nuxt, NuxtOptions } from 'nuxt/schema'
|
||||
import { version as nuxtVersion } from '../../package.json'
|
||||
import { distDir } from '../dirs'
|
||||
import { toArray } from '../utils'
|
||||
import { template as defaultSpaLoadingTemplate } from '../../../ui-templates/dist/templates/spa-loading-icon'
|
||||
import { ImportProtectionPlugin, nuxtImportProtections } from './plugins/import-protection'
|
||||
import { nuxtImportProtections } from './plugins/import-protection'
|
||||
|
||||
const logLevelMapReverse = {
|
||||
silent: 0,
|
||||
@ -358,9 +359,8 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
||||
nitroConfig.rollupConfig!.plugins = await nitroConfig.rollupConfig!.plugins || []
|
||||
nitroConfig.rollupConfig!.plugins = toArray(nitroConfig.rollupConfig!.plugins)
|
||||
nitroConfig.rollupConfig!.plugins!.push(
|
||||
ImportProtectionPlugin.rollup({
|
||||
rootDir: nuxt.options.rootDir,
|
||||
modulesDir: nuxt.options.modulesDir,
|
||||
ImpoundPlugin.rollup({
|
||||
cwd: nuxt.options.rootDir,
|
||||
patterns: nuxtImportProtections(nuxt, { isNitro: true }),
|
||||
exclude: [/core[\\/]runtime[\\/]nitro[\\/]renderer/],
|
||||
}),
|
||||
@ -517,26 +517,30 @@ export async function initNitro (nuxt: Nuxt & { _nitro?: Nitro }) {
|
||||
})
|
||||
}
|
||||
|
||||
async function symlinkDist () {
|
||||
if (nitro.options.static) {
|
||||
const distDir = resolve(nuxt.options.rootDir, 'dist')
|
||||
if (!existsSync(distDir)) {
|
||||
await fsp.symlink(nitro.options.output.publicDir, distDir, 'junction').catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nuxt build/dev
|
||||
nuxt.hook('build:done', async () => {
|
||||
await nuxt.callHook('nitro:build:before', nitro)
|
||||
if (nuxt.options.dev) {
|
||||
await build(nitro)
|
||||
} else {
|
||||
await prepare(nitro)
|
||||
await prerender(nitro)
|
||||
|
||||
logger.restoreAll()
|
||||
await build(nitro)
|
||||
logger.wrapAll()
|
||||
|
||||
if (nitro.options.static) {
|
||||
const distDir = resolve(nuxt.options.rootDir, 'dist')
|
||||
if (!existsSync(distDir)) {
|
||||
await fsp.symlink(nitro.options.output.publicDir, distDir, 'junction').catch(() => {})
|
||||
}
|
||||
}
|
||||
return build(nitro)
|
||||
}
|
||||
|
||||
await prepare(nitro)
|
||||
await prerender(nitro)
|
||||
|
||||
logger.restoreAll()
|
||||
await build(nitro)
|
||||
logger.wrapAll()
|
||||
|
||||
await symlinkDist()
|
||||
})
|
||||
|
||||
// nuxt dev
|
||||
|
@ -15,13 +15,14 @@ import { colorize } from 'consola/utils'
|
||||
import { updateConfig } from 'c12/update'
|
||||
import { formatDate, resolveCompatibilityDatesFromEnv } from 'compatx'
|
||||
import type { DateString } from 'compatx'
|
||||
|
||||
import escapeRE from 'escape-string-regexp'
|
||||
import { withTrailingSlash, withoutLeadingSlash } from 'ufo'
|
||||
|
||||
import { ImpoundPlugin } from 'impound'
|
||||
import type { ImpoundOptions } from 'impound'
|
||||
import defu from 'defu'
|
||||
import { gt, satisfies } from 'semver'
|
||||
import { hasTTY, isCI } from 'std-env'
|
||||
|
||||
import pagesModule from '../pages/module'
|
||||
import metaModule from '../head/module'
|
||||
import componentsModule from '../components/module'
|
||||
@ -31,7 +32,7 @@ import { distDir, pkgDir } from '../dirs'
|
||||
import { version } from '../../package.json'
|
||||
import { scriptsStubsPreset } from '../imports/presets'
|
||||
import { resolveTypePath } from './utils/types'
|
||||
import { ImportProtectionPlugin, nuxtImportProtections } from './plugins/import-protection'
|
||||
import { nuxtImportProtections } from './plugins/import-protection'
|
||||
import type { UnctxTransformPluginOptions } from './plugins/unctx'
|
||||
import { UnctxTransformPlugin } from './plugins/unctx'
|
||||
import type { TreeShakeComposablesPluginOptions } from './plugins/tree-shake'
|
||||
@ -103,7 +104,7 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
|
||||
const shouldShowPrompt = nuxt.options.dev && hasTTY && !isCI
|
||||
if (!shouldShowPrompt) {
|
||||
console.log(`Using \`${fallbackCompatibilityDate}\` as fallback compatibility date.`)
|
||||
logger.info(`Using \`${fallbackCompatibilityDate}\` as fallback compatibility date.`)
|
||||
}
|
||||
|
||||
async function promptAndUpdate () {
|
||||
@ -112,7 +113,7 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
default: true,
|
||||
})
|
||||
if (result !== true) {
|
||||
console.log(`Using \`${fallbackCompatibilityDate}\` as fallback compatibility date.`)
|
||||
logger.info(`Using \`${fallbackCompatibilityDate}\` as fallback compatibility date.`)
|
||||
return
|
||||
}
|
||||
|
||||
@ -146,7 +147,7 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
consola.error(`Failed to update config: ${message}`)
|
||||
}
|
||||
|
||||
console.log(`Using \`${fallbackCompatibilityDate}\` as fallback compatibility date.`)
|
||||
logger.info(`Using \`${fallbackCompatibilityDate}\` as fallback compatibility date.`)
|
||||
}
|
||||
|
||||
nuxt.hooks.hookOnce('nitro:init', (nitro) => {
|
||||
@ -155,7 +156,7 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
nitro.hooks.hookOnce('compiled', () => {
|
||||
warnedAboutCompatDate = true
|
||||
// Print warning
|
||||
console.info(`Nuxt now supports pinning the behavior of provider and deployment presets with a compatibility date. We recommend you specify a \`compatibilityDate\` in your \`nuxt.config\` file, or set an environment variable, such as \`COMPATIBILITY_DATE=${todaysDate}\`.`)
|
||||
logger.info(`Nuxt now supports pinning the behavior of provider and deployment presets with a compatibility date. We recommend you specify a \`compatibilityDate\` in your \`nuxt.config\` file, or set an environment variable, such as \`COMPATIBILITY_DATE=${todaysDate}\`.`)
|
||||
if (shouldShowPrompt) { promptAndUpdate() }
|
||||
})
|
||||
})
|
||||
@ -245,18 +246,19 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
addBuildPlugin(RemovePluginMetadataPlugin(nuxt))
|
||||
|
||||
// Add import protection
|
||||
const config = {
|
||||
rootDir: nuxt.options.rootDir,
|
||||
const config: ImpoundOptions = {
|
||||
cwd: nuxt.options.rootDir,
|
||||
// Exclude top-level resolutions by plugins
|
||||
exclude: [join(nuxt.options.srcDir, 'index.html')],
|
||||
patterns: nuxtImportProtections(nuxt),
|
||||
modulesDir: nuxt.options.modulesDir,
|
||||
}
|
||||
addVitePlugin(() => ImportProtectionPlugin.vite(config))
|
||||
addWebpackPlugin(() => ImportProtectionPlugin.webpack(config))
|
||||
addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...config, error: false }), { name: 'nuxt:import-protection' }), { client: false })
|
||||
addVitePlugin(() => Object.assign(ImpoundPlugin.vite({ ...config, error: true }), { name: 'nuxt:import-protection' }), { server: false })
|
||||
addWebpackPlugin(() => ImpoundPlugin.webpack(config))
|
||||
|
||||
// add resolver for modules used in virtual files
|
||||
addVitePlugin(() => resolveDeepImportsPlugin(nuxt))
|
||||
addVitePlugin(() => resolveDeepImportsPlugin(nuxt), { client: false })
|
||||
addVitePlugin(() => resolveDeepImportsPlugin(nuxt), { server: false })
|
||||
|
||||
// Add transform for `onPrehydrate` lifecycle hook
|
||||
addBuildPlugin(prehydrateTransformPlugin(nuxt))
|
||||
@ -664,7 +666,7 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
// Show compatibility version banner when Nuxt is running with a compatibility version
|
||||
// that is different from the current major version
|
||||
if (!(satisfies(nuxt._version, nuxt.options.future.compatibilityVersion + '.x'))) {
|
||||
console.info(`Running with compatibility version \`${nuxt.options.future.compatibilityVersion}\``)
|
||||
logger.info(`Running with compatibility version \`${nuxt.options.future.compatibilityVersion}\``)
|
||||
}
|
||||
|
||||
await nuxt.callHook('ready', nuxt)
|
||||
|
@ -1,7 +1,4 @@
|
||||
import { createUnplugin } from 'unplugin'
|
||||
import { logger } from '@nuxt/kit'
|
||||
import { resolvePath } from 'mlly'
|
||||
import { isAbsolute, join, relative, resolve } from 'pathe'
|
||||
import { relative, resolve } from 'pathe'
|
||||
import escapeRE from 'escape-string-regexp'
|
||||
import type { NuxtOptions } from 'nuxt/schema'
|
||||
|
||||
@ -53,41 +50,3 @@ export const nuxtImportProtections = (nuxt: { options: NuxtOptions }, options: {
|
||||
|
||||
return patterns
|
||||
}
|
||||
|
||||
export const ImportProtectionPlugin = createUnplugin(function (options: ImportProtectionOptions) {
|
||||
const cache: Record<string, Map<string | RegExp, boolean>> = {}
|
||||
const importersToExclude = options?.exclude || []
|
||||
const proxy = resolvePath('unenv/runtime/mock/proxy', { url: options.modulesDir })
|
||||
return {
|
||||
name: 'nuxt:import-protection',
|
||||
enforce: 'pre',
|
||||
resolveId (id, importer) {
|
||||
if (!importer) { return }
|
||||
if (id[0] === '.') {
|
||||
id = join(importer, '..', id)
|
||||
}
|
||||
if (isAbsolute(id)) {
|
||||
id = relative(options.rootDir, id)
|
||||
}
|
||||
if (importersToExclude.some(p => typeof p === 'string' ? importer === p : p.test(importer))) { return }
|
||||
|
||||
const invalidImports = options.patterns.filter(([pattern]) => pattern instanceof RegExp ? pattern.test(id) : pattern === id)
|
||||
let matched = false
|
||||
for (const match of invalidImports) {
|
||||
cache[id] = cache[id] || new Map()
|
||||
const [pattern, warning] = match
|
||||
// Skip if already warned
|
||||
if (cache[id].has(pattern)) { continue }
|
||||
|
||||
const relativeImporter = isAbsolute(importer) ? relative(options.rootDir, importer) : importer
|
||||
logger.error(warning || 'Invalid import', `[importing \`${id}\` from \`${relativeImporter}\`]`)
|
||||
cache[id].set(pattern, true)
|
||||
matched = true
|
||||
}
|
||||
if (matched) {
|
||||
return proxy
|
||||
}
|
||||
return null
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@ -149,29 +149,12 @@ export const RemovePluginMetadataPlugin = (nuxt: Nuxt) => createUnplugin(() => {
|
||||
if (_node.type === 'ImportSpecifier' && (_node.imported.name === 'defineNuxtPlugin' || _node.imported.name === 'definePayloadPlugin')) {
|
||||
wrapperNames.add(_node.local.name)
|
||||
}
|
||||
if (_node.type === 'ExportDefaultDeclaration' && (_node.declaration.type === 'FunctionDeclaration' || _node.declaration.type === 'ArrowFunctionExpression')) {
|
||||
if ('params' in _node.declaration && _node.declaration.params.length > 1) {
|
||||
logger.warn(`Plugin \`${plugin.src}\` is in legacy Nuxt 2 format (context, inject) which is likely to be broken and will be ignored.`)
|
||||
s.overwrite(0, code.length, 'export default () => {}')
|
||||
wrapped = true // silence a duplicate error
|
||||
return
|
||||
}
|
||||
}
|
||||
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
|
||||
const node = _node as CallExpression & { start: number, end: number }
|
||||
const name = 'name' in node.callee && node.callee.name
|
||||
if (!name || !wrapperNames.has(name)) { return }
|
||||
wrapped = true
|
||||
|
||||
if (node.arguments[0].type !== 'ObjectExpression') {
|
||||
// TODO: Warn if legacy plugin format is detected
|
||||
if ('params' in node.arguments[0] && node.arguments[0].params.length > 1) {
|
||||
logger.warn(`Plugin \`${plugin.src}\` is in legacy Nuxt 2 format (context, inject) which is likely to be broken and will be ignored.`)
|
||||
s.overwrite(0, code.length, 'export default () => {}')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Remove metadata that already has been extracted
|
||||
if (!('order' in plugin) && !('name' in plugin)) { return }
|
||||
for (const [argIndex, _arg] of node.arguments.entries()) {
|
||||
|
@ -8,20 +8,25 @@ import { pkgDir } from '../../dirs'
|
||||
|
||||
export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin {
|
||||
const exclude: string[] = ['virtual:', '\0virtual:', '/__skip_vite']
|
||||
let conditions: string[]
|
||||
return {
|
||||
name: 'nuxt:resolve-bare-imports',
|
||||
enforce: 'post',
|
||||
async resolveId (id, importer, options) {
|
||||
configResolved (config) {
|
||||
conditions = config.mode === 'test' ? [...config.resolve.conditions, 'import', 'require'] : config.resolve.conditions
|
||||
},
|
||||
async resolveId (id, importer) {
|
||||
if (!importer || isAbsolute(id) || (!isAbsolute(importer) && !importer.startsWith('virtual:')) || exclude.some(e => id.startsWith(e))) {
|
||||
return
|
||||
}
|
||||
id = normalize(id)
|
||||
id = resolveAlias(id, nuxt.options.alias)
|
||||
const { dir } = parseNodeModulePath(importer)
|
||||
return await this.resolve?.(id, dir || pkgDir, { skipSelf: true }) ?? await resolvePath(id, {
|
||||
url: [dir || pkgDir, ...nuxt.options.modulesDir],
|
||||
// TODO: respect nitro runtime conditions
|
||||
conditions: options.ssr ? ['node', 'import', 'require'] : ['import', 'require'],
|
||||
|
||||
const normalisedId = resolveAlias(normalize(id), nuxt.options.alias)
|
||||
const normalisedImporter = importer.replace(/^\0?virtual:(?:nuxt:)?/, '')
|
||||
const dir = parseNodeModulePath(normalisedImporter).dir || pkgDir
|
||||
|
||||
return await this.resolve?.(normalisedId, dir, { skipSelf: true }) ?? await resolvePath(id, {
|
||||
url: [dir, ...nuxt.options.modulesDir],
|
||||
conditions,
|
||||
}).catch(() => {
|
||||
logger.debug('Could not resolve id', id, importer)
|
||||
return null
|
||||
|
@ -20,8 +20,8 @@ export default defineDriver((opts) => {
|
||||
...fs, // fall back to file system - only the bottom three methods are used in renderer
|
||||
async setItem (key, value, opts) {
|
||||
await Promise.all([
|
||||
fs.setItem(normalizeFsKey(key), value, opts),
|
||||
lru.setItem(key, value, opts),
|
||||
fs.setItem?.(normalizeFsKey(key), value, opts),
|
||||
lru.setItem?.(key, value, opts),
|
||||
])
|
||||
},
|
||||
async hasItem (key, opts) {
|
||||
|
@ -77,7 +77,8 @@ export default (nitroApp: NitroApp) => {
|
||||
const ctx = asyncContext.tryUse()
|
||||
if (!ctx) { return }
|
||||
try {
|
||||
htmlContext.bodyAppend.unshift(`<script type="application/json" data-nuxt-logs="${appId}">${stringify(ctx.logs, { ...devReducers, ...ctx.event.context._payloadReducers })}</script>`)
|
||||
const reducers = Object.assign(Object.create(null), devReducers, ctx.event.context._payloadReducers)
|
||||
htmlContext.bodyAppend.unshift(`<script type="application/json" data-nuxt-logs="${appId}">${stringify(ctx.logs, reducers)}</script>`)
|
||||
} catch (e) {
|
||||
const shortError = e instanceof Error && 'toString' in e ? ` Received \`${e.toString()}\`.` : ''
|
||||
console.warn(`[nuxt] Failed to stringify dev server logs.${shortError} You can define your own reducer/reviver for rich types following the instructions in https://nuxt.com/docs/api/composables/use-nuxt-app#payload.`)
|
||||
|
@ -31,7 +31,7 @@ export default <NitroErrorHandler> async function errorhandler (error: H3Error,
|
||||
error.fatal && '[fatal]',
|
||||
Number(errorObject.statusCode) !== 200 && `[${errorObject.statusCode}]`,
|
||||
].filter(Boolean).join(' ')
|
||||
console.error(tags, errorObject.message + '\n' + stack.map(l => ' ' + l.text).join(' \n'))
|
||||
console.error(tags, (error.message || error.toString() || 'internal server error') + '\n' + stack.map(l => ' ' + l.text).join(' \n'))
|
||||
}
|
||||
|
||||
if (event.handled) { return }
|
||||
@ -119,7 +119,7 @@ function normalizeError (error: any) {
|
||||
// Hide details of unhandled/fatal errors in production
|
||||
const hideDetails = !import.meta.dev && error.unhandled
|
||||
|
||||
const stack = hideDetails
|
||||
const stack = hideDetails && !import.meta.prerender
|
||||
? []
|
||||
: ((error.stack as string) || '')
|
||||
.split('\n')
|
||||
|
@ -16,11 +16,10 @@ import { stringify, uneval } from 'devalue'
|
||||
import destr from 'destr'
|
||||
import { getQuery as getURLQuery, joinURL, withoutTrailingSlash } from 'ufo'
|
||||
import { renderToString as _renderToString } from 'vue/server-renderer'
|
||||
import { hash } from 'ohash'
|
||||
import { propsToString, renderSSRHead } from '@unhead/ssr'
|
||||
import type { HeadEntryOptions } from '@unhead/schema'
|
||||
import type { Head, HeadEntryOptions } from '@unhead/schema'
|
||||
import type { Link, Script, Style } from '@unhead/vue'
|
||||
import { createServerHead } from '@unhead/vue'
|
||||
import { createServerHead, resolveUnrefHeadInput } from '@unhead/vue'
|
||||
|
||||
import { defineRenderHandler, getRouteRules, useNitroApp, useRuntimeConfig, useStorage } from 'nitro/runtime'
|
||||
|
||||
@ -79,10 +78,7 @@ export interface NuxtIslandContext {
|
||||
export interface NuxtIslandResponse {
|
||||
id?: string
|
||||
html: string
|
||||
head: {
|
||||
link: (Record<string, string>)[]
|
||||
style: ({ innerHTML: string, key: string })[]
|
||||
}
|
||||
head: Head
|
||||
props?: Record<string, Record<string, any>>
|
||||
components?: Record<string, NuxtIslandClientResponse>
|
||||
slots?: Record<string, NuxtIslandSlotResponse>
|
||||
@ -162,9 +158,7 @@ const getSPARenderer = lazyCachedFunction(async () => {
|
||||
const renderToString = (ssrContext: NuxtSSRContext) => {
|
||||
const config = useRuntimeConfig(ssrContext.event)
|
||||
ssrContext.modules = ssrContext.modules || new Set<string>()
|
||||
ssrContext!.payload = {
|
||||
serverRendered: false,
|
||||
}
|
||||
ssrContext.payload.serverRendered = false
|
||||
ssrContext.config = {
|
||||
public: config.public,
|
||||
app: config.app,
|
||||
@ -288,6 +282,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
const head = createServerHead({
|
||||
plugins: unheadPlugins,
|
||||
})
|
||||
|
||||
// needed for hash hydration plugin to work
|
||||
const headEntryOptions: HeadEntryOptions = { mode: 'server' }
|
||||
if (!isRenderingIsland) {
|
||||
@ -308,7 +303,7 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
error: !!ssrError,
|
||||
nuxt: undefined!, /* NuxtApp */
|
||||
payload: (ssrError ? { error: ssrError } : {}) as NuxtPayload,
|
||||
_payloadReducers: {},
|
||||
_payloadReducers: Object.create(null),
|
||||
modules: new Set(),
|
||||
islandContext,
|
||||
}
|
||||
@ -394,7 +389,9 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
}
|
||||
|
||||
// 2. Styles
|
||||
head.push({ style: inlinedStyles })
|
||||
if (inlinedStyles.length) {
|
||||
head.push({ style: inlinedStyles })
|
||||
}
|
||||
if (!isRenderingIsland || import.meta.dev) {
|
||||
const link: Link[] = []
|
||||
for (const style in styles) {
|
||||
@ -411,7 +408,9 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
link.push({ rel: 'stylesheet', href: renderer.rendererContext.buildAssetsURL(resource.file) })
|
||||
}
|
||||
}
|
||||
head.push({ link }, headEntryOptions)
|
||||
if (link.length) {
|
||||
head.push({ link }, headEntryOptions)
|
||||
}
|
||||
}
|
||||
|
||||
if (!NO_SCRIPTS && !isRenderingIsland) {
|
||||
@ -460,17 +459,21 @@ export default defineRenderHandler(async (event): Promise<Partial<RenderResponse
|
||||
|
||||
// Response for component islands
|
||||
if (isRenderingIsland && islandContext) {
|
||||
const islandHead: NuxtIslandResponse['head'] = {
|
||||
link: [],
|
||||
style: [],
|
||||
}
|
||||
for (const tag of await head.resolveTags()) {
|
||||
if (tag.tag === 'link') {
|
||||
islandHead.link.push({ key: 'island-link-' + hash(tag.props), ...tag.props })
|
||||
} else if (tag.tag === 'style' && tag.innerHTML) {
|
||||
islandHead.style.push({ key: 'island-style-' + hash(tag.innerHTML), innerHTML: tag.innerHTML })
|
||||
const islandHead: Head = {}
|
||||
for (const entry of head.headEntries()) {
|
||||
for (const [key, value] of Object.entries(resolveUnrefHeadInput(entry.input) as Head)) {
|
||||
const currentValue = islandHead[key as keyof Head]
|
||||
if (Array.isArray(currentValue)) {
|
||||
currentValue.push(...value)
|
||||
}
|
||||
islandHead[key as keyof Head] = value
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove for v4
|
||||
islandHead.link = islandHead.link || []
|
||||
islandHead.style = islandHead.style || []
|
||||
|
||||
const islandResponse: NuxtIslandResponse = {
|
||||
id: islandContext.id,
|
||||
head: islandHead,
|
||||
|
@ -120,11 +120,21 @@ export const pluginsDeclaration: NuxtTemplate = {
|
||||
const relativePath = relative(typesDir, pluginPath)
|
||||
|
||||
const correspondingDeclaration = pluginPath.replace(/\.(?<letter>[cm])?jsx?$/, '.d.$<letter>ts')
|
||||
// if `.d.ts` file exists alongside a `.js` plugin, or if `.d.mts` file exists alongside a `.mjs` plugin, we can use the entire path
|
||||
if (correspondingDeclaration !== pluginPath && exists(correspondingDeclaration)) {
|
||||
tsImports.push(relativePath)
|
||||
continue
|
||||
}
|
||||
|
||||
const incorrectDeclaration = pluginPath.replace(/\.[cm]jsx?$/, '.d.ts')
|
||||
// if `.d.ts` file exists, but plugin is `.mjs`, add `.js` extension to the import
|
||||
// to hotfix issue until ecosystem updates to `@nuxt/module-builder@>=0.8.0`
|
||||
if (incorrectDeclaration !== pluginPath && exists(incorrectDeclaration)) {
|
||||
tsImports.push(relativePath.replace(/\.[cm](jsx?)$/, '.$1'))
|
||||
continue
|
||||
}
|
||||
|
||||
// if there is no declaration we only want to remove the extension if it's a TypeScript file
|
||||
if (exists(pluginPath)) {
|
||||
if (TS_RE.test(pluginPath)) {
|
||||
tsImports.push(relativePath.replace(EXTENSION_RE, ''))
|
||||
@ -181,7 +191,7 @@ export const schemaTemplate: NuxtTemplate = {
|
||||
}
|
||||
}
|
||||
|
||||
const moduleOptionsInterface = (jsdocTags: boolean) => [
|
||||
const moduleOptionsInterface = (options: { addJSDocTags: boolean, unresolved: boolean }) => [
|
||||
...modules.flatMap(([configKey, importName, mod]) => {
|
||||
let link: string | undefined
|
||||
|
||||
@ -211,30 +221,32 @@ export const schemaTemplate: NuxtTemplate = {
|
||||
return [
|
||||
` /**`,
|
||||
` * Configuration for \`${importName}\``,
|
||||
...jsdocTags && link
|
||||
? [
|
||||
` * @see ${link}`,
|
||||
]
|
||||
: [],
|
||||
...options.addJSDocTags && link ? [` * @see ${link}`] : [],
|
||||
` */`,
|
||||
` [${configKey}]?: typeof ${genDynamicImport(importName, { wrapper: false })}.default extends NuxtModule<infer O> ? Partial<O> : Record<string, any>`,
|
||||
` [${configKey}]${options.unresolved ? '?' : ''}: typeof ${genDynamicImport(importName, { wrapper: false })}.default extends NuxtModule<infer O> ? ${options.unresolved ? 'Partial<O>' : 'O'} : Record<string, any>`,
|
||||
]
|
||||
}),
|
||||
modules.length > 0 ? ` modules?: (undefined | null | false | NuxtModule | string | [NuxtModule | string, Record<string, any>] | ${modules.map(([configKey, importName, mod]) => `[${genString(mod.meta?.rawPath || importName)}, Exclude<NuxtConfig[${configKey}], boolean>]`).join(' | ')})[],` : '',
|
||||
modules.length > 0 && options.unresolved ? ` modules?: (undefined | null | false | NuxtModule<any> | string | [NuxtModule | string, Record<string, any>] | ${modules.map(([configKey, importName, mod]) => `[${genString(mod.meta?.rawPath || importName)}, Exclude<NuxtConfig[${configKey}], boolean>]`).join(' | ')})[],` : '',
|
||||
].filter(Boolean)
|
||||
|
||||
return [
|
||||
'import { NuxtModule, RuntimeConfig } from \'@nuxt/schema\'',
|
||||
'declare module \'@nuxt/schema\' {',
|
||||
' interface NuxtOptions {',
|
||||
...moduleOptionsInterface({ addJSDocTags: false, unresolved: false }),
|
||||
' }',
|
||||
' interface NuxtConfig {',
|
||||
// TypeScript will duplicate the jsdoc tags if we augment it twice
|
||||
// So here we only generate tags for `nuxt/schema`
|
||||
...moduleOptionsInterface(false),
|
||||
...moduleOptionsInterface({ addJSDocTags: false, unresolved: true }),
|
||||
' }',
|
||||
'}',
|
||||
'declare module \'nuxt/schema\' {',
|
||||
' interface NuxtOptions {',
|
||||
...moduleOptionsInterface({ addJSDocTags: true, unresolved: false }),
|
||||
' }',
|
||||
' interface NuxtConfig {',
|
||||
...moduleOptionsInterface(true),
|
||||
...moduleOptionsInterface({ addJSDocTags: true, unresolved: true }),
|
||||
' }',
|
||||
generateTypes(await resolveSchema(privateRuntimeConfig as Record<string, JSValue>),
|
||||
{
|
||||
@ -267,7 +279,7 @@ export const layoutTemplate: NuxtTemplate = {
|
||||
filename: 'layouts.mjs',
|
||||
getContents ({ app }) {
|
||||
const layoutsObject = genObjectFromRawEntries(Object.values(app.layouts).map(({ name, file }) => {
|
||||
return [name, genDynamicImport(file, { interopDefault: true })]
|
||||
return [name, genDynamicImport(file)]
|
||||
}))
|
||||
return [
|
||||
`export default ${layoutsObject}`,
|
||||
@ -504,6 +516,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
|
||||
`export const appId = ${JSON.stringify(ctx.nuxt.options.appId)}`,
|
||||
`export const outdatedBuildInterval = ${ctx.nuxt.options.experimental.checkOutdatedBuildInterval}`,
|
||||
`export const multiApp = ${!!ctx.nuxt.options.future.multiApp}`,
|
||||
`export const chunkErrorEvent = ${ctx.nuxt.options.experimental.emitRouteChunkError ? ctx.nuxt.options.builder === '@nuxt/vite-builder' ? '"vite:preloadError"' : '"nuxt:preloadError"' : 'false'}`,
|
||||
].join('\n\n')
|
||||
},
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { addTemplate, addTypeTemplate, addVitePlugin, addWebpackPlugin, defineNuxtModule, isIgnored, logger, resolveAlias, tryResolveModule, updateTemplates, useNuxt } from '@nuxt/kit'
|
||||
import { addBuildPlugin, addTemplate, addTypeTemplate, defineNuxtModule, isIgnored, logger, 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'
|
||||
import type { ImportPresetWithDeprecation, ImportsOptions, ResolvedNuxtTemplate } from 'nuxt/schema'
|
||||
import escapeRE from 'escape-string-regexp'
|
||||
|
||||
import { lookupNodeModuleSubpath, parseNodeModulePath } from 'mlly'
|
||||
import { isDirectory } from '../utils'
|
||||
@ -15,7 +16,7 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
|
||||
name: 'imports',
|
||||
configKey: 'imports',
|
||||
},
|
||||
defaults: {
|
||||
defaults: nuxt => ({
|
||||
autoImport: true,
|
||||
scan: true,
|
||||
presets: defaultPresets,
|
||||
@ -23,11 +24,13 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
|
||||
imports: [],
|
||||
dirs: [],
|
||||
transform: {
|
||||
include: [],
|
||||
include: [
|
||||
new RegExp('^' + escapeRE(nuxt.options.buildDir)),
|
||||
],
|
||||
exclude: undefined,
|
||||
},
|
||||
virtualImports: ['#imports'],
|
||||
},
|
||||
}),
|
||||
async setup (options, nuxt) {
|
||||
// TODO: fix sharing of defaults between invocations of modules
|
||||
const presets = JSON.parse(JSON.stringify(options.presets)) as ImportPresetWithDeprecation[]
|
||||
@ -40,6 +43,7 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
|
||||
|
||||
// Create a context to share state between module internals
|
||||
const ctx = createUnimport({
|
||||
injectAtEnd: true,
|
||||
...options,
|
||||
addons: {
|
||||
vueTemplate: options.autoImport,
|
||||
@ -91,8 +95,7 @@ export default defineNuxtModule<Partial<ImportsOptions>>({
|
||||
nuxt.options.alias['#imports'] = join(nuxt.options.buildDir, 'imports')
|
||||
|
||||
// Transform to inject imports in production mode
|
||||
addVitePlugin(() => TransformPlugin.vite({ ctx, options, sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client }))
|
||||
addWebpackPlugin(() => TransformPlugin.webpack({ ctx, options, sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client }))
|
||||
addBuildPlugin(TransformPlugin({ ctx, options, sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client }))
|
||||
|
||||
const priorities = nuxt.options._layers.map((layer, i) => [layer.config.srcDir, -i] as const).sort(([a], [b]) => b.length - a.length)
|
||||
|
||||
|
@ -42,7 +42,7 @@ const granularAppPresets: InlinePreset[] = [
|
||||
from: '#app/composables/asyncData',
|
||||
},
|
||||
{
|
||||
imports: ['useHydration', 'createVisibleLoader', 'createIdleLoader', 'createEventLoader'],
|
||||
imports: ['useHydration'],
|
||||
from: '#app/composables/hydrate',
|
||||
},
|
||||
{
|
||||
@ -229,12 +229,8 @@ const vuePreset = defineUnimportPreset({
|
||||
'useTransitionState',
|
||||
'useId',
|
||||
'useTemplateRef',
|
||||
'hydrateOnInteraction',
|
||||
'hydrateOnMediaQuery',
|
||||
'hydrateOnVisible',
|
||||
'hydrateOnIdle',
|
||||
'useHost',
|
||||
'useShadowRoot',
|
||||
'useCssVars',
|
||||
],
|
||||
})
|
||||
|
||||
|
@ -8,7 +8,7 @@ import { isJS, isVue } from '../core/utils'
|
||||
const NODE_MODULES_RE = /[\\/]node_modules[\\/]/
|
||||
const IMPORTS_RE = /(['"])#imports\1/
|
||||
|
||||
export const TransformPlugin = createUnplugin(({ ctx, options, sourcemap }: { ctx: Unimport, options: Partial<ImportsOptions>, sourcemap?: boolean }) => {
|
||||
export const TransformPlugin = ({ ctx, options, sourcemap }: { ctx: Unimport, options: Partial<ImportsOptions>, sourcemap?: boolean }) => createUnplugin(() => {
|
||||
return {
|
||||
name: 'nuxt:imports-transform',
|
||||
enforce: 'post',
|
||||
|
@ -515,7 +515,7 @@ export default defineNuxtModule({
|
||||
const namedMiddleware = app.middleware.filter(mw => !mw.global)
|
||||
return [
|
||||
'import type { NavigationGuard } from \'vue-router\'',
|
||||
`export type MiddlewareKey = ${namedMiddleware.map(mw => genString(mw.name)).join(' | ') || 'string'}`,
|
||||
`export type MiddlewareKey = ${namedMiddleware.map(mw => genString(mw.name)).join(' | ') || 'never'}`,
|
||||
`declare module ${genString(composablesFile)} {`,
|
||||
' interface PageMeta {',
|
||||
' middleware?: MiddlewareKey | NavigationGuard | Array<MiddlewareKey | NavigationGuard>',
|
||||
|
@ -18,30 +18,33 @@ export async function extractRouteRules (code: string): Promise<NitroRouteConfig
|
||||
}
|
||||
if (!ROUTE_RULE_RE.test(code)) { return null }
|
||||
|
||||
const script = extractScriptContent(code)
|
||||
code = script?.code || code
|
||||
|
||||
let rule: NitroRouteConfig | null = null
|
||||
const contents = extractScriptContent(code)
|
||||
for (const script of contents) {
|
||||
if (rule) { break }
|
||||
|
||||
const js = await transform(code, { loader: script?.loader || 'ts' })
|
||||
walk(parse(js.code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
}) as Node, {
|
||||
enter (_node) {
|
||||
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
|
||||
const node = _node as CallExpression & { start: number, end: number }
|
||||
const name = 'name' in node.callee && node.callee.name
|
||||
if (name === 'defineRouteRules') {
|
||||
const rulesString = js.code.slice(node.start, node.end)
|
||||
try {
|
||||
rule = JSON.parse(runInNewContext(rulesString.replace('defineRouteRules', 'JSON.stringify'), {}))
|
||||
} catch {
|
||||
throw new Error('[nuxt] Error parsing route rules. They should be JSON-serializable.')
|
||||
code = script?.code || code
|
||||
|
||||
const js = await transform(code, { loader: script?.loader || 'ts' })
|
||||
walk(parse(js.code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
}) as Node, {
|
||||
enter (_node) {
|
||||
if (_node.type !== 'CallExpression' || (_node as CallExpression).callee.type !== 'Identifier') { return }
|
||||
const node = _node as CallExpression & { start: number, end: number }
|
||||
const name = 'name' in node.callee && node.callee.name
|
||||
if (name === 'defineRouteRules') {
|
||||
const rulesString = js.code.slice(node.start, node.end)
|
||||
try {
|
||||
rule = JSON.parse(runInNewContext(rulesString.replace('defineRouteRules', 'JSON.stringify'), {}))
|
||||
} catch {
|
||||
throw new Error('[nuxt] Error parsing route rules. They should be JSON-serializable.')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
ruleCache[code] = rule
|
||||
return rule
|
||||
|
@ -122,6 +122,7 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
|
||||
for (const key in _route.value) {
|
||||
Object.defineProperty(route, key, {
|
||||
get: () => _route.value[key as keyof RouteLocation],
|
||||
enumerable: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -12,13 +12,10 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||
if (result === true) {
|
||||
return
|
||||
}
|
||||
if (import.meta.server) {
|
||||
return result
|
||||
}
|
||||
|
||||
const error = createError({
|
||||
statusCode: 404,
|
||||
statusMessage: `Page Not Found: ${to.fullPath}`,
|
||||
statusCode: (result && result.statusCode) || 404,
|
||||
statusMessage: (result && result.statusMessage) || `Page Not Found: ${to.fullPath}`,
|
||||
data: {
|
||||
path: to.fullPath,
|
||||
},
|
||||
@ -32,7 +29,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||
// We pretend to have navigated to the invalid route so
|
||||
// that the user can return to the previous page with
|
||||
// the back button.
|
||||
window.history.pushState({}, '', to.fullPath)
|
||||
window?.history.pushState({}, '', to.fullPath)
|
||||
})
|
||||
// We stop the navigation immediately before it resolves
|
||||
// if there is no other route matching it.
|
||||
|
@ -168,21 +168,23 @@ export async function augmentPages (routes: NuxtPage[], vfs: Record<string, stri
|
||||
return augmentedPages
|
||||
}
|
||||
|
||||
const SFC_SCRIPT_RE = /<script(?<attrs>[^>]*)>(?<content>[\s\S]*?)<\/script[^>]*>/i
|
||||
const SFC_SCRIPT_RE = /<script(?<attrs>[^>]*)>(?<content>[\s\S]*?)<\/script[^>]*>/gi
|
||||
export function extractScriptContent (html: string) {
|
||||
const groups = html.match(SFC_SCRIPT_RE)?.groups || {}
|
||||
|
||||
if (groups.content) {
|
||||
return {
|
||||
loader: groups.attrs.includes('tsx') ? 'tsx' : 'ts',
|
||||
code: groups.content.trim(),
|
||||
} as const
|
||||
const contents: Array<{ loader: 'tsx' | 'ts', code: string }> = []
|
||||
for (const match of html.matchAll(SFC_SCRIPT_RE)) {
|
||||
if (match?.groups?.content) {
|
||||
contents.push({
|
||||
loader: match.groups.attrs.includes('tsx') ? 'tsx' : 'ts',
|
||||
code: match.groups.content.trim(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
return contents
|
||||
}
|
||||
|
||||
const PAGE_META_RE = /definePageMeta\([\s\S]*?\)/
|
||||
const extractionKeys = ['name', 'path', 'alias', 'redirect'] as const
|
||||
const DYNAMIC_META_KEY = '__nuxt_dynamic_meta_key' as const
|
||||
|
||||
const pageContentsCache: Record<string, string> = {}
|
||||
@ -197,100 +199,101 @@ export async function getRouteMeta (contents: string, absolutePath: string): Pro
|
||||
if (absolutePath in metaCache) { return metaCache[absolutePath] }
|
||||
|
||||
const loader = getLoader(absolutePath)
|
||||
const script = !loader ? null : loader === 'vue' ? extractScriptContent(contents) : { code: contents, loader }
|
||||
if (!script) {
|
||||
const scriptBlocks = !loader ? null : loader === 'vue' ? extractScriptContent(contents) : [{ code: contents, loader }]
|
||||
if (!scriptBlocks) {
|
||||
metaCache[absolutePath] = {}
|
||||
return {}
|
||||
}
|
||||
|
||||
if (!PAGE_META_RE.test(script.code)) {
|
||||
metaCache[absolutePath] = {}
|
||||
return {}
|
||||
}
|
||||
|
||||
const js = await transform(script.code, { loader: script.loader })
|
||||
const ast = parse(js.code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
ranges: true,
|
||||
}) as unknown as Program
|
||||
|
||||
const extractedMeta = {} as Partial<Record<keyof NuxtPage, any>>
|
||||
const extractionKeys = ['name', 'path', 'alias', 'redirect'] as const
|
||||
const dynamicProperties = new Set<keyof NuxtPage>()
|
||||
|
||||
let foundMeta = false
|
||||
for (const script of scriptBlocks) {
|
||||
if (!PAGE_META_RE.test(script.code)) {
|
||||
continue
|
||||
}
|
||||
|
||||
walk(ast, {
|
||||
enter (node) {
|
||||
if (foundMeta) { return }
|
||||
const js = await transform(script.code, { loader: script.loader })
|
||||
const ast = parse(js.code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
ranges: true,
|
||||
}) as unknown as Program
|
||||
|
||||
if (node.type !== 'ExpressionStatement' || node.expression.type !== 'CallExpression' || node.expression.callee.type !== 'Identifier' || node.expression.callee.name !== 'definePageMeta') { return }
|
||||
const dynamicProperties = new Set<keyof NuxtPage>()
|
||||
|
||||
foundMeta = true
|
||||
const pageMetaArgument = ((node as ExpressionStatement).expression as CallExpression).arguments[0] as ObjectExpression
|
||||
let foundMeta = false
|
||||
|
||||
for (const key of extractionKeys) {
|
||||
const property = pageMetaArgument.properties.find(property => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === key) as Property
|
||||
if (!property) { continue }
|
||||
walk(ast, {
|
||||
enter (node) {
|
||||
if (foundMeta) { return }
|
||||
|
||||
if (property.value.type === 'ObjectExpression') {
|
||||
const valueString = js.code.slice(property.value.range![0], property.value.range![1])
|
||||
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}\`).`)
|
||||
dynamicProperties.add(key)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (node.type !== 'ExpressionStatement' || node.expression.type !== 'CallExpression' || node.expression.callee.type !== 'Identifier' || node.expression.callee.name !== 'definePageMeta') { return }
|
||||
|
||||
if (property.value.type === 'ArrayExpression') {
|
||||
const values: string[] = []
|
||||
for (const element of property.value.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}\`).`)
|
||||
foundMeta = true
|
||||
const pageMetaArgument = ((node as ExpressionStatement).expression as CallExpression).arguments[0] as ObjectExpression
|
||||
|
||||
for (const key of extractionKeys) {
|
||||
const property = pageMetaArgument.properties.find(property => property.type === 'Property' && property.key.type === 'Identifier' && property.key.name === key) as Property
|
||||
if (!property) { continue }
|
||||
|
||||
if (property.value.type === 'ObjectExpression') {
|
||||
const valueString = js.code.slice(property.value.range![0], property.value.range![1])
|
||||
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}\`).`)
|
||||
dynamicProperties.add(key)
|
||||
continue
|
||||
}
|
||||
values.push(element.value)
|
||||
}
|
||||
extractedMeta[key] = values
|
||||
continue
|
||||
|
||||
if (property.value.type === 'ArrayExpression') {
|
||||
const values: string[] = []
|
||||
for (const element of property.value.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}\`).`)
|
||||
dynamicProperties.add(key)
|
||||
continue
|
||||
}
|
||||
values.push(element.value)
|
||||
}
|
||||
extractedMeta[key] = values
|
||||
continue
|
||||
}
|
||||
|
||||
if (property.value.type !== 'Literal' || typeof property.value.value !== 'string') {
|
||||
console.debug(`[nuxt] 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
|
||||
}
|
||||
|
||||
if (property.value.type !== 'Literal' || typeof property.value.value !== 'string') {
|
||||
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`)
|
||||
dynamicProperties.add(key)
|
||||
continue
|
||||
for (const property of pageMetaArgument.properties) {
|
||||
if (property.type !== 'Property') {
|
||||
continue
|
||||
}
|
||||
const isIdentifierOrLiteral = property.key.type === 'Literal' || property.key.type === 'Identifier'
|
||||
if (!isIdentifierOrLiteral) {
|
||||
continue
|
||||
}
|
||||
const name = property.key.type === 'Identifier' ? property.key.name : String(property.value)
|
||||
if (!(extractionKeys as unknown as string[]).includes(name)) {
|
||||
dynamicProperties.add('meta')
|
||||
break
|
||||
}
|
||||
}
|
||||
extractedMeta[key] = property.value.value
|
||||
}
|
||||
|
||||
for (const property of pageMetaArgument.properties) {
|
||||
if (property.type !== 'Property') {
|
||||
continue
|
||||
if (dynamicProperties.size) {
|
||||
extractedMeta.meta ??= {}
|
||||
extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties
|
||||
}
|
||||
const isIdentifierOrLiteral = property.key.type === 'Literal' || property.key.type === 'Identifier'
|
||||
if (!isIdentifierOrLiteral) {
|
||||
continue
|
||||
}
|
||||
const name = property.key.type === 'Identifier' ? property.key.name : String(property.value)
|
||||
if (!(extractionKeys as unknown as string[]).includes(name)) {
|
||||
dynamicProperties.add('meta')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (dynamicProperties.size) {
|
||||
extractedMeta.meta ??= {}
|
||||
extractedMeta.meta[DYNAMIC_META_KEY] = dynamicProperties
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
metaCache[absolutePath] = extractedMeta
|
||||
return extractedMeta
|
||||
@ -518,7 +521,7 @@ export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> =
|
||||
metaImports.add(genImport(file, [{ name: 'default', as: pageImportName }]))
|
||||
}
|
||||
|
||||
const pageImport = page._sync && page.mode !== 'client' ? pageImportName : genDynamicImport(file, { interopDefault: true })
|
||||
const pageImport = page._sync && page.mode !== 'client' ? pageImportName : genDynamicImport(file)
|
||||
|
||||
const metaRoute: NormalizedRoute = {
|
||||
name: `${metaImportName}?.name ?? ${route.name}`,
|
||||
|
@ -2,7 +2,7 @@
|
||||
"pushed route, skips generation from file": [
|
||||
{
|
||||
"alias": "["pushed-route-alias"].concat(mockMeta?.alias || [])",
|
||||
"component": "() => import("pages/route-file.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/route-file.vue")",
|
||||
"meta": "{ ...(mockMeta || {}), ...{"someMetaData":true} }",
|
||||
"name": "mockMeta?.name ?? "pushed-route"",
|
||||
"path": "mockMeta?.path ?? "/"",
|
||||
@ -20,7 +20,7 @@
|
||||
"route.meta generated from file": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/page-with-meta.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/page-with-meta.vue")",
|
||||
"meta": "{ ...(mockMeta || {}), ...{"test":1} }",
|
||||
"name": "mockMeta?.name ?? "page-with-meta"",
|
||||
"path": "mockMeta?.path ?? "/page-with-meta"",
|
||||
@ -30,7 +30,7 @@
|
||||
"should allow pages with `:` in their path": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/test:name.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/test:name.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "test:name"",
|
||||
"path": "mockMeta?.path ?? "/test\\:name"",
|
||||
@ -46,7 +46,7 @@
|
||||
"children": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/param/index/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/param/index/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "param-index"",
|
||||
"path": "mockMeta?.path ?? """,
|
||||
@ -54,14 +54,14 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("layer/pages/param/index/sibling.vue").then(m => m.default || m)",
|
||||
"component": "() => import("layer/pages/param/index/sibling.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "param-index-sibling"",
|
||||
"path": "mockMeta?.path ?? "sibling"",
|
||||
"redirect": "mockMeta?.redirect",
|
||||
},
|
||||
],
|
||||
"component": "() => import("layer/pages/param/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("layer/pages/param/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? undefined",
|
||||
"path": "mockMeta?.path ?? """,
|
||||
@ -69,14 +69,14 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/param/sibling.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/param/sibling.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "param-sibling"",
|
||||
"path": "mockMeta?.path ?? "sibling"",
|
||||
"redirect": "mockMeta?.redirect",
|
||||
},
|
||||
],
|
||||
"component": "() => import("pages/param.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/param.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? undefined",
|
||||
"path": "mockMeta?.path ?? "/param"",
|
||||
@ -87,7 +87,7 @@
|
||||
"children": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("layer/pages/wrapper-expose/other/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("layer/pages/wrapper-expose/other/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "wrapper-expose-other"",
|
||||
"path": "mockMeta?.path ?? """,
|
||||
@ -95,14 +95,14 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/wrapper-expose/other/sibling.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/wrapper-expose/other/sibling.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "wrapper-expose-other-sibling"",
|
||||
"path": "mockMeta?.path ?? "sibling"",
|
||||
"redirect": "mockMeta?.redirect",
|
||||
},
|
||||
],
|
||||
"component": "() => import("pages/wrapper-expose/other.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/wrapper-expose/other.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? undefined",
|
||||
"path": "mockMeta?.path ?? "/wrapper-expose/other"",
|
||||
@ -112,7 +112,7 @@
|
||||
"should extract serializable values and override fallback when normalized with `overrideMeta: true`": [
|
||||
{
|
||||
"alias": "["sweet-home"].concat(mockMeta?.alias || [])",
|
||||
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "home"",
|
||||
"path": "mockMeta?.path ?? "/"",
|
||||
@ -122,7 +122,7 @@
|
||||
"should generate correct catch-all route": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/[...slug].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[...slug].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "slug"",
|
||||
"path": "mockMeta?.path ?? "/:slug(.*)*"",
|
||||
@ -130,7 +130,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "index"",
|
||||
"path": "mockMeta?.path ?? "/"",
|
||||
@ -140,7 +140,7 @@
|
||||
"should generate correct dynamic routes": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "index"",
|
||||
"path": "mockMeta?.path ?? "/"",
|
||||
@ -148,7 +148,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/[slug].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[slug].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "slug"",
|
||||
"path": "mockMeta?.path ?? "/:slug()"",
|
||||
@ -159,14 +159,14 @@
|
||||
"children": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/[[foo]]/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[[foo]]/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "foo"",
|
||||
"path": "mockMeta?.path ?? """,
|
||||
"redirect": "mockMeta?.redirect",
|
||||
},
|
||||
],
|
||||
"component": "() => import("pages/[[foo]]").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[[foo]]")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? undefined",
|
||||
"path": "mockMeta?.path ?? "/:foo?"",
|
||||
@ -174,7 +174,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/optional/[[opt]].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/optional/[[opt]].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "optional-opt"",
|
||||
"path": "mockMeta?.path ?? "/optional/:opt?"",
|
||||
@ -182,7 +182,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/optional/prefix-[[opt]].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/optional/prefix-[[opt]].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "optional-prefix-opt"",
|
||||
"path": "mockMeta?.path ?? "/optional/prefix-:opt?"",
|
||||
@ -190,7 +190,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/optional/[[opt]]-postfix.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/optional/[[opt]]-postfix.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "optional-opt-postfix"",
|
||||
"path": "mockMeta?.path ?? "/optional/:opt?-postfix"",
|
||||
@ -198,7 +198,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/optional/prefix-[[opt]]-postfix.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/optional/prefix-[[opt]]-postfix.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "optional-prefix-opt-postfix"",
|
||||
"path": "mockMeta?.path ?? "/optional/prefix-:opt?-postfix"",
|
||||
@ -206,7 +206,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/[bar]/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[bar]/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "bar"",
|
||||
"path": "mockMeta?.path ?? "/:bar()"",
|
||||
@ -214,7 +214,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/nonopt/[slug].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/nonopt/[slug].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "nonopt-slug"",
|
||||
"path": "mockMeta?.path ?? "/nonopt/:slug()"",
|
||||
@ -222,7 +222,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/opt/[[slug]].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/opt/[[slug]].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "opt-slug"",
|
||||
"path": "mockMeta?.path ?? "/opt/:slug?"",
|
||||
@ -230,7 +230,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/[[sub]]/route-[slug].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[[sub]]/route-[slug].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "sub-route-slug"",
|
||||
"path": "mockMeta?.path ?? "/:sub?/route-:slug()"",
|
||||
@ -240,7 +240,7 @@
|
||||
"should generate correct id for catchall (order 1)": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/[...stories].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[...stories].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "stories"",
|
||||
"path": "mockMeta?.path ?? "/:stories(.*)*"",
|
||||
@ -248,7 +248,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/stories/[id].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/stories/[id].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "stories-id"",
|
||||
"path": "mockMeta?.path ?? "/stories/:id()"",
|
||||
@ -258,7 +258,7 @@
|
||||
"should generate correct id for catchall (order 2)": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/stories/[id].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/stories/[id].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "stories-id"",
|
||||
"path": "mockMeta?.path ?? "/stories/:id()"",
|
||||
@ -266,7 +266,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/[...stories].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[...stories].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "stories"",
|
||||
"path": "mockMeta?.path ?? "/:stories(.*)*"",
|
||||
@ -276,7 +276,7 @@
|
||||
"should generate correct route for kebab-case file": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/kebab-case.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/kebab-case.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "kebab-case"",
|
||||
"path": "mockMeta?.path ?? "/kebab-case"",
|
||||
@ -286,7 +286,7 @@
|
||||
"should generate correct route for snake_case file": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/snake_case.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/snake_case.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "snake_case"",
|
||||
"path": "mockMeta?.path ?? "/snake_case"",
|
||||
@ -296,7 +296,7 @@
|
||||
"should generate correct routes for index pages": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "index"",
|
||||
"path": "mockMeta?.path ?? "/"",
|
||||
@ -304,7 +304,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/parent/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/parent/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "parent"",
|
||||
"path": "mockMeta?.path ?? "/parent"",
|
||||
@ -312,7 +312,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/parent/child/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/parent/child/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "parent-child"",
|
||||
"path": "mockMeta?.path ?? "/parent/child"",
|
||||
@ -325,14 +325,14 @@
|
||||
"children": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/parent/child.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/parent/child.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "parent-child"",
|
||||
"path": "mockMeta?.path ?? "child"",
|
||||
"redirect": "mockMeta?.redirect",
|
||||
},
|
||||
],
|
||||
"component": "() => import("pages/parent.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/parent.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "parent"",
|
||||
"path": "mockMeta?.path ?? "/parent"",
|
||||
@ -342,7 +342,7 @@
|
||||
"should handle route groups": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/(foo)/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/(foo)/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "index"",
|
||||
"path": "mockMeta?.path ?? "/"",
|
||||
@ -353,14 +353,14 @@
|
||||
"children": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/(bar)/about/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/(bar)/about/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "about"",
|
||||
"path": "mockMeta?.path ?? """,
|
||||
"redirect": "mockMeta?.redirect",
|
||||
},
|
||||
],
|
||||
"component": "() => import("pages/(foo)/about.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/(foo)/about.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? undefined",
|
||||
"path": "mockMeta?.path ?? "/about"",
|
||||
@ -373,14 +373,14 @@
|
||||
"children": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/index/index/all.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/index/index/all.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "index-index-all"",
|
||||
"path": "mockMeta?.path ?? "all"",
|
||||
"redirect": "mockMeta?.redirect",
|
||||
},
|
||||
],
|
||||
"component": "() => import("pages/index/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/index/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "index"",
|
||||
"path": "mockMeta?.path ?? "/"",
|
||||
@ -390,7 +390,7 @@
|
||||
"should merge route.meta with meta from file": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/page-with-meta.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/page-with-meta.vue")",
|
||||
"meta": "{ ...(mockMeta || {}), ...{"test":1} }",
|
||||
"name": "mockMeta?.name ?? "page-with-meta"",
|
||||
"path": "mockMeta?.path ?? "/page-with-meta"",
|
||||
@ -400,7 +400,7 @@
|
||||
"should not generate colliding route names when hyphens are in file name": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/parent/[child].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/parent/[child].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "parent-child"",
|
||||
"path": "mockMeta?.path ?? "/parent/:child()"",
|
||||
@ -408,7 +408,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/parent-[child].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/parent-[child].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "parent-child"",
|
||||
"path": "mockMeta?.path ?? "/parent-:child()"",
|
||||
@ -418,7 +418,7 @@
|
||||
"should not merge required param as a child of optional param": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/[[foo]].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[[foo]].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "foo"",
|
||||
"path": "mockMeta?.path ?? "/:foo?"",
|
||||
@ -426,7 +426,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/[foo].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[foo].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "foo"",
|
||||
"path": "mockMeta?.path ?? "/:foo()"",
|
||||
@ -436,7 +436,7 @@
|
||||
"should only allow "_" & "." as special character for dynamic route": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/[a1_1a].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[a1_1a].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "a1_1a"",
|
||||
"path": "mockMeta?.path ?? "/:a1_1a()"",
|
||||
@ -444,7 +444,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/[b2.2b].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[b2.2b].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "b2.2b"",
|
||||
"path": "mockMeta?.path ?? "/:b2.2b()"",
|
||||
@ -452,7 +452,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/[b2]_[2b].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[b2]_[2b].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "b2_2b"",
|
||||
"path": "mockMeta?.path ?? "/:b2()_:2b()"",
|
||||
@ -460,7 +460,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/[[c3@3c]].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[[c3@3c]].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "c33c"",
|
||||
"path": "mockMeta?.path ?? "/:c33c?"",
|
||||
@ -468,7 +468,7 @@
|
||||
},
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/[[d4-4d]].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[[d4-4d]].vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "d44d"",
|
||||
"path": "mockMeta?.path ?? "/:d44d?"",
|
||||
@ -478,7 +478,7 @@
|
||||
"should properly override route name if definePageMeta name override is defined.": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "home"",
|
||||
"path": "mockMeta?.path ?? "/"",
|
||||
@ -488,7 +488,7 @@
|
||||
"should use fallbacks when normalized with `overrideMeta: true`": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "index"",
|
||||
"path": "mockMeta?.path ?? "/"",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"pushed route, skips generation from file": [
|
||||
{
|
||||
"alias": "["pushed-route-alias"]",
|
||||
"component": "() => import("pages/route-file.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/route-file.vue")",
|
||||
"meta": "{"someMetaData":true}",
|
||||
"name": ""pushed-route"",
|
||||
"path": ""/"",
|
||||
@ -18,7 +18,7 @@
|
||||
],
|
||||
"route.meta generated from file": [
|
||||
{
|
||||
"component": "() => import("pages/page-with-meta.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/page-with-meta.vue")",
|
||||
"meta": "{"test":1}",
|
||||
"name": ""page-with-meta"",
|
||||
"path": ""/page-with-meta"",
|
||||
@ -26,7 +26,7 @@
|
||||
],
|
||||
"should allow pages with `:` in their path": [
|
||||
{
|
||||
"component": "() => import("pages/test:name.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/test:name.vue")",
|
||||
"name": ""test:name"",
|
||||
"path": ""/test\\:name"",
|
||||
},
|
||||
@ -37,44 +37,44 @@
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"component": "() => import("pages/param/index/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/param/index/index.vue")",
|
||||
"name": ""param-index"",
|
||||
"path": """",
|
||||
},
|
||||
{
|
||||
"component": "() => import("layer/pages/param/index/sibling.vue").then(m => m.default || m)",
|
||||
"component": "() => import("layer/pages/param/index/sibling.vue")",
|
||||
"name": ""param-index-sibling"",
|
||||
"path": ""sibling"",
|
||||
},
|
||||
],
|
||||
"component": "() => import("layer/pages/param/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("layer/pages/param/index.vue")",
|
||||
"name": "mockMeta?.name",
|
||||
"path": """",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/param/sibling.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/param/sibling.vue")",
|
||||
"name": ""param-sibling"",
|
||||
"path": ""sibling"",
|
||||
},
|
||||
],
|
||||
"component": "() => import("pages/param.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/param.vue")",
|
||||
"name": "mockMeta?.name",
|
||||
"path": ""/param"",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"component": "() => import("layer/pages/wrapper-expose/other/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("layer/pages/wrapper-expose/other/index.vue")",
|
||||
"name": ""wrapper-expose-other"",
|
||||
"path": """",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/wrapper-expose/other/sibling.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/wrapper-expose/other/sibling.vue")",
|
||||
"name": ""wrapper-expose-other-sibling"",
|
||||
"path": ""sibling"",
|
||||
},
|
||||
],
|
||||
"component": "() => import("pages/wrapper-expose/other.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/wrapper-expose/other.vue")",
|
||||
"name": "mockMeta?.name",
|
||||
"path": ""/wrapper-expose/other"",
|
||||
},
|
||||
@ -82,7 +82,7 @@
|
||||
"should extract serializable values and override fallback when normalized with `overrideMeta: true`": [
|
||||
{
|
||||
"alias": "["sweet-home"]",
|
||||
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": ""home"",
|
||||
"path": ""/"",
|
||||
@ -91,131 +91,131 @@
|
||||
],
|
||||
"should generate correct catch-all route": [
|
||||
{
|
||||
"component": "() => import("pages/[...slug].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[...slug].vue")",
|
||||
"name": ""slug"",
|
||||
"path": ""/:slug(.*)*"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/index.vue")",
|
||||
"name": ""index"",
|
||||
"path": ""/"",
|
||||
},
|
||||
],
|
||||
"should generate correct dynamic routes": [
|
||||
{
|
||||
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/index.vue")",
|
||||
"name": ""index"",
|
||||
"path": ""/"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/[slug].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[slug].vue")",
|
||||
"name": ""slug"",
|
||||
"path": ""/:slug()"",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"component": "() => import("pages/[[foo]]/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[[foo]]/index.vue")",
|
||||
"name": ""foo"",
|
||||
"path": """",
|
||||
},
|
||||
],
|
||||
"component": "() => import("pages/[[foo]]").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[[foo]]")",
|
||||
"name": "mockMeta?.name",
|
||||
"path": ""/:foo?"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/optional/[[opt]].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/optional/[[opt]].vue")",
|
||||
"name": ""optional-opt"",
|
||||
"path": ""/optional/:opt?"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/optional/prefix-[[opt]].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/optional/prefix-[[opt]].vue")",
|
||||
"name": ""optional-prefix-opt"",
|
||||
"path": ""/optional/prefix-:opt?"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/optional/[[opt]]-postfix.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/optional/[[opt]]-postfix.vue")",
|
||||
"name": ""optional-opt-postfix"",
|
||||
"path": ""/optional/:opt?-postfix"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/optional/prefix-[[opt]]-postfix.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/optional/prefix-[[opt]]-postfix.vue")",
|
||||
"name": ""optional-prefix-opt-postfix"",
|
||||
"path": ""/optional/prefix-:opt?-postfix"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/[bar]/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[bar]/index.vue")",
|
||||
"name": ""bar"",
|
||||
"path": ""/:bar()"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/nonopt/[slug].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/nonopt/[slug].vue")",
|
||||
"name": ""nonopt-slug"",
|
||||
"path": ""/nonopt/:slug()"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/opt/[[slug]].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/opt/[[slug]].vue")",
|
||||
"name": ""opt-slug"",
|
||||
"path": ""/opt/:slug?"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/[[sub]]/route-[slug].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[[sub]]/route-[slug].vue")",
|
||||
"name": ""sub-route-slug"",
|
||||
"path": ""/:sub?/route-:slug()"",
|
||||
},
|
||||
],
|
||||
"should generate correct id for catchall (order 1)": [
|
||||
{
|
||||
"component": "() => import("pages/[...stories].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[...stories].vue")",
|
||||
"name": ""stories"",
|
||||
"path": ""/:stories(.*)*"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/stories/[id].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/stories/[id].vue")",
|
||||
"name": ""stories-id"",
|
||||
"path": ""/stories/:id()"",
|
||||
},
|
||||
],
|
||||
"should generate correct id for catchall (order 2)": [
|
||||
{
|
||||
"component": "() => import("pages/stories/[id].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/stories/[id].vue")",
|
||||
"name": ""stories-id"",
|
||||
"path": ""/stories/:id()"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/[...stories].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[...stories].vue")",
|
||||
"name": ""stories"",
|
||||
"path": ""/:stories(.*)*"",
|
||||
},
|
||||
],
|
||||
"should generate correct route for kebab-case file": [
|
||||
{
|
||||
"component": "() => import("pages/kebab-case.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/kebab-case.vue")",
|
||||
"name": ""kebab-case"",
|
||||
"path": ""/kebab-case"",
|
||||
},
|
||||
],
|
||||
"should generate correct route for snake_case file": [
|
||||
{
|
||||
"component": "() => import("pages/snake_case.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/snake_case.vue")",
|
||||
"name": ""snake_case"",
|
||||
"path": ""/snake_case"",
|
||||
},
|
||||
],
|
||||
"should generate correct routes for index pages": [
|
||||
{
|
||||
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/index.vue")",
|
||||
"name": ""index"",
|
||||
"path": ""/"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/parent/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/parent/index.vue")",
|
||||
"name": ""parent"",
|
||||
"path": ""/parent"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/parent/child/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/parent/child/index.vue")",
|
||||
"name": ""parent-child"",
|
||||
"path": ""/parent/child"",
|
||||
},
|
||||
@ -224,31 +224,31 @@
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"component": "() => import("pages/parent/child.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/parent/child.vue")",
|
||||
"name": ""parent-child"",
|
||||
"path": ""child"",
|
||||
},
|
||||
],
|
||||
"component": "() => import("pages/parent.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/parent.vue")",
|
||||
"name": ""parent"",
|
||||
"path": ""/parent"",
|
||||
},
|
||||
],
|
||||
"should handle route groups": [
|
||||
{
|
||||
"component": "() => import("pages/(foo)/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/(foo)/index.vue")",
|
||||
"name": ""index"",
|
||||
"path": ""/"",
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"component": "() => import("pages/(bar)/about/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/(bar)/about/index.vue")",
|
||||
"name": ""about"",
|
||||
"path": """",
|
||||
},
|
||||
],
|
||||
"component": "() => import("pages/(foo)/about.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/(foo)/about.vue")",
|
||||
"name": "mockMeta?.name",
|
||||
"path": ""/about"",
|
||||
},
|
||||
@ -257,19 +257,19 @@
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"component": "() => import("pages/index/index/all.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/index/index/all.vue")",
|
||||
"name": ""index-index-all"",
|
||||
"path": ""all"",
|
||||
},
|
||||
],
|
||||
"component": "() => import("pages/index/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/index/index.vue")",
|
||||
"name": ""index"",
|
||||
"path": ""/"",
|
||||
},
|
||||
],
|
||||
"should merge route.meta with meta from file": [
|
||||
{
|
||||
"component": "() => import("pages/page-with-meta.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/page-with-meta.vue")",
|
||||
"meta": "{ ...(mockMeta || {}), ...{"test":1} }",
|
||||
"name": ""page-with-meta"",
|
||||
"path": ""/page-with-meta"",
|
||||
@ -277,58 +277,58 @@
|
||||
],
|
||||
"should not generate colliding route names when hyphens are in file name": [
|
||||
{
|
||||
"component": "() => import("pages/parent/[child].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/parent/[child].vue")",
|
||||
"name": ""parent-child"",
|
||||
"path": ""/parent/:child()"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/parent-[child].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/parent-[child].vue")",
|
||||
"name": ""parent-child"",
|
||||
"path": ""/parent-:child()"",
|
||||
},
|
||||
],
|
||||
"should not merge required param as a child of optional param": [
|
||||
{
|
||||
"component": "() => import("pages/[[foo]].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[[foo]].vue")",
|
||||
"name": ""foo"",
|
||||
"path": ""/:foo?"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/[foo].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[foo].vue")",
|
||||
"name": ""foo"",
|
||||
"path": ""/:foo()"",
|
||||
},
|
||||
],
|
||||
"should only allow "_" & "." as special character for dynamic route": [
|
||||
{
|
||||
"component": "() => import("pages/[a1_1a].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[a1_1a].vue")",
|
||||
"name": ""a1_1a"",
|
||||
"path": ""/:a1_1a()"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/[b2.2b].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[b2.2b].vue")",
|
||||
"name": ""b2.2b"",
|
||||
"path": ""/:b2.2b()"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/[b2]_[2b].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[b2]_[2b].vue")",
|
||||
"name": ""b2_2b"",
|
||||
"path": ""/:b2()_:2b()"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/[[c3@3c]].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[[c3@3c]].vue")",
|
||||
"name": ""c33c"",
|
||||
"path": ""/:c33c?"",
|
||||
},
|
||||
{
|
||||
"component": "() => import("pages/[[d4-4d]].vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/[[d4-4d]].vue")",
|
||||
"name": ""d44d"",
|
||||
"path": ""/:d44d?"",
|
||||
},
|
||||
],
|
||||
"should properly override route name if definePageMeta name override is defined.": [
|
||||
{
|
||||
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/index.vue")",
|
||||
"name": ""home"",
|
||||
"path": ""/"",
|
||||
},
|
||||
@ -336,7 +336,7 @@
|
||||
"should use fallbacks when normalized with `overrideMeta: true`": [
|
||||
{
|
||||
"alias": "mockMeta?.alias || []",
|
||||
"component": "() => import("pages/index.vue").then(m => m.default || m)",
|
||||
"component": "() => import("pages/index.vue")",
|
||||
"meta": "mockMeta || {}",
|
||||
"name": "mockMeta?.name ?? "index"",
|
||||
"path": ""/"",
|
||||
|
@ -18,10 +18,11 @@ describe('imports:transform', () => {
|
||||
]
|
||||
|
||||
const ctx = createUnimport({
|
||||
injectAtEnd: true,
|
||||
imports,
|
||||
})
|
||||
|
||||
const transformPlugin = TransformPlugin.raw({ ctx, options: { transform: { exclude: [/node_modules/] } } }, { framework: 'rollup' }) as Plugin
|
||||
const transformPlugin = TransformPlugin({ ctx, options: { transform: { exclude: [/node_modules/] } } }).raw({}, { framework: 'rollup' }) as Plugin
|
||||
const transform = async (source: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
const result = await (transformPlugin.transform! as Function).call({ error: null, warn: null } as any, source, '')
|
||||
@ -171,7 +172,6 @@ const excludedVueHelpers = [
|
||||
'hydrate',
|
||||
'initDirectivesForSSR',
|
||||
'render',
|
||||
'useCssVars',
|
||||
'vModelCheckbox',
|
||||
'vModelDynamic',
|
||||
'vModelRadio',
|
||||
@ -183,6 +183,13 @@ const excludedVueHelpers = [
|
||||
'ErrorCodes',
|
||||
'TrackOpTypes',
|
||||
'TriggerOpTypes',
|
||||
'useHost',
|
||||
'hydrateOnVisible',
|
||||
'hydrateOnMediaQuery',
|
||||
'hydrateOnInteraction',
|
||||
'hydrateOnIdle',
|
||||
'onWatcherCleanup',
|
||||
'getCurrentWatcher',
|
||||
]
|
||||
|
||||
describe('imports:vue', () => {
|
||||
|
@ -15,7 +15,8 @@ describe('components:transform', () => {
|
||||
|
||||
const code = await transform('import { Foo, Bar } from \'#components\'', '/app.vue')
|
||||
expect(code).toMatchInlineSnapshot(`
|
||||
"import Foo from '/Foo.vue';
|
||||
"
|
||||
import Foo from '/Foo.vue';
|
||||
import { Bar } from '/Bar.vue';
|
||||
"
|
||||
`)
|
||||
@ -28,7 +29,8 @@ describe('components:transform', () => {
|
||||
|
||||
const code = await transform('import { Foo, LazyFoo } from \'#components\'', '/app.vue')
|
||||
expect(code).toMatchInlineSnapshot(`
|
||||
"import Foo from '/Foo.vue?nuxt_component=server&nuxt_component_name=Foo&nuxt_component_export=default';
|
||||
"
|
||||
import Foo from '/Foo.vue?nuxt_component=server&nuxt_component_name=Foo&nuxt_component_export=default';
|
||||
import LazyFoo from '/Foo.vue?nuxt_component=server,async&nuxt_component_name=Foo&nuxt_component_export=default';
|
||||
"
|
||||
`)
|
||||
@ -54,7 +56,8 @@ describe('components:transform', () => {
|
||||
|
||||
const code = await transform('import { Foo, LazyFoo } from \'#components\'', '/app.vue')
|
||||
expect(code).toMatchInlineSnapshot(`
|
||||
"import Foo from '/Foo.vue?nuxt_component=client&nuxt_component_name=Foo&nuxt_component_export=default';
|
||||
"
|
||||
import Foo from '/Foo.vue?nuxt_component=client&nuxt_component_name=Foo&nuxt_component_export=default';
|
||||
import LazyFoo from '/Foo.vue?nuxt_component=client,async&nuxt_component_name=Foo&nuxt_component_export=default';
|
||||
"
|
||||
`)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { normalize } from 'pathe'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { ImportProtectionPlugin, nuxtImportProtections } from '../src/core/plugins/import-protection'
|
||||
import { ImpoundPlugin } from 'impound'
|
||||
import { nuxtImportProtections } from '../src/core/plugins/import-protection'
|
||||
import type { NuxtOptions } from '../schema'
|
||||
|
||||
const testsToTriggerOn = [
|
||||
@ -39,9 +39,8 @@ describe('import protection', () => {
|
||||
})
|
||||
|
||||
const transformWithImportProtection = (id: string, importer: string) => {
|
||||
const plugin = ImportProtectionPlugin.rollup({
|
||||
rootDir: '/root',
|
||||
modulesDir: [fileURLToPath(new URL('..', import.meta.url))],
|
||||
const plugin = ImpoundPlugin.rollup({
|
||||
cwd: '/root',
|
||||
patterns: nuxtImportProtections({
|
||||
options: {
|
||||
modules: ['some-nuxt-module'],
|
||||
@ -51,5 +50,5 @@ const transformWithImportProtection = (id: string, importer: string) => {
|
||||
}),
|
||||
})
|
||||
|
||||
return (plugin as any).resolveId(id, importer)
|
||||
return (plugin as any).resolveId.call({ error: () => {} }, id, importer)
|
||||
}
|
||||
|
@ -58,6 +58,41 @@ describe('page metadata', () => {
|
||||
`)
|
||||
})
|
||||
|
||||
it('should extract serialisable metadata from files with multiple blocks', async () => {
|
||||
const meta = await getRouteMeta(`
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'thing'
|
||||
}
|
||||
</script>
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
name: 'some-custom-name',
|
||||
path: '/some-custom-path',
|
||||
validate: () => true,
|
||||
middleware: [
|
||||
function () {},
|
||||
],
|
||||
otherValue: {
|
||||
foo: 'bar',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
`, filePath)
|
||||
|
||||
expect(meta).toMatchInlineSnapshot(`
|
||||
{
|
||||
"meta": {
|
||||
"__nuxt_dynamic_meta_key": Set {
|
||||
"meta",
|
||||
},
|
||||
},
|
||||
"name": "some-custom-name",
|
||||
"path": "/some-custom-path",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('should extract serialisable metadata in options api', async () => {
|
||||
const meta = await getRouteMeta(`
|
||||
<script>
|
||||
@ -135,21 +170,21 @@ describe('normalizeRoutes', () => {
|
||||
|
||||
const { routes, imports } = normalizeRoutes([page], new Set(), true)
|
||||
expect({ routes, imports }).toMatchInlineSnapshot(`
|
||||
{
|
||||
"imports": Set {
|
||||
"import { default as indexN6pT4Un8hYMeta } from "/app/pages/index.vue?macro=true";",
|
||||
},
|
||||
"routes": "[
|
||||
{
|
||||
"imports": Set {
|
||||
"import { default as indexN6pT4Un8hYMeta } from "/app/pages/index.vue?macro=true";",
|
||||
},
|
||||
"routes": "[
|
||||
{
|
||||
name: "some-custom-name",
|
||||
path: indexN6pT4Un8hYMeta?.path ?? "/",
|
||||
meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} },
|
||||
redirect: "/",
|
||||
component: () => import("/app/pages/index.vue").then(m => m.default || m)
|
||||
}
|
||||
]",
|
||||
name: "some-custom-name",
|
||||
path: indexN6pT4Un8hYMeta?.path ?? "/",
|
||||
meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} },
|
||||
redirect: "/",
|
||||
component: () => import("/app/pages/index.vue")
|
||||
}
|
||||
`)
|
||||
]",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('should produce valid route objects when used without extracted meta', () => {
|
||||
@ -160,21 +195,21 @@ describe('normalizeRoutes', () => {
|
||||
|
||||
const { routes, imports } = normalizeRoutes([page], new Set())
|
||||
expect({ routes, imports }).toMatchInlineSnapshot(`
|
||||
{
|
||||
"imports": Set {
|
||||
"import { default as indexN6pT4Un8hYMeta } from "/app/pages/index.vue?macro=true";",
|
||||
},
|
||||
"routes": "[
|
||||
{
|
||||
"imports": Set {
|
||||
"import { default as indexN6pT4Un8hYMeta } from "/app/pages/index.vue?macro=true";",
|
||||
},
|
||||
"routes": "[
|
||||
{
|
||||
name: indexN6pT4Un8hYMeta?.name ?? undefined,
|
||||
path: indexN6pT4Un8hYMeta?.path ?? "/",
|
||||
meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} },
|
||||
alias: indexN6pT4Un8hYMeta?.alias || [],
|
||||
redirect: indexN6pT4Un8hYMeta?.redirect,
|
||||
component: () => import("/app/pages/index.vue").then(m => m.default || m)
|
||||
}
|
||||
]",
|
||||
name: indexN6pT4Un8hYMeta?.name ?? undefined,
|
||||
path: indexN6pT4Un8hYMeta?.path ?? "/",
|
||||
meta: { ...(indexN6pT4Un8hYMeta || {}), ...{"layout":"test","foo":"bar"} },
|
||||
alias: indexN6pT4Un8hYMeta?.alias || [],
|
||||
redirect: indexN6pT4Un8hYMeta?.redirect,
|
||||
component: () => import("/app/pages/index.vue")
|
||||
}
|
||||
`)
|
||||
]",
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
@ -40,7 +40,6 @@ describe('plugin-metadata', () => {
|
||||
it('should overwrite invalid plugins', () => {
|
||||
const invalidPlugins = [
|
||||
'export const plugin = {}',
|
||||
'export default function (ctx, inject) {}',
|
||||
]
|
||||
for (const plugin of invalidPlugins) {
|
||||
expect(transformPlugin.transform.call({ parse }, plugin, 'my-plugin.mjs').code).toBe('export default () => {}')
|
||||
|
@ -241,6 +241,8 @@ it('components:scanComponents', async () => {
|
||||
for (const c of scannedComponents) {
|
||||
// @ts-expect-error filePath is not optional but we don't want it to be in the snapshot
|
||||
delete c.filePath
|
||||
// @ts-expect-error _scanned is added internally but we don't want it to be in the snapshot
|
||||
delete c._scanned
|
||||
}
|
||||
expect(scannedComponents).deep.eq(expectedComponents)
|
||||
})
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user