mirror of
https://github.com/nuxt/nuxt.git
synced 2025-01-30 15:22:39 +00:00
Merge branch 'main' into patch-21
This commit is contained in:
commit
c7e154e11a
@ -1,4 +1,4 @@
|
||||
FROM node:lts@sha256:35a5dd72bcac4bce43266408b58a02be6ff0b6098ffa6f5435aeea980a8951d7
|
||||
FROM node:lts@sha256:0e910f435308c36ea60b4cfd7b80208044d77a074d16b768a81901ce938a62dc
|
||||
|
||||
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 && \
|
||||
|
1
.github/assets/bluesky.svg
vendored
Normal file
1
.github/assets/bluesky.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="#A1A1AA" d="M12 11.388c-.906-1.761-3.372-5.044-5.665-6.662c-2.197-1.55-3.034-1.283-3.583-1.033C2.116 3.978 2 4.955 2 5.528c0 .575.315 4.709.52 5.4c.68 2.28 3.094 3.05 5.32 2.803c-3.26.483-6.157 1.67-2.36 5.898c4.178 4.325 5.726-.927 6.52-3.59c.794 2.663 1.708 7.726 6.444 3.59c3.556-3.59.977-5.415-2.283-5.898c2.225.247 4.64-.523 5.319-2.803c.205-.69.52-4.825.52-5.399c0-.575-.116-1.55-.752-1.838c-.549-.248-1.386-.517-3.583 1.033c-2.293 1.621-4.76 4.904-5.665 6.664"/></svg>
|
After Width: | Height: | Size: 571 B |
10
.github/codeql/codeql-config.yml
vendored
10
.github/codeql/codeql-config.yml
vendored
@ -1,10 +0,0 @@
|
||||
paths:
|
||||
- 'packages/*/dist/**'
|
||||
- 'packages/nuxt/bin/**'
|
||||
- 'packages/schema/schema/**'
|
||||
paths-ignore:
|
||||
- 'test/**'
|
||||
- '**/*.test.js'
|
||||
- '**/*.test.ts'
|
||||
- '**/*.test.tsx'
|
||||
- '**/__tests__/**'
|
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
@ -60,7 +60,7 @@ jobs:
|
||||
run: pnpm test:attw
|
||||
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
retention-days: 3
|
||||
name: dist
|
||||
@ -69,6 +69,9 @@ jobs:
|
||||
codeql:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
language: ['javascript-typescript', 'actions']
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
@ -78,7 +81,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9
|
||||
uses: github/codeql-action/init@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||
with:
|
||||
config: |
|
||||
paths:
|
||||
@ -90,13 +93,14 @@ jobs:
|
||||
- '**/*.spec.ts'
|
||||
- '**/*.test.ts'
|
||||
- '**/__snapshots__/**'
|
||||
languages: javascript-typescript
|
||||
queries: +security-and-quality
|
||||
# codeql bug: #L20C9:9: A parse error occurred: `Unexpected token`.
|
||||
- 'packages/vite/src/runtime/vite-node.mjs'
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9
|
||||
uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||
with:
|
||||
category: "/language:javascript-typescript"
|
||||
category: "/language:${{ matrix.language }}"
|
||||
|
||||
typecheck:
|
||||
runs-on: ${{ matrix.os }}
|
||||
@ -251,7 +255,7 @@ jobs:
|
||||
TEST_PAYLOAD: ${{ matrix.payload }}
|
||||
SKIP_BUNDLE_SIZE: ${{ github.event_name != 'push' || matrix.env == 'dev' || matrix.builder == 'webpack' || matrix.context == 'default' || matrix.payload == 'js' || runner.os == 'Windows' }}
|
||||
|
||||
- uses: codecov/codecov-action@7f8b4b4bde536c465e797be725718b88c5d95e0e # v5.1.1
|
||||
- uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
|
||||
if: github.event_name != 'push' && matrix.env == 'built' && matrix.builder == 'vite' && matrix.context == 'default' && matrix.os == 'ubuntu-latest' && matrix.manifest == 'manifest-on'
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
2
.github/workflows/docs-check-links.yml
vendored
2
.github/workflows/docs-check-links.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Lychee link checker
|
||||
uses: lycheeverse/lychee-action@4aa18b6ccdac05029fab067313a6a04f941e6494 # for v1.8.0
|
||||
uses: lycheeverse/lychee-action@f796c8b7d468feb9b8c0a46da3fac0af6874d374 # for v1.8.0
|
||||
with:
|
||||
# arguments with file types to check
|
||||
args: >-
|
||||
|
4
.github/workflows/scorecards.yml
vendored
4
.github/workflows/scorecards.yml
vendored
@ -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@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.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@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9
|
||||
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||
if: github.repository == 'nuxt/nuxt' && success()
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -44,6 +44,9 @@ coverage
|
||||
# Intellij idea
|
||||
*.iml
|
||||
.idea
|
||||
!.idea/nuxt.iml
|
||||
!.idea/modules.xml
|
||||
!.idea/inspectionProfiles/Project_Default.xml
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
6
.idea/inspectionProfiles/Project_Default.xml
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/nuxt.iml" filepath="$PROJECT_DIR$/.idea/nuxt.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
18
.idea/nuxt.iml
Normal file
18
.idea/nuxt.iml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/nuxt/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/schema/schema" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/playground/.output" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/test/fixtures/minimal/.nuxt-inline" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/test/fixtures/minimal/.output" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/test/fixtures/minimal/.output-inline" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
@ -109,7 +109,7 @@ Follow the docs to [Set Up Your Local Development Environment](https://nuxt.com/
|
||||
## <a name="follow-us">🔗 Follow Us</a>
|
||||
|
||||
<p valign="center">
|
||||
<a href="https://go.nuxt.com/discord"><img width="20px" src="./.github/assets/discord.svg" alt="Discord"></a> <a href="https://go.nuxt.com/x"><img width="20px" src="./.github/assets/twitter.svg" alt="Twitter"></a> <a href="https://go.nuxt.com/github"><img width="20px" src="./.github/assets/github.svg" alt="GitHub"></a>
|
||||
<a href="https://go.nuxt.com/discord"><img width="20px" src="./.github/assets/discord.svg" alt="Discord"></a> <a href="https://go.nuxt.com/x"><img width="20px" src="./.github/assets/twitter.svg" alt="Twitter"></a> <a href="https://go.nuxt.com/github"><img width="20px" src="./.github/assets/github.svg" alt="GitHub"></a> <a href="https://go.nuxt.com/bluesky"><img width="20px" src="./.github/assets/bluesky.svg" alt="Bluesky"></a>
|
||||
</p>
|
||||
|
||||
## <a name="license">⚖️ License</a>
|
||||
|
@ -85,6 +85,9 @@ export default defineNuxtConfig({
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// features: {
|
||||
// inlineStyles: true
|
||||
// },
|
||||
// unhead: {
|
||||
// renderSSRHeadOptions: {
|
||||
// omitLineBreaks: false
|
||||
@ -281,6 +284,28 @@ export default defineNuxtConfig({
|
||||
})
|
||||
```
|
||||
|
||||
#### More Granular Inline Styles
|
||||
|
||||
🚦 **Impact Level**: Moderate
|
||||
|
||||
Nuxt will now only inline styles for Vue components, not global CSS.
|
||||
|
||||
##### What Changed
|
||||
|
||||
Previously, Nuxt would inline all CSS, including global styles, and remove `<link>` elements to separate CSS files. Now, Nuxt will only do this for Vue components (which previously produced separate chunks of CSS). We think this is a better balance of reducing separate network requests (just as before, there will not be separate requests for individual `.css` files per-page or per-component on the initial load), as well as allowing caching of a single global CSS file and reducing the document download size of the initial request.
|
||||
|
||||
##### Migration Steps
|
||||
|
||||
This feature is fully configurable and you can revert to the previous behavior by setting `inlineStyles: true` to inline global CSS as well as per-component CSS.
|
||||
|
||||
```ts twoslash [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
features: {
|
||||
inlineStyles: true
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### Scan Page Meta After Resolution
|
||||
|
||||
🚦 **Impact Level**: Minimal
|
||||
|
@ -159,7 +159,7 @@ prerenderRoutes(["/some/other/url"]);
|
||||
|
||||
This is called before prerendering for additional routes to be registered.
|
||||
|
||||
```ts [nitro.config.ts]
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
hooks: {
|
||||
async "prerender:routes"(ctx) {
|
||||
@ -178,7 +178,7 @@ export default defineNuxtConfig({
|
||||
|
||||
This is called for each route during prerendering. You can use this for fine grained handling of each route that gets prerendered.
|
||||
|
||||
```ts [nitro.config.ts]
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
nitro: {
|
||||
hooks: {
|
||||
|
@ -143,6 +143,28 @@ export default defineNuxtConfig({
|
||||
|
||||
This will disable auto-imports completely but it's still possible to use [explicit imports](#explicit-imports) from `#imports`.
|
||||
|
||||
### Partially Disabling Auto-imports
|
||||
|
||||
If you want framework-specific functions like `ref` to remain auto-imported but wish to disable auto-imports for your own code (e.g., custom composables), you can set the `imports.scan` option to `false` in your `nuxt.config.ts` file:
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
imports: {
|
||||
scan: false
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
With this configuration:
|
||||
- Framework functions like `ref`, `computed`, or `watch` will still work without needing manual imports.
|
||||
- Custom code, such as composables, will need to be manually imported in your files.
|
||||
|
||||
::warning
|
||||
**Caution:** This setup has certain limitations:
|
||||
- If you structure your project with layers, you will need to explicitly import the composables from each layer, rather than relying on auto-imports.
|
||||
- This breaks the layer system’s override feature. If you use `imports.scan: false`, ensure you understand this side-effect and adjust your architecture accordingly.
|
||||
::
|
||||
|
||||
## Auto-imported Components
|
||||
|
||||
Nuxt also automatically imports components from your `~/components` directory, although this is configured separately from auto-importing composables and utility functions.
|
||||
|
@ -8,9 +8,7 @@ description: "Nuxt supports ESLint out of the box"
|
||||
The recommended approach for Nuxt is to enable ESLint support using the [`@nuxt/eslint`](https://eslint.nuxt.com/packages/module) module, that will setup project-aware ESLint configuration for you.
|
||||
|
||||
:::callout{icon="i-ph-lightbulb"}
|
||||
The module is designed for the [new ESLint flat config format](https://eslint.org/docs/latest/use/configure/configuration-files-new) with is the [default format since ESLint v9](https://eslint.org/blog/2024/04/eslint-v9.0.0-released/).
|
||||
|
||||
If you are using the legacy `.eslintrc` config, you will need to [configure manually with `@nuxt/eslint-config`](https://eslint.nuxt.com/packages/config#legacy-config-format). We highly recommend you to migrate over the flat config to be future-proof.
|
||||
The module is designed for the [new ESLint flat config format](https://eslint.org/docs/latest/use/configure/configuration-files-new) with is the [default format since ESLint v9](https://eslint.org/blog/2024/04/eslint-v9.0.0-released/). If you are using the legacy `.eslintrc` config, you will need to [configure manually with `@nuxt/eslint-config`](https://eslint.nuxt.com/packages/config#legacy-config-format). We highly recommend you to migrate over the flat config to be future-proof.
|
||||
:::
|
||||
|
||||
## Quick Setup
|
||||
|
@ -266,17 +266,27 @@ console.log(route.meta.title) // My home page
|
||||
|
||||
If you are using nested routes, the page metadata from all these routes will be merged into a single object. For more on route meta, see the [vue-router docs](https://router.vuejs.org/guide/advanced/meta.html#route-meta-fields).
|
||||
|
||||
Much like `defineEmits` or `defineProps` (see [Vue docs](https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits)), `definePageMeta` is a **compiler macro**. It will be compiled away so you cannot reference it within your component. Instead, the metadata passed to it will be hoisted out of the component. Therefore, the page meta object cannot reference the component (or values defined on the component). However, it can reference imported bindings.
|
||||
Much like `defineEmits` or `defineProps` (see [Vue docs](https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits)), `definePageMeta` is a **compiler macro**. It will be compiled away so you cannot reference it within your component. Instead, the metadata passed to it will be hoisted out of the component.
|
||||
Therefore, the page meta object cannot reference the component. However, it can reference imported bindings, as well as locally defined **pure functions**.
|
||||
|
||||
::warning
|
||||
Make sure not to reference any reactive data or functions that cause side effects. This can lead to unexpected behavior.
|
||||
::
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { someData } from '~/utils/example'
|
||||
|
||||
function validateIdParam(route) {
|
||||
return route.params.id && !isNaN(Number(route.params.id))
|
||||
}
|
||||
|
||||
const title = ref('')
|
||||
|
||||
definePageMeta({
|
||||
title, // This will create an error
|
||||
someData
|
||||
validate: validateIdParam,
|
||||
someData,
|
||||
title, // do not do this, the ref will be hoisted out of the component
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
@ -16,7 +16,7 @@ You can also pass a function that receives the path of a Vue component and retur
|
||||
```ts [nuxt.config.ts]
|
||||
export default defineNuxtConfig({
|
||||
features: {
|
||||
inlineStyles: true // or a function to determine inlining
|
||||
inlineStyles: false // or a function to determine inlining
|
||||
}
|
||||
})
|
||||
```
|
||||
@ -74,6 +74,9 @@ export default defineNuxtConfig({
|
||||
}
|
||||
}
|
||||
},
|
||||
features: {
|
||||
inlineStyles: true
|
||||
},
|
||||
unhead: {
|
||||
renderSSRHeadOptions: {
|
||||
omitLineBreaks: false
|
||||
|
203
docs/2.guide/4.recipes/4.sessions-and-authentication.md
Normal file
203
docs/2.guide/4.recipes/4.sessions-and-authentication.md
Normal file
@ -0,0 +1,203 @@
|
||||
---
|
||||
title: 'Sessions and Authentication'
|
||||
description: "Authentication is an extremely common requirement in web apps. This recipe will show you how to implement basic user registration and authentication in your Nuxt app."
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
In this recipe we'll be setting up authentication in a full-stack Nuxt app using [Nuxt Auth Utils](https://github.com/Atinux/nuxt-auth-utils) which provides convenient utilities for managing client-side and server-side session data.
|
||||
|
||||
The module uses secured & sealed cookies to store session data, so you don't need to setup a database to store session data.
|
||||
|
||||
## Install nuxt-auth-utils
|
||||
|
||||
Install the `nuxt-auth-utils` module using the `nuxi` CLI.
|
||||
|
||||
```bash [Terminal]
|
||||
npx nuxi@latest module add auth-utils
|
||||
```
|
||||
|
||||
::callout
|
||||
This command will install `nuxt-auth-utils` as dependency and push it in the `modules` section of our `nuxt.config.ts`
|
||||
::
|
||||
|
||||
## Cookie Encryption Key
|
||||
|
||||
As `nuxt-auth-utils` uses sealed cookies to store session data, session cookies are encrypted using a secret key from the `NUXT_SESSION_PASSWORD` environment variable.
|
||||
|
||||
::note
|
||||
If not set, this environment variable will be added to your `.env` automatically when running in development mode.
|
||||
::
|
||||
|
||||
```dotenv [.env]
|
||||
NUXT_SESSION_PASSWORD=a-random-password-with-at-least-32-characters
|
||||
```
|
||||
|
||||
::important
|
||||
You'll need to add this environment variable to your production environment before deploying.
|
||||
::
|
||||
|
||||
## Login API Route
|
||||
|
||||
For this recipe, we'll create a simple API route to sign-in a user based on static data.
|
||||
|
||||
Let's create a `/api/login` API route that will accept a POST request with the email and password in the request body.
|
||||
|
||||
```ts [server/api/login.post.ts]
|
||||
import { z } from 'zod'
|
||||
|
||||
const bodySchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8)
|
||||
})
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { email, password } = await readValidatedBody(event, bodySchema.parse)
|
||||
|
||||
if (email === 'admin@admin.com' && password === 'iamtheadmin') {
|
||||
// set the user session in the cookie
|
||||
// this server util is auto-imported by the auth-utils module
|
||||
await setUserSession(event, {
|
||||
user: {
|
||||
name: 'John Doe'
|
||||
}
|
||||
})
|
||||
return {}
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
message: 'Bad credentials'
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
::callout
|
||||
Make sure to install the `zod` dependency in your project (`npm i zod`).
|
||||
::
|
||||
|
||||
::tip{to="https://github.com/atinux/nuxt-auth-utils#server-utils"}
|
||||
Read more about the `setUserSession` server helper exposed by `nuxt-auth-utils`.
|
||||
::
|
||||
|
||||
## Login Page
|
||||
|
||||
The module exposes a Vue composable to know if a user is authenticated in our application:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
const { loggedIn, session, user, clear, fetch } = useUserSession()
|
||||
</script>
|
||||
```
|
||||
|
||||
Let's create a login page with a form to submit the login data to our `/api/login` route.
|
||||
|
||||
```vue [pages/login.vue]
|
||||
<script setup lang="ts">
|
||||
const { loggedIn, user, fetch: refreshSession } = useUserSession()
|
||||
const credentials = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
async function login() {
|
||||
$fetch('/api/login', {
|
||||
method: 'POST',
|
||||
body: credentials
|
||||
})
|
||||
.then(async () => {
|
||||
// Refresh the session on client-side and redirect to the home page
|
||||
await refreshSession()
|
||||
await navigateTo('/')
|
||||
})
|
||||
.catch(() => alert('Bad credentials'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="login">
|
||||
<input v-model="credentials.email" type="email" placeholder="Email" />
|
||||
<input v-model="credentials.password" type="password" placeholder="Password" />
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Protect API Routes
|
||||
|
||||
Protecting server routes is key to making sure your data is safe. Client-side middleware is helpful for the user, but without server-side protection your data can still be accessed. It is critical to protect any routes with sensitive data, we should return a 401 error if the user is not logged in on those.
|
||||
|
||||
The `auth-utils` module provides the `requireUserSession` utility function to help make sure that users are logged in and have an active session.
|
||||
|
||||
Let's create an example of a `/api/user/stats` route that only authenticated users can access.
|
||||
|
||||
```ts [server/api/user/stats.get.ts]
|
||||
export default defineEventHandler(async (event) => {
|
||||
// make sure the user is logged in
|
||||
// This will throw a 401 error if the request doesn't come from a valid user session
|
||||
const { user } = await requireUserSession(event)
|
||||
|
||||
// TODO: Fetch some stats based on the user
|
||||
|
||||
return {}
|
||||
});
|
||||
```
|
||||
|
||||
## Protect App Routes
|
||||
|
||||
Our data is safe with the server-side route in place, but without doing anything else, unauthenticated users would probably get some odd data when trying to access the `/users` page. We should create a [client-side middleware](https://nuxt.com/docs/guide/directory-structure/middleware) to protect the route on the client side and redirect users to the login page.
|
||||
|
||||
`nuxt-auth-utils` provides a convenient `useUserSession` composable which we'll use to check if the user is logged in, and redirect them if they are not.
|
||||
|
||||
We'll create a middleware in the `/middleware` directory. Unlike on the server, client-side middleware is not automatically applied to all endpoints, and we'll need to specify where we want it applied.
|
||||
|
||||
```typescript [middleware/authenticated.ts]
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
const { loggedIn } = useUserSession()
|
||||
|
||||
// redirect the user to the login screen if they're not authenticated
|
||||
if (!loggedIn.value) {
|
||||
return navigateTo('/login')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Home Page
|
||||
|
||||
Now that we have our app middleware to protect our routes, we can use it on our home page that display our authenticated user informations. If the user is not authenticated, they will be redirected to the login page.
|
||||
|
||||
We'll use [`definePageMeta`](/docs/api/utils/define-page-meta) to apply the middleware to the route that we want to protect.
|
||||
|
||||
```vue [pages/index.vue]
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
middleware: ['authenticated'],
|
||||
})
|
||||
|
||||
const { user, clear: clearSession } = useUserSession()
|
||||
|
||||
async function logout() {
|
||||
await clearSession()
|
||||
await navigateTo('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>Welcome {{ user.name }}</h1>
|
||||
<button @click="logout">Logout</button>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
We also added a logout button to clear the session and redirect the user to the login page.
|
||||
|
||||
## Conclusion
|
||||
|
||||
We've successfully set up a very basic user authentication and session management in our Nuxt app. We've also protected sensitive routes on the server and client side to ensure that only authenticated users can access them.
|
||||
|
||||
As next steps, you can:
|
||||
- Add authentication using the [20+ supported OAuth providers](https://github.com/atinux/nuxt-auth-utils?tab=readme-ov-file#supported-oauth-providers)
|
||||
- Add a database to store users, see [Nitro SQL Database](https://nitro.build/guide/database) or [NuxtHub SQL Database](https://hub.nuxt.com/docs/features/database)
|
||||
- Let user signup with email & password using [password hashing](https://github.com/atinux/nuxt-auth-utils?tab=readme-ov-file#password-hashing)
|
||||
- Add support for [WebAuthn / Passkeys](https://github.com/atinux/nuxt-auth-utils?tab=readme-ov-file#webauthn-passkey)
|
||||
|
||||
Checkout the open source [atidone repository](https://github.com/atinux/atidone) for a full example of a Nuxt app with OAuth authentication, database and CRUD operations.
|
@ -8,16 +8,30 @@ links:
|
||||
size: xs
|
||||
---
|
||||
|
||||
<!--add-cmd-->
|
||||
```bash [Terminal]
|
||||
npx nuxi add [--cwd] [--force] <TEMPLATE> <NAME>
|
||||
npx nuxi add <TEMPLATE> <NAME> [--cwd=<directory>] [--logLevel=<silent|info|verbose>] [--force]
|
||||
```
|
||||
<!--/add-cmd-->
|
||||
|
||||
Option | Default | Description
|
||||
-------------------------|-----------------|------------------
|
||||
`TEMPLATE` | - | Specify a template of the file to be generated.
|
||||
`NAME` | - | Specify a name of the file that will be created.
|
||||
`--cwd` | `.` | The directory of the target application.
|
||||
`--force` | `false` | Force override file if it already exists.
|
||||
### Arguments
|
||||
|
||||
<!--add-args-->
|
||||
Argument | Description
|
||||
--- | ---
|
||||
`TEMPLATE` | Specify which template to generate (options: <api\|plugin\|component\|composable\|middleware\|layout\|page>)
|
||||
`NAME` | Specify name of the generated file
|
||||
<!--/add-args-->
|
||||
|
||||
### Options
|
||||
|
||||
<!--add-opts-->
|
||||
Option | Default | Description
|
||||
--- | --- | ---
|
||||
`--cwd=<directory>` | `.` | Specify the working directory
|
||||
`--logLevel=<silent\|info\|verbose>` | | Specify build-time log level
|
||||
`--force` | `false` | Force override file if it already exists
|
||||
<!--/add-opts-->
|
||||
|
||||
**Modifiers:**
|
||||
|
||||
|
@ -8,15 +8,33 @@ links:
|
||||
size: xs
|
||||
---
|
||||
|
||||
<!--analyze-cmd-->
|
||||
```bash [Terminal]
|
||||
npx nuxi analyze [--log-level] [rootDir]
|
||||
npx nuxi analyze [ROOTDIR] [--cwd=<directory>] [--logLevel=<silent|info|verbose>] [--dotenv] [--name=<name>] [--no-serve]
|
||||
```
|
||||
<!--/analyze-cmd-->
|
||||
|
||||
The `analyze` command builds Nuxt and analyzes the production bundle (experimental).
|
||||
|
||||
Option | Default | Description
|
||||
-------------------------|-----------------|------------------
|
||||
`rootDir` | `.` | The directory of the target application.
|
||||
## Arguments
|
||||
|
||||
<!--analyze-args-->
|
||||
Argument | Description
|
||||
--- | ---
|
||||
`ROOTDIR="."` | Specifies the working directory (default: `.`)
|
||||
<!--/analyze-args-->
|
||||
|
||||
## Options
|
||||
|
||||
<!--analyze-opts-->
|
||||
Option | Default | Description
|
||||
--- | --- | ---
|
||||
`--cwd=<directory>` | | Specify the working directory, this takes precedence over ROOTDIR (default: `.`)
|
||||
`--logLevel=<silent\|info\|verbose>` | | Specify build-time log level
|
||||
`--dotenv` | | Path to `.env` file to load, relative to the root directory
|
||||
`--name=<name>` | `default` | Name of the analysis
|
||||
`--no-serve` | | Skip serving the analysis results
|
||||
<!--/analyze-opts-->
|
||||
|
||||
::note
|
||||
This command sets `process.env.NODE_ENV` to `production`.
|
||||
|
@ -8,17 +8,35 @@ links:
|
||||
size: xs
|
||||
---
|
||||
|
||||
<!--build-module-cmd-->
|
||||
```bash [Terminal]
|
||||
npx nuxi build-module [--stub] [rootDir]
|
||||
npx nuxi build-module [ROOTDIR] [--cwd=<directory>] [--logLevel=<silent|info|verbose>] [--build] [--stub] [--sourcemap] [--prepare]
|
||||
```
|
||||
<!--/build-module-cmd-->
|
||||
|
||||
The `build-module` command runs `@nuxt/module-builder` to generate `dist` directory within your `rootDir` that contains the full build for your **nuxt-module**.
|
||||
|
||||
Option | Default | Description
|
||||
-------------------------|-----------------|------------------
|
||||
`rootDir` | `.` | The root directory of the module to bundle.
|
||||
`--stub` | `false` | Stub out your module for development using [jiti](https://github.com/unjs/jiti#jiti). (**note:** This is mainly for development purposes.)
|
||||
## Arguments
|
||||
|
||||
::read-more{to="https://github.com/nuxt/module-builder" icon="i-simple-icons-github" color="gray" target="_blank"}
|
||||
<!--build-module-args-->
|
||||
Argument | Description
|
||||
--- | ---
|
||||
`ROOTDIR="."` | Specifies the working directory (default: `.`)
|
||||
<!--/build-module-args-->
|
||||
|
||||
## Options
|
||||
|
||||
<!--build-module-opts-->
|
||||
Option | Default | Description
|
||||
--- | --- | ---
|
||||
`--cwd=<directory>` | | Specify the working directory, this takes precedence over ROOTDIR (default: `.`)
|
||||
`--logLevel=<silent\|info\|verbose>` | | Specify build-time log level
|
||||
`--build` | `false` | Build module for distribution
|
||||
`--stub` | `false` | Stub dist instead of actually building it for development
|
||||
`--sourcemap` | `false` | Generate sourcemaps
|
||||
`--prepare` | `false` | Prepare module for local development
|
||||
<!--/build-module-opts-->
|
||||
|
||||
::read-more{to="https://github.com/nuxt/module-builder" icon="i-simple-icons-github" color="gray" target="\_blank"}
|
||||
Read more about `@nuxt/module-builder`.
|
||||
::
|
||||
|
@ -8,19 +8,34 @@ links:
|
||||
size: xs
|
||||
---
|
||||
|
||||
<!--build-cmd-->
|
||||
```bash [Terminal]
|
||||
npx nuxi build [--prerender] [--preset] [--dotenv] [--log-level] [rootDir]
|
||||
npx nuxi build [ROOTDIR] [--cwd=<directory>] [--logLevel=<silent|info|verbose>] [--prerender] [--preset] [--dotenv] [--envName]
|
||||
```
|
||||
<!--/build-cmd-->
|
||||
|
||||
The `build` command creates a `.output` directory with all your application, server and dependencies ready for production.
|
||||
|
||||
Option | Default | Description
|
||||
-------------------------|-----------------|------------------
|
||||
`rootDir` | `.` | The root directory of the application to bundle.
|
||||
`--prerender` | `false` | Pre-render every route of your application. (**note:** This is an experimental flag. The behavior might be changed.)
|
||||
`--preset` | - | Set a [Nitro preset](https://nitro.unjs.io/deploy#changing-the-deployment-preset)
|
||||
`--dotenv` | `.` | Point to another `.env` file to load, **relative** to the root directory.
|
||||
`--log-level` | `info` | Specify build-time logging level, allowing `silent` \| `info` \| `verbose`.
|
||||
## Arguments
|
||||
|
||||
<!--build-args-->
|
||||
Argument | Description
|
||||
--- | ---
|
||||
`ROOTDIR="."` | Specifies the working directory (default: `.`)
|
||||
<!--/build-args-->
|
||||
|
||||
## Options
|
||||
|
||||
<!--build-opts-->
|
||||
Option | Default | Description
|
||||
--- | --- | ---
|
||||
`--cwd=<directory>` | | Specify the working directory, this takes precedence over ROOTDIR (default: `.`)
|
||||
`--logLevel=<silent\|info\|verbose>` | | Specify build-time log level
|
||||
`--prerender` | | Build Nuxt and prerender static routes
|
||||
`--preset` | | Nitro server preset
|
||||
`--dotenv` | | Path to `.env` file to load, relative to the root directory
|
||||
`--envName` | | The environment to use when resolving configuration overrides (default is `production` when building, and `development` when running the dev server)
|
||||
<!--/build-opts-->
|
||||
|
||||
::note
|
||||
This command sets `process.env.NODE_ENV` to `production`.
|
||||
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: 'nuxi cleanup'
|
||||
description: "Remove common generated Nuxt files and caches."
|
||||
description: 'Remove common generated Nuxt files and caches.'
|
||||
links:
|
||||
- label: Source
|
||||
icon: i-simple-icons-github
|
||||
@ -8,16 +8,31 @@ links:
|
||||
size: xs
|
||||
---
|
||||
|
||||
<!--cleanup-cmd-->
|
||||
```bash [Terminal]
|
||||
npx nuxi cleanup [rootDir]
|
||||
npx nuxi cleanup [ROOTDIR] [--cwd=<directory>]
|
||||
```
|
||||
<!--/cleanup-cmd-->
|
||||
|
||||
The `cleanup` command removes common generated Nuxt files and caches, including:
|
||||
|
||||
- `.nuxt`
|
||||
- `.output`
|
||||
- `node_modules/.vite`
|
||||
- `node_modules/.cache`
|
||||
|
||||
Option | Default | Description
|
||||
-------------------------|-----------------|------------------
|
||||
`rootDir` | `.` | The root directory of the project.
|
||||
## Arguments
|
||||
|
||||
<!--cleanup-args-->
|
||||
Argument | Description
|
||||
--- | ---
|
||||
`ROOTDIR="."` | Specifies the working directory (default: `.`)
|
||||
<!--/cleanup-args-->
|
||||
|
||||
## Options
|
||||
|
||||
<!--cleanup-opts-->
|
||||
Option | Default | Description
|
||||
--- | --- | ---
|
||||
`--cwd=<directory>` | | Specify the working directory, this takes precedence over ROOTDIR (default: `.`)
|
||||
<!--/cleanup-opts-->
|
||||
|
@ -8,25 +8,45 @@ links:
|
||||
size: xs
|
||||
---
|
||||
|
||||
<!--dev-cmd-->
|
||||
```bash [Terminal]
|
||||
npx nuxi dev [rootDir] [--dotenv] [--log-level] [--clipboard] [--open, -o] [--no-clear] [--port, -p] [--host, -h] [--https] [--ssl-cert] [--ssl-key] [--tunnel]
|
||||
npx nuxi dev [ROOTDIR] [--cwd=<directory>] [--logLevel=<silent|info|verbose>] [--dotenv] [--envName] [--no-clear] [--no-fork] [-p, --port] [-h, --host] [--clipboard] [-o, --open] [--https] [--publicURL] [--qr] [--public] [--tunnel] [--sslCert] [--sslKey]
|
||||
```
|
||||
<!--/dev-cmd-->
|
||||
|
||||
The `dev` command starts a development server with hot module replacement at [http://localhost:3000](https://localhost:3000)
|
||||
|
||||
Option | Default | Description
|
||||
-------------------------|-----------------|------------------
|
||||
`rootDir` | `.` | The root directory of the application to serve.
|
||||
`--dotenv` | `.` | Point to another `.env` file to load, **relative** to the root directory.
|
||||
`--open, -o` | `false` | Open URL in browser.
|
||||
`--clipboard` | `false` | Copy URL to clipboard.
|
||||
`--no-clear` | `false` | Does not clear the console after startup.
|
||||
`--port, -p` | `3000` | Port to listen.
|
||||
`--host, -h` | `localhost` | Hostname of the server.
|
||||
`--https` | `false` | Listen with `https` protocol with a self-signed certificate by default.
|
||||
`--ssl-cert` |`null` | Specify a certificate for https.
|
||||
`--ssl-key` |`null` | Specify the key for the https certificate.
|
||||
`--tunnel` | `false` | Tunnel your local server to the internet with [unjs/untun](https://github.com/unjs/untun)
|
||||
## Arguments
|
||||
|
||||
<!--dev-args-->
|
||||
Argument | Description
|
||||
--- | ---
|
||||
`ROOTDIR="."` | Specifies the working directory (default: `.`)
|
||||
<!--/dev-args-->
|
||||
|
||||
## Options
|
||||
|
||||
<!--dev-opts-->
|
||||
Option | Default | Description
|
||||
--- | --- | ---
|
||||
`--cwd=<directory>` | | Specify the working directory, this takes precedence over ROOTDIR (default: `.`)
|
||||
`--logLevel=<silent\|info\|verbose>` | | Specify build-time log level
|
||||
`--dotenv` | | Path to `.env` file to load, relative to the root directory
|
||||
`--envName` | | The environment to use when resolving configuration overrides (default is `production` when building, and `development` when running the dev server)
|
||||
`--no-clear` | | Disable clear console on restart
|
||||
`--no-fork` | | Disable forked mode
|
||||
`-p, --port` | | Port to listen on (default: `NUXT_PORT \|\| NITRO_PORT \|\| PORT \|\| nuxtOptions.devServer.port`)
|
||||
`-h, --host` | | Host to listen on (default: `NUXT_HOST \|\| NITRO_HOST \|\| HOST \|\| nuxtOptions._layers?.[0]?.devServer?.host`)
|
||||
`--clipboard` | `false` | Copy the URL to the clipboard
|
||||
`-o, --open` | `false` | Open the URL in the browser
|
||||
`--https` | | Enable HTTPS
|
||||
`--publicURL` | | Displayed public URL (used for QR code)
|
||||
`--qr` | | Display The QR code of public URL when available
|
||||
`--public` | | Listen to all network interfaces
|
||||
`--tunnel` | | Open a tunnel using https://github.com/unjs/untun
|
||||
`--sslCert` | | (DEPRECATED) Use `--https.cert` instead.
|
||||
`--sslKey` | | (DEPRECATED) Use `--https.key` instead.
|
||||
<!--/dev-opts-->
|
||||
|
||||
The port and host can also be set via NUXT_PORT, PORT, NUXT_HOST or HOST environment variables.
|
||||
|
||||
|
@ -8,16 +8,31 @@ links:
|
||||
size: xs
|
||||
---
|
||||
|
||||
<!--devtools-cmd-->
|
||||
```bash [Terminal]
|
||||
npx nuxi devtools enable|disable [rootDir]
|
||||
npx nuxi devtools <COMMAND> [ROOTDIR] [--cwd=<directory>]
|
||||
```
|
||||
<!--/devtools-cmd-->
|
||||
|
||||
Running `nuxi devtools enable` will install the Nuxt DevTools globally, and also enable it within the particular project you are using. It is saved as a preference in your user-level `.nuxtrc`. If you want to remove devtools support for a particular project, you can run `nuxi devtools disable`.
|
||||
|
||||
Option | Default | Description
|
||||
-------------------------|-----------------|------------------
|
||||
`rootDir` | `.` | The root directory of the app you want to enable devtools for.
|
||||
## Arguments
|
||||
|
||||
::read-more{icon="i-simple-icons-nuxtdotjs" to="https://devtools.nuxt.com" target="_blank"}
|
||||
<!--devtools-args-->
|
||||
Argument | Description
|
||||
--- | ---
|
||||
`COMMAND` | Command to run (options: <enable\|disable>)
|
||||
`ROOTDIR="."` | Specifies the working directory (default: `.`)
|
||||
<!--/devtools-args-->
|
||||
|
||||
## Options
|
||||
|
||||
<!--devtools-opts-->
|
||||
Option | Default | Description
|
||||
--- | --- | ---
|
||||
`--cwd=<directory>` | | Specify the working directory, this takes precedence over ROOTDIR (default: `.`)
|
||||
<!--/devtools-opts-->
|
||||
|
||||
::read-more{icon="i-simple-icons-nuxtdotjs" to="https://devtools.nuxt.com" target="\_blank"}
|
||||
Read more about the **Nuxt DevTools**.
|
||||
::
|
||||
|
@ -8,16 +8,33 @@ links:
|
||||
size: xs
|
||||
---
|
||||
|
||||
<!--generate-cmd-->
|
||||
```bash [Terminal]
|
||||
npx nuxi generate [rootDir] [--dotenv]
|
||||
npx nuxi generate [ROOTDIR] [--cwd=<directory>] [--logLevel=<silent|info|verbose>] [--preset] [--dotenv] [--envName]
|
||||
```
|
||||
<!--/generate-cmd-->
|
||||
|
||||
The `generate` command pre-renders every route of your application and stores the result in plain HTML files that you can deploy on any static hosting services. The command triggers the `nuxi build` command with the `prerender` argument set to `true`
|
||||
|
||||
Option | Default | Description
|
||||
-------------------------|-----------------|------------------
|
||||
`rootDir` | `.` | The root directory of the application to generate
|
||||
`--dotenv` | `.` | Point to another `.env` file to load, **relative** to the root directory.
|
||||
## Arguments
|
||||
|
||||
<!--generate-args-->
|
||||
Argument | Description
|
||||
--- | ---
|
||||
`ROOTDIR="."` | Specifies the working directory (default: `.`)
|
||||
<!--/generate-args-->
|
||||
|
||||
## Options
|
||||
|
||||
<!--generate-opts-->
|
||||
Option | Default | Description
|
||||
--- | --- | ---
|
||||
`--cwd=<directory>` | | Specify the working directory, this takes precedence over ROOTDIR (default: `.`)
|
||||
`--logLevel=<silent\|info\|verbose>` | | Specify build-time log level
|
||||
`--preset` | | Nitro server preset
|
||||
`--dotenv` | | Path to `.env` file to load, relative to the root directory
|
||||
`--envName` | | The environment to use when resolving configuration overrides (default is `production` when building, and `development` when running the dev server)
|
||||
<!--/generate-opts-->
|
||||
|
||||
::read-more{to="/docs/getting-started/deployment#static-hosting"}
|
||||
Read more about pre-rendering and static hosting.
|
||||
|
@ -8,12 +8,26 @@ links:
|
||||
size: xs
|
||||
---
|
||||
|
||||
<!--info-cmd-->
|
||||
```bash [Terminal]
|
||||
npx nuxi info [rootDir]
|
||||
npx nuxi info [ROOTDIR] [--cwd=<directory>]
|
||||
```
|
||||
<!--/info-cmd-->
|
||||
|
||||
The `info` command logs information about the current or specified Nuxt project.
|
||||
|
||||
Option | Default | Description
|
||||
-------------------------|-----------------|------------------
|
||||
`rootDir` | `.` | The directory of the target application.
|
||||
## Arguments
|
||||
|
||||
<!--info-args-->
|
||||
Argument | Description
|
||||
--- | ---
|
||||
`ROOTDIR="."` | Specifies the working directory (default: `.`)
|
||||
<!--/info-args-->
|
||||
|
||||
## Options
|
||||
|
||||
<!--info-opts-->
|
||||
Option | Default | Description
|
||||
--- | --- | ---
|
||||
`--cwd=<directory>` | | Specify the working directory, this takes precedence over ROOTDIR (default: `.`)
|
||||
<!--/info-opts-->
|
||||
|
@ -8,27 +8,37 @@ links:
|
||||
size: xs
|
||||
---
|
||||
|
||||
<!--init-cmd-->
|
||||
```bash [Terminal]
|
||||
npx nuxi init [--verbose|-v] [--template,-t] [dir]
|
||||
npx nuxi init [DIR] [--cwd=<directory>] [-t, --template] [-f, --force] [--offline] [--preferOffline] [--no-install] [--gitInit] [--shell] [--packageManager]
|
||||
```
|
||||
<!--/init-cmd-->
|
||||
|
||||
The `init` command initializes a fresh Nuxt project using [unjs/giget](https://github.com/unjs/giget).
|
||||
|
||||
## Arguments
|
||||
|
||||
<!--init-args-->
|
||||
Argument | Description
|
||||
--- | ---
|
||||
`DIR=""` | Project directory
|
||||
<!--/init-args-->
|
||||
|
||||
## Options
|
||||
|
||||
Option | Default | Description
|
||||
-------------------------|-----------------|------------------
|
||||
`--cwd` | | Current working directory
|
||||
`--log-level` | | Log level
|
||||
`--template, -t` | `v3` | Specify template name or git repository to use as a template. Format is `gh:org/name` to use a custom github template.
|
||||
`--force, -f` | `false` | Force clone to any existing directory.
|
||||
`--offline` | `false` | Force offline mode (do not attempt to download template from GitHub and only use local cache).
|
||||
`--prefer-offline` | `false` | Prefer offline mode (try local cache first to download templates).
|
||||
`--no-install` | `false` | Skip installing dependencies.
|
||||
`--git-init` | `false` | Initialize git repository.
|
||||
`--shell` | `false` | Start shell after installation in project directory (experimental).
|
||||
`--package-manager` | `npm` | Package manager choice (npm, pnpm, yarn, bun).
|
||||
`--dir` | | Project directory.
|
||||
<!--init-opts-->
|
||||
Option | Default | Description
|
||||
--- | --- | ---
|
||||
`--cwd=<directory>` | `.` | Specify the working directory
|
||||
`-t, --template` | | Template name
|
||||
`-f, --force` | | Override existing directory
|
||||
`--offline` | | Force offline mode
|
||||
`--preferOffline` | | Prefer offline mode
|
||||
`--no-install` | | Skip installing dependencies
|
||||
`--gitInit` | | Initialize git repository
|
||||
`--shell` | | Start shell after installation in project directory
|
||||
`--packageManager` | | Package manager choice (npm, pnpm, yarn, bun)
|
||||
<!--/init-opts-->
|
||||
|
||||
## Environment variables
|
||||
|
||||
|
@ -12,17 +12,31 @@ Nuxi provides a few utilities to work with [Nuxt modules](/modules) seamlessly.
|
||||
|
||||
## nuxi module add
|
||||
|
||||
<!--module-add-cmd-->
|
||||
```bash [Terminal]
|
||||
npx nuxi module add <NAME>
|
||||
npx nuxi module add <MODULENAME> [--cwd=<directory>] [--logLevel=<silent|info|verbose>] [--skipInstall] [--skipConfig]
|
||||
```
|
||||
<!--/module-add-cmd-->
|
||||
|
||||
Option | Default | Description
|
||||
-------------------------|-----------------|------------------
|
||||
`NAME` | - | The name of the module to install.
|
||||
<!--module-add-args-->
|
||||
Argument | Description
|
||||
--- | ---
|
||||
`MODULENAME` | Module name
|
||||
<!--/module-add-args-->
|
||||
|
||||
<!--module-add-opts-->
|
||||
Option | Default | Description
|
||||
--- | --- | ---
|
||||
`--cwd=<directory>` | `.` | Specify the working directory
|
||||
`--logLevel=<silent\|info\|verbose>` | | Specify build-time log level
|
||||
`--skipInstall` | | Skip npm install
|
||||
`--skipConfig` | | Skip nuxt.config.ts update
|
||||
<!--/module-add-opts-->
|
||||
|
||||
The command lets you install [Nuxt modules](/modules) in your application with no manual work.
|
||||
|
||||
When running the command, it will:
|
||||
|
||||
- install the module as a dependency using your package manager
|
||||
- add it to your [package.json](/docs/guide/directory-structure/package) file
|
||||
- update your [`nuxt.config`](/docs/guide/directory-structure/nuxt-config) file
|
||||
@ -30,19 +44,35 @@ When running the command, it will:
|
||||
**Example:**
|
||||
|
||||
Installing the [`Pinia`](/modules/pinia) module
|
||||
|
||||
```bash [Terminal]
|
||||
npx nuxi module add pinia
|
||||
npx nuxi module add pinia
|
||||
```
|
||||
|
||||
## nuxi module search
|
||||
|
||||
<!--module-search-cmd-->
|
||||
```bash [Terminal]
|
||||
npx nuxi module search <QUERY>
|
||||
npx nuxi module search <QUERY> [--cwd=<directory>] [--nuxtVersion=<2|3>]
|
||||
```
|
||||
<!--/module-search-cmd-->
|
||||
|
||||
Option | Default | Description
|
||||
-------------------------|-----------------|------------------
|
||||
`QUERY` | - | The name of the module to search for.
|
||||
### Arguments
|
||||
|
||||
<!--module-search-args-->
|
||||
Argument | Description
|
||||
--- | ---
|
||||
`QUERY` | keywords to search for
|
||||
<!--/module-search-args-->
|
||||
|
||||
### Options
|
||||
|
||||
<!--module-search-opts-->
|
||||
Option | Default | Description
|
||||
--- | --- | ---
|
||||
`--cwd=<directory>` | `.` | Specify the working directory
|
||||
`--nuxtVersion=<2\|3>` | | Filter by Nuxt version and list compatible modules only (auto detected by default)
|
||||
<!--/module-search-opts-->
|
||||
|
||||
The command searches for Nuxt modules matching your query that are compatible with your Nuxt version.
|
||||
|
||||
|
@ -8,12 +8,29 @@ links:
|
||||
size: xs
|
||||
---
|
||||
|
||||
<!--prepare-cmd-->
|
||||
```bash [Terminal]
|
||||
npx nuxi prepare [--log-level] [rootDir]
|
||||
npx nuxi prepare [ROOTDIR] [--dotenv] [--cwd=<directory>] [--logLevel=<silent|info|verbose>] [--envName]
|
||||
```
|
||||
<!--/prepare-cmd-->
|
||||
|
||||
The `prepare` command creates a [`.nuxt`](/docs/guide/directory-structure/nuxt) directory in your application and generates types. This can be useful in a CI environment or as a `postinstall` command in your [`package.json`](/docs/guide/directory-structure/package).
|
||||
|
||||
Option | Default | Description
|
||||
-------------------------|-----------------|------------------
|
||||
`rootDir` | `.` | The root directory of the application to prepare.
|
||||
## Arguments
|
||||
|
||||
<!--prepare-args-->
|
||||
Argument | Description
|
||||
--- | ---
|
||||
`ROOTDIR="."` | Specifies the working directory (default: `.`)
|
||||
<!--/prepare-args-->
|
||||
|
||||
## Options
|
||||
|
||||
<!--prepare-opts-->
|
||||
Option | Default | Description
|
||||
--- | --- | ---
|
||||
`--dotenv` | | Path to `.env` file to load, relative to the root directory
|
||||
`--cwd=<directory>` | | Specify the working directory, this takes precedence over ROOTDIR (default: `.`)
|
||||
`--logLevel=<silent\|info\|verbose>` | | Specify build-time log level
|
||||
`--envName` | | The environment to use when resolving configuration overrides (default is `production` when building, and `development` when running the dev server)
|
||||
<!--/prepare-opts-->
|
||||
|
@ -8,16 +8,32 @@ links:
|
||||
size: xs
|
||||
---
|
||||
|
||||
<!--preview-cmd-->
|
||||
```bash [Terminal]
|
||||
npx nuxi preview|start [rootDir] [--dotenv]
|
||||
npx nuxi preview [ROOTDIR] [--cwd=<directory>] [--logLevel=<silent|info|verbose>] [--envName] [--dotenv]
|
||||
```
|
||||
<!--/preview-cmd-->
|
||||
|
||||
The `preview` command starts a server to preview your Nuxt application after running the `build` command. The `start` command is an alias for `preview`. When running your application in production refer to the [Deployment section](/docs/getting-started/deployment).
|
||||
|
||||
Option | Default | Description
|
||||
-------------------------|-----------------|------------------
|
||||
`rootDir` | `.` | The root directory of the application to preview.
|
||||
`--dotenv` | `.` | Point to another `.env` file to load, **relative** to the root directory.
|
||||
## Arguments
|
||||
|
||||
<!--preview-args-->
|
||||
Argument | Description
|
||||
--- | ---
|
||||
`ROOTDIR="."` | Specifies the working directory (default: `.`)
|
||||
<!--/preview-args-->
|
||||
|
||||
## Options
|
||||
|
||||
<!--preview-opts-->
|
||||
Option | Default | Description
|
||||
--- | --- | ---
|
||||
`--cwd=<directory>` | | Specify the working directory, this takes precedence over ROOTDIR (default: `.`)
|
||||
`--logLevel=<silent\|info\|verbose>` | | Specify build-time log level
|
||||
`--envName` | | The environment to use when resolving configuration overrides (default is `production` when building, and `development` when running the dev server)
|
||||
`--dotenv` | | Path to `.env` file to load, relative to the root directory
|
||||
<!--/preview-opts-->
|
||||
|
||||
This command sets `process.env.NODE_ENV` to `production`. To override, define `NODE_ENV` in a `.env` file or as command-line argument.
|
||||
|
||||
|
@ -8,15 +8,30 @@ links:
|
||||
size: xs
|
||||
---
|
||||
|
||||
<!--typecheck-cmd-->
|
||||
```bash [Terminal]
|
||||
npx nuxi typecheck [--log-level] [rootDir]
|
||||
npx nuxi typecheck [ROOTDIR] [--cwd=<directory>] [--logLevel=<silent|info|verbose>]
|
||||
```
|
||||
<!--/typecheck-cmd-->
|
||||
|
||||
The `typecheck` command runs [`vue-tsc`](https://github.com/vuejs/language-tools/tree/master/packages/tsc) to check types throughout your app.
|
||||
|
||||
Option | Default | Description
|
||||
-------------------------|-----------------|------------------
|
||||
`rootDir` | `.` | The directory of the target application.
|
||||
## Arguments
|
||||
|
||||
<!--typecheck-args-->
|
||||
Argument | Description
|
||||
--- | ---
|
||||
`ROOTDIR="."` | Specifies the working directory (default: `.`)
|
||||
<!--/typecheck-args-->
|
||||
|
||||
## Options
|
||||
|
||||
<!--typecheck-opts-->
|
||||
Option | Default | Description
|
||||
--- | --- | ---
|
||||
`--cwd=<directory>` | | Specify the working directory, this takes precedence over ROOTDIR (default: `.`)
|
||||
`--logLevel=<silent\|info\|verbose>` | | Specify build-time log level
|
||||
<!--/typecheck-opts-->
|
||||
|
||||
::note
|
||||
This command sets `process.env.NODE_ENV` to `production`. To override, define `NODE_ENV` in a [`.env`](/docs/guide/directory-structure/env) file or as a command-line argument.
|
||||
|
@ -8,13 +8,29 @@ links:
|
||||
size: xs
|
||||
---
|
||||
|
||||
<!--upgrade-cmd-->
|
||||
```bash [Terminal]
|
||||
npx nuxi upgrade [--force|-f]
|
||||
npx nuxi upgrade [ROOTDIR] [--cwd=<directory>] [--logLevel=<silent|info|verbose>] [-f, --force] [-ch, --channel=<stable|nightly>]
|
||||
```
|
||||
<!--/upgrade-cmd-->
|
||||
|
||||
The `upgrade` command upgrades Nuxt to the latest version.
|
||||
|
||||
Option | Default | Description
|
||||
-------------------------|-----------------|------------------
|
||||
`--force, -f` | `false` | Removes `node_modules` and lock files before upgrade.
|
||||
`--channel, -ch` | `"stable"` | Specify a channel to install from ("nightly" or "stable")
|
||||
## Arguments
|
||||
|
||||
<!--upgrade-args-->
|
||||
Argument | Description
|
||||
--- | ---
|
||||
`ROOTDIR="."` | Specifies the working directory (default: `.`)
|
||||
<!--/upgrade-args-->
|
||||
|
||||
## Options
|
||||
|
||||
<!--upgrade-opts-->
|
||||
Option | Default | Description
|
||||
--- | --- | ---
|
||||
`--cwd=<directory>` | | Specify the working directory, this takes precedence over ROOTDIR (default: `.`)
|
||||
`--logLevel=<silent\|info\|verbose>` | | Specify build-time log level
|
||||
`-f, --force` | | Force upgrade to recreate lockfile and node_modules
|
||||
`-ch, --channel=<stable\|nightly>` | `stable` | Specify a channel to install from (default: stable)
|
||||
<!--/upgrade-opts-->
|
||||
|
34
package.json
34
package.json
@ -50,25 +50,25 @@
|
||||
"@vue/shared": "3.5.13",
|
||||
"c12": "2.0.1",
|
||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||
"jiti": "2.4.1",
|
||||
"jiti": "2.4.2",
|
||||
"magic-string": "^0.30.17",
|
||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
|
||||
"nuxt": "workspace:*",
|
||||
"ohash": "1.1.4",
|
||||
"postcss": "8.4.49",
|
||||
"rollup": "4.28.1",
|
||||
"rollup": "4.29.1",
|
||||
"send": ">=1.1.0",
|
||||
"typescript": "5.6.3",
|
||||
"typescript": "5.7.2",
|
||||
"ufo": "1.5.4",
|
||||
"unbuild": "3.0.1",
|
||||
"unhead": "1.11.14",
|
||||
"unimport": "3.14.5",
|
||||
"vite": "6.0.3",
|
||||
"vite": "6.0.6",
|
||||
"vue": "3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "0.17.1",
|
||||
"@nuxt/eslint-config": "0.7.3",
|
||||
"@arethetypeswrong/cli": "0.17.2",
|
||||
"@nuxt/eslint-config": "0.7.4",
|
||||
"@nuxt/kit": "workspace:*",
|
||||
"@nuxt/rspack-builder": "workspace:*",
|
||||
"@nuxt/test-utils": "3.15.1",
|
||||
@ -83,23 +83,23 @@
|
||||
"autoprefixer": "10.4.20",
|
||||
"case-police": "0.7.2",
|
||||
"changelogen": "0.5.7",
|
||||
"consola": "3.2.3",
|
||||
"consola": "3.3.3",
|
||||
"cssnano": "7.0.6",
|
||||
"destr": "2.0.3",
|
||||
"devalue": "5.1.1",
|
||||
"eslint": "9.17.0",
|
||||
"eslint-plugin-no-only-tests": "3.3.0",
|
||||
"eslint-plugin-perfectionist": "4.3.0",
|
||||
"eslint-plugin-perfectionist": "4.4.0",
|
||||
"eslint-typegen": "0.3.2",
|
||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||
"happy-dom": "15.11.7",
|
||||
"happy-dom": "16.0.1",
|
||||
"installed-check": "9.3.0",
|
||||
"jiti": "2.4.1",
|
||||
"knip": "5.40.0",
|
||||
"jiti": "2.4.2",
|
||||
"knip": "5.41.1",
|
||||
"markdownlint-cli": "0.43.0",
|
||||
"memfs": "~4.14.1",
|
||||
"memfs": "4.15.1",
|
||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
|
||||
"nuxi": "3.16.0",
|
||||
"nuxi": "3.17.2",
|
||||
"nuxt": "workspace:*",
|
||||
"nuxt-content-twoslash": "0.1.2",
|
||||
"ofetch": "1.4.1",
|
||||
@ -110,15 +110,15 @@
|
||||
"std-env": "3.8.0",
|
||||
"tinyexec": "0.3.1",
|
||||
"tinyglobby": "0.2.10",
|
||||
"typescript": "5.6.3",
|
||||
"typescript": "5.7.2",
|
||||
"ufo": "1.5.4",
|
||||
"vitest": "2.1.8",
|
||||
"vitest-environment-nuxt": "1.0.1",
|
||||
"vue": "3.5.13",
|
||||
"vue-tsc": "2.1.10",
|
||||
"webpack": "5.96.1"
|
||||
"vue-tsc": "2.2.0",
|
||||
"webpack": "5.97.1"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.0",
|
||||
"packageManager": "pnpm@9.15.1",
|
||||
"engines": {
|
||||
"node": "^18.20.4 || ^20.9.0 || ^22.0.0 || >=23.0.0"
|
||||
},
|
||||
|
@ -29,33 +29,33 @@
|
||||
"dependencies": {
|
||||
"@nuxt/schema": "workspace:*",
|
||||
"c12": "^2.0.1",
|
||||
"consola": "^3.2.3",
|
||||
"consola": "^3.3.3",
|
||||
"defu": "^6.1.4",
|
||||
"destr": "^2.0.3",
|
||||
"errx": "^0.1.0",
|
||||
"globby": "^14.0.2",
|
||||
"ignore": "^6.0.2",
|
||||
"jiti": "^2.4.1",
|
||||
"ignore": "^7.0.0",
|
||||
"jiti": "^2.4.2",
|
||||
"klona": "^2.0.6",
|
||||
"mlly": "^1.7.3",
|
||||
"ohash": "^1.1.4",
|
||||
"pathe": "^1.1.2",
|
||||
"pkg-types": "^1.2.1",
|
||||
"pkg-types": "^1.3.0",
|
||||
"scule": "^1.3.0",
|
||||
"semver": "^7.6.3",
|
||||
"ufo": "^1.5.4",
|
||||
"unctx": "^2.4.0",
|
||||
"unctx": "^2.4.1",
|
||||
"unimport": "^3.14.5",
|
||||
"untyped": "^1.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rspack/core": "1.1.6",
|
||||
"@rspack/core": "1.1.8",
|
||||
"@types/semver": "7.5.8",
|
||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
|
||||
"unbuild": "3.0.1",
|
||||
"vite": "6.0.3",
|
||||
"vite": "6.0.6",
|
||||
"vitest": "2.1.8",
|
||||
"webpack": "5.96.1"
|
||||
"webpack": "5.97.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.5"
|
||||
|
@ -230,6 +230,7 @@ export async function _generateTypes (nuxt: Nuxt) {
|
||||
: nuxt.options.buildDir
|
||||
|
||||
tsConfig.compilerOptions = tsConfig.compilerOptions || {}
|
||||
tsConfig.compilerOptions.paths = tsConfig.compilerOptions.paths || {}
|
||||
tsConfig.include = tsConfig.include || []
|
||||
|
||||
for (const alias in aliases) {
|
||||
|
@ -65,10 +65,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/devalue": "^2.0.2",
|
||||
"@nuxt/devtools": "^1.6.4",
|
||||
"@nuxt/devtools": "^1.7.0",
|
||||
"@nuxt/kit": "workspace:*",
|
||||
"@nuxt/schema": "workspace:*",
|
||||
"@nuxt/telemetry": "^2.6.0",
|
||||
"@nuxt/telemetry": "^2.6.2",
|
||||
"@nuxt/vite-builder": "workspace:*",
|
||||
"@unhead/dom": "^1.11.14",
|
||||
"@unhead/shared": "^1.11.14",
|
||||
@ -77,36 +77,36 @@
|
||||
"@vue/shared": "^3.5.13",
|
||||
"acorn": "8.14.0",
|
||||
"c12": "^2.0.1",
|
||||
"chokidar": "^4.0.1",
|
||||
"chokidar": "^4.0.3",
|
||||
"compatx": "^0.1.8",
|
||||
"consola": "^3.2.3",
|
||||
"consola": "^3.3.3",
|
||||
"cookie-es": "^1.2.2",
|
||||
"defu": "^6.1.4",
|
||||
"destr": "^2.0.3",
|
||||
"devalue": "^5.1.1",
|
||||
"errx": "^0.1.0",
|
||||
"esbuild": "^0.24.0",
|
||||
"esbuild": "^0.24.2",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"estree-walker": "^3.0.3",
|
||||
"globby": "^14.0.2",
|
||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||
"hookable": "^5.5.3",
|
||||
"ignore": "^6.0.2",
|
||||
"ignore": "^7.0.0",
|
||||
"impound": "^0.2.0",
|
||||
"jiti": "^2.4.1",
|
||||
"jiti": "^2.4.2",
|
||||
"klona": "^2.0.6",
|
||||
"knitwork": "^1.2.0",
|
||||
"magic-string": "^0.30.17",
|
||||
"mlly": "^1.7.3",
|
||||
"nanotar": "^0.1.1",
|
||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
|
||||
"nuxi": "^3.16.0",
|
||||
"nuxi": "^3.17.2",
|
||||
"nypm": "^0.4.1",
|
||||
"ofetch": "^1.4.1",
|
||||
"ohash": "^1.1.4",
|
||||
"pathe": "^1.1.2",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"pkg-types": "^1.2.1",
|
||||
"pkg-types": "^1.3.0",
|
||||
"radix3": "^1.1.2",
|
||||
"scule": "^1.3.0",
|
||||
"semver": "^7.6.3",
|
||||
@ -116,13 +116,13 @@
|
||||
"ufo": "^1.5.4",
|
||||
"ultrahtml": "^1.5.3",
|
||||
"uncrypto": "^0.1.3",
|
||||
"unctx": "^2.4.0",
|
||||
"unctx": "^2.4.1",
|
||||
"unenv": "^1.10.0",
|
||||
"unhead": "^1.11.14",
|
||||
"unimport": "^3.14.5",
|
||||
"unplugin": "^2.1.0",
|
||||
"unplugin-vue-router": "^0.10.9",
|
||||
"unstorage": "^1.13.1",
|
||||
"unstorage": "^1.14.4",
|
||||
"untyped": "^1.5.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-bundle-renderer": "^2.1.1",
|
||||
@ -136,7 +136,7 @@
|
||||
"@vitejs/plugin-vue": "5.2.1",
|
||||
"@vue/compiler-sfc": "3.5.13",
|
||||
"unbuild": "3.0.1",
|
||||
"vite": "6.0.3",
|
||||
"vite": "6.0.6",
|
||||
"vitest": "2.1.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@ -1,13 +1,10 @@
|
||||
import { createElementBlock, defineComponent, onMounted, ref } from 'vue'
|
||||
import { createElementBlock, defineComponent, onMounted, ref, useId } from 'vue'
|
||||
import { useState } from '../composables/state'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NuxtClientFallback',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
uid: {
|
||||
type: String,
|
||||
},
|
||||
fallbackTag: {
|
||||
type: String,
|
||||
default: () => 'div',
|
||||
@ -30,8 +27,7 @@ export default defineComponent({
|
||||
emits: ['ssr-error'],
|
||||
setup (props, ctx) {
|
||||
const mounted = ref(false)
|
||||
// This is deliberate - `uid` should not be provided by user but by a transform plugin and will not be reactive.
|
||||
const ssrFailed = useState(`${props.uid}`)
|
||||
const ssrFailed = useState(useId())
|
||||
|
||||
if (ssrFailed.value) {
|
||||
onMounted(() => { mounted.value = true })
|
||||
|
@ -1,18 +1,14 @@
|
||||
import { defineComponent, getCurrentInstance, onErrorCaptured, ref } from 'vue'
|
||||
import { defineComponent, getCurrentInstance, onErrorCaptured, ref, useId } from 'vue'
|
||||
import { ssrRenderAttrs, ssrRenderSlot, ssrRenderVNode } from 'vue/server-renderer'
|
||||
|
||||
import { isPromise } from '@vue/shared'
|
||||
import { useState } from '../composables/state'
|
||||
import { useNuxtApp } from '../nuxt'
|
||||
import { createBuffer } from './utils'
|
||||
|
||||
const NuxtClientFallbackServer = defineComponent({
|
||||
name: 'NuxtClientFallback',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
uid: {
|
||||
type: String,
|
||||
},
|
||||
fallbackTag: {
|
||||
type: String,
|
||||
default: () => 'div',
|
||||
@ -37,11 +33,10 @@ const NuxtClientFallbackServer = defineComponent({
|
||||
return true
|
||||
},
|
||||
},
|
||||
async setup (props, ctx) {
|
||||
async setup (_, ctx) {
|
||||
const vm = getCurrentInstance()
|
||||
const ssrFailed = ref(false)
|
||||
const nuxtApp = useNuxtApp()
|
||||
const error = useState<boolean | undefined>(`${props.uid}`)
|
||||
const error = useState<boolean | undefined>(useId())
|
||||
|
||||
onErrorCaptured((err) => {
|
||||
error.value = true
|
||||
@ -68,7 +63,7 @@ const NuxtClientFallbackServer = defineComponent({
|
||||
return { ssrFailed, ssrVNodes }
|
||||
} catch (ssrError) {
|
||||
// catch in dev
|
||||
nuxtApp.runWithContext(() => useState(`${props.uid}`, () => true))
|
||||
error.value = true
|
||||
ctx.emit('ssr-error', ssrError)
|
||||
return { ssrFailed: true, ssrVNodes: [] }
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ import { cancelIdleCallback, requestIdleCallback } from '../compat/idle-callback
|
||||
// @ts-expect-error virtual file
|
||||
import { nuxtLinkDefaults } from '#build/nuxt.config.mjs'
|
||||
|
||||
import { hashMode } from '#build/router.options'
|
||||
|
||||
const firstNonUndefined = <T> (...args: (T | undefined)[]) => args.find(arg => arg !== undefined)
|
||||
|
||||
const NuxtLinkDevKeySymbol: InjectionKey<boolean> = Symbol('nuxt-link-dev-key')
|
||||
@ -110,6 +112,10 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
function isHashLinkWithoutHashMode (link: unknown): boolean {
|
||||
return !hashMode && typeof link === 'string' && link.startsWith('#')
|
||||
}
|
||||
|
||||
function resolveTrailingSlashBehavior (to: string, resolve: Router['resolve']): string
|
||||
function resolveTrailingSlashBehavior (to: RouteLocationRaw, resolve: Router['resolve']): Exclude<RouteLocationRaw, string>
|
||||
function resolveTrailingSlashBehavior (to: RouteLocationRaw | undefined, resolve: Router['resolve']): RouteLocationRaw | RouteLocation | undefined {
|
||||
@ -176,7 +182,9 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||
|
||||
// Resolves `to` value if it's a route location object
|
||||
const href = computed(() => {
|
||||
if (!to.value || isAbsoluteUrl.value) { return to.value as string }
|
||||
if (!to.value || isAbsoluteUrl.value || isHashLinkWithoutHashMode(to.value)) {
|
||||
return to.value as string
|
||||
}
|
||||
|
||||
if (isExternal.value) {
|
||||
const path = typeof to.value === 'object' && 'path' in to.value ? resolveRouteObject(to.value) : to.value
|
||||
@ -373,7 +381,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (!isExternal.value && !hasTarget.value) {
|
||||
if (!isExternal.value && !hasTarget.value && !isHashLinkWithoutHashMode(to.value)) {
|
||||
const routerLinkProps: RouterLinkProps & VNodeProps & AllowedComponentProps & AnchorHTMLAttributes = {
|
||||
ref: elRef,
|
||||
to: to.value,
|
||||
|
@ -350,6 +350,12 @@ export function useAsyncData<
|
||||
if (import.meta.client) {
|
||||
// Setup hook callbacks once per instance
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
// @ts-expect-error - instance.sp is an internal vue property
|
||||
if (instance && fetchOnServer && options.immediate && !instance.sp) {
|
||||
// @ts-expect-error - internal vue property. This force vue to mark the component as async boundary client-side to avoid useId hydration issue since we treeshake onServerPrefetch
|
||||
instance.sp = []
|
||||
}
|
||||
if (import.meta.dev && !nuxtApp.isHydrating && !nuxtApp._processingMiddleware /* internal flag */ && (!instance || instance?.isMounted)) {
|
||||
// @ts-expect-error private property
|
||||
console.warn(`[nuxt] [${options._functionName || 'useAsyncData'}] Component is already mounted, please use $fetch instead. See https://nuxt.com/docs/getting-started/data-fetching`)
|
||||
|
@ -1,18 +1,23 @@
|
||||
import { useNuxtApp } from '../nuxt'
|
||||
|
||||
type CallOnceOptions = {
|
||||
mode?: 'navigation' | 'render'
|
||||
}
|
||||
|
||||
/**
|
||||
* An SSR-friendly utility to call a method once
|
||||
* @param key a unique key ensuring the function can be properly de-duplicated across requests
|
||||
* @param fn a function to call
|
||||
* @param options Setup the mode, e.g. to re-execute on navigation
|
||||
* @see https://nuxt.com/docs/api/utils/call-once
|
||||
* @since 3.9.0
|
||||
*/
|
||||
export function callOnce (key?: string, fn?: (() => any | Promise<any>)): Promise<void>
|
||||
export function callOnce (fn?: (() => any | Promise<any>)): Promise<void>
|
||||
export function callOnce (key?: string, fn?: (() => any | Promise<any>), options?: CallOnceOptions): Promise<void>
|
||||
export function callOnce (fn?: (() => any | Promise<any>), options?: CallOnceOptions): Promise<void>
|
||||
export async function callOnce (...args: any): Promise<void> {
|
||||
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
|
||||
if (typeof args[0] !== 'string') { args.unshift(autoKey) }
|
||||
const [_key, fn] = args as [string, (() => any | Promise<any>)]
|
||||
const [_key, fn, options] = args as [string, (() => any | Promise<any>), CallOnceOptions | undefined]
|
||||
if (!_key || typeof _key !== 'string') {
|
||||
throw new TypeError('[nuxt] [callOnce] key must be a string: ' + _key)
|
||||
}
|
||||
@ -20,10 +25,18 @@ export async function callOnce (...args: any): Promise<void> {
|
||||
throw new Error('[nuxt] [callOnce] fn must be a function: ' + fn)
|
||||
}
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
if (options?.mode === 'navigation') {
|
||||
nuxtApp.hooks.hookOnce('page:start', () => {
|
||||
nuxtApp.payload.once.delete(_key)
|
||||
})
|
||||
}
|
||||
|
||||
// If key already ran
|
||||
if (nuxtApp.payload.once.has(_key)) {
|
||||
return
|
||||
}
|
||||
|
||||
nuxtApp._once = nuxtApp._once || {}
|
||||
nuxtApp._once[_key] = nuxtApp._once[_key] || fn() || true
|
||||
await nuxtApp._once[_key]
|
||||
|
@ -152,6 +152,9 @@ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: Na
|
||||
|
||||
// Early redirect on client-side
|
||||
if (import.meta.client && !isExternal && inMiddleware) {
|
||||
if (options?.replace) {
|
||||
return typeof to === 'string' ? { path: to, replace: true } : { ...to, replace: true }
|
||||
}
|
||||
return to
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ import { distDir } from '../dirs'
|
||||
import { componentNamesTemplate, componentsIslandsTemplate, componentsMetadataTemplate, componentsPluginTemplate, componentsTypeTemplate } from './templates'
|
||||
import { scanComponents } from './scan'
|
||||
|
||||
import { ClientFallbackAutoIdPlugin } from './plugins/client-fallback-auto-id'
|
||||
import { LoaderPlugin } from './plugins/loader'
|
||||
import { ComponentsChunkPlugin, IslandsTransformPlugin } from './plugins/islands-transform'
|
||||
import { TransformPlugin } from './plugins/transform'
|
||||
@ -218,11 +217,6 @@ export default defineNuxtModule<ComponentsOptions>({
|
||||
|
||||
addBuildPlugin(TreeShakeTemplatePlugin({ sourcemap: !!nuxt.options.sourcemap.server, getComponents }), { client: false })
|
||||
|
||||
if (nuxt.options.experimental.clientFallback) {
|
||||
addBuildPlugin(ClientFallbackAutoIdPlugin({ sourcemap: !!nuxt.options.sourcemap.client, rootDir: nuxt.options.rootDir }), { server: false })
|
||||
addBuildPlugin(ClientFallbackAutoIdPlugin({ sourcemap: !!nuxt.options.sourcemap.server, rootDir: nuxt.options.rootDir }), { client: false })
|
||||
}
|
||||
|
||||
const sharedLoaderOptions = {
|
||||
getComponents,
|
||||
serverComponentRuntime,
|
||||
|
@ -1,55 +0,0 @@
|
||||
import { createUnplugin } from 'unplugin'
|
||||
import type { ComponentsOptions } from '@nuxt/schema'
|
||||
import MagicString from 'magic-string'
|
||||
import { isAbsolute, relative } from 'pathe'
|
||||
import { hash } from 'ohash'
|
||||
import { isVue } from '../../core/utils'
|
||||
|
||||
interface LoaderOptions {
|
||||
sourcemap?: boolean
|
||||
transform?: ComponentsOptions['transform']
|
||||
rootDir: string
|
||||
}
|
||||
const CLIENT_FALLBACK_RE = /<(?:NuxtClientFallback|nuxt-client-fallback)(?: [^>]*)?>/
|
||||
const CLIENT_FALLBACK_GLOBAL_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/g
|
||||
const UID_RE = / :?uid=/
|
||||
export const ClientFallbackAutoIdPlugin = (options: LoaderOptions) => createUnplugin(() => {
|
||||
const exclude = options.transform?.exclude || []
|
||||
const include = options.transform?.include || []
|
||||
|
||||
return {
|
||||
name: 'nuxt:client-fallback-auto-id',
|
||||
enforce: 'pre',
|
||||
transformInclude (id) {
|
||||
if (exclude.some(pattern => pattern.test(id))) {
|
||||
return false
|
||||
}
|
||||
if (include.some(pattern => pattern.test(id))) {
|
||||
return true
|
||||
}
|
||||
return isVue(id)
|
||||
},
|
||||
transform (code, id) {
|
||||
if (!CLIENT_FALLBACK_RE.test(code)) { return }
|
||||
|
||||
const s = new MagicString(code)
|
||||
const relativeID = isAbsolute(id) ? relative(options.rootDir, id) : id
|
||||
let count = 0
|
||||
|
||||
s.replace(CLIENT_FALLBACK_GLOBAL_RE, (full, name, attrs) => {
|
||||
count++
|
||||
if (UID_RE.test(attrs)) { return full }
|
||||
return `<${name} :uid="'${hash(relativeID)}' + JSON.stringify($props) + '${count}'" ${attrs ?? ''}>`
|
||||
})
|
||||
|
||||
if (s.hasChanged()) {
|
||||
return {
|
||||
code: s.toString(),
|
||||
map: options.sourcemap
|
||||
? s.generateMap({ hires: true })
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
@ -18,6 +18,8 @@ export const createClientPage = (loader: AsyncComponentLoader) => {
|
||||
setup (_, { attrs }) {
|
||||
const nuxtApp = useNuxtApp()
|
||||
if (import.meta.server || nuxtApp.isHydrating) {
|
||||
// wrapped with div to avoid Transition issues
|
||||
// @see https://github.com/nuxt/nuxt/pull/25037#issuecomment-1877423894
|
||||
return () => h('div', [
|
||||
h(ClientOnly, undefined, {
|
||||
default: () => h(page, attrs),
|
||||
|
@ -106,7 +106,12 @@ function createWatcher () {
|
||||
],
|
||||
})
|
||||
|
||||
watcher.on('all', (event, path) => nuxt.callHook('builder:watch', event, normalize(path)))
|
||||
watcher.on('all', (event, path) => {
|
||||
if (event === 'all' || event === 'ready' || event === 'error' || event === 'raw') {
|
||||
return
|
||||
}
|
||||
nuxt.callHook('builder:watch', event, normalize(path))
|
||||
})
|
||||
nuxt.hook('close', () => watcher?.close())
|
||||
}
|
||||
|
||||
@ -134,6 +139,9 @@ function createGranularWatcher () {
|
||||
const watchers: Record<string, FSWatcher> = {}
|
||||
|
||||
watcher.on('all', (event, path) => {
|
||||
if (event === 'all' || event === 'ready' || event === 'error' || event === 'raw') {
|
||||
return
|
||||
}
|
||||
path = normalize(path)
|
||||
if (!pending) {
|
||||
nuxt.callHook('builder:watch', event, path)
|
||||
@ -144,7 +152,12 @@ function createGranularWatcher () {
|
||||
}
|
||||
if (event === 'addDir' && path !== dir && !ignoredDirs.has(path) && !pathsToWatch.includes(path) && !(path in watchers) && !isIgnored(path)) {
|
||||
const pathWatcher = watchers[path] = chokidarWatch(path, { ...nuxt.options.watchers.chokidar, ignored: [isIgnored] })
|
||||
pathWatcher.on('all', (event, p) => nuxt.callHook('builder:watch', event, normalize(p)))
|
||||
pathWatcher.on('all', (event, p) => {
|
||||
if (event === 'all' || event === 'ready' || event === 'error' || event === 'raw') {
|
||||
return
|
||||
}
|
||||
nuxt.callHook('builder:watch', event, normalize(p))
|
||||
})
|
||||
nuxt.hook('close', () => pathWatcher?.close())
|
||||
}
|
||||
})
|
||||
|
@ -380,7 +380,7 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
|
||||
// Transform initial composable call within `<script setup>` to preserve context
|
||||
if (nuxt.options.experimental.asyncContext) {
|
||||
addBuildPlugin(AsyncContextInjectionPlugin(nuxt))
|
||||
addBuildPlugin(AsyncContextInjectionPlugin(nuxt), { client: false })
|
||||
}
|
||||
|
||||
// TODO: [Experimental] Avoid emitting assets when flag is enabled
|
||||
|
@ -3,11 +3,10 @@ import { createUnplugin } from 'unplugin'
|
||||
import { isAbsolute, relative } from 'pathe'
|
||||
import MagicString from 'magic-string'
|
||||
import { hash } from 'ohash'
|
||||
import type { Pattern } from 'estree'
|
||||
import { parseQuery, parseURL } from 'ufo'
|
||||
import escapeRE from 'escape-string-regexp'
|
||||
import { findStaticImports, parseStaticImport } from 'mlly'
|
||||
import { parseAndWalk, walk } from '../../core/utils/parse'
|
||||
import { ScopeTracker, parseAndWalk, walk } from '../utils/parse'
|
||||
|
||||
import { matchWithStringOrRegex } from '../utils/plugins'
|
||||
|
||||
@ -53,33 +52,18 @@ export const ComposableKeysPlugin = (options: ComposableKeysOptions) => createUn
|
||||
const { pathname: relativePathname } = parseURL(relativeID)
|
||||
|
||||
// To handle variables hoisting we need a pre-pass to collect variable and function declarations with scope info.
|
||||
let scopeTracker = new ScopeTracker()
|
||||
const varCollector = new ScopedVarsCollector()
|
||||
const scopeTracker = new ScopeTracker({
|
||||
keepExitedScopes: true,
|
||||
})
|
||||
const ast = parseAndWalk(script, id, {
|
||||
enter (node) {
|
||||
if (node.type === 'BlockStatement') {
|
||||
scopeTracker.enterScope()
|
||||
varCollector.refresh(scopeTracker.curScopeKey)
|
||||
} else if (node.type === 'FunctionDeclaration' && node.id) {
|
||||
varCollector.addVar(node.id.name)
|
||||
} else if (node.type === 'VariableDeclarator') {
|
||||
varCollector.collect(node.id)
|
||||
}
|
||||
},
|
||||
leave (_node) {
|
||||
if (_node.type === 'BlockStatement') {
|
||||
scopeTracker.leaveScope()
|
||||
varCollector.refresh(scopeTracker.curScopeKey)
|
||||
}
|
||||
},
|
||||
scopeTracker,
|
||||
})
|
||||
|
||||
scopeTracker = new ScopeTracker()
|
||||
scopeTracker.freeze()
|
||||
|
||||
walk(ast, {
|
||||
scopeTracker,
|
||||
enter (node) {
|
||||
if (node.type === 'BlockStatement') {
|
||||
scopeTracker.enterScope()
|
||||
}
|
||||
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
|
||||
const name = node.callee.name
|
||||
if (!name || !keyedFunctions.has(name) || node.arguments.length >= maxLength) { return }
|
||||
@ -89,7 +73,9 @@ export const ComposableKeysPlugin = (options: ComposableKeysOptions) => createUn
|
||||
|
||||
const meta = composableMeta[name]
|
||||
|
||||
if (varCollector.hasVar(scopeTracker.curScopeKey, name)) {
|
||||
const declaration = scopeTracker.getDeclaration(name)
|
||||
|
||||
if (declaration && declaration.type !== 'Import') {
|
||||
let skip = true
|
||||
if (meta.source) {
|
||||
skip = !matchWithStringOrRegex(relativePathname, meta.source)
|
||||
@ -125,11 +111,6 @@ export const ComposableKeysPlugin = (options: ComposableKeysOptions) => createUn
|
||||
(node.arguments.length && !endsWithComma ? ', ' : '') + '\'$' + hash(`${relativeID}-${++count}`) + '\'',
|
||||
)
|
||||
},
|
||||
leave (_node) {
|
||||
if (_node.type === 'BlockStatement') {
|
||||
scopeTracker.leaveScope()
|
||||
}
|
||||
},
|
||||
})
|
||||
if (s.hasChanged()) {
|
||||
return {
|
||||
@ -143,97 +124,6 @@ export const ComposableKeysPlugin = (options: ComposableKeysOptions) => createUn
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
* track scopes with unique keys. for example
|
||||
* ```js
|
||||
* // root scope, marked as ''
|
||||
* function a () { // '0'
|
||||
* function b () {} // '0-0'
|
||||
* function c () {} // '0-1'
|
||||
* }
|
||||
* function d () {} // '1'
|
||||
* // ''
|
||||
* ```
|
||||
* */
|
||||
class ScopeTracker {
|
||||
// the top of the stack is not a part of current key, it is used for next level
|
||||
scopeIndexStack: number[]
|
||||
curScopeKey: string
|
||||
|
||||
constructor () {
|
||||
this.scopeIndexStack = [0]
|
||||
this.curScopeKey = ''
|
||||
}
|
||||
|
||||
getKey () {
|
||||
return this.scopeIndexStack.slice(0, -1).join('-')
|
||||
}
|
||||
|
||||
enterScope () {
|
||||
this.scopeIndexStack.push(0)
|
||||
this.curScopeKey = this.getKey()
|
||||
}
|
||||
|
||||
leaveScope () {
|
||||
this.scopeIndexStack.pop()
|
||||
this.curScopeKey = this.getKey()
|
||||
this.scopeIndexStack[this.scopeIndexStack.length - 1]!++
|
||||
}
|
||||
}
|
||||
|
||||
class ScopedVarsCollector {
|
||||
curScopeKey: string
|
||||
all: Map<string, Set<string>>
|
||||
|
||||
constructor () {
|
||||
this.all = new Map()
|
||||
this.curScopeKey = ''
|
||||
}
|
||||
|
||||
refresh (scopeKey: string) {
|
||||
this.curScopeKey = scopeKey
|
||||
}
|
||||
|
||||
addVar (name: string) {
|
||||
let vars = this.all.get(this.curScopeKey)
|
||||
if (!vars) {
|
||||
vars = new Set()
|
||||
this.all.set(this.curScopeKey, vars)
|
||||
}
|
||||
vars.add(name)
|
||||
}
|
||||
|
||||
hasVar (scopeKey: string, name: string) {
|
||||
const indices = scopeKey.split('-').map(Number)
|
||||
for (let i = indices.length; i >= 0; i--) {
|
||||
if (this.all.get(indices.slice(0, i).join('-'))?.has(name)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
collect (pattern: Pattern) {
|
||||
if (pattern.type === 'Identifier') {
|
||||
this.addVar(pattern.name)
|
||||
} else if (pattern.type === 'RestElement') {
|
||||
this.collect(pattern.argument)
|
||||
} else if (pattern.type === 'AssignmentPattern') {
|
||||
this.collect(pattern.left)
|
||||
} else if (pattern.type === 'ArrayPattern') {
|
||||
for (const element of pattern.elements) {
|
||||
if (element) {
|
||||
this.collect(element.type === 'RestElement' ? element.argument : element)
|
||||
}
|
||||
}
|
||||
} else if (pattern.type === 'ObjectPattern') {
|
||||
for (const property of pattern.properties) {
|
||||
this.collect(property.type === 'RestElement' ? property.argument : property.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const NUXT_IMPORT_RE = /nuxt|#app|#imports/
|
||||
|
||||
export function detectImportNames (code: string, composableMeta: Record<string, { source?: string | RegExp }>) {
|
||||
|
@ -7,7 +7,7 @@ import type { Nuxt } from '@nuxt/schema'
|
||||
import { pkgDir } from '../../dirs'
|
||||
|
||||
export function resolveDeepImportsPlugin (nuxt: Nuxt): Plugin {
|
||||
const exclude: string[] = ['virtual:', '\0virtual:', '/__skip_vite']
|
||||
const exclude: string[] = ['virtual:', '\0virtual:', '/__skip_vite', '@vitest/']
|
||||
let conditions: string[]
|
||||
return {
|
||||
name: 'nuxt:resolve-bare-imports',
|
||||
|
@ -1,6 +1,17 @@
|
||||
import { walk as _walk } from 'estree-walker'
|
||||
import type { Node, SyncHandler } from 'estree-walker'
|
||||
import type { Program as ESTreeProgram } from 'estree'
|
||||
import type {
|
||||
ArrowFunctionExpression,
|
||||
CatchClause,
|
||||
Program as ESTreeProgram,
|
||||
FunctionDeclaration,
|
||||
FunctionExpression,
|
||||
Identifier,
|
||||
ImportDefaultSpecifier,
|
||||
ImportNamespaceSpecifier,
|
||||
ImportSpecifier,
|
||||
VariableDeclaration,
|
||||
} from 'estree'
|
||||
import { parse } from 'acorn'
|
||||
import type { Program } from 'acorn'
|
||||
|
||||
@ -9,20 +20,30 @@ export type { Node }
|
||||
type WithLocations<T> = T & { start: number, end: number }
|
||||
type WalkerCallback = (this: ThisParameterType<SyncHandler>, node: WithLocations<Node>, parent: WithLocations<Node> | null, ctx: { key: string | number | symbol | null | undefined, index: number | null | undefined, ast: Program | Node }) => void
|
||||
|
||||
export function walk (ast: Program | Node, callback: { enter?: WalkerCallback, leave?: WalkerCallback }) {
|
||||
interface WalkOptions {
|
||||
enter: WalkerCallback
|
||||
leave: WalkerCallback
|
||||
scopeTracker: ScopeTracker
|
||||
}
|
||||
|
||||
export function walk (ast: Program | Node, callback: Partial<WalkOptions>) {
|
||||
return _walk(ast as unknown as ESTreeProgram | Node, {
|
||||
enter (node, parent, key, index) {
|
||||
// @ts-expect-error - accessing a protected property
|
||||
callback.scopeTracker?.processNodeEnter(node as WithLocations<Node>)
|
||||
callback.enter?.call(this, node as WithLocations<Node>, parent as WithLocations<Node> | null, { key, index, ast })
|
||||
},
|
||||
leave (node, parent, key, index) {
|
||||
// @ts-expect-error - accessing a protected property
|
||||
callback.scopeTracker?.processNodeLeave(node as WithLocations<Node>)
|
||||
callback.leave?.call(this, node as WithLocations<Node>, parent as WithLocations<Node> | null, { key, index, ast })
|
||||
},
|
||||
}) as Program | Node | null
|
||||
}
|
||||
|
||||
export function parseAndWalk (code: string, sourceFilename: string, callback: WalkerCallback): Program
|
||||
export function parseAndWalk (code: string, sourceFilename: string, object: { enter?: WalkerCallback, leave?: WalkerCallback }): Program
|
||||
export function parseAndWalk (code: string, _sourceFilename: string, callback: { enter?: WalkerCallback, leave?: WalkerCallback } | WalkerCallback) {
|
||||
export function parseAndWalk (code: string, sourceFilename: string, object: Partial<WalkOptions>): Program
|
||||
export function parseAndWalk (code: string, _sourceFilename: string, callback: Partial<WalkOptions> | WalkerCallback) {
|
||||
const ast = parse (code, { sourceType: 'module', ecmaVersion: 'latest', locations: true })
|
||||
walk(ast, typeof callback === 'function' ? { enter: callback } : callback)
|
||||
return ast
|
||||
@ -31,3 +52,506 @@ export function parseAndWalk (code: string, _sourceFilename: string, callback: {
|
||||
export function withLocations<T> (node: T): WithLocations<T> {
|
||||
return node as WithLocations<T>
|
||||
}
|
||||
|
||||
abstract class BaseNode<T extends Node = Node> {
|
||||
abstract type: string
|
||||
node: WithLocations<T>
|
||||
|
||||
constructor (node: WithLocations<T>) {
|
||||
this.node = node
|
||||
}
|
||||
|
||||
/**
|
||||
* The starting position of the entire relevant node in the code.
|
||||
* For instance, for a function parameter, this would be the start of the function declaration.
|
||||
*/
|
||||
abstract get start (): number
|
||||
|
||||
/**
|
||||
* The ending position of the entire relevant node in the code.
|
||||
* For instance, for a function parameter, this would be the end of the function declaration.
|
||||
*/
|
||||
abstract get end (): number
|
||||
}
|
||||
|
||||
class IdentifierNode extends BaseNode<Identifier> {
|
||||
override type = 'Identifier' as const
|
||||
|
||||
get start () {
|
||||
return this.node.start
|
||||
}
|
||||
|
||||
get end () {
|
||||
return this.node.end
|
||||
}
|
||||
}
|
||||
|
||||
class FunctionParamNode extends BaseNode {
|
||||
type = 'FunctionParam' as const
|
||||
fnNode: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression>
|
||||
|
||||
constructor (node: WithLocations<Node>, fnNode: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression>) {
|
||||
super(node)
|
||||
this.fnNode = fnNode
|
||||
}
|
||||
|
||||
get start () {
|
||||
return this.fnNode.start
|
||||
}
|
||||
|
||||
get end () {
|
||||
return this.fnNode.end
|
||||
}
|
||||
}
|
||||
|
||||
class FunctionNode extends BaseNode<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression> {
|
||||
type = 'Function' as const
|
||||
|
||||
get start () {
|
||||
return this.node.start
|
||||
}
|
||||
|
||||
get end () {
|
||||
return this.node.end
|
||||
}
|
||||
}
|
||||
|
||||
class VariableNode extends BaseNode<Identifier> {
|
||||
type = 'Variable' as const
|
||||
variableNode: WithLocations<VariableDeclaration>
|
||||
|
||||
constructor (node: WithLocations<Identifier>, variableNode: WithLocations<VariableDeclaration>) {
|
||||
super(node)
|
||||
this.variableNode = variableNode
|
||||
}
|
||||
|
||||
get start () {
|
||||
return this.variableNode.start
|
||||
}
|
||||
|
||||
get end () {
|
||||
return this.variableNode.end
|
||||
}
|
||||
}
|
||||
|
||||
class ImportNode extends BaseNode<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier> {
|
||||
type = 'Import' as const
|
||||
importNode: WithLocations<Node>
|
||||
|
||||
constructor (node: WithLocations<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier>, importNode: WithLocations<Node>) {
|
||||
super(node)
|
||||
this.importNode = importNode
|
||||
}
|
||||
|
||||
get start () {
|
||||
return this.importNode.start
|
||||
}
|
||||
|
||||
get end () {
|
||||
return this.importNode.end
|
||||
}
|
||||
}
|
||||
|
||||
class CatchParamNode extends BaseNode {
|
||||
type = 'CatchParam' as const
|
||||
catchNode: WithLocations<CatchClause>
|
||||
|
||||
constructor (node: WithLocations<Node>, catchNode: WithLocations<CatchClause>) {
|
||||
super(node)
|
||||
this.catchNode = catchNode
|
||||
}
|
||||
|
||||
get start () {
|
||||
return this.catchNode.start
|
||||
}
|
||||
|
||||
get end () {
|
||||
return this.catchNode.end
|
||||
}
|
||||
}
|
||||
|
||||
export type ScopeTrackerNode =
|
||||
| FunctionParamNode
|
||||
| FunctionNode
|
||||
| VariableNode
|
||||
| IdentifierNode
|
||||
| ImportNode
|
||||
| CatchParamNode
|
||||
|
||||
interface ScopeTrackerOptions {
|
||||
/**
|
||||
* If true, the scope tracker will keep exited scopes in memory.
|
||||
* @default false
|
||||
*/
|
||||
keepExitedScopes?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A class to track variable scopes and declarations of identifiers within a JavaScript AST.
|
||||
* It maintains a stack of scopes, where each scope is a map of identifier names to their corresponding
|
||||
* declaration nodes - allowing to get to the declaration easily.
|
||||
*
|
||||
* The class has integration with the `walk` function to automatically track scopes and declarations
|
||||
* and that's why only the informative methods are exposed.
|
||||
*
|
||||
* ### Scope tracking
|
||||
* Scopes are created when entering a block statement, however, they are also created
|
||||
* for function parameters, loop variable declarations, etc. (e.g. `i` in `for (let i = 0; i < 10; i++) { ... }`).
|
||||
* This means that the behaviour is not 100% equivalent to JavaScript's scoping rules, because internally,
|
||||
* one JavaScript scope can be spread across multiple scopes in this class.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const scopeTracker = new ScopeTracker()
|
||||
* walk(code, {
|
||||
* scopeTracker,
|
||||
* enter(node) {
|
||||
* // ...
|
||||
* },
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @see parseAndWalk
|
||||
* @see walk
|
||||
*/
|
||||
export class ScopeTracker {
|
||||
protected scopeIndexStack: number[] = []
|
||||
protected scopeIndexKey = ''
|
||||
protected scopes: Map<string, Map<string, WithLocations<ScopeTrackerNode>>> = new Map()
|
||||
|
||||
protected options: Partial<ScopeTrackerOptions>
|
||||
protected isFrozen = false
|
||||
|
||||
constructor (options: ScopeTrackerOptions = {}) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
protected updateScopeIndexKey () {
|
||||
this.scopeIndexKey = this.scopeIndexStack.slice(0, -1).join('-')
|
||||
}
|
||||
|
||||
protected pushScope () {
|
||||
this.scopeIndexStack.push(0)
|
||||
this.updateScopeIndexKey()
|
||||
}
|
||||
|
||||
protected popScope () {
|
||||
this.scopeIndexStack.pop()
|
||||
if (this.scopeIndexStack[this.scopeIndexStack.length - 1] !== undefined) {
|
||||
this.scopeIndexStack[this.scopeIndexStack.length - 1]!++
|
||||
}
|
||||
|
||||
if (!this.options.keepExitedScopes) {
|
||||
this.scopes.delete(this.scopeIndexKey)
|
||||
}
|
||||
|
||||
this.updateScopeIndexKey()
|
||||
}
|
||||
|
||||
protected declareIdentifier (name: string, data: ScopeTrackerNode) {
|
||||
if (this.isFrozen) { return }
|
||||
|
||||
let scope = this.scopes.get(this.scopeIndexKey)
|
||||
if (!scope) {
|
||||
scope = new Map()
|
||||
this.scopes.set(this.scopeIndexKey, scope)
|
||||
}
|
||||
scope.set(name, data)
|
||||
}
|
||||
|
||||
protected declareFunctionParameter (param: WithLocations<Node>, fn: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression>) {
|
||||
if (this.isFrozen) { return }
|
||||
|
||||
const identifiers = getPatternIdentifiers(param)
|
||||
for (const identifier of identifiers) {
|
||||
this.declareIdentifier(identifier.name, new FunctionParamNode(identifier, fn))
|
||||
}
|
||||
}
|
||||
|
||||
protected declarePattern (pattern: WithLocations<Node>, parent: WithLocations<FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | VariableDeclaration | CatchClause>) {
|
||||
if (this.isFrozen) { return }
|
||||
|
||||
const identifiers = getPatternIdentifiers(pattern)
|
||||
for (const identifier of identifiers) {
|
||||
this.declareIdentifier(
|
||||
identifier.name,
|
||||
parent.type === 'VariableDeclaration'
|
||||
? new VariableNode(identifier, parent)
|
||||
: parent.type === 'CatchClause'
|
||||
? new CatchParamNode(identifier, parent)
|
||||
: new FunctionParamNode(identifier, parent),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected processNodeEnter (node: WithLocations<Node>) {
|
||||
switch (node.type) {
|
||||
case 'Program':
|
||||
case 'BlockStatement':
|
||||
case 'StaticBlock':
|
||||
this.pushScope()
|
||||
break
|
||||
|
||||
case 'FunctionDeclaration':
|
||||
// declare function name for named functions, skip for `export default`
|
||||
if (node.id?.name) {
|
||||
this.declareIdentifier(node.id.name, new FunctionNode(node))
|
||||
}
|
||||
this.pushScope()
|
||||
for (const param of node.params) {
|
||||
this.declareFunctionParameter(withLocations(param), node)
|
||||
}
|
||||
break
|
||||
|
||||
case 'FunctionExpression':
|
||||
// make the name of the function available only within the function
|
||||
// e.g. const foo = function bar() { // bar is only available within the function body
|
||||
this.pushScope()
|
||||
// can be undefined, for example in class method definitions
|
||||
if (node.id?.name) {
|
||||
this.declareIdentifier(node.id.name, new FunctionNode(node))
|
||||
}
|
||||
|
||||
this.pushScope()
|
||||
for (const param of node.params) {
|
||||
this.declareFunctionParameter(withLocations(param), node)
|
||||
}
|
||||
break
|
||||
case 'ArrowFunctionExpression':
|
||||
this.pushScope()
|
||||
for (const param of node.params) {
|
||||
this.declareFunctionParameter(withLocations(param), node)
|
||||
}
|
||||
break
|
||||
|
||||
case 'VariableDeclaration':
|
||||
for (const decl of node.declarations) {
|
||||
this.declarePattern(withLocations(decl.id), node)
|
||||
}
|
||||
break
|
||||
|
||||
case 'ClassDeclaration':
|
||||
// declare class name for named classes, skip for `export default`
|
||||
if (node.id?.name) {
|
||||
this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id)))
|
||||
}
|
||||
break
|
||||
|
||||
case 'ClassExpression':
|
||||
// make the name of the class available only within the class
|
||||
// e.g. const MyClass = class InternalClassName { // InternalClassName is only available within the class body
|
||||
this.pushScope()
|
||||
if (node.id?.name) {
|
||||
this.declareIdentifier(node.id.name, new IdentifierNode(withLocations(node.id)))
|
||||
}
|
||||
break
|
||||
|
||||
case 'ImportDeclaration':
|
||||
for (const specifier of node.specifiers) {
|
||||
this.declareIdentifier(specifier.local.name, new ImportNode(withLocations(specifier), node))
|
||||
}
|
||||
break
|
||||
|
||||
case 'CatchClause':
|
||||
this.pushScope()
|
||||
if (node.param) {
|
||||
this.declarePattern(withLocations(node.param), node)
|
||||
}
|
||||
break
|
||||
|
||||
case 'ForStatement':
|
||||
case 'ForOfStatement':
|
||||
case 'ForInStatement':
|
||||
// make the variables defined in for loops available only within the loop
|
||||
// e.g. for (let i = 0; i < 10; i++) { // i is only available within the loop block scope
|
||||
this.pushScope()
|
||||
|
||||
if (node.type === 'ForStatement' && node.init?.type === 'VariableDeclaration') {
|
||||
for (const decl of node.init.declarations) {
|
||||
this.declarePattern(withLocations(decl.id), withLocations(node.init))
|
||||
}
|
||||
} else if ((node.type === 'ForOfStatement' || node.type === 'ForInStatement') && node.left.type === 'VariableDeclaration') {
|
||||
for (const decl of node.left.declarations) {
|
||||
this.declarePattern(withLocations(decl.id), withLocations(node.left))
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
protected processNodeLeave (node: WithLocations<Node>) {
|
||||
switch (node.type) {
|
||||
case 'Program':
|
||||
case 'BlockStatement':
|
||||
case 'CatchClause':
|
||||
case 'FunctionDeclaration':
|
||||
case 'ArrowFunctionExpression':
|
||||
case 'StaticBlock':
|
||||
case 'ClassExpression':
|
||||
case 'ForStatement':
|
||||
case 'ForOfStatement':
|
||||
case 'ForInStatement':
|
||||
this.popScope()
|
||||
break
|
||||
case 'FunctionExpression':
|
||||
this.popScope()
|
||||
this.popScope()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
isDeclared (name: string) {
|
||||
if (!this.scopeIndexKey) {
|
||||
return this.scopes.get('')?.has(name) || false
|
||||
}
|
||||
|
||||
const indices = this.scopeIndexKey.split('-').map(Number)
|
||||
for (let i = indices.length; i >= 0; i--) {
|
||||
if (this.scopes.get(indices.slice(0, i).join('-'))?.has(name)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
getDeclaration (name: string): ScopeTrackerNode | null {
|
||||
if (!this.scopeIndexKey) {
|
||||
return this.scopes.get('')?.get(name) ?? null
|
||||
}
|
||||
|
||||
const indices = this.scopeIndexKey.split('-').map(Number)
|
||||
for (let i = indices.length; i >= 0; i--) {
|
||||
const node = this.scopes.get(indices.slice(0, i).join('-'))?.get(name)
|
||||
if (node) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Freezes the scope tracker, preventing further declarations.
|
||||
* It also resets the scope index stack to its initial state, so that the scope tracker can be reused.
|
||||
*
|
||||
* This is useful for second passes through the AST.
|
||||
*/
|
||||
freeze () {
|
||||
this.isFrozen = true
|
||||
this.scopeIndexStack = []
|
||||
this.updateScopeIndexKey()
|
||||
}
|
||||
}
|
||||
|
||||
function getPatternIdentifiers (pattern: WithLocations<Node>) {
|
||||
const identifiers: WithLocations<Identifier>[] = []
|
||||
|
||||
function collectIdentifiers (pattern: WithLocations<Node>) {
|
||||
switch (pattern.type) {
|
||||
case 'Identifier':
|
||||
identifiers.push(pattern)
|
||||
break
|
||||
case 'AssignmentPattern':
|
||||
collectIdentifiers(withLocations(pattern.left))
|
||||
break
|
||||
case 'RestElement':
|
||||
collectIdentifiers(withLocations(pattern.argument))
|
||||
break
|
||||
case 'ArrayPattern':
|
||||
for (const element of pattern.elements) {
|
||||
if (element) {
|
||||
collectIdentifiers(withLocations(element.type === 'RestElement' ? element.argument : element))
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'ObjectPattern':
|
||||
for (const property of pattern.properties) {
|
||||
collectIdentifiers(withLocations(property.type === 'RestElement' ? property.argument : property.value))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
collectIdentifiers(pattern)
|
||||
|
||||
return identifiers
|
||||
}
|
||||
|
||||
export function isNotReferencePosition (node: WithLocations<Node>, parent: WithLocations<Node> | null) {
|
||||
if (!parent || node.type !== 'Identifier') { return false }
|
||||
|
||||
switch (parent.type) {
|
||||
case 'FunctionDeclaration':
|
||||
case 'FunctionExpression':
|
||||
case 'ArrowFunctionExpression':
|
||||
// function name or parameters
|
||||
if (parent.type !== 'ArrowFunctionExpression' && parent.id === node) { return true }
|
||||
if (parent.params.length) {
|
||||
for (const param of parent.params) {
|
||||
const identifiers = getPatternIdentifiers(withLocations(param))
|
||||
if (identifiers.includes(node)) { return true }
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
case 'ClassDeclaration':
|
||||
case 'ClassExpression':
|
||||
// class name
|
||||
return parent.id === node
|
||||
|
||||
case 'MethodDefinition':
|
||||
// class method name
|
||||
return parent.key === node
|
||||
|
||||
case 'PropertyDefinition':
|
||||
// class property name
|
||||
return parent.key === node
|
||||
|
||||
case 'VariableDeclarator':
|
||||
// variable name
|
||||
return getPatternIdentifiers(withLocations(parent.id)).includes(node)
|
||||
|
||||
case 'CatchClause':
|
||||
// catch clause param
|
||||
if (!parent.param) { return false }
|
||||
return getPatternIdentifiers(withLocations(parent.param)).includes(node)
|
||||
|
||||
case 'Property':
|
||||
// property key if not used as a shorthand
|
||||
return parent.key === node && parent.value !== node
|
||||
|
||||
case 'MemberExpression':
|
||||
// member expression properties
|
||||
return parent.property === node
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function getUndeclaredIdentifiersInFunction (node: FunctionDeclaration | FunctionExpression | ArrowFunctionExpression) {
|
||||
const scopeTracker = new ScopeTracker({
|
||||
keepExitedScopes: true,
|
||||
})
|
||||
const undeclaredIdentifiers = new Set<string>()
|
||||
|
||||
function isIdentifierUndeclared (node: WithLocations<Identifier>, parent: WithLocations<Node> | null) {
|
||||
return !isNotReferencePosition(node, parent) && !scopeTracker.isDeclared(node.name)
|
||||
}
|
||||
|
||||
// first pass to collect all declarations and hoist them
|
||||
walk(node, {
|
||||
scopeTracker,
|
||||
})
|
||||
|
||||
scopeTracker.freeze()
|
||||
|
||||
walk(node, {
|
||||
scopeTracker,
|
||||
enter (node, parent) {
|
||||
if (node.type === 'Identifier' && isIdentifierUndeclared(node, parent)) {
|
||||
undeclaredIdentifiers.add(node.name)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return Array.from(undeclaredIdentifiers)
|
||||
}
|
||||
|
1
packages/nuxt/src/pages/build.d.ts
vendored
1
packages/nuxt/src/pages/build.d.ts
vendored
@ -1,6 +1,7 @@
|
||||
declare module '#build/router.options' {
|
||||
import type { RouterOptions } from '@nuxt/schema'
|
||||
|
||||
export const hashMode: boolean
|
||||
const _default: RouterOptions
|
||||
export default _default
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ export default defineNuxtModule({
|
||||
delete tsConfig.compilerOptions.paths['#vue-router/*']
|
||||
})
|
||||
|
||||
const builtInRouterOptions = await findPath(resolve(runtimeDir, 'router.options')) || resolve(runtimeDir, 'router.options')
|
||||
async function resolveRouterOptions () {
|
||||
const context = {
|
||||
files: [] as Array<{ path: string, optional?: boolean }>,
|
||||
@ -53,7 +54,7 @@ export default defineNuxtModule({
|
||||
}
|
||||
|
||||
// Add default options at beginning
|
||||
context.files.unshift({ path: await findPath(resolve(runtimeDir, 'router.options')) || resolve(runtimeDir, 'router.options'), optional: true })
|
||||
context.files.unshift({ path: builtInRouterOptions, optional: true })
|
||||
|
||||
await nuxt.callHook('pages:routerOptions', context)
|
||||
return context.files
|
||||
@ -128,6 +129,16 @@ export default defineNuxtModule({
|
||||
'export const START_LOCATION = Symbol(\'router:start-location\')',
|
||||
].join('\n'),
|
||||
})
|
||||
// used by `<NuxtLink>`
|
||||
addTemplate({
|
||||
filename: 'router.options.mjs',
|
||||
getContents: () => {
|
||||
return [
|
||||
'export const hashMode = false',
|
||||
'export default {}',
|
||||
].join('\n')
|
||||
},
|
||||
})
|
||||
addTypeTemplate({
|
||||
filename: 'types/middleware.d.ts',
|
||||
getContents: () => [
|
||||
@ -535,6 +546,7 @@ export default defineNuxtModule({
|
||||
return [
|
||||
...routerOptionsFiles.map((file, index) => genImport(file.path, `routerOptions${index}`)),
|
||||
`const configRouterOptions = ${configRouterOptions}`,
|
||||
`export const hashMode = ${[...routerOptionsFiles.filter(o => o.path !== builtInRouterOptions).map((_, index) => `routerOptions${index}.hashMode`).reverse(), nuxt.options.router.options.hashMode].join(' ?? ')}`,
|
||||
'export default {',
|
||||
'...configRouterOptions,',
|
||||
...routerOptionsFiles.map((_, index) => `...routerOptions${index},`),
|
||||
@ -632,6 +644,7 @@ if (import.meta.hot) {
|
||||
|
||||
export function handleHotUpdate(_router) {
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.data ||= {}
|
||||
import.meta.hot.data.router = _router
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,19 @@ import { createUnplugin } from 'unplugin'
|
||||
import { parseQuery, parseURL } from 'ufo'
|
||||
import type { StaticImport } from 'mlly'
|
||||
import { findExports, findStaticImports, parseStaticImport } from 'mlly'
|
||||
import { walk } from 'estree-walker'
|
||||
import MagicString from 'magic-string'
|
||||
import { isAbsolute } from 'pathe'
|
||||
import { logger } from '@nuxt/kit'
|
||||
|
||||
import { parseAndWalk, withLocations } from '../../core/utils/parse'
|
||||
import {
|
||||
ScopeTracker,
|
||||
type ScopeTrackerNode,
|
||||
getUndeclaredIdentifiersInFunction,
|
||||
isNotReferencePosition,
|
||||
parseAndWalk,
|
||||
walk,
|
||||
withLocations,
|
||||
} from '../../core/utils/parse'
|
||||
|
||||
interface PageMetaPluginOptions {
|
||||
dev?: boolean
|
||||
@ -119,38 +126,130 @@ export const PageMetaPlugin = (options: PageMetaPluginOptions = {}) => createUnp
|
||||
}
|
||||
}
|
||||
|
||||
parseAndWalk(code, id, (node) => {
|
||||
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
|
||||
if (!('name' in node.callee) || node.callee.name !== 'definePageMeta') { return }
|
||||
function isStaticIdentifier (name: string | false): name is string {
|
||||
return !!(name && importMap.has(name))
|
||||
}
|
||||
|
||||
const meta = withLocations(node.arguments[0])
|
||||
function addImport (name: string | false) {
|
||||
if (!isStaticIdentifier(name)) { return }
|
||||
const importValue = importMap.get(name)!.code.trim()
|
||||
if (!addedImports.has(importValue)) {
|
||||
addedImports.add(importValue)
|
||||
}
|
||||
}
|
||||
|
||||
if (!meta) { return }
|
||||
const declarationNodes: ScopeTrackerNode[] = []
|
||||
const addedDeclarations = new Set<string>()
|
||||
|
||||
let contents = `const __nuxt_page_meta = ${code!.slice(meta.start, meta.end) || 'null'}\nexport default __nuxt_page_meta` + (options.dev ? CODE_HMR : '')
|
||||
function addDeclaration (node: ScopeTrackerNode) {
|
||||
const codeSectionKey = `${node.start}-${node.end}`
|
||||
if (addedDeclarations.has(codeSectionKey)) { return }
|
||||
addedDeclarations.add(codeSectionKey)
|
||||
declarationNodes.push(node)
|
||||
}
|
||||
|
||||
function addImport (name: string | false) {
|
||||
if (name && importMap.has(name)) {
|
||||
const importValue = importMap.get(name)!.code
|
||||
if (!addedImports.has(importValue)) {
|
||||
contents = importMap.get(name)!.code + '\n' + contents
|
||||
addedImports.add(importValue)
|
||||
}
|
||||
/**
|
||||
* Adds an import or a declaration to the extracted code.
|
||||
* @param name The name of the import or declaration to add.
|
||||
* @param node The node that is currently being processed. (To detect self-references)
|
||||
*/
|
||||
function addImportOrDeclaration (name: string, node?: ScopeTrackerNode) {
|
||||
if (isStaticIdentifier(name)) {
|
||||
addImport(name)
|
||||
} else {
|
||||
const declaration = scopeTracker.getDeclaration(name)
|
||||
/*
|
||||
Without checking for `declaration !== node`, we would end up in an infinite loop
|
||||
when, for example, a variable is declared and then used in its own initializer.
|
||||
(we shouldn't mask the underlying error by throwing a `Maximum call stack size exceeded` error)
|
||||
|
||||
```ts
|
||||
const a = { b: a }
|
||||
```
|
||||
*/
|
||||
if (declaration && declaration !== node) {
|
||||
processDeclaration(declaration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(meta, {
|
||||
enter (node) {
|
||||
if (node.type === 'CallExpression' && 'name' in node.callee) {
|
||||
addImport(node.callee.name)
|
||||
}
|
||||
if (node.type === 'Identifier') {
|
||||
addImport(node.name)
|
||||
}
|
||||
},
|
||||
})
|
||||
const scopeTracker = new ScopeTracker()
|
||||
|
||||
s.overwrite(0, code.length, contents)
|
||||
function processDeclaration (scopeTrackerNode: ScopeTrackerNode | null) {
|
||||
if (scopeTrackerNode?.type === 'Variable') {
|
||||
addDeclaration(scopeTrackerNode)
|
||||
|
||||
for (const decl of scopeTrackerNode.variableNode.declarations) {
|
||||
if (!decl.init) { continue }
|
||||
walk(decl.init, {
|
||||
enter: (node, parent) => {
|
||||
if (node.type === 'AwaitExpression') {
|
||||
logger.error(`[nuxt] Await expressions are not supported in definePageMeta. File: '${id}'`)
|
||||
throw new Error('await in definePageMeta')
|
||||
}
|
||||
if (
|
||||
isNotReferencePosition(node, parent)
|
||||
|| node.type !== 'Identifier' // checking for `node.type` to narrow down the type
|
||||
) { return }
|
||||
|
||||
addImportOrDeclaration(node.name, scopeTrackerNode)
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if (scopeTrackerNode?.type === 'Function') {
|
||||
// arrow functions are going to be assigned to a variable
|
||||
if (scopeTrackerNode.node.type === 'ArrowFunctionExpression') { return }
|
||||
const name = scopeTrackerNode.node.id?.name
|
||||
if (!name) { return }
|
||||
addDeclaration(scopeTrackerNode)
|
||||
|
||||
const undeclaredIdentifiers = getUndeclaredIdentifiersInFunction(scopeTrackerNode.node)
|
||||
for (const name of undeclaredIdentifiers) {
|
||||
addImportOrDeclaration(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseAndWalk(code, id, {
|
||||
scopeTracker,
|
||||
enter: (node) => {
|
||||
if (node.type !== 'CallExpression' || node.callee.type !== 'Identifier') { return }
|
||||
if (!('name' in node.callee) || node.callee.name !== 'definePageMeta') { return }
|
||||
|
||||
const meta = withLocations(node.arguments[0])
|
||||
|
||||
if (!meta) { return }
|
||||
|
||||
walk(meta, {
|
||||
enter (node, parent) {
|
||||
if (
|
||||
isNotReferencePosition(node, parent)
|
||||
|| node.type !== 'Identifier' // checking for `node.type` to narrow down the type
|
||||
) { return }
|
||||
|
||||
if (isStaticIdentifier(node.name)) {
|
||||
addImport(node.name)
|
||||
} else {
|
||||
processDeclaration(scopeTracker.getDeclaration(node.name))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const importStatements = Array.from(addedImports).join('\n')
|
||||
|
||||
const declarations = declarationNodes
|
||||
.sort((a, b) => a.start - b.start)
|
||||
.map(node => code.slice(node.start, node.end))
|
||||
.join('\n')
|
||||
|
||||
const extracted = [
|
||||
importStatements,
|
||||
declarations,
|
||||
`const __nuxt_page_meta = ${code!.slice(meta.start, meta.end) || 'null'}\nexport default __nuxt_page_meta` + (options.dev ? CODE_HMR : ''),
|
||||
].join('\n')
|
||||
|
||||
s.overwrite(0, code.length, extracted.trim())
|
||||
},
|
||||
})
|
||||
|
||||
if (!s.hasChanged() && !code.includes('__nuxt_page_meta')) {
|
||||
|
@ -7,7 +7,7 @@ import { defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt'
|
||||
import { prerenderRoutes } from '#app/composables/ssr'
|
||||
// @ts-expect-error virtual file
|
||||
import _routes from '#build/routes'
|
||||
import routerOptions from '#build/router.options'
|
||||
import routerOptions, { hashMode } from '#build/router.options'
|
||||
// @ts-expect-error virtual file
|
||||
import { crawlLinks } from '#build/nuxt.config.mjs'
|
||||
|
||||
@ -16,7 +16,7 @@ let routes: string[]
|
||||
let _routeRulesMatcher: undefined | ReturnType<typeof toRouteMatcher> = undefined
|
||||
|
||||
export default defineNuxtPlugin(async () => {
|
||||
if (!import.meta.server || !import.meta.prerender || routerOptions.hashMode) {
|
||||
if (!import.meta.server || !import.meta.prerender || hashMode) {
|
||||
return
|
||||
}
|
||||
if (routes && !routes.length) { return }
|
||||
|
@ -19,7 +19,7 @@ import { navigateTo } from '#app/composables/router'
|
||||
import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs'
|
||||
// @ts-expect-error virtual file
|
||||
import _routes, { handleHotUpdate } from '#build/routes'
|
||||
import routerOptions from '#build/router.options'
|
||||
import routerOptions, { hashMode } from '#build/router.options'
|
||||
// @ts-expect-error virtual file
|
||||
import { globalMiddleware, namedMiddleware } from '#build/middleware'
|
||||
|
||||
@ -51,13 +51,13 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
|
||||
enforce: 'pre',
|
||||
async setup (nuxtApp) {
|
||||
let routerBase = useRuntimeConfig().app.baseURL
|
||||
if (routerOptions.hashMode && !routerBase.includes('#')) {
|
||||
if (hashMode && !routerBase.includes('#')) {
|
||||
// allow the user to provide a `#` in the middle: `/base/#/app`
|
||||
routerBase += '#'
|
||||
}
|
||||
|
||||
const history = routerOptions.history?.(routerBase) ?? (import.meta.client
|
||||
? (routerOptions.hashMode ? createWebHashHistory(routerBase) : createWebHistory(routerBase))
|
||||
? (hashMode ? createWebHashHistory(routerBase) : createWebHistory(routerBase))
|
||||
: createMemoryHistory(routerBase)
|
||||
)
|
||||
|
||||
|
31
packages/nuxt/test/fixture/scope-tracker.ts
Normal file
31
packages/nuxt/test/fixture/scope-tracker.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { ScopeTracker } from '../../src/core/utils/parse'
|
||||
|
||||
export class TestScopeTracker extends ScopeTracker {
|
||||
getScopes () {
|
||||
return this.scopes
|
||||
}
|
||||
|
||||
getScopeIndexKey () {
|
||||
return this.scopeIndexKey
|
||||
}
|
||||
|
||||
getScopeIndexStack () {
|
||||
return this.scopeIndexStack
|
||||
}
|
||||
|
||||
isDeclaredInScope (identifier: string, scope: string) {
|
||||
const oldKey = this.scopeIndexKey
|
||||
this.scopeIndexKey = scope
|
||||
const result = this.isDeclared(identifier)
|
||||
this.scopeIndexKey = oldKey
|
||||
return result
|
||||
}
|
||||
|
||||
getDeclarationFromScope (identifier: string, scope: string) {
|
||||
const oldKey = this.scopeIndexKey
|
||||
this.scopeIndexKey = scope
|
||||
const result = this.getDeclaration(identifier)
|
||||
this.scopeIndexKey = oldKey
|
||||
return result
|
||||
}
|
||||
}
|
@ -119,6 +119,11 @@ describe('nuxt-link:isExternal', () => {
|
||||
expect(nuxtLink({ to: '/foo/bar', target: '_blank' }).type).toBe(EXTERNAL)
|
||||
expect(nuxtLink({ to: '/foo/bar?baz=qux', target: '_blank' }).type).toBe(EXTERNAL)
|
||||
})
|
||||
|
||||
it('returns `true` if link starts with hash', () => {
|
||||
expect(nuxtLink({ href: '#hash' }).type).toBe(EXTERNAL)
|
||||
expect(nuxtLink({ to: '#hash' }).type).toBe(EXTERNAL)
|
||||
})
|
||||
})
|
||||
|
||||
describe('nuxt-link:propsOrAttributes', () => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { compileScript, parse } from '@vue/compiler-sfc'
|
||||
import * as Parser from 'acorn'
|
||||
|
||||
import { transform as esbuildTransform } from 'esbuild'
|
||||
import { PageMetaPlugin } from '../src/pages/plugins/page-meta'
|
||||
import { getRouteMeta, normalizeRoutes } from '../src/pages/utils'
|
||||
import type { NuxtPage } from '../schema'
|
||||
@ -310,4 +310,238 @@ definePageMeta({
|
||||
export default __nuxt_page_meta"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should extract local functions', () => {
|
||||
const sfc = `
|
||||
<script setup lang="ts">
|
||||
function isNumber(value) {
|
||||
return value && !isNaN(Number(value))
|
||||
}
|
||||
|
||||
function validateIdParam (route) {
|
||||
return isNumber(route.params.id)
|
||||
}
|
||||
|
||||
definePageMeta({
|
||||
validate: validateIdParam,
|
||||
test: () => 'hello',
|
||||
})
|
||||
</script>
|
||||
`
|
||||
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
|
||||
expect(transformPlugin.transform.call({
|
||||
parse: (code: string, opts: any = {}) => Parser.parse(code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
locations: true,
|
||||
...opts,
|
||||
}),
|
||||
}, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
|
||||
"function isNumber(value) {
|
||||
return value && !isNaN(Number(value))
|
||||
}
|
||||
function validateIdParam (route) {
|
||||
return isNumber(route.params.id)
|
||||
}
|
||||
const __nuxt_page_meta = {
|
||||
validate: validateIdParam,
|
||||
test: () => 'hello',
|
||||
}
|
||||
export default __nuxt_page_meta"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should extract user imports', () => {
|
||||
const sfc = `
|
||||
<script setup lang="ts">
|
||||
import { validateIdParam } from './utils'
|
||||
|
||||
definePageMeta({
|
||||
validate: validateIdParam,
|
||||
dynamic: ref(true),
|
||||
})
|
||||
</script>
|
||||
`
|
||||
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
|
||||
expect(transformPlugin.transform.call({
|
||||
parse: (code: string, opts: any = {}) => Parser.parse(code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
locations: true,
|
||||
...opts,
|
||||
}),
|
||||
}, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
|
||||
"import { validateIdParam } from './utils'
|
||||
|
||||
const __nuxt_page_meta = {
|
||||
validate: validateIdParam,
|
||||
dynamic: ref(true),
|
||||
}
|
||||
export default __nuxt_page_meta"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should work with esbuild.keepNames = true', async () => {
|
||||
const sfc = `
|
||||
<script setup lang="ts">
|
||||
import { foo } from './utils'
|
||||
|
||||
const checkNum = (value) => {
|
||||
return !isNaN(Number(foo(value)))
|
||||
}
|
||||
|
||||
function isNumber (value) {
|
||||
return value && checkNum(value)
|
||||
}
|
||||
|
||||
definePageMeta({
|
||||
validate: ({ params }) => {
|
||||
return isNumber(params.id)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
`
|
||||
const compiled = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
|
||||
const res = await esbuildTransform(compiled.content, {
|
||||
loader: 'ts',
|
||||
keepNames: true,
|
||||
})
|
||||
expect(transformPlugin.transform.call({
|
||||
parse: (code: string, opts: any = {}) => Parser.parse(code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
locations: true,
|
||||
...opts,
|
||||
}),
|
||||
}, res.code, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
|
||||
"import { foo } from "./utils";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
||||
const checkNum = /* @__PURE__ */ __name((value) => {
|
||||
return !isNaN(Number(foo(value)));
|
||||
}, "checkNum");
|
||||
function isNumber(value) {
|
||||
return value && checkNum(value);
|
||||
}
|
||||
const __nuxt_page_meta = {
|
||||
validate: /* @__PURE__ */ __name(({ params }) => {
|
||||
return isNumber(params.id);
|
||||
}, "validate")
|
||||
}
|
||||
export default __nuxt_page_meta"
|
||||
`)
|
||||
})
|
||||
|
||||
it('should throw for await expressions', async () => {
|
||||
const sfc = `
|
||||
<script setup lang="ts">
|
||||
const asyncValue = await Promise.resolve('test')
|
||||
|
||||
definePageMeta({
|
||||
key: asyncValue,
|
||||
})
|
||||
</script>
|
||||
`
|
||||
const compiled = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
|
||||
const res = await esbuildTransform(compiled.content, {
|
||||
loader: 'ts',
|
||||
})
|
||||
|
||||
let wasErrorThrown = false
|
||||
|
||||
try {
|
||||
transformPlugin.transform.call({
|
||||
parse: (code: string, opts: any = {}) => Parser.parse(code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
locations: true,
|
||||
...opts,
|
||||
}),
|
||||
}, res.code, 'component.vue?macro=true')
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
expect(e.message).toMatch(/await in definePageMeta/)
|
||||
wasErrorThrown = true
|
||||
}
|
||||
}
|
||||
|
||||
expect(wasErrorThrown).toBe(true)
|
||||
})
|
||||
|
||||
it('should only add definitions for reference identifiers', () => {
|
||||
const sfc = `
|
||||
<script setup lang="ts">
|
||||
const foo = 'foo'
|
||||
const bar = { bar: 'bar' }.bar, baz = { baz: 'baz' }.baz, x = { foo }
|
||||
const test = 'test'
|
||||
const prop = 'prop'
|
||||
const num = 1
|
||||
|
||||
const val = 'val'
|
||||
|
||||
const useVal = () => ({ val: 'val' })
|
||||
|
||||
function recursive () {
|
||||
recursive()
|
||||
}
|
||||
|
||||
definePageMeta({
|
||||
middleware: [
|
||||
() => {
|
||||
console.log(bar, baz)
|
||||
recursive()
|
||||
|
||||
const val = useVal().val
|
||||
const obj = {
|
||||
num,
|
||||
prop: 'prop',
|
||||
}
|
||||
|
||||
const c = class test {
|
||||
prop = 'prop'
|
||||
test () {}
|
||||
}
|
||||
},
|
||||
],
|
||||
})
|
||||
</script>
|
||||
`
|
||||
const res = compileScript(parse(sfc).descriptor, { id: 'component.vue' })
|
||||
expect(transformPlugin.transform.call({
|
||||
parse: (code: string, opts: any = {}) => Parser.parse(code, {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 'latest',
|
||||
locations: true,
|
||||
...opts,
|
||||
}),
|
||||
}, res.content, 'component.vue?macro=true')?.code).toMatchInlineSnapshot(`
|
||||
"const foo = 'foo'
|
||||
const num = 1
|
||||
const bar = { bar: 'bar' }.bar, baz = { baz: 'baz' }.baz, x = { foo }
|
||||
const useVal = () => ({ val: 'val' })
|
||||
function recursive () {
|
||||
recursive()
|
||||
}
|
||||
const __nuxt_page_meta = {
|
||||
middleware: [
|
||||
() => {
|
||||
console.log(bar, baz)
|
||||
recursive()
|
||||
|
||||
const val = useVal().val
|
||||
const obj = {
|
||||
num,
|
||||
prop: 'prop',
|
||||
}
|
||||
|
||||
const c = class test {
|
||||
prop = 'prop'
|
||||
test () {}
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
export default __nuxt_page_meta"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
670
packages/nuxt/test/parse.test.ts
Normal file
670
packages/nuxt/test/parse.test.ts
Normal file
@ -0,0 +1,670 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getUndeclaredIdentifiersInFunction, parseAndWalk } from '../src/core/utils/parse'
|
||||
import { TestScopeTracker } from './fixture/scope-tracker'
|
||||
|
||||
const filename = 'test.ts'
|
||||
|
||||
describe('scope tracker', () => {
|
||||
it('should throw away exited scopes', () => {
|
||||
const code = `
|
||||
const a = 1
|
||||
{
|
||||
const b = 2
|
||||
}
|
||||
`
|
||||
|
||||
const scopeTracker = new TestScopeTracker()
|
||||
|
||||
parseAndWalk(code, filename, {
|
||||
scopeTracker,
|
||||
})
|
||||
|
||||
expect(scopeTracker.getScopes().size).toBe(0)
|
||||
})
|
||||
|
||||
it ('should keep exited scopes', () => {
|
||||
const code = `
|
||||
const a = 1
|
||||
{
|
||||
const b = 2
|
||||
}
|
||||
`
|
||||
|
||||
const scopeTracker = new TestScopeTracker({ keepExitedScopes: true })
|
||||
|
||||
parseAndWalk(code, filename, {
|
||||
scopeTracker,
|
||||
})
|
||||
|
||||
expect(scopeTracker.getScopes().size).toBe(2)
|
||||
})
|
||||
|
||||
it('should generate scope key correctly and not allocate unnecessary scopes', () => {
|
||||
const code = `
|
||||
// starting in global scope ("")
|
||||
const a = 1
|
||||
// pushing scope for function parameters ("0")
|
||||
// pushing scope for function body ("0-0")
|
||||
function foo (param) {
|
||||
const b = 2
|
||||
// pushing scope for for loop variable declaration ("0-0-0")
|
||||
// pushing scope for for loop body ("0-0-0-0")
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const c = 3
|
||||
|
||||
// pushing scope for block statement ("0-0-0-0-0")
|
||||
try {
|
||||
const d = 4
|
||||
}
|
||||
// in for loop body scope ("0-0-0-0")
|
||||
// pushing scope for catch clause param ("0-0-0-0-1")
|
||||
// pushing scope for block statement ("0-0-0-0-1-0")
|
||||
catch (e) {
|
||||
const f = 4
|
||||
}
|
||||
|
||||
// in for loop body scope ("0-0-0-0")
|
||||
|
||||
const cc = 3
|
||||
}
|
||||
|
||||
// in function body scope ("0-0")
|
||||
|
||||
// pushing scope for for of loop variable declaration ("0-0-1")
|
||||
// pushing scope for for of loop body ("0-0-1-0")
|
||||
for (const i of [1, 2, 3]) {
|
||||
const dd = 3
|
||||
}
|
||||
|
||||
// in function body scope ("0-0")
|
||||
|
||||
// pushing scope for for in loop variable declaration ("0-0-2")
|
||||
// pushing scope for for in loop body ("0-0-2-0")
|
||||
for (const i in [1, 2, 3]) {
|
||||
const ddd = 3
|
||||
}
|
||||
|
||||
// in function body scope ("0-0")
|
||||
|
||||
// pushing scope for while loop body ("0-0-3")
|
||||
while (true) {
|
||||
const e = 3
|
||||
}
|
||||
}
|
||||
|
||||
// in global scope ("")
|
||||
|
||||
// pushing scope for function expression name ("1")
|
||||
// pushing scope for function parameters ("1-0")
|
||||
// pushing scope for function body ("1-0-0")
|
||||
const baz = function bar (param) {
|
||||
const g = 5
|
||||
|
||||
// pushing scope for block statement ("1-0-0-0")
|
||||
if (true) {
|
||||
const h = 6
|
||||
}
|
||||
}
|
||||
|
||||
// in global scope ("")
|
||||
|
||||
// pushing scope for function expression name ("2")
|
||||
{
|
||||
const i = 7
|
||||
// pushing scope for block statement ("2-0")
|
||||
{
|
||||
const j = 8
|
||||
}
|
||||
}
|
||||
|
||||
// in global scope ("")
|
||||
|
||||
// pushing scope for arrow function parameters ("3")
|
||||
// pushing scope for arrow function body ("3-0")
|
||||
const arrow = (param) => {
|
||||
const k = 9
|
||||
}
|
||||
|
||||
// in global scope ("")
|
||||
|
||||
// pushing scope for class expression name ("4")
|
||||
const classExpression = class InternalClassName {
|
||||
classAttribute = 10
|
||||
// pushing scope for constructor function expression name ("4-0")
|
||||
// pushing scope for constructor parameters ("4-0-0")
|
||||
// pushing scope for constructor body ("4-0-0-0")
|
||||
constructor(constructorParam) {
|
||||
const l = 10
|
||||
}
|
||||
|
||||
// in class body scope ("4")
|
||||
|
||||
// pushing scope for static block ("4-1")
|
||||
static {
|
||||
const m = 11
|
||||
}
|
||||
}
|
||||
|
||||
// in global scope ("")
|
||||
|
||||
class NoScopePushedForThis {
|
||||
// pushing scope for constructor function expression name ("5")
|
||||
// pushing scope for constructor parameters ("5-0")
|
||||
// pushing scope for constructor body ("5-0-0")
|
||||
constructor() {
|
||||
const n = 12
|
||||
}
|
||||
}
|
||||
|
||||
`
|
||||
|
||||
const scopeTracker = new TestScopeTracker({
|
||||
keepExitedScopes: true,
|
||||
})
|
||||
|
||||
// is in global scope initially
|
||||
expect(scopeTracker.getScopeIndexKey()).toBe('')
|
||||
|
||||
parseAndWalk(code, filename, {
|
||||
scopeTracker,
|
||||
})
|
||||
|
||||
// is in global scope after parsing
|
||||
expect(scopeTracker.getScopeIndexKey()).toBe('')
|
||||
|
||||
// check that the scopes are correct
|
||||
const scopes = scopeTracker.getScopes()
|
||||
|
||||
const expectedScopesInOrder = [
|
||||
'',
|
||||
'0',
|
||||
'0-0',
|
||||
'0-0-0',
|
||||
'0-0-0-0',
|
||||
'0-0-0-0-0',
|
||||
'0-0-0-0-1',
|
||||
'0-0-0-0-1-0',
|
||||
'0-0-1',
|
||||
'0-0-1-0',
|
||||
'0-0-2',
|
||||
'0-0-2-0',
|
||||
'0-0-3',
|
||||
'1',
|
||||
'1-0',
|
||||
'1-0-0',
|
||||
'1-0-0-0',
|
||||
'2',
|
||||
'2-0',
|
||||
'3',
|
||||
'3-0',
|
||||
'4',
|
||||
// '4-0', -> DO NOT UNCOMMENT - class constructor method definition doesn't provide a function expression id (scope doesn't have any identifiers)
|
||||
'4-0-0',
|
||||
'4-0-0-0',
|
||||
'4-1',
|
||||
// '5', -> DO NOT UNCOMMENT - class constructor - same as above
|
||||
// '5-0', -> DO NOT UNCOMMENT - class constructor parameters (none in this case, so the scope isn't stored)
|
||||
'5-0-0',
|
||||
]
|
||||
|
||||
expect(scopes.size).toBe(expectedScopesInOrder.length)
|
||||
|
||||
const scopeKeys = Array.from(scopes.keys())
|
||||
|
||||
expect(scopeKeys).toEqual(expectedScopesInOrder)
|
||||
})
|
||||
|
||||
it ('should track variable declarations', () => {
|
||||
const code = `
|
||||
const a = 1
|
||||
let x, y = 2
|
||||
|
||||
{
|
||||
let b = 2
|
||||
}
|
||||
`
|
||||
|
||||
const scopeTracker = new TestScopeTracker({
|
||||
keepExitedScopes: true,
|
||||
})
|
||||
|
||||
parseAndWalk(code, filename, {
|
||||
scopeTracker,
|
||||
})
|
||||
|
||||
const scopes = scopeTracker.getScopes()
|
||||
|
||||
const globalScope = scopes.get('')
|
||||
expect(globalScope?.get('a')?.type).toEqual('Variable')
|
||||
expect(globalScope?.get('b')).toBeUndefined()
|
||||
expect(globalScope?.get('x')?.type).toEqual('Variable')
|
||||
expect(globalScope?.get('y')?.type).toEqual('Variable')
|
||||
|
||||
const blockScope = scopes.get('0')
|
||||
expect(blockScope?.get('b')?.type).toEqual('Variable')
|
||||
expect(blockScope?.get('a')).toBeUndefined()
|
||||
expect(blockScope?.get('x')).toBeUndefined()
|
||||
expect(blockScope?.get('y')).toBeUndefined()
|
||||
|
||||
expect(scopeTracker.isDeclaredInScope('a', '')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('a', '0')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('y', '')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('y', '0')).toBe(true)
|
||||
|
||||
expect(scopeTracker.isDeclaredInScope('b', '')).toBe(false)
|
||||
expect(scopeTracker.isDeclaredInScope('b', '0')).toBe(true)
|
||||
})
|
||||
|
||||
it ('should separate variables in different scopes', () => {
|
||||
const code = `
|
||||
const a = 1
|
||||
|
||||
{
|
||||
let a = 2
|
||||
}
|
||||
|
||||
function foo (a) {
|
||||
// scope "1-0"
|
||||
let b = a
|
||||
}
|
||||
`
|
||||
|
||||
const scopeTracker = new TestScopeTracker({
|
||||
keepExitedScopes: true,
|
||||
})
|
||||
|
||||
parseAndWalk(code, filename, {
|
||||
scopeTracker,
|
||||
})
|
||||
|
||||
const globalA = scopeTracker.getDeclarationFromScope('a', '')
|
||||
expect(globalA?.type).toEqual('Variable')
|
||||
expect(globalA?.type === 'Variable' && globalA.variableNode.type).toEqual('VariableDeclaration')
|
||||
|
||||
const blockA = scopeTracker.getDeclarationFromScope('a', '0')
|
||||
expect(blockA?.type).toEqual('Variable')
|
||||
expect(blockA?.type === 'Variable' && blockA.variableNode.type).toEqual('VariableDeclaration')
|
||||
|
||||
// check that the two `a` variables are different
|
||||
expect(globalA?.type === 'Variable' && globalA.variableNode).not.toBe(blockA?.type === 'Variable' && blockA.variableNode)
|
||||
|
||||
// check that the `a` in the function scope is a function param and not a variable
|
||||
const fooA = scopeTracker.getDeclarationFromScope('a', '1-0')
|
||||
expect(fooA?.type).toEqual('FunctionParam')
|
||||
})
|
||||
|
||||
it ('should handle patterns', () => {
|
||||
const code = `
|
||||
const { a, b: c } = { a: 1, b: 2 }
|
||||
const [d, [e]] = [3, [4]]
|
||||
const { f: { g } } = { f: { g: 5 } }
|
||||
|
||||
function foo ({ h, i: j } = {}, [k, [l, m], ...rest]) {
|
||||
}
|
||||
|
||||
try {} catch ({ message }) {}
|
||||
`
|
||||
|
||||
const scopeTracker = new TestScopeTracker({
|
||||
keepExitedScopes: true,
|
||||
})
|
||||
|
||||
parseAndWalk(code, filename, {
|
||||
scopeTracker,
|
||||
})
|
||||
|
||||
const scopes = scopeTracker.getScopes()
|
||||
expect(scopes.size).toBe(3)
|
||||
|
||||
const globalScope = scopes.get('')
|
||||
expect(globalScope?.size).toBe(6)
|
||||
|
||||
expect(globalScope?.get('a')?.type).toEqual('Variable')
|
||||
expect(globalScope?.get('b')?.type).toBeUndefined()
|
||||
expect(globalScope?.get('c')?.type).toEqual('Variable')
|
||||
expect(globalScope?.get('d')?.type).toEqual('Variable')
|
||||
expect(globalScope?.get('e')?.type).toEqual('Variable')
|
||||
expect(globalScope?.get('f')?.type).toBeUndefined()
|
||||
expect(globalScope?.get('g')?.type).toEqual('Variable')
|
||||
expect(globalScope?.get('foo')?.type).toEqual('Function')
|
||||
|
||||
const fooScope = scopes.get('0')
|
||||
expect(fooScope?.size).toBe(6)
|
||||
|
||||
expect(fooScope?.get('h')?.type).toEqual('FunctionParam')
|
||||
expect(fooScope?.get('i')?.type).toBeUndefined()
|
||||
expect(fooScope?.get('j')?.type).toEqual('FunctionParam')
|
||||
expect(fooScope?.get('k')?.type).toEqual('FunctionParam')
|
||||
expect(fooScope?.get('l')?.type).toEqual('FunctionParam')
|
||||
expect(fooScope?.get('m')?.type).toEqual('FunctionParam')
|
||||
expect(fooScope?.get('rest')?.type).toEqual('FunctionParam')
|
||||
|
||||
const catchScope = scopes.get('2')
|
||||
expect(catchScope?.size).toBe(1)
|
||||
expect(catchScope?.get('message')?.type).toEqual('CatchParam')
|
||||
|
||||
expect(scopeTracker.isDeclaredInScope('a', '')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('b', '')).toBe(false)
|
||||
expect(scopeTracker.isDeclaredInScope('c', '')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('d', '')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('e', '')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('f', '')).toBe(false)
|
||||
expect(scopeTracker.isDeclaredInScope('g', '')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('h', '0')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('i', '0')).toBe(false)
|
||||
expect(scopeTracker.isDeclaredInScope('j', '0')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('k', '0')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('l', '0')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('m', '0')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('rest', '0')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('message', '2')).toBe(true)
|
||||
})
|
||||
|
||||
it ('should handle loops', () => {
|
||||
const code = `
|
||||
for (let i = 0, getI = () => i; i < 3; i++) {
|
||||
console.log(getI());
|
||||
}
|
||||
|
||||
let j = 0;
|
||||
for (; j < 3; j++) { }
|
||||
|
||||
const obj = { a: 1, b: 2, c: 3 }
|
||||
for (const property in obj) { }
|
||||
|
||||
const arr = ['a', 'b', 'c']
|
||||
for (const element of arr) { }
|
||||
`
|
||||
|
||||
const scopeTracker = new TestScopeTracker({
|
||||
keepExitedScopes: true,
|
||||
})
|
||||
|
||||
parseAndWalk(code, filename, {
|
||||
scopeTracker,
|
||||
})
|
||||
|
||||
const scopes = scopeTracker.getScopes()
|
||||
expect(scopes.size).toBe(4)
|
||||
|
||||
const globalScope = scopes.get('')
|
||||
expect(globalScope?.size).toBe(3)
|
||||
expect(globalScope?.get('j')?.type).toEqual('Variable')
|
||||
expect(globalScope?.get('obj')?.type).toEqual('Variable')
|
||||
expect(globalScope?.get('arr')?.type).toEqual('Variable')
|
||||
|
||||
const forScope1 = scopes.get('0')
|
||||
expect(forScope1?.size).toBe(2)
|
||||
expect(forScope1?.get('i')?.type).toEqual('Variable')
|
||||
expect(forScope1?.get('getI')?.type).toEqual('Variable')
|
||||
|
||||
const forScope2 = scopes.get('1')
|
||||
expect(forScope2).toBeUndefined()
|
||||
|
||||
const forScope3 = scopes.get('2')
|
||||
expect(forScope3?.size).toBe(1)
|
||||
expect(forScope3?.get('property')?.type).toEqual('Variable')
|
||||
|
||||
const forScope4 = scopes.get('3')
|
||||
expect(forScope4?.size).toBe(1)
|
||||
expect(forScope4?.get('element')?.type).toEqual('Variable')
|
||||
|
||||
expect(scopeTracker.isDeclaredInScope('i', '')).toBe(false)
|
||||
expect(scopeTracker.isDeclaredInScope('getI', '')).toBe(false)
|
||||
expect(scopeTracker.isDeclaredInScope('i', '0-0')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('getI', '0-0')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('j', '')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('j', '1-0')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('property', '')).toBe(false)
|
||||
expect(scopeTracker.isDeclaredInScope('element', '')).toBe(false)
|
||||
})
|
||||
|
||||
it ('should handle imports', () => {
|
||||
const code = `
|
||||
import { a, b as c } from 'module-a'
|
||||
import d from 'module-b'
|
||||
`
|
||||
|
||||
const scopeTracker = new TestScopeTracker({
|
||||
keepExitedScopes: true,
|
||||
})
|
||||
|
||||
parseAndWalk(code, filename, {
|
||||
scopeTracker,
|
||||
})
|
||||
|
||||
expect(scopeTracker.isDeclaredInScope('a', '')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('b', '')).toBe(false)
|
||||
expect(scopeTracker.isDeclaredInScope('c', '')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('d', '')).toBe(true)
|
||||
|
||||
expect(scopeTracker.getScopes().get('')?.size).toBe(3)
|
||||
})
|
||||
|
||||
it ('should handle classes', () => {
|
||||
const code = `
|
||||
// ""
|
||||
|
||||
class Foo {
|
||||
someProperty = 1
|
||||
|
||||
// "0" - function expression name
|
||||
// "0-0" - constructor parameters
|
||||
// "0-0-0" - constructor body
|
||||
constructor(param) {
|
||||
let a = 1
|
||||
this.b = 1
|
||||
}
|
||||
|
||||
// "1" - method name
|
||||
// "1-0" - method parameters
|
||||
// "1-0-0" - method body
|
||||
someMethod(param) {
|
||||
let c = 1
|
||||
}
|
||||
|
||||
// "2" - method name
|
||||
// "2-0" - method parameters
|
||||
// "2-0-0" - method body
|
||||
get d() {
|
||||
let e = 1
|
||||
return 1
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const scopeTracker = new TestScopeTracker({
|
||||
keepExitedScopes: true,
|
||||
})
|
||||
|
||||
parseAndWalk(code, filename, {
|
||||
scopeTracker,
|
||||
})
|
||||
|
||||
const scopes = scopeTracker.getScopes()
|
||||
|
||||
// only the scopes containing identifiers are stored
|
||||
const expectedScopes = [
|
||||
'',
|
||||
'0-0',
|
||||
'0-0-0',
|
||||
'1-0',
|
||||
'1-0-0',
|
||||
'2-0-0',
|
||||
]
|
||||
|
||||
expect(scopes.size).toBe(expectedScopes.length)
|
||||
|
||||
const scopeKeys = Array.from(scopes.keys())
|
||||
expect(scopeKeys).toEqual(expectedScopes)
|
||||
|
||||
expect(scopeTracker.isDeclaredInScope('Foo', '')).toBe(true)
|
||||
|
||||
// properties should be accessible through the class
|
||||
expect(scopeTracker.isDeclaredInScope('someProperty', '')).toBe(false)
|
||||
expect(scopeTracker.isDeclaredInScope('someProperty', '0')).toBe(false)
|
||||
|
||||
expect(scopeTracker.isDeclaredInScope('a', '0-0-0')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('b', '0-0-0')).toBe(false)
|
||||
|
||||
// method definitions don't have names in function expressions, so it is not stored
|
||||
// they should be accessed through the class
|
||||
expect(scopeTracker.isDeclaredInScope('someMethod', '1')).toBe(false)
|
||||
expect(scopeTracker.isDeclaredInScope('someMethod', '1-0-0')).toBe(false)
|
||||
expect(scopeTracker.isDeclaredInScope('someMethod', '')).toBe(false)
|
||||
expect(scopeTracker.isDeclaredInScope('c', '1-0-0')).toBe(true)
|
||||
|
||||
expect(scopeTracker.isDeclaredInScope('d', '2')).toBe(false)
|
||||
expect(scopeTracker.isDeclaredInScope('d', '2-0-0')).toBe(false)
|
||||
expect(scopeTracker.isDeclaredInScope('d', '')).toBe(false)
|
||||
expect(scopeTracker.isDeclaredInScope('e', '2-0-0')).toBe(true)
|
||||
})
|
||||
|
||||
it ('should freeze scopes', () => {
|
||||
let code = `
|
||||
const a = 1
|
||||
{
|
||||
const b = 2
|
||||
}
|
||||
`
|
||||
|
||||
const scopeTracker = new TestScopeTracker({
|
||||
keepExitedScopes: true,
|
||||
})
|
||||
|
||||
parseAndWalk(code, filename, {
|
||||
scopeTracker,
|
||||
})
|
||||
|
||||
expect(scopeTracker.getScopes().size).toBe(2)
|
||||
|
||||
code = code + '\n' + `
|
||||
{
|
||||
const c = 3
|
||||
}
|
||||
`
|
||||
|
||||
parseAndWalk(code, filename, {
|
||||
scopeTracker,
|
||||
})
|
||||
|
||||
expect(scopeTracker.getScopes().size).toBe(3)
|
||||
|
||||
scopeTracker.freeze()
|
||||
|
||||
code = code + '\n' + `
|
||||
{
|
||||
const d = 4
|
||||
}
|
||||
`
|
||||
|
||||
parseAndWalk(code, filename, {
|
||||
scopeTracker,
|
||||
})
|
||||
|
||||
expect(scopeTracker.getScopes().size).toBe(3)
|
||||
|
||||
expect(scopeTracker.isDeclaredInScope('a', '')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('b', '0')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('c', '1')).toBe(true)
|
||||
expect(scopeTracker.isDeclaredInScope('d', '2')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parsing', () => {
|
||||
it ('should correctly get identifiers not declared in a function', () => {
|
||||
const functionParams = `(param, { param1, temp: param2 } = {}, [param3, [param4]], ...rest)`
|
||||
const functionBody = `{
|
||||
const c = 1, d = 2
|
||||
console.log(undeclaredIdentifier1, foo)
|
||||
const obj = {
|
||||
key1: param,
|
||||
key2: undeclaredIdentifier1,
|
||||
undeclaredIdentifier2: undeclaredIdentifier2,
|
||||
undeclaredIdentifier3,
|
||||
undeclaredIdentifier4,
|
||||
}
|
||||
nonExistentFunction()
|
||||
|
||||
console.log(a, b, c, d, param, param1, param2, param3, param4, param['test']['key'], rest)
|
||||
console.log(param3[0].access['someKey'], obj, obj.key1, obj.key2, obj.undeclaredIdentifier2, obj.undeclaredIdentifier3)
|
||||
|
||||
try {} catch (error) { console.log(error) }
|
||||
|
||||
class Foo { constructor() { console.log(Foo) } }
|
||||
const cls = class Bar { constructor() { console.log(Bar, cls) } }
|
||||
const cls2 = class Baz {
|
||||
someProperty = someValue
|
||||
someMethod() { }
|
||||
}
|
||||
console.log(Baz)
|
||||
|
||||
function f() {
|
||||
console.log(hoisted, nonHoisted)
|
||||
}
|
||||
let hoisted = 1
|
||||
f()
|
||||
}`
|
||||
|
||||
const code = `
|
||||
import { a } from 'module-a'
|
||||
const b = 1
|
||||
|
||||
// "0"
|
||||
function foo ${functionParams} ${functionBody}
|
||||
|
||||
// "1"
|
||||
const f = ${functionParams} => ${functionBody}
|
||||
|
||||
// "2-0"
|
||||
const bar = function ${functionParams} ${functionBody}
|
||||
|
||||
// "3-0"
|
||||
const baz = function foo ${functionParams} ${functionBody}
|
||||
|
||||
// "4"
|
||||
function emptyParams() {
|
||||
console.log(param)
|
||||
}
|
||||
`
|
||||
|
||||
const scopeTracker = new TestScopeTracker({
|
||||
keepExitedScopes: true,
|
||||
})
|
||||
|
||||
let processedFunctions = 0
|
||||
|
||||
parseAndWalk(code, filename, {
|
||||
scopeTracker,
|
||||
enter: (node) => {
|
||||
const currentScope = scopeTracker.getScopeIndexKey()
|
||||
if ((node.type !== 'FunctionDeclaration' && node.type !== 'FunctionExpression' && node.type !== 'ArrowFunctionExpression') || !['0', '1', '2-0', '3-0', '4'].includes(currentScope)) { return }
|
||||
|
||||
const undeclaredIdentifiers = getUndeclaredIdentifiersInFunction(node)
|
||||
expect(undeclaredIdentifiers).toEqual(currentScope === '4'
|
||||
? [
|
||||
'console',
|
||||
'param',
|
||||
]
|
||||
: [
|
||||
'console',
|
||||
'undeclaredIdentifier1',
|
||||
...(node.type === 'ArrowFunctionExpression' || (node.type === 'FunctionExpression' && !node.id) ? ['foo'] : []),
|
||||
'undeclaredIdentifier2',
|
||||
'undeclaredIdentifier3',
|
||||
'undeclaredIdentifier4',
|
||||
'nonExistentFunction',
|
||||
'a', // import is outside the scope of the function
|
||||
'b', // variable is outside the scope of the function
|
||||
'someValue',
|
||||
'Baz',
|
||||
'nonHoisted',
|
||||
])
|
||||
|
||||
processedFunctions++
|
||||
},
|
||||
})
|
||||
|
||||
expect(processedFunctions).toBe(5)
|
||||
})
|
||||
})
|
@ -32,7 +32,7 @@
|
||||
"dependencies": {
|
||||
"@nuxt/friendly-errors-webpack-plugin": "^2.6.0",
|
||||
"@nuxt/kit": "workspace:*",
|
||||
"@rspack/core": "^1.1.6",
|
||||
"@rspack/core": "^1.1.8",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||
@ -44,11 +44,11 @@
|
||||
"fork-ts-checker-webpack-plugin": "^9.0.2",
|
||||
"globby": "^14.0.2",
|
||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||
"jiti": "^2.4.1",
|
||||
"jiti": "^2.4.2",
|
||||
"knitwork": "^1.2.0",
|
||||
"lodash-es": "4.17.21",
|
||||
"magic-string": "^0.30.17",
|
||||
"memfs": "^4.14.1",
|
||||
"memfs": "^4.15.1",
|
||||
"ohash": "^1.1.4",
|
||||
"pathe": "^1.1.2",
|
||||
"pify": "^6.1.0",
|
||||
@ -77,7 +77,7 @@
|
||||
"@types/pify": "5.0.4",
|
||||
"@types/webpack-bundle-analyzer": "4.7.0",
|
||||
"@types/webpack-hot-middleware": "2.25.9",
|
||||
"rollup": "4.28.1",
|
||||
"rollup": "4.29.1",
|
||||
"unbuild": "3.0.1",
|
||||
"vue": "3.5.13"
|
||||
},
|
||||
|
@ -42,32 +42,32 @@
|
||||
"@vitejs/plugin-vue-jsx": "4.1.1",
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/compiler-sfc": "3.5.13",
|
||||
"@vue/language-core": "2.1.10",
|
||||
"@vue/language-core": "2.2.0",
|
||||
"esbuild-loader": "4.2.2",
|
||||
"file-loader": "6.2.0",
|
||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||
"ignore": "6.0.2",
|
||||
"ignore": "7.0.0",
|
||||
"nitro": "npm:nitro-nightly@3.0.0-beta-28796231.359af68d",
|
||||
"ofetch": "1.4.1",
|
||||
"sass-loader": "16.0.4",
|
||||
"unbuild": "3.0.1",
|
||||
"unctx": "2.4.0",
|
||||
"vite": "6.0.3",
|
||||
"unctx": "2.4.1",
|
||||
"vite": "6.0.6",
|
||||
"vue": "3.5.13",
|
||||
"vue-bundle-renderer": "2.1.1",
|
||||
"vue-loader": "17.4.2",
|
||||
"vue-router": "4.5.0",
|
||||
"webpack": "5.96.1",
|
||||
"webpack": "5.97.1",
|
||||
"webpack-dev-middleware": "7.4.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"c12": "^2.0.1",
|
||||
"compatx": "^0.1.8",
|
||||
"consola": "^3.2.3",
|
||||
"consola": "^3.3.3",
|
||||
"defu": "^6.1.4",
|
||||
"hookable": "^5.5.3",
|
||||
"pathe": "^1.1.2",
|
||||
"pkg-types": "^1.2.1",
|
||||
"pkg-types": "^1.3.0",
|
||||
"scule": "^1.3.0",
|
||||
"std-env": "^3.8.0",
|
||||
"ufo": "^1.5.4",
|
||||
|
@ -28,6 +28,11 @@ export default defineUntypedSchema({
|
||||
*/
|
||||
imports: {
|
||||
global: false,
|
||||
/**
|
||||
* Whether to scan your `composables/` and `utils/` directories for composables to auto-import.
|
||||
* Auto-imports registered by Nuxt or other modules, such as imports from `vue` or `nuxt`, will still be enabled.
|
||||
*/
|
||||
scan: true,
|
||||
|
||||
/**
|
||||
* An array of custom directories that will be auto-imported.
|
||||
|
@ -146,8 +146,7 @@ export default defineUntypedSchema({
|
||||
*/
|
||||
keyedComposables: {
|
||||
$resolve: (val: Array<{ name: string, argumentLength: string }> | undefined) => [
|
||||
{ name: 'useId', argumentLength: 1 },
|
||||
{ name: 'callOnce', argumentLength: 2 },
|
||||
{ name: 'callOnce', argumentLength: 3 },
|
||||
{ name: 'defineNuxtComponent', argumentLength: 2 },
|
||||
{ name: 'useState', argumentLength: 2 },
|
||||
{ name: 'useFetch', argumentLength: 3 },
|
||||
|
@ -59,8 +59,8 @@ export default defineUntypedSchema({
|
||||
if (val === false || (await get('dev')) || (await get('ssr')) === false || (await get('builder')) === '@nuxt/webpack-builder') {
|
||||
return false
|
||||
}
|
||||
// Enabled by default for vite prod with ssr
|
||||
return val ?? true
|
||||
// Enabled by default for vite prod with ssr (for vue components)
|
||||
return val ?? ((await get('future') as Record<string, unknown>).compatibilityVersion === 4 ? (id: string) => id && id.includes('.vue') : true)
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -17,11 +17,11 @@
|
||||
"prerender": "pnpm build && jiti ./lib/prerender"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@unocss/reset": "0.65.1",
|
||||
"@unocss/reset": "0.65.3",
|
||||
"beasties": "0.2.0",
|
||||
"html-validate": "8.27.0",
|
||||
"html-validate": "9.1.0",
|
||||
"htmlnano": "2.1.1",
|
||||
"jiti": "2.4.1",
|
||||
"jiti": "2.4.2",
|
||||
"knitwork": "1.2.0",
|
||||
"pathe": "1.1.2",
|
||||
"prettier": "3.4.2",
|
||||
@ -29,8 +29,8 @@
|
||||
"svgo": "3.3.2",
|
||||
"tinyexec": "0.3.1",
|
||||
"tinyglobby": "0.2.10",
|
||||
"unocss": "0.65.1",
|
||||
"vite": "6.0.3"
|
||||
"unocss": "0.65.3",
|
||||
"vite": "6.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
|
@ -26,8 +26,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/schema": "workspace:*",
|
||||
"@types/clear": "0.1.4",
|
||||
"rollup": "4.28.1",
|
||||
"rollup": "4.29.1",
|
||||
"unbuild": "3.0.1",
|
||||
"vue": "3.5.13"
|
||||
},
|
||||
@ -37,28 +36,27 @@
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"clear": "^0.1.0",
|
||||
"consola": "^3.2.3",
|
||||
"consola": "^3.3.3",
|
||||
"cssnano": "^7.0.6",
|
||||
"defu": "^6.1.4",
|
||||
"esbuild": "^0.24.0",
|
||||
"esbuild": "^0.24.2",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"externality": "^1.0.2",
|
||||
"get-port-please": "^3.1.2",
|
||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||
"jiti": "^2.4.1",
|
||||
"jiti": "^2.4.2",
|
||||
"knitwork": "^1.2.0",
|
||||
"magic-string": "^0.30.17",
|
||||
"mlly": "^1.7.3",
|
||||
"pathe": "^1.1.2",
|
||||
"pkg-types": "^1.2.1",
|
||||
"pkg-types": "^1.3.0",
|
||||
"postcss": "^8.4.49",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"rollup-plugin-visualizer": "^5.13.1",
|
||||
"std-env": "^3.8.0",
|
||||
"ufo": "^1.5.4",
|
||||
"unenv": "^1.10.0",
|
||||
"unplugin": "^2.1.0",
|
||||
"vite": "^6.0.3",
|
||||
"vite": "^6.0.6",
|
||||
"vite-node": "^2.1.8",
|
||||
"vite-plugin-checker": "^0.8.0",
|
||||
"vue-bundle-renderer": "^2.1.1"
|
||||
|
@ -103,6 +103,9 @@ export async function buildClient (ctx: ViteBuildContext) {
|
||||
'ufo',
|
||||
'unctx',
|
||||
'unenv',
|
||||
|
||||
// these will never be imported on the client
|
||||
'#app-manifest',
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import type * as vite from 'vite'
|
||||
import { createLogger } from 'vite'
|
||||
import { logger } from '@nuxt/kit'
|
||||
import { colorize } from 'consola/utils'
|
||||
import { hasTTY, isCI } from 'std-env'
|
||||
import clear from 'clear'
|
||||
import type { NuxtOptions } from '@nuxt/schema'
|
||||
import { useResolveFromPublicAssets } from '../plugins/public-dirs'
|
||||
|
||||
@ -27,6 +27,13 @@ const RUNTIME_RESOLVE_REF_RE = /^([^ ]+) referenced in/m
|
||||
export function createViteLogger (config: vite.InlineConfig): vite.Logger {
|
||||
const loggedErrors = new WeakSet<any>()
|
||||
const canClearScreen = hasTTY && !isCI && config.clearScreen
|
||||
const _logger = createLogger()
|
||||
const clear = () => {
|
||||
_logger.clearScreen(
|
||||
// @ts-expect-error silent is a log level but not a valid option for clearScreens
|
||||
'silent',
|
||||
)
|
||||
}
|
||||
const clearScreen = canClearScreen ? clear : () => {}
|
||||
|
||||
const { resolveFromPublicAssets } = useResolveFromPublicAssets()
|
||||
|
@ -43,11 +43,11 @@
|
||||
"fork-ts-checker-webpack-plugin": "^9.0.2",
|
||||
"globby": "^14.0.2",
|
||||
"h3": "npm:h3-nightly@2.0.0-1718872656.6765a6e",
|
||||
"jiti": "^2.4.1",
|
||||
"jiti": "^2.4.2",
|
||||
"knitwork": "^1.2.0",
|
||||
"lodash-es": "4.17.21",
|
||||
"magic-string": "^0.30.17",
|
||||
"memfs": "^4.14.1",
|
||||
"memfs": "^4.15.1",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"ohash": "^1.1.4",
|
||||
"pathe": "^1.1.2",
|
||||
@ -66,7 +66,7 @@
|
||||
"url-loader": "^4.1.1",
|
||||
"vue-bundle-renderer": "^2.1.1",
|
||||
"vue-loader": "^17.4.2",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack": "^5.97.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-dev-middleware": "^7.4.2",
|
||||
"webpack-hot-middleware": "^2.26.1",
|
||||
@ -74,12 +74,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/schema": "workspace:*",
|
||||
"@rspack/core": "1.1.6",
|
||||
"@rspack/core": "1.1.8",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/pify": "5.0.4",
|
||||
"@types/webpack-bundle-analyzer": "4.7.0",
|
||||
"@types/webpack-hot-middleware": "2.25.9",
|
||||
"rollup": "4.28.1",
|
||||
"rollup": "4.29.1",
|
||||
"unbuild": "3.0.1",
|
||||
"vue": "3.5.13"
|
||||
},
|
||||
|
@ -88,9 +88,18 @@ export function dynamicRequire ({ dir, ignore, inline }: Options): Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
type WebpackChunk = {
|
||||
id: string
|
||||
ids: string[]
|
||||
modules: Record<string, unknown>
|
||||
__webpack_id__?: string
|
||||
__webpack_ids__?: string[]
|
||||
__webpack_modules__?: Record<string, unknown>
|
||||
}
|
||||
|
||||
async function getWebpackChunkMeta (src: string) {
|
||||
const chunk = await importModule<{ id: string, ids: string[], modules: Record<string, unknown> }>(src) || {}
|
||||
const { id, ids, modules } = chunk
|
||||
const chunk = await importModule<WebpackChunk>(src) || {}
|
||||
const { __webpack_id__, __webpack_ids__, __webpack_modules__, id = __webpack_id__, ids = __webpack_ids__, modules = __webpack_modules__ } = chunk
|
||||
if (!id && !ids) {
|
||||
return null // Not a webpack chunk
|
||||
}
|
||||
|
3223
pnpm-lock.yaml
3223
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -31,12 +31,6 @@
|
||||
"@nuxt/kit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupName": "typescript",
|
||||
"matchPackageNames": [
|
||||
"typescript"
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupName": "webpack",
|
||||
"matchPackageNames": [
|
||||
|
@ -1223,6 +1223,15 @@ describe('composables', () => {
|
||||
const { page } = await renderPage('/once')
|
||||
expect(await page.getByText('once:').textContent()).toContain('once: 2')
|
||||
})
|
||||
it('`callOnce` should run code once with navigation mode during initial render', async () => {
|
||||
const html = await $fetch<string>('/once-nav-initial')
|
||||
|
||||
expect(html).toContain('once.vue')
|
||||
expect(html).toContain('once: 2')
|
||||
|
||||
const { page } = await renderPage('/once-nav-initial')
|
||||
expect(await page.getByText('once:').textContent()).toContain('once: 2')
|
||||
})
|
||||
it('`useId` should generate unique ids', async () => {
|
||||
// TODO: work around interesting Vue bug where async components are loaded in a different order on first import
|
||||
await $fetch<string>('/use-id')
|
||||
@ -2616,6 +2625,11 @@ describe.skipIf(isWindows)('useAsyncData', () => {
|
||||
|
||||
await page.close()
|
||||
})
|
||||
it('works with useId', async () => {
|
||||
const html = await $fetch<string>('/useAsyncData/use-id')
|
||||
expect(html).toContain('<div>v-0-0-0</div> v-0-0</div>')
|
||||
await expectNoClientErrors('/useAsyncData/use-id')
|
||||
})
|
||||
})
|
||||
|
||||
describe.runIf(isDev())('component testing', () => {
|
||||
|
@ -21,8 +21,8 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
|
||||
const [clientStats, clientStatsInlined] = await Promise.all((['.output', '.output-inline'])
|
||||
.map(outputDir => analyzeSizes(['**/*.js'], join(rootDir, outputDir, 'public'))))
|
||||
|
||||
expect.soft(roundToKilobytes(clientStats!.totalBytes)).toMatchInlineSnapshot(`"115k"`)
|
||||
expect.soft(roundToKilobytes(clientStatsInlined!.totalBytes)).toMatchInlineSnapshot(`"115k"`)
|
||||
expect.soft(roundToKilobytes(clientStats!.totalBytes)).toMatchInlineSnapshot(`"116k"`)
|
||||
expect.soft(roundToKilobytes(clientStatsInlined!.totalBytes)).toMatchInlineSnapshot(`"116k"`)
|
||||
|
||||
const files = new Set([...clientStats!.files, ...clientStatsInlined!.files].map(f => f.replace(/\..*\.js/, '.js')))
|
||||
|
||||
|
5
test/fixtures/basic-types/types.ts
vendored
5
test/fixtures/basic-types/types.ts
vendored
@ -467,11 +467,6 @@ describe('composables', () => {
|
||||
expectTypeOf(useFetch('/test', { default: () => 500 }).data).toEqualTypeOf<Ref<unknown>>()
|
||||
})
|
||||
|
||||
it('prevents passing string to `useId`', () => {
|
||||
// @ts-expect-error providing a key is not allowed
|
||||
useId('test')
|
||||
})
|
||||
|
||||
it('enforces readonly cookies', () => {
|
||||
// @ts-expect-error readonly cookie
|
||||
useCookie('test', { readonly: true }).value = 'thing'
|
||||
|
14
test/fixtures/basic/pages/once-nav-initial.vue
vendored
Normal file
14
test/fixtures/basic/pages/once-nav-initial.vue
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
<script setup>
|
||||
const counter = useState('once', () => 0)
|
||||
|
||||
await callOnce(() => counter.value++, { mode: 'navigation' })
|
||||
await callOnce('same-key', () => counter.value++, { mode: 'navigation' })
|
||||
await callOnce('same-key', () => counter.value++, { mode: 'navigation' })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div>once.vue</div>
|
||||
<div>once: {{ counter }}</div>
|
||||
</div>
|
||||
</template>
|
18
test/fixtures/basic/pages/useAsyncData/use-id.vue
vendored
Normal file
18
test/fixtures/basic/pages/useAsyncData/use-id.vue
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
const Child = defineComponent({
|
||||
setup () {
|
||||
const id = useId()
|
||||
return () => h('div', id)
|
||||
},
|
||||
})
|
||||
|
||||
const id = useId()
|
||||
useAsyncData('test', () => Promise.resolve('A'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Child />
|
||||
{{ id }}
|
||||
</div>
|
||||
</template>
|
@ -29,6 +29,10 @@ if (process.env.TEST_ENV !== 'built' && !isWindows) {
|
||||
const indexVue = await fsp.readFile(join(fixturePath, 'pages/index.vue'), 'utf8')
|
||||
|
||||
describe('hmr', () => {
|
||||
it('should load dev server', async () => {
|
||||
await expectWithPolling(() => $fetch<string>('/').then(r => r.includes('Home page')).catch(() => null), true)
|
||||
})
|
||||
|
||||
it('should work', async () => {
|
||||
const { page, pageErrors, consoleLogs } = await renderPage('/')
|
||||
|
||||
|
2
test/mocks/router-options.ts
Normal file
2
test/mocks/router-options.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export default {}
|
||||
export const hashMode = false
|
@ -1,6 +1,6 @@
|
||||
/// <reference path="../fixtures/basic/.nuxt/nuxt.d.ts" />
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defineEventHandler } from 'h3'
|
||||
import { destr } from 'destr'
|
||||
|
||||
@ -623,6 +623,18 @@ describe('routing utilities: `navigateTo`', () => {
|
||||
expect(() => navigateTo(url, { external: true })).toThrowError(`Cannot navigate to a URL with '${protocol}:' protocol.`)
|
||||
}
|
||||
})
|
||||
it('navigateTo should replace current navigation state if called within middleware', () => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
nuxtApp._processingMiddleware = true
|
||||
expect(navigateTo('/')).toMatchInlineSnapshot(`"/"`)
|
||||
expect(navigateTo('/', { replace: true })).toMatchInlineSnapshot(`
|
||||
{
|
||||
"path": "/",
|
||||
"replace": true,
|
||||
}
|
||||
`)
|
||||
nuxtApp._processingMiddleware = false
|
||||
})
|
||||
})
|
||||
|
||||
describe('routing utilities: `resolveRouteObject`', () => {
|
||||
@ -766,33 +778,55 @@ describe('useCookie', () => {
|
||||
})
|
||||
|
||||
describe('callOnce', () => {
|
||||
it('should only call composable once', async () => {
|
||||
const fn = vi.fn()
|
||||
const execute = () => callOnce(fn)
|
||||
await execute()
|
||||
await execute()
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
describe.each([
|
||||
['without options', undefined],
|
||||
['with "render" option', { mode: 'render' as const }],
|
||||
['with "navigation" option', { mode: 'navigation' as const }],
|
||||
])('%s', (_name, options) => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
afterEach(() => {
|
||||
nuxtApp.payload.once.clear()
|
||||
})
|
||||
it('should only call composable once', async () => {
|
||||
const fn = vi.fn()
|
||||
const execute = () => options ? callOnce(fn, options) : callOnce(fn)
|
||||
await execute()
|
||||
await execute()
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should only call composable once when called in parallel', async () => {
|
||||
const fn = vi.fn().mockImplementation(() => new Promise(resolve => setTimeout(resolve, 1)))
|
||||
const execute = () => callOnce(fn)
|
||||
await Promise.all([execute(), execute(), execute()])
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
it('should only call composable once when called in parallel', async () => {
|
||||
const fn = vi.fn().mockImplementation(() => new Promise(resolve => setTimeout(resolve, 1)))
|
||||
const execute = () => options ? callOnce(fn, options) : callOnce(fn)
|
||||
await Promise.all([execute(), execute(), execute()])
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
|
||||
const fnSync = vi.fn().mockImplementation(() => {})
|
||||
const executeSync = () => callOnce(fnSync)
|
||||
await Promise.all([executeSync(), executeSync(), executeSync()])
|
||||
expect(fnSync).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
const fnSync = vi.fn().mockImplementation(() => {})
|
||||
const executeSync = () => options ? callOnce(fnSync, options) : callOnce(fnSync)
|
||||
await Promise.all([executeSync(), executeSync(), executeSync()])
|
||||
expect(fnSync).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should use key to dedupe', async () => {
|
||||
const fn = vi.fn()
|
||||
const execute = (key?: string) => callOnce(key, fn)
|
||||
await execute('first')
|
||||
await execute('first')
|
||||
await execute('second')
|
||||
expect(fn).toHaveBeenCalledTimes(2)
|
||||
it('should use key to dedupe', async () => {
|
||||
const fn = vi.fn()
|
||||
const execute = (key?: string) => options ? callOnce(key, fn, options) : callOnce(key, fn)
|
||||
await execute('first')
|
||||
await execute('first')
|
||||
await execute('second')
|
||||
expect(fn).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it.runIf(options?.mode === 'navigation')('should rerun on navigation', async () => {
|
||||
const fn = vi.fn()
|
||||
const execute = () => options ? callOnce(fn, options) : callOnce(fn)
|
||||
await execute()
|
||||
await execute()
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
|
||||
await nuxtApp.callHook('page:start')
|
||||
await execute()
|
||||
expect(fn).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isWindows } from 'std-env'
|
||||
import { $fetch, createPage, setup, url } from '@nuxt/test-utils/e2e'
|
||||
import { $fetch, createPage, fetch, setup, url } from '@nuxt/test-utils/e2e'
|
||||
import { expectWithPolling } from '../utils'
|
||||
|
||||
const isWebpack =
|
||||
process.env.TEST_BUILDER === 'webpack' ||
|
||||
@ -25,6 +26,9 @@ await setup({
|
||||
})
|
||||
|
||||
describe('spaLoadingTemplateLocation flag is set to `within`', () => {
|
||||
it.runIf(isDev)('should load dev server', async () => {
|
||||
await expectWithPolling(() => fetch('/').then(r => r.status === 200).catch(() => null), true)
|
||||
})
|
||||
it('should render loader inside appTag', async () => {
|
||||
const html = await $fetch<string>('/spa')
|
||||
expect(html).toContain(`<div id="__nuxt"><div data-testid="loader">loading...</div></div>`)
|
||||
|
@ -1,14 +1,16 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isWindows } from 'std-env'
|
||||
import { createPage, setup, url } from '@nuxt/test-utils/e2e'
|
||||
import { createPage, fetch, setup, url } from '@nuxt/test-utils/e2e'
|
||||
import type { Page } from 'playwright-core'
|
||||
import { expectWithPolling } from '../utils'
|
||||
|
||||
const isWebpack = process.env.TEST_BUILDER === 'webpack' || process.env.TEST_BUILDER === 'rspack'
|
||||
const isDev = process.env.TEST_ENV === 'dev'
|
||||
|
||||
await setup({
|
||||
rootDir: fileURLToPath(new URL('../fixtures/spa-loader', import.meta.url)),
|
||||
dev: process.env.TEST_ENV === 'dev',
|
||||
dev: isDev,
|
||||
server: true,
|
||||
browser: true,
|
||||
setupTimeout: (isWindows ? 360 : 120) * 1000,
|
||||
@ -22,6 +24,9 @@ await setup({
|
||||
})
|
||||
|
||||
describe('spaLoadingTemplateLocation flag is set to `body`', () => {
|
||||
it.runIf(isDev)('should load dev server', async () => {
|
||||
await expectWithPolling(() => fetch('/').then(r => r.status === 200).catch(() => null), true)
|
||||
})
|
||||
it('should render spa-loader', async () => {
|
||||
const page = await createPage()
|
||||
await page.goto(url('/spa'), { waitUntil: 'domcontentloaded' })
|
||||
|
@ -8,6 +8,7 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'#build/nuxt.config.mjs': resolve('./test/mocks/nuxt-config'),
|
||||
'#build/router.options': resolve('./test/mocks/router-options'),
|
||||
'#internal/nuxt/paths': resolve('./test/mocks/paths'),
|
||||
'#build/app.config.mjs': resolve('./test/mocks/app-config'),
|
||||
'#app': resolve('./packages/nuxt/dist/app'),
|
||||
|
Loading…
Reference in New Issue
Block a user