├── .devcontainer └── devcontainer.json ├── .github └── workflows │ ├── autofix.yml │ ├── changelog.yml │ ├── ci.yml │ ├── release.yml │ └── size.yml ├── .gitignore ├── CODEOWNERS ├── LICENSE ├── README.md ├── eslint.config.js ├── knip.json ├── package.json ├── packages ├── create-nuxt-app │ ├── bin │ │ └── create-nuxt-app.mjs │ └── package.json ├── create-nuxt │ ├── bin │ │ └── create-nuxt.mjs │ ├── build.config.ts │ ├── package.json │ └── src │ │ ├── index.ts │ │ ├── main.ts │ │ └── run.ts ├── nuxi │ ├── bin │ │ └── nuxi.mjs │ ├── build.config.ts │ ├── package.json │ ├── src │ │ ├── commands │ │ │ ├── _shared.ts │ │ │ ├── add.ts │ │ │ ├── analyze.ts │ │ │ ├── build.ts │ │ │ ├── cleanup.ts │ │ │ ├── dev-child.ts │ │ │ ├── dev.ts │ │ │ ├── devtools.ts │ │ │ ├── generate.ts │ │ │ ├── index.ts │ │ │ ├── info.ts │ │ │ ├── init.ts │ │ │ ├── module │ │ │ │ ├── _utils.ts │ │ │ │ ├── add.ts │ │ │ │ ├── index.ts │ │ │ │ └── search.ts │ │ │ ├── prepare.ts │ │ │ ├── preview.ts │ │ │ ├── test.ts │ │ │ ├── typecheck.ts │ │ │ └── upgrade.ts │ │ ├── index.ts │ │ ├── main.ts │ │ ├── run.ts │ │ └── utils │ │ │ ├── ascii.ts │ │ │ ├── banner.ts │ │ │ ├── console.ts │ │ │ ├── dev.ts │ │ │ ├── engines.ts │ │ │ ├── env.ts │ │ │ ├── error.ts │ │ │ ├── fs.ts │ │ │ ├── kit.ts │ │ │ ├── logger.ts │ │ │ ├── nuxt.ts │ │ │ ├── packageManagers.ts │ │ │ └── templates │ │ │ ├── api.ts │ │ │ ├── app-config.ts │ │ │ ├── app.ts │ │ │ ├── component.ts │ │ │ ├── composable.ts │ │ │ ├── error.ts │ │ │ ├── index.ts │ │ │ ├── layer.ts │ │ │ ├── layout.ts │ │ │ ├── middleware.ts │ │ │ ├── module.ts │ │ │ ├── page.ts │ │ │ ├── plugin.ts │ │ │ ├── server-middleware.ts │ │ │ ├── server-plugin.ts │ │ │ ├── server-route.ts │ │ │ └── server-util.ts │ └── test │ │ └── unit │ │ ├── commands │ │ └── module │ │ │ └── add.spec.ts │ │ └── templates.spec.ts └── nuxt-cli │ ├── bin │ └── nuxi.mjs │ ├── build.config.ts │ ├── package.json │ ├── playground │ ├── .gitignore │ ├── nuxt.config.ts │ ├── package.json │ ├── pages │ │ ├── index.vue │ │ └── ws.vue │ ├── pnpm-lock.yaml │ ├── server │ │ └── routes │ │ │ └── _ws.ts │ └── tsconfig.json │ ├── src │ ├── index.ts │ ├── main.ts │ └── run.ts │ └── test │ └── e2e │ └── commands.spec.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── renovate.json ├── scripts ├── release.mjs └── update-changelog.ts ├── tsconfig.json ├── types.d.ts └── vitest.config.ts /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // https://code.visualstudio.com/docs/devcontainers/containers 2 | // https://containers.dev/implementors/json_reference/ 3 | { 4 | "image": "node:22.13.1", 5 | "features": {}, 6 | "customizations": { 7 | "vscode": { 8 | "settings": {}, 9 | "extensions": [ 10 | "ms-azuretools.vscode-docker", 11 | "dbaeumer.vscode-eslint", 12 | "github.vscode-github-actions", 13 | "esbenp.prettier-vscode" 14 | ] 15 | } 16 | }, 17 | "postStartCommand": "corepack enable && pnpm install && pnpm build --stub", 18 | "mounts": [ 19 | "type=volume,target=${containerWorkspaceFolder}/node_modules", 20 | "type=volume,target=${containerWorkspaceFolder}/dist" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yml: -------------------------------------------------------------------------------- 1 | name: autofix.ci # needed to securely identify the workflow 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | autofix: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 16 | with: 17 | fetch-depth: 0 18 | - run: npm i -g --force corepack && corepack enable 19 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 20 | with: 21 | node-version: lts/* 22 | cache: pnpm 23 | 24 | - name: 📦 Install dependencies 25 | run: pnpm install 26 | 27 | - name: 🔠 Lint project (+ fix) 28 | run: pnpm run lint:fix 29 | 30 | - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef 31 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | pull-requests: write 10 | contents: write 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.event.number || github.sha }} 14 | cancel-in-progress: ${{ github.event_name != 'push' }} 15 | 16 | jobs: 17 | update-changelog: 18 | if: github.repository_owner == 'nuxt' && !startsWith(github.event.head_commit.message, 'v') 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 23 | with: 24 | fetch-depth: 0 25 | - run: npm i -g --force corepack && corepack enable 26 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 27 | with: 28 | node-version: lts/* 29 | cache: pnpm 30 | 31 | - name: 📦 Install dependencies 32 | run: pnpm install 33 | 34 | - name: 🚧 Update changelog 35 | run: node --experimental-strip-types ./scripts/update-changelog.ts 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 16 | with: 17 | fetch-depth: 0 18 | - run: npm i -g --force corepack && corepack enable 19 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 20 | with: 21 | node-version: lts/* 22 | cache: pnpm 23 | 24 | - name: 📦 Install dependencies 25 | run: pnpm install 26 | 27 | - name: 🔠 Lint project 28 | run: pnpm lint 29 | 30 | - name: ✂️ Knip project 31 | run: pnpm test:knip 32 | 33 | # - name: ⚙️ Check package engines 34 | # run: pnpm test:engines 35 | 36 | ci: 37 | strategy: 38 | matrix: 39 | os: [ubuntu-latest, windows-latest] 40 | runs-on: ${{ matrix.os }} 41 | steps: 42 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 43 | with: 44 | fetch-depth: 0 45 | - run: npm i -g --force corepack && corepack enable 46 | - uses: actions/setup-node@v4 47 | with: 48 | node-version: 18 49 | cache: pnpm 50 | 51 | - name: 📦 Install dependencies 52 | run: pnpm install 53 | 54 | - name: 💪 Test types 55 | run: pnpm test:types 56 | 57 | - name: 🛠 Build project 58 | run: pnpm build 59 | 60 | - name: 🧪 Test built `nuxi` 61 | run: pnpm test:dist 62 | 63 | - name: 🧪 Test (unit) 64 | run: pnpm test:unit 65 | 66 | - if: matrix.os != 'windows-latest' 67 | uses: codecov/codecov-action@v5 68 | 69 | release: 70 | runs-on: ubuntu-latest 71 | permissions: 72 | id-token: write 73 | if: github.repository_owner == 'nuxt' 74 | steps: 75 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 76 | with: 77 | fetch-depth: 0 78 | - run: npm i -g --force corepack && corepack enable 79 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 80 | with: 81 | node-version: lts/* 82 | cache: pnpm 83 | 84 | - name: 📦 Install dependencies 85 | run: pnpm install 86 | 87 | - name: 🛠 Build project 88 | run: pnpm build 89 | 90 | - name: 📦 release pkg.pr.new 91 | if: github.event_name != 'push' 92 | run: pnpm pkg-pr-new publish --compact --template './playground' ./packages/create-nuxt ./packages/nuxi ./packages/nuxt-cli 93 | 94 | - name: 📦 release nightly 95 | if: github.event_name == 'push' 96 | run: | 97 | echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> ~/.npmrc && 98 | node ./scripts/release.mjs 99 | env: 100 | RELEASE_TYPE: nightly 101 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 102 | NPM_CONFIG_PROVENANCE: true 103 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | pull_request_target: 5 | types: [closed] 6 | branches: 7 | - main 8 | 9 | # Remove default permissions of GITHUB_TOKEN for security 10 | # https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs 11 | permissions: {} 12 | 13 | jobs: 14 | release: 15 | if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'v') 16 | concurrency: 17 | group: release 18 | permissions: 19 | contents: write 20 | id-token: write 21 | runs-on: ubuntu-latest 22 | timeout-minutes: 20 23 | steps: 24 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | with: 26 | fetch-depth: 0 27 | - run: npm i -g --force corepack && corepack enable 28 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 29 | with: 30 | node-version: lts/* 31 | registry-url: 'https://registry.npmjs.org/' 32 | cache: pnpm 33 | 34 | - name: 📦 Install dependencies 35 | run: pnpm install 36 | 37 | - name: 🛠 Build project 38 | run: pnpm build 39 | 40 | - name: 📦 Release 41 | run: node ./scripts/release.mjs 42 | env: 43 | NODE_AUTH_TOKEN: ${{ secrets.NPM_RELEASE_TOKEN }} 44 | NPM_CONFIG_PROVENANCE: true 45 | 46 | - name: 🏷️ Create tag 47 | run: | 48 | TAG_NAME=${{ github.event.pull_request.head.ref }} 49 | git tag $TAG_NAME 50 | git push origin $TAG_NAME 51 | 52 | - name: 🛳️ Create GitHub release 53 | run: gh release create $TAG_NAME --title "$RELEASE_NAME" --notes "$BODY" 54 | env: 55 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | TAG_NAME: ${{ github.event.pull_request.head.ref }} 57 | RELEASE_NAME: ${{ github.event.pull_request.head.ref }} 58 | BODY: ${{ github.event.pull_request.body }} 59 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: 3 | # this action will error unless run in a pr context 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | env: 9 | BUNDLE_SIZE: true 10 | 11 | jobs: 12 | # Build current and upload stats.json 13 | # You may replace this with your own build method. All that 14 | # is required is that the stats.json be an artifact 15 | build-head: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | steps: 20 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 21 | with: 22 | ref: ${{ github.event.pull_request.head.sha }} 23 | - run: npm i -g --force corepack && corepack enable 24 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 25 | with: 26 | node-version: lts/* 27 | cache: pnpm 28 | 29 | - name: 📦 Install dependencies 30 | run: pnpm install 31 | 32 | - name: 🛠 Build project 33 | run: pnpm build 34 | 35 | - name: ⏫ Upload stats.json 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: head-stats 39 | path: ./packages/*/stats.json 40 | 41 | # Build base for comparison and upload stats.json 42 | # You may replace this with your own build method. All that 43 | # is required is that the stats.json be an artifact 44 | build-base: 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: read 48 | steps: 49 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 50 | with: 51 | ref: ${{ github.base_ref }} 52 | - run: npm i -g --force corepack && corepack enable 53 | - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 54 | with: 55 | node-version: lts/* 56 | cache: pnpm 57 | 58 | - name: 📦 Install dependencies 59 | run: pnpm install 60 | 61 | - name: 🛠 Build project 62 | run: pnpm build 63 | 64 | - name: ⏫ Upload stats.json 65 | uses: actions/upload-artifact@v4 66 | with: 67 | name: base-stats 68 | path: ./packages/*/stats.json 69 | 70 | # run the action against the stats.json files 71 | compare: 72 | runs-on: ubuntu-latest 73 | needs: [build-base, build-head] 74 | permissions: 75 | pull-requests: write 76 | issues: write 77 | strategy: 78 | matrix: 79 | package: [nuxi, nuxt-cli, create-nuxt] 80 | steps: 81 | - name: ⏬ Download stats.json 82 | uses: actions/download-artifact@v4 83 | - uses: twk3/rollup-size-compare-action@v1.2.0 84 | with: 85 | title: ${{ matrix.package }} size comparison 86 | github-token: ${{ secrets.GITHUB_TOKEN }} 87 | current-stats-json-path: ./head-stats/${{ matrix.package }}/stats.json 88 | base-stats-json-path: ./base-stats/${{ matrix.package }}/stats.json 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vscode 4 | .idea 5 | .nuxt 6 | nuxt-app 7 | .pnpm-store 8 | coverage 9 | stats.json 10 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @danielroe 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Nuxt Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt CLI (nuxi) 2 | 3 | ⚡️ [Nuxt](https://nuxt.com/) Generation CLI Experience. 4 | 5 | ## Commands 6 | 7 | All commands are listed on https://nuxt.com/docs/api/commands. 8 | 9 | ## Contributing 10 | 11 | ```bash 12 | # Install dependencies 13 | pnpm i 14 | 15 | # Generate type stubs 16 | pnpm dev:prepare 17 | 18 | # Go to the playground directory 19 | cd packages/nuxt-cli/playground 20 | 21 | # And run any commands 22 | pnpm nuxi 23 | ``` 24 | 25 | ## License 26 | 27 | [MIT](./LICENSE) 28 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import antfu from '@antfu/eslint-config' 3 | import { createConfigForNuxt } from '@nuxt/eslint-config/flat' 4 | 5 | export default createConfigForNuxt({ 6 | features: { 7 | tooling: true, 8 | standalone: false, 9 | stylistic: true, 10 | }, 11 | dirs: { 12 | src: [ 13 | './packages/nuxi/playground', 14 | ], 15 | }, 16 | }, await antfu()).append({ 17 | rules: { 18 | 'vue/singleline-html-element-content-newline': 'off', 19 | // TODO: remove usage of `any` throughout codebase 20 | '@typescript-eslint/no-explicit-any': 'off', 21 | 'style/indent-binary-ops': 'off', 22 | }, 23 | }, { 24 | files: ['packages/nuxt-cli/playground/**'], 25 | rules: { 26 | 'no-console': 'off', 27 | }, 28 | }, { 29 | files: ['**/*.yml'], 30 | rules: { 31 | '@stylistic/spaced-comment': 'off', 32 | }, 33 | }) 34 | -------------------------------------------------------------------------------- /knip.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/knip@5/schema.json", 3 | "workspaces": { 4 | ".": { 5 | "ignoreDependencies": [ 6 | "vue" 7 | ], 8 | "entry": [ 9 | "scripts/*" 10 | ] 11 | }, 12 | "packages/nuxt-cli/playground": { 13 | "ignoreDependencies": [ 14 | "nuxt" 15 | ] 16 | }, 17 | "packages/nuxt-cli": { 18 | "ignoreDependencies": [ 19 | "c12", 20 | "chokidar", 21 | "clipboardy", 22 | "consola", 23 | "defu", 24 | "fuse.js", 25 | "giget", 26 | "h3", 27 | "httpxy", 28 | "jiti", 29 | "listhen", 30 | "nypm", 31 | "ofetch", 32 | "ohash", 33 | "pathe", 34 | "perfect-debounce", 35 | "pkg-types", 36 | "scule", 37 | "semver", 38 | "ufo", 39 | "youch" 40 | ] 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxi-workspace", 3 | "type": "module", 4 | "private": true, 5 | "packageManager": "pnpm@10.11.1", 6 | "description": "⚡️ Nuxt Generation CLI Experience", 7 | "license": "MIT", 8 | "repository": "nuxt/cli", 9 | "scripts": { 10 | "dev:prepare": "pnpm -r dev:prepare", 11 | "build": "pnpm -r build", 12 | "build:stub": "pnpm -r dev:prepare", 13 | "lint": "eslint .", 14 | "lint:fix": "eslint --fix .", 15 | "nuxi": "node ./packages/nuxi/bin/nuxi.mjs", 16 | "nuxi-bun": "bun --bun ./packages/nuxi/bin/nuxi.mjs", 17 | "prepack": "pnpm -r build", 18 | "test:types": "tsc --noEmit", 19 | "test:knip": "knip", 20 | "test:dist": "pnpm -r test:dist", 21 | "test:unit": "vitest --coverage" 22 | }, 23 | "devDependencies": { 24 | "@antfu/eslint-config": "^4.13.2", 25 | "@nuxt/eslint-config": "^1.4.1", 26 | "@types/node": "^22.15.29", 27 | "@types/semver": "^7.7.0", 28 | "@vitest/coverage-v8": "^3.2.0", 29 | "changelogen": "^0.6.1", 30 | "eslint": "^9.28.0", 31 | "knip": "^5.59.1", 32 | "pkg-pr-new": "^0.0.51", 33 | "semver": "^7.7.2", 34 | "std-env": "^3.9.0", 35 | "tinyexec": "^1.0.1", 36 | "typescript": "^5.8.3", 37 | "vitest": "^3.2.0", 38 | "vue": "^3.5.16" 39 | }, 40 | "resolutions": { 41 | "@nuxt/cli": "workspace:*", 42 | "@nuxt/schema": "3.17.4", 43 | "create-nuxt": "workspace:*", 44 | "create-nuxt-app": "workspace:*", 45 | "eslint-plugin-jsdoc": "50.7.1", 46 | "eslint-plugin-unicorn": "59.0.1", 47 | "h3": "^1.15.3", 48 | "nuxi": "workspace:*" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/create-nuxt-app/bin/create-nuxt-app.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'create-nuxt/cli' 3 | -------------------------------------------------------------------------------- /packages/create-nuxt-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-nuxt-app", 3 | "type": "module", 4 | "version": "6.0.0", 5 | "description": "Create a Nuxt app in seconds", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/nuxt/cli.git", 10 | "directory": "packages/create-nuxt-app" 11 | }, 12 | "bin": { 13 | "create-nuxt-app": "bin/create-nuxt-app.mjs" 14 | }, 15 | "files": [ 16 | "bin" 17 | ], 18 | "dependencies": { 19 | "create-nuxt": "workspace:*" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/create-nuxt/bin/create-nuxt.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { fileURLToPath } from 'node:url' 4 | import { runMain } from '../dist/index.mjs' 5 | 6 | globalThis.__nuxt_cli__ = { 7 | startTime: Date.now(), 8 | entry: fileURLToPath(import.meta.url), 9 | } 10 | 11 | runMain() 12 | -------------------------------------------------------------------------------- /packages/create-nuxt/build.config.ts: -------------------------------------------------------------------------------- 1 | import type { InputPluginOption } from 'rollup' 2 | import process from 'node:process' 3 | import { visualizer } from 'rollup-plugin-visualizer' 4 | import { defineBuildConfig } from 'unbuild' 5 | import { purgePolyfills } from 'unplugin-purge-polyfills' 6 | 7 | const isAnalysingSize = process.env.BUNDLE_SIZE === 'true' 8 | 9 | export default defineBuildConfig({ 10 | declaration: !isAnalysingSize, 11 | failOnWarn: !isAnalysingSize, 12 | hooks: { 13 | 'rollup:options': function (ctx, options) { 14 | const plugins = (options.plugins ||= []) as InputPluginOption[] 15 | plugins.push(purgePolyfills.rollup({ logLevel: 'verbose' })) 16 | if (isAnalysingSize) { 17 | plugins.unshift(visualizer({ template: 'raw-data' })) 18 | } 19 | }, 20 | }, 21 | rollup: { 22 | dts: { 23 | respectExternal: false, 24 | }, 25 | inlineDependencies: true, 26 | resolve: { 27 | exportConditions: ['production', 'node'], 28 | }, 29 | }, 30 | entries: ['src/index'], 31 | externals: [ 32 | '@nuxt/test-utils', 33 | 'fsevents', 34 | 'node:url', 35 | 'node:buffer', 36 | 'node:path', 37 | 'node:child_process', 38 | 'node:process', 39 | 'node:path', 40 | 'node:os', 41 | ], 42 | }) 43 | -------------------------------------------------------------------------------- /packages/create-nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-nuxt", 3 | "type": "module", 4 | "version": "3.25.1", 5 | "description": "Create a Nuxt app in seconds", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/nuxt/cli.git", 10 | "directory": "packages/create-nuxt" 11 | }, 12 | "exports": { 13 | ".": "./dist/index.mjs", 14 | "./cli": "./bin/create-nuxt.mjs" 15 | }, 16 | "types": "./dist/index.d.ts", 17 | "bin": { 18 | "create-nuxt": "bin/create-nuxt.mjs" 19 | }, 20 | "files": [ 21 | "bin", 22 | "dist" 23 | ], 24 | "engines": { 25 | "node": "^16.10.0 || >=18.0.0" 26 | }, 27 | "scripts": { 28 | "dev:prepare": "unbuild --stub", 29 | "build": "unbuild", 30 | "prepack": "unbuild" 31 | }, 32 | "dependencies": { 33 | "citty": "^0.1.6" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^22.15.29", 37 | "rollup": "^4.41.1", 38 | "rollup-plugin-visualizer": "^6.0.1", 39 | "typescript": "^5.8.3", 40 | "unbuild": "^3.5.0", 41 | "unplugin-purge-polyfills": "^0.1.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/create-nuxt/src/index.ts: -------------------------------------------------------------------------------- 1 | export { main } from './main' 2 | export { runCommand, runMain } from './run' 3 | -------------------------------------------------------------------------------- /packages/create-nuxt/src/main.ts: -------------------------------------------------------------------------------- 1 | import { defineCommand } from 'citty' 2 | import { provider } from 'std-env' 3 | 4 | import init from '../../nuxi/src/commands/init' 5 | import { setupGlobalConsole } from '../../nuxi/src/utils/console' 6 | import { checkEngines } from '../../nuxi/src/utils/engines' 7 | import { logger } from '../../nuxi/src/utils/logger' 8 | import { description, name, version } from '../package.json' 9 | 10 | export const main = defineCommand({ 11 | meta: { 12 | name, 13 | version, 14 | description, 15 | }, 16 | args: init.args, 17 | async setup(ctx) { 18 | setupGlobalConsole({ dev: false }) 19 | 20 | // Check Node.js version and CLI updates in background 21 | if (provider !== 'stackblitz') { 22 | await checkEngines().catch(err => logger.error(err)) 23 | } 24 | 25 | await init.run?.(ctx) 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /packages/create-nuxt/src/run.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | import { runCommand as _runCommand, runMain as _runMain } from 'citty' 5 | 6 | import init from '../../nuxi/src/commands/init' 7 | import { main } from './main' 8 | 9 | globalThis.__nuxt_cli__ = globalThis.__nuxt_cli__ || { 10 | // Programmatic usage fallback 11 | startTime: Date.now(), 12 | entry: fileURLToPath( 13 | new URL( 14 | import.meta.url.endsWith('.ts') 15 | ? '../bin/nuxi.mjs' 16 | : '../../bin/nuxi.mjs', 17 | import.meta.url, 18 | ), 19 | ), 20 | } 21 | 22 | export const runMain = () => _runMain(main) 23 | 24 | export async function runCommand( 25 | name: 'init', 26 | argv: string[] = process.argv.slice(2), 27 | data: { overrides?: Record } = {}, 28 | ) { 29 | return await _runCommand(init, { 30 | rawArgs: argv, 31 | data: { 32 | overrides: data.overrides || {}, 33 | }, 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /packages/nuxi/bin/nuxi.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { fileURLToPath } from 'node:url' 4 | import { runMain } from '../dist/index.mjs' 5 | 6 | globalThis.__nuxt_cli__ = { 7 | startTime: Date.now(), 8 | entry: fileURLToPath(import.meta.url), 9 | } 10 | 11 | runMain() 12 | -------------------------------------------------------------------------------- /packages/nuxi/build.config.ts: -------------------------------------------------------------------------------- 1 | import type { InputPluginOption } from 'rollup' 2 | import process from 'node:process' 3 | import { visualizer } from 'rollup-plugin-visualizer' 4 | import { defineBuildConfig } from 'unbuild' 5 | import { purgePolyfills } from 'unplugin-purge-polyfills' 6 | 7 | const isAnalysingSize = process.env.BUNDLE_SIZE === 'true' 8 | 9 | export default defineBuildConfig({ 10 | declaration: !isAnalysingSize, 11 | failOnWarn: !isAnalysingSize, 12 | hooks: { 13 | 'rollup:options': function (ctx, options) { 14 | const plugins = (options.plugins ||= []) as InputPluginOption[] 15 | plugins.push(purgePolyfills.rollup({ logLevel: 'verbose' })) 16 | if (isAnalysingSize) { 17 | plugins.unshift(visualizer({ template: 'raw-data' })) 18 | } 19 | }, 20 | }, 21 | rollup: { 22 | dts: { 23 | respectExternal: false, 24 | }, 25 | inlineDependencies: true, 26 | resolve: { 27 | exportConditions: ['production', 'node'], 28 | }, 29 | }, 30 | entries: ['src/index'], 31 | externals: [ 32 | '@nuxt/test-utils', 33 | 'fsevents', 34 | 'node:url', 35 | 'node:buffer', 36 | 'node:path', 37 | 'node:child_process', 38 | 'node:process', 39 | 'node:path', 40 | 'node:os', 41 | ], 42 | }) 43 | -------------------------------------------------------------------------------- /packages/nuxi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxi", 3 | "type": "module", 4 | "version": "3.25.1", 5 | "description": "Nuxt CLI", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/nuxt/cli.git", 10 | "directory": "packages/nuxi" 11 | }, 12 | "exports": { 13 | ".": "./dist/index.mjs", 14 | "./cli": "./bin/nuxi.mjs" 15 | }, 16 | "types": "./dist/index.d.ts", 17 | "bin": { 18 | "nuxi": "bin/nuxi.mjs", 19 | "nuxi-ng": "bin/nuxi.mjs", 20 | "nuxt": "bin/nuxi.mjs", 21 | "nuxt-cli": "bin/nuxi.mjs" 22 | }, 23 | "files": [ 24 | "bin", 25 | "dist" 26 | ], 27 | "engines": { 28 | "node": "^16.10.0 || >=18.0.0" 29 | }, 30 | "scripts": { 31 | "dev:prepare": "unbuild --stub", 32 | "build": "unbuild", 33 | "build:stub": "unbuild --stub", 34 | "dev": "node ./bin/nuxi.mjs dev ./playground", 35 | "dev:bun": "bun --bun ./bin/nuxi.mjs dev ./playground", 36 | "nuxi": "node ./bin/nuxi.mjs", 37 | "nuxi-bun": "bun --bun ./bin/nuxi.mjs", 38 | "prepack": "unbuild", 39 | "test:dist": "node ./bin/nuxi.mjs info ./playground" 40 | }, 41 | "devDependencies": { 42 | "@nuxt/kit": "^3.17.4", 43 | "@nuxt/schema": "^3.17.4", 44 | "@nuxt/test-utils": "^3.19.1", 45 | "@types/node": "^22.15.29", 46 | "@types/semver": "^7.7.0", 47 | "c12": "^3.0.4", 48 | "chokidar": "^4.0.3", 49 | "citty": "^0.1.6", 50 | "clipboardy": "^4.0.0", 51 | "consola": "^3.4.2", 52 | "defu": "^6.1.4", 53 | "fuse.js": "^7.1.0", 54 | "giget": "^2.0.0", 55 | "h3": "^1.15.3", 56 | "httpxy": "^0.1.7", 57 | "jiti": "^2.4.2", 58 | "listhen": "^1.9.0", 59 | "magicast": "^0.3.5", 60 | "nitropack": "npm:nitropack-nightly", 61 | "nypm": "^0.6.0", 62 | "ofetch": "^1.4.1", 63 | "ohash": "^2.0.11", 64 | "pathe": "^2.0.3", 65 | "perfect-debounce": "^1.0.0", 66 | "pkg-types": "^2.1.0", 67 | "rollup": "^4.41.1", 68 | "rollup-plugin-visualizer": "^6.0.1", 69 | "scule": "^1.3.0", 70 | "semver": "^7.7.2", 71 | "std-env": "^3.9.0", 72 | "tinyexec": "^1.0.1", 73 | "typescript": "^5.8.3", 74 | "ufo": "^1.6.1", 75 | "unbuild": "^3.5.0", 76 | "unplugin-purge-polyfills": "^0.1.0", 77 | "vitest": "^3.2.0", 78 | "youch": "^4.1.0-beta.8" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/_shared.ts: -------------------------------------------------------------------------------- 1 | import type { ArgDef } from 'citty' 2 | 3 | export const cwdArgs = { 4 | cwd: { 5 | type: 'string', 6 | description: 'Specify the working directory', 7 | valueHint: 'directory', 8 | default: '.', 9 | }, 10 | } as const satisfies Record 11 | 12 | export const logLevelArgs = { 13 | logLevel: { 14 | type: 'string', 15 | description: 'Specify build-time log level', 16 | valueHint: 'silent|info|verbose', 17 | }, 18 | } as const satisfies Record 19 | 20 | export const envNameArgs = { 21 | envName: { 22 | type: 'string', 23 | description: 'The environment to use when resolving configuration overrides (default is `production` when building, and `development` when running the dev server)', 24 | }, 25 | } as const satisfies Record 26 | 27 | export const dotEnvArgs = { 28 | dotenv: { 29 | type: 'string', 30 | description: 'Path to `.env` file to load, relative to the root directory', 31 | }, 32 | } as const satisfies Record 33 | 34 | export const legacyRootDirArgs = { 35 | // cwd falls back to rootDir's default (indirect default) 36 | cwd: { 37 | ...cwdArgs.cwd, 38 | description: 'Specify the working directory, this takes precedence over ROOTDIR (default: `.`)', 39 | default: undefined, 40 | }, 41 | rootDir: { 42 | type: 'positional', 43 | description: 'Specifies the working directory (default: `.`)', 44 | required: false, 45 | default: '.', 46 | }, 47 | } as const satisfies Record 48 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/add.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, promises as fsp } from 'node:fs' 2 | import process from 'node:process' 3 | 4 | import { defineCommand } from 'citty' 5 | import { dirname, extname, resolve } from 'pathe' 6 | 7 | import { loadKit } from '../utils/kit' 8 | import { logger } from '../utils/logger' 9 | import { templates } from '../utils/templates' 10 | import { cwdArgs, logLevelArgs } from './_shared' 11 | 12 | const templateNames = Object.keys(templates) 13 | 14 | export default defineCommand({ 15 | meta: { 16 | name: 'add', 17 | description: 'Create a new template file.', 18 | }, 19 | args: { 20 | ...cwdArgs, 21 | ...logLevelArgs, 22 | force: { 23 | type: 'boolean', 24 | description: 'Force override file if it already exists', 25 | default: false, 26 | }, 27 | template: { 28 | type: 'positional', 29 | required: true, 30 | valueHint: templateNames.join('|'), 31 | description: `Specify which template to generate`, 32 | }, 33 | name: { 34 | type: 'positional', 35 | required: true, 36 | description: 'Specify name of the generated file', 37 | }, 38 | }, 39 | async run(ctx) { 40 | const cwd = resolve(ctx.args.cwd) 41 | 42 | const templateName = ctx.args.template 43 | 44 | // Validate template name 45 | if (!templateNames.includes(templateName)) { 46 | logger.error( 47 | `Template ${templateName} is not supported. Possible values: ${Object.keys( 48 | templates, 49 | ).join(', ')}`, 50 | ) 51 | process.exit(1) 52 | } 53 | 54 | // Validate options 55 | const ext = extname(ctx.args.name) 56 | const name 57 | = ext === '.vue' || ext === '.ts' 58 | ? ctx.args.name.replace(ext, '') 59 | : ctx.args.name 60 | 61 | if (!name) { 62 | logger.error('name argument is missing!') 63 | process.exit(1) 64 | } 65 | 66 | // Load config in order to respect srcDir 67 | const kit = await loadKit(cwd) 68 | const config = await kit.loadNuxtConfig({ cwd }) 69 | 70 | // Resolve template 71 | const template = templates[templateName as keyof typeof templates] 72 | 73 | const res = template({ name, args: ctx.args, nuxtOptions: config }) 74 | 75 | // Ensure not overriding user code 76 | if (!ctx.args.force && existsSync(res.path)) { 77 | logger.error( 78 | `File exists: ${res.path} . Use --force to override or use a different name.`, 79 | ) 80 | process.exit(1) 81 | } 82 | 83 | // Ensure parent directory exists 84 | const parentDir = dirname(res.path) 85 | if (!existsSync(parentDir)) { 86 | logger.info('Creating directory', parentDir) 87 | if (templateName === 'page') { 88 | logger.info('This enables vue-router functionality!') 89 | } 90 | await fsp.mkdir(parentDir, { recursive: true }) 91 | } 92 | 93 | // Write file 94 | await fsp.writeFile(res.path, `${res.contents.trim()}\n`) 95 | logger.info(`🪄 Generated a new ${templateName} in ${res.path}`) 96 | }, 97 | }) 98 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/analyze.ts: -------------------------------------------------------------------------------- 1 | import type { NuxtAnalyzeMeta } from '@nuxt/schema' 2 | 3 | import { promises as fsp } from 'node:fs' 4 | import process from 'node:process' 5 | 6 | import { defineCommand } from 'citty' 7 | import { defu } from 'defu' 8 | import { createApp, eventHandler, lazyEventHandler, toNodeListener } from 'h3' 9 | import { listen } from 'listhen' 10 | import { join, resolve } from 'pathe' 11 | 12 | import { overrideEnv } from '../utils/env' 13 | import { clearDir } from '../utils/fs' 14 | import { loadKit } from '../utils/kit' 15 | import { logger } from '../utils/logger' 16 | import { cwdArgs, dotEnvArgs, legacyRootDirArgs, logLevelArgs } from './_shared' 17 | 18 | export default defineCommand({ 19 | meta: { 20 | name: 'analyze', 21 | description: 'Build nuxt and analyze production bundle (experimental)', 22 | }, 23 | args: { 24 | ...cwdArgs, 25 | ...logLevelArgs, 26 | ...legacyRootDirArgs, 27 | ...dotEnvArgs, 28 | name: { 29 | type: 'string', 30 | description: 'Name of the analysis', 31 | default: 'default', 32 | valueHint: 'name', 33 | }, 34 | serve: { 35 | type: 'boolean', 36 | description: 'Serve the analysis results', 37 | negativeDescription: 'Skip serving the analysis results', 38 | default: true, 39 | }, 40 | }, 41 | async run(ctx) { 42 | overrideEnv('production') 43 | 44 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 45 | const name = ctx.args.name || 'default' 46 | const slug = name.trim().replace(/[^\w-]/g, '_') 47 | 48 | const startTime = Date.now() 49 | 50 | const { loadNuxt, buildNuxt } = await loadKit(cwd) 51 | 52 | const nuxt = await loadNuxt({ 53 | cwd, 54 | dotenv: { 55 | cwd, 56 | fileName: ctx.args.dotenv, 57 | }, 58 | overrides: defu(ctx.data?.overrides, { 59 | build: { 60 | analyze: { 61 | enabled: true, 62 | }, 63 | }, 64 | vite: { 65 | build: { 66 | rollupOptions: { 67 | output: { 68 | chunkFileNames: '_nuxt/[name].js', 69 | entryFileNames: '_nuxt/[name].js', 70 | }, 71 | }, 72 | }, 73 | }, 74 | logLevel: ctx.args.logLevel, 75 | }), 76 | }) 77 | 78 | const analyzeDir = nuxt.options.analyzeDir 79 | const buildDir = nuxt.options.buildDir 80 | const outDir 81 | = nuxt.options.nitro.output?.dir || join(nuxt.options.rootDir, '.output') 82 | 83 | nuxt.options.build.analyze = defu(nuxt.options.build.analyze, { 84 | filename: join(analyzeDir, 'client.html'), 85 | }) 86 | 87 | await clearDir(analyzeDir) 88 | await buildNuxt(nuxt) 89 | 90 | const endTime = Date.now() 91 | 92 | const meta: NuxtAnalyzeMeta = { 93 | name, 94 | slug, 95 | startTime, 96 | endTime, 97 | analyzeDir, 98 | buildDir, 99 | outDir, 100 | } 101 | 102 | await nuxt.callHook('build:analyze:done', meta) 103 | await fsp.writeFile( 104 | join(analyzeDir, 'meta.json'), 105 | JSON.stringify(meta, null, 2), 106 | 'utf-8', 107 | ) 108 | 109 | logger.info(`Analyze results are available at: \`${analyzeDir}\``) 110 | logger.warn('Do not deploy analyze results! Use `nuxi build` before deploying.') 111 | 112 | if (ctx.args.serve !== false && !process.env.CI) { 113 | const app = createApp() 114 | 115 | const serveFile = (filePath: string) => 116 | lazyEventHandler(async () => { 117 | const contents = await fsp.readFile(filePath, 'utf-8') 118 | return eventHandler((event) => { 119 | event.node.res.end(contents) 120 | }) 121 | }) 122 | 123 | logger.info('Starting stats server...') 124 | 125 | app.use('/client', serveFile(join(analyzeDir, 'client.html'))) 126 | app.use('/nitro', serveFile(join(analyzeDir, 'nitro.html'))) 127 | app.use( 128 | eventHandler( 129 | () => ` 130 | 131 | 132 | 133 | Nuxt Bundle Stats (experimental) 134 | 135 |

Nuxt Bundle Stats (experimental)

136 | 144 | 145 | `, 146 | ), 147 | ) 148 | 149 | await listen(toNodeListener(app)) 150 | } 151 | }, 152 | }) 153 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/build.ts: -------------------------------------------------------------------------------- 1 | import type { Nitro } from 'nitropack' 2 | 3 | import process from 'node:process' 4 | 5 | import { defineCommand } from 'citty' 6 | import { relative, resolve } from 'pathe' 7 | 8 | import { showVersions } from '../utils/banner' 9 | import { overrideEnv } from '../utils/env' 10 | import { clearBuildDir } from '../utils/fs' 11 | import { loadKit } from '../utils/kit' 12 | import { logger } from '../utils/logger' 13 | import { cwdArgs, dotEnvArgs, envNameArgs, legacyRootDirArgs, logLevelArgs } from './_shared' 14 | 15 | export default defineCommand({ 16 | meta: { 17 | name: 'build', 18 | description: 'Build Nuxt for production deployment', 19 | }, 20 | args: { 21 | ...cwdArgs, 22 | ...logLevelArgs, 23 | prerender: { 24 | type: 'boolean', 25 | description: 'Build Nuxt and prerender static routes', 26 | }, 27 | preset: { 28 | type: 'string', 29 | description: 'Nitro server preset', 30 | }, 31 | ...dotEnvArgs, 32 | ...envNameArgs, 33 | ...legacyRootDirArgs, 34 | }, 35 | async run(ctx) { 36 | overrideEnv('production') 37 | 38 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 39 | 40 | await showVersions(cwd) 41 | 42 | const kit = await loadKit(cwd) 43 | 44 | const nuxt = await kit.loadNuxt({ 45 | cwd, 46 | dotenv: { 47 | cwd, 48 | fileName: ctx.args.dotenv, 49 | }, 50 | envName: ctx.args.envName, // c12 will fall back to NODE_ENV 51 | overrides: { 52 | logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose', 53 | // TODO: remove in 3.8 54 | _generate: ctx.args.prerender, 55 | nitro: { 56 | static: ctx.args.prerender, 57 | preset: ctx.args.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET, 58 | }, 59 | ...ctx.data?.overrides, 60 | }, 61 | }) 62 | 63 | let nitro: Nitro | undefined 64 | // In Bridge, if Nitro is not enabled, useNitro will throw an error 65 | try { 66 | // Use ? for backward compatibility for Nuxt <= RC.10 67 | nitro = kit.useNitro?.() 68 | logger.info(`Building for Nitro preset: \`${nitro.options.preset}\``) 69 | } 70 | catch { 71 | // 72 | } 73 | 74 | await clearBuildDir(nuxt.options.buildDir) 75 | 76 | await kit.writeTypes(nuxt) 77 | 78 | nuxt.hook('build:error', (err) => { 79 | logger.error('Nuxt Build Error:', err) 80 | process.exit(1) 81 | }) 82 | 83 | await kit.buildNuxt(nuxt) 84 | 85 | if (ctx.args.prerender) { 86 | if (!nuxt.options.ssr) { 87 | logger.warn( 88 | 'HTML content not prerendered because `ssr: false` was set. You can read more in `https://nuxt.com/docs/getting-started/deployment#static-hosting`.', 89 | ) 90 | } 91 | // TODO: revisit later if/when nuxt build --prerender will output hybrid 92 | const dir = nitro?.options.output.publicDir 93 | const publicDir = dir ? relative(process.cwd(), dir) : '.output/public' 94 | logger.success( 95 | `You can now deploy \`${publicDir}\` to any static hosting!`, 96 | ) 97 | } 98 | }, 99 | }) 100 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/cleanup.ts: -------------------------------------------------------------------------------- 1 | import { defineCommand } from 'citty' 2 | import { resolve } from 'pathe' 3 | 4 | import { loadKit } from '../utils/kit' 5 | import { cleanupNuxtDirs } from '../utils/nuxt' 6 | import { cwdArgs, legacyRootDirArgs } from './_shared' 7 | 8 | export default defineCommand({ 9 | meta: { 10 | name: 'cleanup', 11 | description: 'Clean up generated Nuxt files and caches', 12 | }, 13 | args: { 14 | ...cwdArgs, 15 | ...legacyRootDirArgs, 16 | }, 17 | async run(ctx) { 18 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 19 | const { loadNuxtConfig } = await loadKit(cwd) 20 | const nuxtOptions = await loadNuxtConfig({ cwd, overrides: { dev: true } }) 21 | await cleanupNuxtDirs(nuxtOptions.rootDir, nuxtOptions.buildDir) 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/dev-child.ts: -------------------------------------------------------------------------------- 1 | import type { NuxtDevContext, NuxtDevIPCMessage } from '../utils/dev' 2 | 3 | import process from 'node:process' 4 | 5 | import { defineCommand } from 'citty' 6 | import defu from 'defu' 7 | import { resolve } from 'pathe' 8 | import { isTest } from 'std-env' 9 | 10 | import { _getDevServerDefaults, _getDevServerOverrides, createNuxtDevServer } from '../utils/dev' 11 | import { overrideEnv } from '../utils/env' 12 | import { logger } from '../utils/logger' 13 | import { cwdArgs, dotEnvArgs, envNameArgs, legacyRootDirArgs, logLevelArgs } from './_shared' 14 | 15 | export default defineCommand({ 16 | meta: { 17 | name: '_dev', 18 | description: 'Run Nuxt development server (internal command to start child process)', 19 | }, 20 | args: { 21 | ...cwdArgs, 22 | ...logLevelArgs, 23 | ...envNameArgs, 24 | ...dotEnvArgs, 25 | ...legacyRootDirArgs, 26 | }, 27 | async run(ctx) { 28 | if (!process.send && !isTest) { 29 | logger.warn('`nuxi _dev` is an internal command and should not be used directly. Please use `nuxi dev` instead.') 30 | } 31 | 32 | // Prepare 33 | overrideEnv('development') 34 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 35 | 36 | // Get dev context info 37 | const devContext: NuxtDevContext = JSON.parse(process.env.__NUXT_DEV__ || 'null') || {} 38 | 39 | // IPC Hooks 40 | function sendIPCMessage(message: T) { 41 | if (process.send) { 42 | process.send(message) 43 | } 44 | else { 45 | logger.info( 46 | 'Dev server event:', 47 | Object.entries(message) 48 | .map(e => `${e[0]}=${JSON.stringify(e[1])}`) 49 | .join(' '), 50 | ) 51 | } 52 | } 53 | 54 | process.once('unhandledRejection', (reason) => { 55 | sendIPCMessage({ type: 'nuxt:internal:dev:rejection', message: reason instanceof Error ? reason.toString() : 'Unhandled Rejection' }) 56 | process.exit() 57 | }) 58 | 59 | const devServerOverrides = _getDevServerOverrides({ 60 | public: devContext.public, 61 | }) 62 | 63 | const devServerDefaults = _getDevServerDefaults({ 64 | hostname: devContext.hostname, 65 | https: devContext.proxy?.https, 66 | }, devContext.publicURLs) 67 | 68 | // Init Nuxt dev 69 | const nuxtDev = await createNuxtDevServer({ 70 | cwd, 71 | overrides: defu(ctx.data?.overrides, devServerOverrides), 72 | defaults: devServerDefaults, 73 | logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose', 74 | clear: !!ctx.args.clear, 75 | dotenv: { cwd, fileName: ctx.args.dotenv }, 76 | envName: ctx.args.envName, 77 | port: process.env._PORT ?? undefined, 78 | devContext, 79 | }) 80 | 81 | nuxtDev.on('loading:error', (_error) => { 82 | sendIPCMessage({ type: 'nuxt:internal:dev:loading:error', error: { 83 | message: _error.message, 84 | stack: _error.stack, 85 | name: _error.name, 86 | code: _error.code, 87 | } }) 88 | }) 89 | nuxtDev.on('loading', (message) => { 90 | sendIPCMessage({ type: 'nuxt:internal:dev:loading', message }) 91 | }) 92 | nuxtDev.on('restart', () => { 93 | sendIPCMessage({ type: 'nuxt:internal:dev:restart' }) 94 | }) 95 | nuxtDev.on('ready', (payload) => { 96 | sendIPCMessage({ type: 'nuxt:internal:dev:ready', port: payload.port }) 97 | }) 98 | 99 | // Init server 100 | await nuxtDev.init() 101 | }, 102 | }) 103 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/dev.ts: -------------------------------------------------------------------------------- 1 | import type { NuxtOptions } from '@nuxt/schema' 2 | import type { ParsedArgs } from 'citty' 3 | import type { HTTPSOptions, ListenOptions } from 'listhen' 4 | import type { ChildProcess } from 'node:child_process' 5 | import type { IncomingMessage, ServerResponse } from 'node:http' 6 | import type { NuxtDevContext, NuxtDevIPCMessage } from '../utils/dev' 7 | 8 | import { fork } from 'node:child_process' 9 | import process from 'node:process' 10 | 11 | import { defineCommand } from 'citty' 12 | import defu from 'defu' 13 | import { createJiti } from 'jiti' 14 | import { getArgs as getListhenArgs, parseArgs as parseListhenArgs } from 'listhen/cli' 15 | import { resolve } from 'pathe' 16 | import { satisfies } from 'semver' 17 | 18 | import { isBun, isTest } from 'std-env' 19 | import { showVersions } from '../utils/banner' 20 | import { _getDevServerDefaults, _getDevServerOverrides } from '../utils/dev' 21 | import { overrideEnv } from '../utils/env' 22 | import { renderError } from '../utils/error' 23 | import { loadKit } from '../utils/kit' 24 | import { logger } from '../utils/logger' 25 | import { cwdArgs, dotEnvArgs, envNameArgs, legacyRootDirArgs, logLevelArgs } from './_shared' 26 | 27 | const forkSupported = !isTest && (!isBun || isBunForkSupported()) 28 | const listhenArgs = getListhenArgs() 29 | 30 | const command = defineCommand({ 31 | meta: { 32 | name: 'dev', 33 | description: 'Run Nuxt development server', 34 | }, 35 | args: { 36 | ...cwdArgs, 37 | ...logLevelArgs, 38 | ...dotEnvArgs, 39 | ...legacyRootDirArgs, 40 | ...envNameArgs, 41 | clear: { 42 | type: 'boolean', 43 | description: 'Clear console on restart', 44 | negativeDescription: 'Disable clear console on restart', 45 | }, 46 | fork: { 47 | type: 'boolean', 48 | description: forkSupported ? 'Disable forked mode' : 'Enable forked mode', 49 | negativeDescription: 'Disable forked mode', 50 | default: forkSupported, 51 | alias: ['f'], 52 | }, 53 | ...{ 54 | ...listhenArgs, 55 | 'port': { 56 | ...listhenArgs.port, 57 | description: 'Port to listen on (default: `NUXT_PORT || NITRO_PORT || PORT || nuxtOptions.devServer.port`)', 58 | alias: ['p'], 59 | }, 60 | 'open': { 61 | ...listhenArgs.open, 62 | alias: ['o'], 63 | default: false, 64 | }, 65 | 'host': { 66 | ...listhenArgs.host, 67 | alias: ['h'], 68 | description: 'Host to listen on (default: `NUXT_HOST || NITRO_HOST || HOST || nuxtOptions.devServer?.host`)', 69 | }, 70 | 'clipboard': { ...listhenArgs.clipboard, default: false }, 71 | 'https.domains': { 72 | ...listhenArgs['https.domains'], 73 | description: 'Comma separated list of domains and IPs, the autogenerated certificate should be valid for (https: true)', 74 | }, 75 | }, 76 | sslCert: { 77 | type: 'string', 78 | description: '(DEPRECATED) Use `--https.cert` instead.', 79 | }, 80 | sslKey: { 81 | type: 'string', 82 | description: '(DEPRECATED) Use `--https.key` instead.', 83 | }, 84 | }, 85 | async run(ctx) { 86 | // Prepare 87 | overrideEnv('development') 88 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 89 | await showVersions(cwd) 90 | 91 | // Load Nuxt Config 92 | const { loadNuxtConfig } = await loadKit(cwd) 93 | const nuxtOptions = await loadNuxtConfig({ 94 | cwd, 95 | dotenv: { cwd, fileName: ctx.args.dotenv }, 96 | envName: ctx.args.envName, // c12 will fall back to NODE_ENV 97 | overrides: { 98 | dev: true, 99 | logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose', 100 | ...ctx.data?.overrides, 101 | }, 102 | }) 103 | 104 | // Start Proxy Listener 105 | const listenOptions = _resolveListenOptions(nuxtOptions, ctx.args) 106 | 107 | if (ctx.args.fork) { 108 | // Fork Nuxt dev process 109 | const devProxy = await _createDevProxy(nuxtOptions, listenOptions) 110 | await _startSubprocess(devProxy, ctx.rawArgs, listenOptions) 111 | return { listener: devProxy?.listener } 112 | } 113 | else { 114 | // Directly start Nuxt dev 115 | const { createNuxtDevServer } = await import('../utils/dev') 116 | 117 | const devServerOverrides = _getDevServerOverrides({ 118 | public: listenOptions.public, 119 | }) 120 | 121 | const devServerDefaults = _getDevServerDefaults({ 122 | hostname: listenOptions.hostname, 123 | https: listenOptions.https, 124 | }) 125 | 126 | const devServer = await createNuxtDevServer( 127 | { 128 | cwd, 129 | overrides: defu(ctx.data?.overrides, devServerOverrides), 130 | defaults: devServerDefaults, 131 | logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose', 132 | clear: ctx.args.clear, 133 | dotenv: { 134 | cwd, 135 | fileName: ctx.args.dotenv, 136 | }, 137 | envName: ctx.args.envName, 138 | loadingTemplate: nuxtOptions.devServer.loadingTemplate, 139 | devContext: {}, 140 | }, 141 | listenOptions, 142 | ) 143 | await devServer.init() 144 | return { listener: devServer?.listener } 145 | } 146 | }, 147 | }) 148 | 149 | export default command 150 | 151 | // --- Internal --- 152 | 153 | type ArgsT = Exclude< 154 | Awaited, 155 | undefined | ((...args: unknown[]) => unknown) 156 | > 157 | 158 | type DevProxy = Awaited> 159 | 160 | async function _createDevProxy(nuxtOptions: NuxtOptions, listenOptions: Partial) { 161 | const jiti = createJiti(nuxtOptions.rootDir) 162 | let loadingMessage = 'Nuxt dev server is starting...' 163 | let error: Error | undefined 164 | let loadingTemplate = nuxtOptions.devServer.loadingTemplate 165 | for (const url of nuxtOptions.modulesDir) { 166 | // @ts-expect-error this is for backwards compatibility 167 | if (loadingTemplate) { 168 | break 169 | } 170 | loadingTemplate = await jiti.import<{ loading: () => string }>('@nuxt/ui-templates', { parentURL: url }).then(r => r.loading) 171 | } 172 | 173 | const { createProxyServer } = await import('httpxy') 174 | const proxy = createProxyServer({}) 175 | 176 | let address: string | undefined 177 | 178 | const handler = (req: IncomingMessage, res: ServerResponse) => { 179 | if (error) { 180 | renderError(req, res, error) 181 | return 182 | } 183 | if (!address) { 184 | res.statusCode = 503 185 | res.setHeader('Content-Type', 'text/html') 186 | res.end(loadingTemplate({ loading: loadingMessage })) 187 | return 188 | } 189 | return proxy.web(req, res, { target: address }) 190 | } 191 | 192 | const wsHandler = (req: IncomingMessage, socket: any, head: any) => { 193 | if (!address) { 194 | socket.destroy() 195 | return 196 | } 197 | return proxy.ws(req, socket, { target: address }, head) 198 | } 199 | 200 | const { listen } = await import('listhen') 201 | const listener = await listen(handler, listenOptions) 202 | listener.server.on('upgrade', wsHandler) 203 | 204 | return { 205 | listener, 206 | handler, 207 | wsHandler, 208 | setAddress: (_addr: string | undefined) => { 209 | address = _addr 210 | }, 211 | setLoadingMessage: (_msg: string) => { 212 | loadingMessage = _msg 213 | }, 214 | setError: (_error: Error) => { 215 | error = _error 216 | }, 217 | clearError() { 218 | error = undefined 219 | }, 220 | } 221 | } 222 | 223 | async function _startSubprocess(devProxy: DevProxy, rawArgs: string[], listenArgs: Partial) { 224 | let childProc: ChildProcess | undefined 225 | 226 | const kill = (signal: NodeJS.Signals | number) => { 227 | if (childProc) { 228 | childProc.kill(signal) 229 | childProc = undefined 230 | } 231 | } 232 | 233 | const restart = async () => { 234 | devProxy.clearError() 235 | // Kill previous process with restart signal (not supported on Windows) 236 | if (process.platform === 'win32') { 237 | kill('SIGTERM') 238 | } 239 | else { 240 | kill('SIGHUP') 241 | } 242 | // Start new process 243 | childProc = fork(globalThis.__nuxt_cli__!.entry!, ['_dev', ...rawArgs], { 244 | execArgv: [ 245 | '--enable-source-maps', 246 | process.argv.find((a: string) => a.includes('--inspect')), 247 | ].filter(Boolean) as string[], 248 | env: { 249 | ...process.env, 250 | __NUXT_DEV__: JSON.stringify({ 251 | hostname: listenArgs.hostname, 252 | public: listenArgs.public, 253 | publicURLs: await devProxy.listener.getURLs().then(r => r.map(r => r.url)), 254 | proxy: { 255 | url: devProxy.listener.url, 256 | urls: await devProxy.listener.getURLs(), 257 | https: devProxy.listener.https, 258 | }, 259 | } satisfies NuxtDevContext), 260 | }, 261 | }) 262 | 263 | // Close main process on child exit with error 264 | childProc.on('close', (errorCode) => { 265 | if (errorCode) { 266 | process.exit(errorCode) 267 | } 268 | }) 269 | 270 | // Listen for IPC messages 271 | childProc.on('message', (message: NuxtDevIPCMessage) => { 272 | if (message.type === 'nuxt:internal:dev:ready') { 273 | devProxy.setAddress(`http://127.0.0.1:${message.port}`) 274 | } 275 | else if (message.type === 'nuxt:internal:dev:loading') { 276 | devProxy.setAddress(undefined) 277 | devProxy.setLoadingMessage(message.message) 278 | devProxy.clearError() 279 | } 280 | else if (message.type === 'nuxt:internal:dev:loading:error') { 281 | devProxy.setAddress(undefined) 282 | devProxy.setError(message.error) 283 | } 284 | else if (message.type === 'nuxt:internal:dev:restart') { 285 | restart() 286 | } 287 | else if (message.type === 'nuxt:internal:dev:rejection') { 288 | logger.info(`Restarting Nuxt due to error: \`${message.message}\``) 289 | restart() 290 | } 291 | }) 292 | } 293 | 294 | // Graceful shutdown 295 | for (const signal of [ 296 | 'exit', 297 | 'SIGTERM' /* Graceful shutdown */, 298 | 'SIGINT' /* Ctrl-C */, 299 | 'SIGQUIT' /* Ctrl-\ */, 300 | ] as const) { 301 | process.once(signal, () => { 302 | kill(signal === 'exit' ? 0 : signal) 303 | }) 304 | } 305 | 306 | await restart() 307 | 308 | return { 309 | restart, 310 | kill, 311 | } 312 | } 313 | 314 | function _resolveListenOptions( 315 | nuxtOptions: NuxtOptions, 316 | args: ParsedArgs, 317 | ): Partial { 318 | const _port = args.port 319 | ?? args.p 320 | ?? process.env.NUXT_PORT 321 | ?? process.env.NITRO_PORT 322 | ?? process.env.PORT 323 | ?? nuxtOptions.devServer.port 324 | 325 | const _hostname = typeof args.host === 'string' 326 | ? args.host 327 | : (args.host === true ? '' : undefined) 328 | ?? process.env.NUXT_HOST 329 | ?? process.env.NITRO_HOST 330 | ?? process.env.HOST 331 | ?? (nuxtOptions.devServer?.host || undefined /* for backwards compatibility with previous '' default */) 332 | ?? undefined 333 | 334 | const _public: boolean | undefined = args.public 335 | ?? (_hostname && !['localhost', '127.0.0.1', '::1'].includes(_hostname)) 336 | ? true 337 | : undefined 338 | 339 | const _httpsCert = args['https.cert'] 340 | || (args.sslCert as string) 341 | || process.env.NUXT_SSL_CERT 342 | || process.env.NITRO_SSL_CERT 343 | || (typeof nuxtOptions.devServer.https !== 'boolean' && nuxtOptions.devServer.https && 'cert' in nuxtOptions.devServer.https && nuxtOptions.devServer.https.cert) 344 | || '' 345 | 346 | const _httpsKey = args['https.key'] 347 | || (args.sslKey as string) 348 | || process.env.NUXT_SSL_KEY 349 | || process.env.NITRO_SSL_KEY 350 | || (typeof nuxtOptions.devServer.https !== 'boolean' && nuxtOptions.devServer.https && 'key' in nuxtOptions.devServer.https && nuxtOptions.devServer.https.key) 351 | || '' 352 | 353 | const _httpsPfx = args['https.pfx'] 354 | || (typeof nuxtOptions.devServer.https !== 'boolean' && nuxtOptions.devServer.https && 'pfx' in nuxtOptions.devServer.https && nuxtOptions.devServer.https.pfx) 355 | || '' 356 | 357 | const _httpsPassphrase = args['https.passphrase'] 358 | || (typeof nuxtOptions.devServer.https !== 'boolean' && nuxtOptions.devServer.https && 'passphrase' in nuxtOptions.devServer.https && nuxtOptions.devServer.https.passphrase) 359 | || '' 360 | 361 | const httpsEnabled = !!(args.https ?? nuxtOptions.devServer.https) 362 | 363 | const _listhenOptions = parseListhenArgs({ 364 | ...args, 365 | 'open': (args.o as boolean) || args.open, 366 | 'https': httpsEnabled, 367 | 'https.cert': _httpsCert, 368 | 'https.key': _httpsKey, 369 | 'https.pfx': _httpsPfx, 370 | 'https.passphrase': _httpsPassphrase, 371 | }) 372 | 373 | const httpsOptions = httpsEnabled && { 374 | ...(nuxtOptions.devServer.https as HTTPSOptions), 375 | ...(_listhenOptions.https as HTTPSOptions), 376 | } 377 | 378 | return { 379 | ..._listhenOptions, 380 | port: _port, 381 | hostname: _hostname, 382 | public: _public, 383 | https: httpsOptions, 384 | baseURL: nuxtOptions.app.baseURL.startsWith('./') ? nuxtOptions.app.baseURL.slice(1) : nuxtOptions.app.baseURL, 385 | } 386 | } 387 | 388 | function isBunForkSupported() { 389 | const bunVersion: string = (globalThis as any).Bun.version 390 | return satisfies(bunVersion, '>=1.2') 391 | } 392 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/devtools.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | import { defineCommand } from 'citty' 4 | import { resolve } from 'pathe' 5 | import { x } from 'tinyexec' 6 | 7 | import { logger } from '../utils/logger' 8 | import { cwdArgs, legacyRootDirArgs } from './_shared' 9 | 10 | export default defineCommand({ 11 | meta: { 12 | name: 'devtools', 13 | description: 'Enable or disable devtools in a Nuxt project', 14 | }, 15 | args: { 16 | ...cwdArgs, 17 | command: { 18 | type: 'positional', 19 | description: 'Command to run', 20 | valueHint: 'enable|disable', 21 | }, 22 | ...legacyRootDirArgs, 23 | }, 24 | async run(ctx) { 25 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 26 | 27 | if (!['enable', 'disable'].includes(ctx.args.command)) { 28 | logger.error(`Unknown command \`${ctx.args.command}\`.`) 29 | process.exit(1) 30 | } 31 | 32 | await x( 33 | 'npx', 34 | ['@nuxt/devtools-wizard@latest', ctx.args.command, cwd], 35 | { 36 | throwOnError: true, 37 | nodeOptions: { 38 | stdio: 'inherit', 39 | cwd, 40 | }, 41 | }, 42 | ) 43 | }, 44 | }) 45 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/generate.ts: -------------------------------------------------------------------------------- 1 | import { defineCommand } from 'citty' 2 | 3 | import { cwdArgs, dotEnvArgs, envNameArgs, legacyRootDirArgs, logLevelArgs } from './_shared' 4 | import buildCommand from './build' 5 | 6 | export default defineCommand({ 7 | meta: { 8 | name: 'generate', 9 | description: 'Build Nuxt and prerender all routes', 10 | }, 11 | args: { 12 | ...cwdArgs, 13 | ...logLevelArgs, 14 | preset: { 15 | type: 'string', 16 | description: 'Nitro server preset', 17 | }, 18 | ...dotEnvArgs, 19 | ...envNameArgs, 20 | ...legacyRootDirArgs, 21 | }, 22 | async run(ctx) { 23 | ctx.args.prerender = true 24 | await buildCommand.run!( 25 | // @ts-expect-error types do not match 26 | ctx, 27 | ) 28 | }, 29 | }) 30 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/index.ts: -------------------------------------------------------------------------------- 1 | import type { CommandDef } from 'citty' 2 | 3 | const _rDefault = (r: any) => (r.default || r) as Promise 4 | 5 | export const commands = { 6 | add: () => import('./add').then(_rDefault), 7 | analyze: () => import('./analyze').then(_rDefault), 8 | build: () => import('./build').then(_rDefault), 9 | cleanup: () => import('./cleanup').then(_rDefault), 10 | _dev: () => import('./dev-child').then(_rDefault), 11 | dev: () => import('./dev').then(_rDefault), 12 | devtools: () => import('./devtools').then(_rDefault), 13 | generate: () => import('./generate').then(_rDefault), 14 | info: () => import('./info').then(_rDefault), 15 | init: () => import('./init').then(_rDefault), 16 | module: () => import('./module').then(_rDefault), 17 | prepare: () => import('./prepare').then(_rDefault), 18 | preview: () => import('./preview').then(_rDefault), 19 | start: () => import('./preview').then(_rDefault), 20 | test: () => import('./test').then(_rDefault), 21 | typecheck: () => import('./typecheck').then(_rDefault), 22 | upgrade: () => import('./upgrade').then(_rDefault), 23 | } as const 24 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/info.ts: -------------------------------------------------------------------------------- 1 | import type { NuxtConfig, NuxtModule } from '@nuxt/schema' 2 | import type { PackageJson } from 'pkg-types' 3 | 4 | import os from 'node:os' 5 | import process from 'node:process' 6 | 7 | import { defineCommand } from 'citty' 8 | import clipboardy from 'clipboardy' 9 | import { createJiti } from 'jiti' 10 | import { detectPackageManager } from 'nypm' 11 | import { resolve } from 'pathe' 12 | import { readPackageJSON } from 'pkg-types' 13 | import { splitByCase } from 'scule' 14 | import { isMinimal } from 'std-env' 15 | 16 | import { version as nuxiVersion } from '../../package.json' 17 | 18 | import { tryResolveNuxt } from '../utils/kit' 19 | import { logger } from '../utils/logger' 20 | import { getPackageManagerVersion } from '../utils/packageManagers' 21 | import { cwdArgs, legacyRootDirArgs } from './_shared' 22 | 23 | export default defineCommand({ 24 | meta: { 25 | name: 'info', 26 | description: 'Get information about Nuxt project', 27 | }, 28 | args: { 29 | ...cwdArgs, 30 | ...legacyRootDirArgs, 31 | }, 32 | async run(ctx) { 33 | // Resolve rootDir 34 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 35 | 36 | // Load Nuxt config 37 | const nuxtConfig = await getNuxtConfig(cwd) 38 | 39 | // Find nearest package.json 40 | const { dependencies = {}, devDependencies = {} } = await readPackageJSON(cwd).catch(() => ({} as PackageJson)) 41 | 42 | // Utils to query a dependency version 43 | const nuxtPath = await tryResolveNuxt(cwd) 44 | async function getDepVersion(name: string) { 45 | for (const url of [cwd, nuxtPath]) { 46 | if (!url) { 47 | continue 48 | } 49 | const pkg = await readPackageJSON(name, { url }).catch(() => null) 50 | if (pkg) { 51 | return pkg.version! 52 | } 53 | } 54 | return dependencies[name] || devDependencies[name] 55 | } 56 | 57 | async function listModules(arr: NonNullable = []) { 58 | const info: string[] = [] 59 | for (let m of arr) { 60 | if (Array.isArray(m)) { 61 | m = m[0] 62 | } 63 | const name = normalizeConfigModule(m, cwd) 64 | if (name) { 65 | const npmName = name!.split('/').splice(0, 2).join('/') // @foo/bar/baz => @foo/bar 66 | const v = await getDepVersion(npmName) 67 | info.push(`\`${v ? `${name}@${v}` : name}\``) 68 | } 69 | } 70 | return info.join(', ') 71 | } 72 | 73 | // Check Nuxt version 74 | const nuxtVersion = await getDepVersion('nuxt') || await getDepVersion('nuxt-nightly') || await getDepVersion('nuxt-edge') || await getDepVersion('nuxt3') || '-' 75 | const isLegacy = nuxtVersion.startsWith('2') 76 | const builder = !isLegacy 77 | ? nuxtConfig.builder /* latest schema */ || '-' 78 | : (nuxtConfig as any /* nuxt v2 */).bridge?.vite 79 | ? 'vite' /* bridge vite implementation */ 80 | : (nuxtConfig as any /* nuxt v2 */).buildModules?.includes('nuxt-vite') 81 | ? 'vite' /* nuxt-vite */ 82 | : 'webpack' 83 | 84 | let packageManager = (await detectPackageManager(cwd))?.name 85 | 86 | if (packageManager) { 87 | packageManager += `@${getPackageManagerVersion(packageManager)}` 88 | } 89 | 90 | const infoObj = { 91 | OperatingSystem: os.type(), 92 | NodeVersion: process.version, 93 | NuxtVersion: nuxtVersion, 94 | CLIVersion: nuxiVersion, 95 | NitroVersion: await getDepVersion('nitropack'), 96 | PackageManager: packageManager ?? 'unknown', 97 | Builder: typeof builder === 'string' ? builder : 'custom', 98 | UserConfig: Object.keys(nuxtConfig) 99 | .map(key => `\`${key}\``) 100 | .join(', '), 101 | RuntimeModules: await listModules(nuxtConfig.modules), 102 | BuildModules: await listModules((nuxtConfig as any /* nuxt v2 */).buildModules || []), 103 | } 104 | 105 | logger.log('Working directory:', cwd) 106 | 107 | let maxLength = 0 108 | const entries = Object.entries(infoObj).map(([key, val]) => { 109 | const label = splitByCase(key).join(' ') 110 | if (label.length > maxLength) { 111 | maxLength = label.length 112 | } 113 | return [label, val || '-'] as const 114 | }) 115 | let infoStr = '' 116 | for (const [label, value] of entries) { 117 | infoStr 118 | += `- ${ 119 | (`${label}: `).padEnd(maxLength + 2) 120 | }${value.includes('`') ? value : `\`${value}\`` 121 | }\n` 122 | } 123 | 124 | const copied = !isMinimal && await clipboardy 125 | .write(infoStr) 126 | .then(() => true) 127 | .catch(() => false) 128 | 129 | const isNuxt3 = !isLegacy 130 | const isBridge = !isNuxt3 && infoObj.BuildModules.includes('bridge') 131 | 132 | const repo = isBridge ? 'nuxt/bridge' : 'nuxt/nuxt' 133 | 134 | const log = [ 135 | (isNuxt3 || isBridge) && `👉 Report an issue: https://github.com/${repo}/issues/new?template=bug-report.yml`, 136 | (isNuxt3 || isBridge) && `👉 Suggest an improvement: https://github.com/${repo}/discussions/new`, 137 | `👉 Read documentation: ${(isNuxt3 || isBridge) ? 'https://nuxt.com' : 'https://v2.nuxt.com'}`, 138 | ].filter(Boolean).join('\n') 139 | 140 | const splitter = '------------------------------' 141 | logger.log(`Nuxt project info: ${copied ? '(copied to clipboard)' : ''}\n\n${splitter}\n${infoStr}${splitter}\n\n${log}\n`) 142 | }, 143 | }) 144 | 145 | function normalizeConfigModule( 146 | module: NuxtModule | string | false | null | undefined, 147 | rootDir: string, 148 | ): string | null { 149 | if (!module) { 150 | return null 151 | } 152 | if (typeof module === 'string') { 153 | return module 154 | .split(rootDir) 155 | .pop()! // Strip rootDir 156 | .split('node_modules') 157 | .pop()! // Strip node_modules 158 | .replace(/^\//, '') 159 | } 160 | if (typeof module === 'function') { 161 | return `${module.name}()` 162 | } 163 | if (Array.isArray(module)) { 164 | return normalizeConfigModule(module[0], rootDir) 165 | } 166 | return null 167 | } 168 | 169 | async function getNuxtConfig(rootDir: string) { 170 | try { 171 | const jiti = createJiti(rootDir, { 172 | interopDefault: true, 173 | // allow using `~` and `@` in `nuxt.config` 174 | alias: { 175 | '~': rootDir, 176 | '@': rootDir, 177 | }, 178 | }) 179 | ;(globalThis as any).defineNuxtConfig = (c: any) => c 180 | const result = await jiti.import('./nuxt.config', { default: true }) as NuxtConfig 181 | delete (globalThis as any).defineNuxtConfig 182 | return result 183 | } 184 | catch { 185 | // TODO: Show error as warning if it is not 404 186 | return {} 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/init.ts: -------------------------------------------------------------------------------- 1 | import type { SelectPromptOptions } from 'consola' 2 | import type { DownloadTemplateResult } from 'giget' 3 | import type { PackageManagerName } from 'nypm' 4 | 5 | import { existsSync } from 'node:fs' 6 | import process from 'node:process' 7 | 8 | import { defineCommand } from 'citty' 9 | import { colors } from 'consola/utils' 10 | import { downloadTemplate, startShell } from 'giget' 11 | import { installDependencies } from 'nypm' 12 | import { $fetch } from 'ofetch' 13 | import { join, relative, resolve } from 'pathe' 14 | import { hasTTY } from 'std-env' 15 | 16 | import { x } from 'tinyexec' 17 | import { runCommand } from '../run' 18 | import { nuxtIcon, themeColor } from '../utils/ascii' 19 | import { logger } from '../utils/logger' 20 | import { cwdArgs, logLevelArgs } from './_shared' 21 | 22 | const DEFAULT_REGISTRY = 'https://raw.githubusercontent.com/nuxt/starter/templates/templates' 23 | const DEFAULT_TEMPLATE_NAME = 'v3' 24 | 25 | const pms: Record = { 26 | npm: undefined, 27 | pnpm: undefined, 28 | yarn: undefined, 29 | bun: undefined, 30 | deno: undefined, 31 | } 32 | 33 | // this is for type safety to prompt updating code in nuxi when nypm adds a new package manager 34 | const packageManagerOptions = Object.keys(pms) as PackageManagerName[] 35 | 36 | export default defineCommand({ 37 | meta: { 38 | name: 'init', 39 | description: 'Initialize a fresh project', 40 | }, 41 | args: { 42 | ...cwdArgs, 43 | ...logLevelArgs, 44 | dir: { 45 | type: 'positional', 46 | description: 'Project directory', 47 | default: '', 48 | }, 49 | template: { 50 | type: 'string', 51 | alias: 't', 52 | description: 'Template name', 53 | }, 54 | force: { 55 | type: 'boolean', 56 | alias: 'f', 57 | description: 'Override existing directory', 58 | }, 59 | offline: { 60 | type: 'boolean', 61 | description: 'Force offline mode', 62 | }, 63 | preferOffline: { 64 | type: 'boolean', 65 | description: 'Prefer offline mode', 66 | }, 67 | install: { 68 | type: 'boolean', 69 | default: true, 70 | description: 'Skip installing dependencies', 71 | }, 72 | gitInit: { 73 | type: 'boolean', 74 | description: 'Initialize git repository', 75 | }, 76 | shell: { 77 | type: 'boolean', 78 | description: 'Start shell after installation in project directory', 79 | }, 80 | packageManager: { 81 | type: 'string', 82 | description: 'Package manager choice (npm, pnpm, yarn, bun)', 83 | }, 84 | modules: { 85 | type: 'string', 86 | required: false, 87 | description: 'Nuxt modules to install (comma separated without spaces)', 88 | alias: 'M', 89 | }, 90 | }, 91 | async run(ctx) { 92 | if (hasTTY) { 93 | process.stdout.write(`\n${nuxtIcon}\n\n`) 94 | } 95 | 96 | logger.info(colors.bold(`Welcome to Nuxt!`.split('').map(m => `${themeColor}${m}`).join(''))) 97 | 98 | if (ctx.args.dir === '') { 99 | ctx.args.dir = await logger.prompt('Where would you like to create your project?', { 100 | placeholder: './nuxt-app', 101 | type: 'text', 102 | default: 'nuxt-app', 103 | cancel: 'reject', 104 | }).catch(() => process.exit(1)) 105 | } 106 | 107 | const cwd = resolve(ctx.args.cwd) 108 | let templateDownloadPath = resolve(cwd, ctx.args.dir) 109 | logger.info(`Creating a new project in ${colors.cyan(relative(cwd, templateDownloadPath) || templateDownloadPath)}.`) 110 | 111 | // Get template name 112 | const templateName = ctx.args.template || DEFAULT_TEMPLATE_NAME 113 | 114 | if (typeof templateName !== 'string') { 115 | logger.error('Please specify a template!') 116 | process.exit(1) 117 | } 118 | 119 | let shouldForce = Boolean(ctx.args.force) 120 | 121 | // Prompt the user if the template download directory already exists 122 | // when no `--force` flag is provided 123 | const shouldVerify = !shouldForce && existsSync(templateDownloadPath) 124 | if (shouldVerify) { 125 | const selectedAction = await logger.prompt( 126 | `The directory ${colors.cyan(templateDownloadPath)} already exists. What would you like to do?`, 127 | { 128 | type: 'select', 129 | options: ['Override its contents', 'Select different directory', 'Abort'], 130 | }, 131 | ) 132 | 133 | switch (selectedAction) { 134 | case 'Override its contents': 135 | shouldForce = true 136 | break 137 | 138 | case 'Select different directory': { 139 | templateDownloadPath = resolve(cwd, await logger.prompt('Please specify a different directory:', { 140 | type: 'text', 141 | cancel: 'reject', 142 | }).catch(() => process.exit(1))) 143 | break 144 | } 145 | 146 | // 'Abort' or Ctrl+C 147 | default: 148 | process.exit(1) 149 | } 150 | } 151 | 152 | // Download template 153 | let template: DownloadTemplateResult 154 | 155 | try { 156 | template = await downloadTemplate(templateName, { 157 | dir: templateDownloadPath, 158 | force: shouldForce, 159 | offline: Boolean(ctx.args.offline), 160 | preferOffline: Boolean(ctx.args.preferOffline), 161 | registry: process.env.NUXI_INIT_REGISTRY || DEFAULT_REGISTRY, 162 | }) 163 | } 164 | catch (err) { 165 | if (process.env.DEBUG) { 166 | throw err 167 | } 168 | logger.error((err as Error).toString()) 169 | process.exit(1) 170 | } 171 | 172 | function detectCurrentPackageManager() { 173 | const userAgent = process.env.npm_config_user_agent 174 | if (!userAgent) { 175 | return 176 | } 177 | const [name] = userAgent.split('/') 178 | if (packageManagerOptions.includes(name as PackageManagerName)) { 179 | return name as PackageManagerName 180 | } 181 | } 182 | 183 | const currentPackageManager = detectCurrentPackageManager() 184 | // Resolve package manager 185 | const packageManagerArg = ctx.args.packageManager as PackageManagerName 186 | const packageManagerSelectOptions = packageManagerOptions.map(pm => ({ 187 | label: pm, 188 | value: pm, 189 | hint: currentPackageManager === pm ? 'current' : undefined, 190 | } satisfies SelectPromptOptions['options'][number])) 191 | const selectedPackageManager = packageManagerOptions.includes(packageManagerArg) 192 | ? packageManagerArg 193 | : await logger.prompt('Which package manager would you like to use?', { 194 | type: 'select', 195 | options: packageManagerSelectOptions, 196 | initial: currentPackageManager, 197 | cancel: 'reject', 198 | }).catch(() => process.exit(1)) 199 | 200 | // Install project dependencies 201 | // or skip installation based on the '--no-install' flag 202 | if (ctx.args.install === false) { 203 | logger.info('Skipping install dependencies step.') 204 | } 205 | else { 206 | logger.start('Installing dependencies...') 207 | 208 | try { 209 | await installDependencies({ 210 | cwd: template.dir, 211 | packageManager: { 212 | name: selectedPackageManager, 213 | command: selectedPackageManager, 214 | }, 215 | }) 216 | } 217 | catch (err) { 218 | if (process.env.DEBUG) { 219 | throw err 220 | } 221 | logger.error((err as Error).toString()) 222 | process.exit(1) 223 | } 224 | 225 | logger.success('Installation completed.') 226 | } 227 | 228 | if (ctx.args.gitInit === undefined) { 229 | ctx.args.gitInit = await logger.prompt('Initialize git repository?', { 230 | type: 'confirm', 231 | cancel: 'reject', 232 | }).catch(() => process.exit(1)) 233 | } 234 | if (ctx.args.gitInit) { 235 | logger.info('Initializing git repository...\n') 236 | try { 237 | await x('git', ['init', template.dir], { 238 | throwOnError: true, 239 | nodeOptions: { 240 | stdio: 'inherit', 241 | }, 242 | }) 243 | } 244 | catch (err) { 245 | logger.warn(`Failed to initialize git repository: ${err}`) 246 | } 247 | } 248 | 249 | const modulesToAdd: string[] = [] 250 | 251 | // Get modules from arg (if provided) 252 | if (ctx.args.modules) { 253 | modulesToAdd.push( 254 | ...ctx.args.modules.split(',').map(module => module.trim()).filter(Boolean), 255 | ) 256 | } 257 | // ...or offer to install official modules (if not offline) 258 | else if (!ctx.args.offline && !ctx.args.preferOffline) { 259 | const response = await $fetch<{ 260 | modules: { 261 | npm: string 262 | type: 'community' | 'official' 263 | description: string 264 | }[] 265 | }>('https://api.nuxt.com/modules') 266 | 267 | const officialModules = response.modules 268 | .filter(module => module.type === 'official' && module.npm !== '@nuxt/devtools') 269 | 270 | const selectedOfficialModules = await logger.prompt( 271 | `Would you like to install any of the official modules?`, 272 | { 273 | type: 'multiselect', 274 | options: officialModules.map(module => ({ 275 | label: `${colors.bold(colors.greenBright(module.npm))} – ${module.description.replace(/\.$/, '')}`, 276 | value: module.npm, 277 | })), 278 | required: false, 279 | }, 280 | ) 281 | 282 | if (selectedOfficialModules === undefined) { 283 | process.exit(1) 284 | } 285 | 286 | if (selectedOfficialModules.length > 0) { 287 | modulesToAdd.push(...(selectedOfficialModules as unknown as string[])) 288 | } 289 | } 290 | 291 | // Add modules 292 | if (modulesToAdd.length > 0) { 293 | const args: string[] = [ 294 | 'add', 295 | ...modulesToAdd, 296 | `--cwd=${join(ctx.args.cwd, ctx.args.dir)}`, 297 | ctx.args.install ? '' : '--skipInstall', 298 | ctx.args.logLevel ? `--logLevel=${ctx.args.logLevel}` : '', 299 | ].filter(Boolean) 300 | 301 | await runCommand('module', args) 302 | } 303 | 304 | // Display next steps 305 | logger.log( 306 | `\n✨ Nuxt project has been created with the \`${template.name}\` template. Next steps:`, 307 | ) 308 | const relativeTemplateDir = relative(process.cwd(), template.dir) || '.' 309 | const runCmd = selectedPackageManager === 'deno' ? 'task' : 'run' 310 | const nextSteps = [ 311 | !ctx.args.shell 312 | && relativeTemplateDir.length > 1 313 | && `\`cd ${relativeTemplateDir}\``, 314 | `Start development server with \`${selectedPackageManager} ${runCmd} dev\``, 315 | ].filter(Boolean) 316 | 317 | for (const step of nextSteps) { 318 | logger.log(` › ${step}`) 319 | } 320 | 321 | if (ctx.args.shell) { 322 | startShell(template.dir) 323 | } 324 | }, 325 | }) 326 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/module/_utils.ts: -------------------------------------------------------------------------------- 1 | import { $fetch } from 'ofetch' 2 | import { readPackageJSON } from 'pkg-types' 3 | import { coerce, satisfies } from 'semver' 4 | 5 | export const categories = [ 6 | 'Analytics', 7 | 'CMS', 8 | 'CSS', 9 | 'Database', 10 | 'Date', 11 | 'Deployment', 12 | 'Devtools', 13 | 'Extensions', 14 | 'Ecommerce', 15 | 'Fonts', 16 | 'Images', 17 | 'Libraries', 18 | 'Monitoring', 19 | 'Payment', 20 | 'Performance', 21 | 'Request', 22 | 'SEO', 23 | 'Security', 24 | 'UI', 25 | ] 26 | 27 | interface NuxtApiModulesResponse { 28 | version: string 29 | generatedAt: string 30 | stats: Stats 31 | maintainers: MaintainerInfo[] 32 | contributors: Contributor[] 33 | modules: NuxtModule[] 34 | } 35 | 36 | interface Contributor { 37 | id: number 38 | username: string 39 | contributions: number 40 | modules: string[] 41 | } 42 | 43 | interface Stats { 44 | downloads: number 45 | stars: number 46 | maintainers: number 47 | contributors: number 48 | modules: number 49 | } 50 | 51 | interface ModuleCompatibility { 52 | nuxt: string 53 | requires: { bridge?: boolean | 'optional' } 54 | versionMap: { 55 | [nuxtVersion: string]: string 56 | } 57 | } 58 | 59 | interface MaintainerInfo { 60 | name: string 61 | github: string 62 | twitter?: string 63 | } 64 | 65 | interface GitHubContributor { 66 | username: string 67 | name?: string 68 | avatar_url?: string 69 | } 70 | 71 | type ModuleType = 'community' | 'official' | '3rd-party' 72 | 73 | export interface NuxtModule { 74 | name: string 75 | description: string 76 | repo: string 77 | npm: string 78 | icon?: string 79 | github: string 80 | website: string 81 | learn_more: string 82 | category: (typeof categories)[number] 83 | type: ModuleType 84 | maintainers: MaintainerInfo[] 85 | contributors?: GitHubContributor[] 86 | compatibility: ModuleCompatibility 87 | aliases?: string[] 88 | stats: Stats 89 | 90 | // Fetched in realtime API for modules.nuxt.org 91 | downloads?: number 92 | tags?: string[] 93 | stars?: number 94 | publishedAt?: number 95 | createdAt?: number 96 | } 97 | 98 | export async function fetchModules(): Promise { 99 | const { modules } = await $fetch( 100 | `https://api.nuxt.com/modules?version=all`, 101 | ) 102 | return modules 103 | } 104 | 105 | export function checkNuxtCompatibility( 106 | module: NuxtModule, 107 | nuxtVersion: string, 108 | ): boolean { 109 | if (!module.compatibility?.nuxt) { 110 | return true 111 | } 112 | 113 | return satisfies(nuxtVersion, module.compatibility.nuxt, { 114 | includePrerelease: true, 115 | }) 116 | } 117 | 118 | export async function getNuxtVersion(cwd: string) { 119 | const nuxtPkg = await readPackageJSON('nuxt', { url: cwd }).catch(() => null) 120 | if (nuxtPkg) { 121 | return nuxtPkg.version! 122 | } 123 | const pkg = await readPackageJSON(cwd) 124 | const pkgDep = pkg?.dependencies?.nuxt || pkg?.devDependencies?.nuxt 125 | return (pkgDep && coerce(pkgDep)?.version) || '3.0.0' 126 | } 127 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/module/add.ts: -------------------------------------------------------------------------------- 1 | import type { FileHandle } from 'node:fs/promises' 2 | import type { PackageJson } from 'pkg-types' 3 | 4 | import type { NuxtModule } from './_utils' 5 | import * as fs from 'node:fs' 6 | import { homedir } from 'node:os' 7 | import { join } from 'node:path' 8 | 9 | import process from 'node:process' 10 | import { updateConfig } from 'c12/update' 11 | import { defineCommand } from 'citty' 12 | import { colors } from 'consola/utils' 13 | import { addDependency } from 'nypm' 14 | import { $fetch } from 'ofetch' 15 | import { resolve } from 'pathe' 16 | import { readPackageJSON } from 'pkg-types' 17 | import { satisfies } from 'semver' 18 | import { joinURL } from 'ufo' 19 | 20 | import { runCommand } from '../../run' 21 | import { logger } from '../../utils/logger' 22 | import { cwdArgs, logLevelArgs } from '../_shared' 23 | import { checkNuxtCompatibility, fetchModules, getNuxtVersion } from './_utils' 24 | 25 | interface RegistryMeta { 26 | registry: string 27 | authToken: string | null 28 | } 29 | 30 | interface ResolvedModule { 31 | nuxtModule?: NuxtModule 32 | pkg: string 33 | pkgName: string 34 | pkgVersion: string 35 | } 36 | type UnresolvedModule = false 37 | type ModuleResolution = ResolvedModule | UnresolvedModule 38 | 39 | export default defineCommand({ 40 | meta: { 41 | name: 'add', 42 | description: 'Add Nuxt modules', 43 | }, 44 | args: { 45 | ...cwdArgs, 46 | ...logLevelArgs, 47 | moduleName: { 48 | type: 'positional', 49 | description: 'Specify one or more modules to install by name, separated by spaces', 50 | }, 51 | skipInstall: { 52 | type: 'boolean', 53 | description: 'Skip npm install', 54 | }, 55 | skipConfig: { 56 | type: 'boolean', 57 | description: 'Skip nuxt.config.ts update', 58 | }, 59 | dev: { 60 | type: 'boolean', 61 | description: 'Install modules as dev dependencies', 62 | }, 63 | }, 64 | async setup(ctx) { 65 | const cwd = resolve(ctx.args.cwd) 66 | const modules = ctx.args._.map(e => e.trim()).filter(Boolean) 67 | const projectPkg = await readPackageJSON(cwd).catch(() => ({} as PackageJson)) 68 | 69 | if (!projectPkg.dependencies?.nuxt && !projectPkg.devDependencies?.nuxt) { 70 | logger.warn(`No \`nuxt\` dependency detected in \`${cwd}\`.`) 71 | 72 | const shouldContinue = await logger.prompt( 73 | `Do you want to continue anyway?`, 74 | { 75 | type: 'confirm', 76 | initial: false, 77 | cancel: 'default', 78 | }, 79 | ) 80 | 81 | if (shouldContinue !== true) { 82 | process.exit(1) 83 | } 84 | } 85 | 86 | const maybeResolvedModules = await Promise.all(modules.map(moduleName => resolveModule(moduleName, cwd))) 87 | const resolvedModules = maybeResolvedModules.filter((x: ModuleResolution): x is ResolvedModule => x != null) 88 | 89 | logger.info(`Resolved \`${resolvedModules.map(x => x.pkgName).join('\`, \`')}\`, adding module${resolvedModules.length > 1 ? 's' : ''}...`) 90 | 91 | await addModules(resolvedModules, { ...ctx.args, cwd }, projectPkg) 92 | 93 | // Run prepare command if install is not skipped 94 | if (!ctx.args.skipInstall) { 95 | const args = Object.entries(ctx.args).filter(([k]) => k in cwdArgs || k in logLevelArgs).map(([k, v]) => `--${k}=${v}`) 96 | 97 | await runCommand('prepare', args) 98 | } 99 | }, 100 | }) 101 | 102 | // -- Internal Utils -- 103 | async function addModules(modules: ResolvedModule[], { skipInstall, skipConfig, cwd, dev }: { skipInstall: boolean, skipConfig: boolean, cwd: string, dev: boolean }, projectPkg: PackageJson) { 104 | // Add dependencies 105 | if (!skipInstall) { 106 | const installedModules: ResolvedModule[] = [] 107 | const notInstalledModules: ResolvedModule[] = [] 108 | 109 | const dependencies = new Set([ 110 | ...Object.keys(projectPkg.dependencies || {}), 111 | ...Object.keys(projectPkg.devDependencies || {}), 112 | ]) 113 | 114 | for (const module of modules) { 115 | if (dependencies.has(module.pkgName)) { 116 | installedModules.push(module) 117 | } 118 | else { 119 | notInstalledModules.push(module) 120 | } 121 | } 122 | 123 | if (installedModules.length > 0) { 124 | const installedModulesList = installedModules.map(module => module.pkgName).join('\`, \`') 125 | const are = installedModules.length > 1 ? 'are' : 'is' 126 | logger.info(`\`${installedModulesList}\` ${are} already installed`) 127 | } 128 | 129 | if (notInstalledModules.length > 0) { 130 | const isDev = Boolean(projectPkg.devDependencies?.nuxt) || dev 131 | 132 | const notInstalledModulesList = notInstalledModules.map(module => module.pkg).join('\`, \`') 133 | const dependency = notInstalledModules.length > 1 ? 'dependencies' : 'dependency' 134 | const a = notInstalledModules.length > 1 ? '' : ' a' 135 | logger.info(`Installing \`${notInstalledModulesList}\` as${a}${isDev ? ' development' : ''} ${dependency}`) 136 | 137 | const res = await addDependency(notInstalledModules.map(module => module.pkg), { 138 | cwd, 139 | dev: isDev, 140 | installPeerDependencies: true, 141 | }).then(() => true).catch( 142 | (error) => { 143 | logger.error(error) 144 | 145 | const failedModulesList = notInstalledModules.map(module => colors.cyan(module.pkg)).join('\`, \`') 146 | const s = notInstalledModules.length > 1 ? 's' : '' 147 | return logger.prompt(`Install failed for \`${failedModulesList}\`. Do you want to continue adding the module${s} to ${colors.cyan('nuxt.config')}?`, { 148 | type: 'confirm', 149 | initial: false, 150 | cancel: 'default', 151 | }) 152 | }, 153 | ) 154 | 155 | if (res !== true) { 156 | return 157 | } 158 | } 159 | } 160 | 161 | // Update nuxt.config.ts 162 | if (!skipConfig) { 163 | await updateConfig({ 164 | cwd, 165 | configFile: 'nuxt.config', 166 | async onCreate() { 167 | logger.info(`Creating \`nuxt.config.ts\``) 168 | 169 | return getDefaultNuxtConfig() 170 | }, 171 | async onUpdate(config) { 172 | if (!config.modules) { 173 | config.modules = [] 174 | } 175 | 176 | for (const resolved of modules) { 177 | if (config.modules.includes(resolved.pkgName)) { 178 | logger.info(`\`${resolved.pkgName}\` is already in the \`modules\``) 179 | 180 | continue 181 | } 182 | 183 | logger.info(`Adding \`${resolved.pkgName}\` to the \`modules\``) 184 | 185 | config.modules.push(resolved.pkgName) 186 | } 187 | }, 188 | }).catch((error) => { 189 | logger.error(`Failed to update \`nuxt.config\`: ${error.message}`) 190 | logger.error(`Please manually add \`${modules.map(module => module.pkgName).join('\`, \`')}\` to the \`modules\` in \`nuxt.config.ts\``) 191 | 192 | return null 193 | }) 194 | } 195 | } 196 | 197 | function getDefaultNuxtConfig() { 198 | return ` 199 | // https://nuxt.com/docs/api/configuration/nuxt-config 200 | export default defineNuxtConfig({ 201 | modules: [] 202 | })` 203 | } 204 | 205 | // Based on https://github.com/dword-design/package-name-regex 206 | const packageRegex 207 | = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?([a-z0-9-~][a-z0-9-._~]*)(@[^@]+)?$/ 208 | 209 | async function resolveModule(moduleName: string, cwd: string): Promise { 210 | let pkgName = moduleName 211 | let pkgVersion: string | undefined 212 | 213 | const reMatch = moduleName.match(packageRegex) 214 | if (reMatch) { 215 | if (reMatch[3]) { 216 | pkgName = `${reMatch[1] || ''}${reMatch[2] || ''}` 217 | pkgVersion = reMatch[3].slice(1) 218 | } 219 | } 220 | else { 221 | logger.error(`Invalid package name \`${pkgName}\`.`) 222 | return false 223 | } 224 | 225 | const modulesDB = await fetchModules().catch((err) => { 226 | logger.warn(`Cannot search in the Nuxt Modules database: ${err}`) 227 | return [] 228 | }) 229 | 230 | const matchedModule = modulesDB.find( 231 | module => 232 | module.name === moduleName 233 | || (pkgVersion && module.name === pkgName) 234 | || module.npm === pkgName 235 | || module.aliases?.includes(pkgName), 236 | ) 237 | 238 | if (matchedModule?.npm) { 239 | pkgName = matchedModule.npm 240 | } 241 | 242 | if (matchedModule && matchedModule.compatibility.nuxt) { 243 | // Get local Nuxt version 244 | const nuxtVersion = await getNuxtVersion(cwd) 245 | 246 | // Check for Module Compatibility 247 | if (!checkNuxtCompatibility(matchedModule, nuxtVersion)) { 248 | logger.warn( 249 | `The module \`${pkgName}\` is not compatible with Nuxt \`${nuxtVersion}\` (requires \`${matchedModule.compatibility.nuxt}\`)`, 250 | ) 251 | const shouldContinue = await logger.prompt( 252 | 'Do you want to continue installing incompatible version?', 253 | { 254 | type: 'confirm', 255 | initial: false, 256 | cancel: 'default', 257 | }, 258 | ) 259 | if (!shouldContinue) { 260 | return false 261 | } 262 | } 263 | 264 | // Match corresponding version of module for local Nuxt version 265 | const versionMap = matchedModule.compatibility.versionMap 266 | if (versionMap) { 267 | for (const [_nuxtVersion, _moduleVersion] of Object.entries(versionMap)) { 268 | if (satisfies(nuxtVersion, _nuxtVersion)) { 269 | if (!pkgVersion) { 270 | pkgVersion = _moduleVersion 271 | } 272 | else { 273 | logger.warn( 274 | `Recommended version of \`${pkgName}\` for Nuxt \`${nuxtVersion}\` is \`${_moduleVersion}\` but you have requested \`${pkgVersion}\``, 275 | ) 276 | pkgVersion = await logger.prompt('Choose a version:', { 277 | type: 'select', 278 | options: [_moduleVersion, pkgVersion], 279 | cancel: 'undefined', 280 | }) 281 | if (!pkgVersion) { 282 | return false 283 | } 284 | } 285 | break 286 | } 287 | } 288 | } 289 | } 290 | 291 | // Fetch package on npm 292 | let version = pkgVersion || 'latest' 293 | const pkgScope = pkgName.startsWith('@') ? pkgName.split('/')[0]! : null 294 | const meta: RegistryMeta = await detectNpmRegistry(pkgScope) 295 | const headers: HeadersInit = {} 296 | 297 | if (meta.authToken) { 298 | headers.Authorization = `Bearer ${meta.authToken}` 299 | } 300 | 301 | const pkgDetails = await $fetch(joinURL(meta.registry, `${pkgName}`), { headers }) 302 | 303 | // fully resolve the version 304 | if (pkgDetails['dist-tags']?.[version]) { 305 | version = pkgDetails['dist-tags'][version] 306 | } 307 | else { 308 | version = Object.keys(pkgDetails.versions)?.findLast(v => satisfies(v, version)) || version 309 | } 310 | 311 | const pkg = pkgDetails.versions[version!] 312 | 313 | const pkgDependencies = Object.assign( 314 | pkg.dependencies || {}, 315 | pkg.devDependencies || {}, 316 | ) 317 | if ( 318 | !pkgDependencies.nuxt 319 | && !pkgDependencies['nuxt-edge'] 320 | && !pkgDependencies['@nuxt/kit'] 321 | ) { 322 | logger.warn(`It seems that \`${pkgName}\` is not a Nuxt module.`) 323 | const shouldContinue = await logger.prompt( 324 | `Do you want to continue installing ${colors.cyan(pkgName)} anyway?`, 325 | { 326 | type: 'confirm', 327 | initial: false, 328 | cancel: 'default', 329 | }, 330 | ) 331 | if (!shouldContinue) { 332 | return false 333 | } 334 | } 335 | 336 | return { 337 | nuxtModule: matchedModule, 338 | pkg: `${pkgName}@${version}`, 339 | pkgName, 340 | pkgVersion: version, 341 | } 342 | } 343 | 344 | function getNpmrcPaths(): string[] { 345 | const userNpmrcPath = join(homedir(), '.npmrc') 346 | const cwdNpmrcPath = join(process.cwd(), '.npmrc') 347 | 348 | return [cwdNpmrcPath, userNpmrcPath] 349 | } 350 | 351 | async function getAuthToken(registry: RegistryMeta['registry']): Promise { 352 | const paths = getNpmrcPaths() 353 | const authTokenRegex = new RegExp(`^//${registry.replace(/^https?:\/\//, '').replace(/\/$/, '')}/:_authToken=(.+)$`, 'm') 354 | 355 | for (const npmrcPath of paths) { 356 | let fd: FileHandle | undefined 357 | try { 358 | fd = await fs.promises.open(npmrcPath, 'r') 359 | if (await fd.stat().then(r => r.isFile())) { 360 | const npmrcContent = await fd.readFile('utf-8') 361 | const authTokenMatch = npmrcContent.match(authTokenRegex)?.[1] 362 | 363 | if (authTokenMatch) { 364 | return authTokenMatch.trim() 365 | } 366 | } 367 | } 368 | catch { 369 | // swallow errors as file does not exist 370 | } 371 | finally { 372 | await fd?.close() 373 | } 374 | } 375 | 376 | return null 377 | } 378 | 379 | async function detectNpmRegistry(scope: string | null): Promise { 380 | const registry = await getRegistry(scope) 381 | const authToken = await getAuthToken(registry) 382 | 383 | return { 384 | registry, 385 | authToken, 386 | } 387 | } 388 | 389 | async function getRegistry(scope: string | null): Promise { 390 | if (process.env.COREPACK_NPM_REGISTRY) { 391 | return process.env.COREPACK_NPM_REGISTRY 392 | } 393 | const registry = await getRegistryFromFile(getNpmrcPaths(), scope) 394 | 395 | if (registry) { 396 | process.env.COREPACK_NPM_REGISTRY = registry 397 | } 398 | 399 | return registry || 'https://registry.npmjs.org' 400 | } 401 | 402 | async function getRegistryFromFile(paths: string[], scope: string | null) { 403 | for (const npmrcPath of paths) { 404 | let fd: FileHandle | undefined 405 | try { 406 | fd = await fs.promises.open(npmrcPath, 'r') 407 | if (await fd.stat().then(r => r.isFile())) { 408 | const npmrcContent = await fd.readFile('utf-8') 409 | 410 | if (scope) { 411 | const scopedRegex = new RegExp(`^${scope}:registry=(.+)$`, 'm') 412 | const scopedMatch = npmrcContent.match(scopedRegex)?.[1] 413 | if (scopedMatch) { 414 | return scopedMatch.trim() 415 | } 416 | } 417 | 418 | // If no scoped registry found or no scope provided, look for the default registry 419 | const defaultRegex = /^\s*registry=(.+)$/m 420 | const defaultMatch = npmrcContent.match(defaultRegex)?.[1] 421 | if (defaultMatch) { 422 | return defaultMatch.trim() 423 | } 424 | } 425 | } 426 | catch { 427 | // swallow errors as file does not exist 428 | } 429 | finally { 430 | await fd?.close() 431 | } 432 | } 433 | return null 434 | } 435 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/module/index.ts: -------------------------------------------------------------------------------- 1 | import { defineCommand } from 'citty' 2 | 3 | export default defineCommand({ 4 | meta: { 5 | name: 'module', 6 | description: 'Manage Nuxt modules', 7 | }, 8 | args: {}, 9 | subCommands: { 10 | add: () => import('./add').then(r => r.default || r), 11 | search: () => import('./search').then(r => r.default || r), 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/module/search.ts: -------------------------------------------------------------------------------- 1 | import { defineCommand } from 'citty' 2 | import { colors } from 'consola/utils' 3 | import Fuse from 'fuse.js' 4 | import { kebabCase, upperFirst } from 'scule' 5 | 6 | import { logger } from '../../utils/logger' 7 | import { cwdArgs } from '../_shared' 8 | import { checkNuxtCompatibility, fetchModules, getNuxtVersion } from './_utils' 9 | 10 | const { format: formatNumber } = Intl.NumberFormat('en-GB', { 11 | notation: 'compact', 12 | maximumFractionDigits: 1, 13 | }) 14 | 15 | export default defineCommand({ 16 | meta: { 17 | name: 'search', 18 | description: 'Search in Nuxt modules', 19 | }, 20 | args: { 21 | ...cwdArgs, 22 | query: { 23 | type: 'positional', 24 | description: 'keywords to search for', 25 | required: true, 26 | }, 27 | nuxtVersion: { 28 | type: 'string', 29 | description: 30 | 'Filter by Nuxt version and list compatible modules only (auto detected by default)', 31 | required: false, 32 | valueHint: '2|3', 33 | }, 34 | }, 35 | async setup(ctx) { 36 | const nuxtVersion = await getNuxtVersion(ctx.args.cwd) 37 | return findModuleByKeywords(ctx.args._.join(' '), nuxtVersion) 38 | }, 39 | }) 40 | 41 | async function findModuleByKeywords(query: string, nuxtVersion: string) { 42 | const allModules = await fetchModules() 43 | const compatibleModules = allModules.filter(m => 44 | checkNuxtCompatibility(m, nuxtVersion), 45 | ) 46 | const fuse = new Fuse(compatibleModules, { 47 | threshold: 0.1, 48 | keys: [ 49 | { name: 'name', weight: 1 }, 50 | { name: 'npm', weight: 1 }, 51 | { name: 'repo', weight: 1 }, 52 | { name: 'tags', weight: 1 }, 53 | { name: 'category', weight: 1 }, 54 | { name: 'description', weight: 0.5 }, 55 | { name: 'maintainers.name', weight: 0.5 }, 56 | { name: 'maintainers.github', weight: 0.5 }, 57 | ], 58 | }) 59 | 60 | const { bold, green, magenta, cyan, gray, yellow } = colors 61 | 62 | const results = fuse.search(query).map((result) => { 63 | const res: Record = { 64 | name: bold(result.item.name), 65 | homepage: cyan(result.item.website), 66 | compatibility: `nuxt: ${result.item.compatibility?.nuxt || '*'}`, 67 | repository: gray(result.item.github), 68 | description: gray(result.item.description), 69 | package: gray(result.item.npm), 70 | install: cyan(`npx nuxi module add ${result.item.name}`), 71 | stars: yellow(formatNumber(result.item.stats.stars)), 72 | monthlyDownloads: yellow(formatNumber(result.item.stats.downloads)), 73 | } 74 | if (result.item.github === result.item.website) { 75 | delete res.homepage 76 | } 77 | if (result.item.name === result.item.npm) { 78 | delete res.packageName 79 | } 80 | return res 81 | }) 82 | 83 | if (!results.length) { 84 | logger.info( 85 | `No Nuxt modules found matching query ${magenta(query)} for Nuxt ${cyan(nuxtVersion)}`, 86 | ) 87 | return 88 | } 89 | 90 | logger.success( 91 | `Found ${results.length} Nuxt ${results.length > 1 ? 'modules' : 'module'} matching ${cyan(query)} ${nuxtVersion ? `for Nuxt ${cyan(nuxtVersion)}` : ''}:\n`, 92 | ) 93 | for (const foundModule of results) { 94 | let maxLength = 0 95 | const entries = Object.entries(foundModule).map(([key, val]) => { 96 | const label = upperFirst(kebabCase(key)).replace(/-/g, ' ') 97 | if (label.length > maxLength) { 98 | maxLength = label.length 99 | } 100 | return [label, val || '-'] as const 101 | }) 102 | let infoStr = '' 103 | for (const [label, value] of entries) { 104 | infoStr 105 | += `${bold(label === 'Install' ? '→ ' : '- ') 106 | + green(label.padEnd(maxLength + 2)) 107 | + value 108 | }\n` 109 | } 110 | logger.log(infoStr) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/prepare.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | import { defineCommand } from 'citty' 4 | import { relative, resolve } from 'pathe' 5 | 6 | import { clearBuildDir } from '../utils/fs' 7 | import { loadKit } from '../utils/kit' 8 | import { logger } from '../utils/logger' 9 | import { cwdArgs, dotEnvArgs, envNameArgs, legacyRootDirArgs, logLevelArgs } from './_shared' 10 | 11 | export default defineCommand({ 12 | meta: { 13 | name: 'prepare', 14 | description: 'Prepare Nuxt for development/build', 15 | }, 16 | args: { 17 | ...dotEnvArgs, 18 | ...cwdArgs, 19 | ...logLevelArgs, 20 | ...envNameArgs, 21 | ...legacyRootDirArgs, 22 | }, 23 | async run(ctx) { 24 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 25 | 26 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 27 | 28 | const { loadNuxt, buildNuxt, writeTypes } = await loadKit(cwd) 29 | const nuxt = await loadNuxt({ 30 | cwd, 31 | dotenv: { 32 | cwd, 33 | fileName: ctx.args.dotenv, 34 | }, 35 | envName: ctx.args.envName, // c12 will fall back to NODE_ENV 36 | overrides: { 37 | _prepare: true, 38 | logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose', 39 | ...ctx.data?.overrides, 40 | }, 41 | }) 42 | await clearBuildDir(nuxt.options.buildDir) 43 | 44 | await buildNuxt(nuxt) 45 | await writeTypes(nuxt) 46 | logger.success( 47 | 'Types generated in', 48 | relative(process.cwd(), nuxt.options.buildDir), 49 | ) 50 | }, 51 | }) 52 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/preview.ts: -------------------------------------------------------------------------------- 1 | import type { ParsedArgs } from 'citty' 2 | import { existsSync, promises as fsp } from 'node:fs' 3 | import { dirname, relative } from 'node:path' 4 | import process from 'node:process' 5 | 6 | import { setupDotenv } from 'c12' 7 | import { defineCommand } from 'citty' 8 | import { box, colors } from 'consola/utils' 9 | import { getArgs as getListhenArgs } from 'listhen/cli' 10 | import { resolve } from 'pathe' 11 | import { x } from 'tinyexec' 12 | 13 | import { loadKit } from '../utils/kit' 14 | import { logger } from '../utils/logger' 15 | import { cwdArgs, dotEnvArgs, envNameArgs, legacyRootDirArgs, logLevelArgs } from './_shared' 16 | 17 | const command = defineCommand({ 18 | meta: { 19 | name: 'preview', 20 | description: 'Launches Nitro server for local testing after `nuxi build`.', 21 | }, 22 | args: { 23 | ...cwdArgs, 24 | ...logLevelArgs, 25 | ...envNameArgs, 26 | ...legacyRootDirArgs, 27 | port: getListhenArgs().port, 28 | ...dotEnvArgs, 29 | }, 30 | async run(ctx) { 31 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 32 | 33 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 34 | 35 | const { loadNuxt } = await loadKit(cwd) 36 | 37 | const resolvedOutputDir = await new Promise((res) => { 38 | loadNuxt({ 39 | cwd, 40 | envName: ctx.args.envName, // c12 will fall back to NODE_ENV 41 | ready: true, 42 | overrides: { 43 | modules: [ 44 | function (_, nuxt) { 45 | nuxt.hook('nitro:init', (nitro) => { 46 | res(resolve(nuxt.options.srcDir || cwd, nitro.options.output.dir || '.output', 'nitro.json')) 47 | }) 48 | }, 49 | ], 50 | }, 51 | }).then(nuxt => nuxt.close()).catch(() => '') 52 | }) 53 | 54 | const defaultOutput = resolve(cwd, '.output', 'nitro.json') // for backwards compatibility 55 | 56 | const nitroJSONPaths = [resolvedOutputDir, defaultOutput].filter(Boolean) 57 | const nitroJSONPath = nitroJSONPaths.find(p => existsSync(p)) 58 | if (!nitroJSONPath) { 59 | logger.error( 60 | 'Cannot find `nitro.json`. Did you run `nuxi build` first? Search path:\n', 61 | nitroJSONPaths, 62 | ) 63 | process.exit(1) 64 | } 65 | const outputPath = dirname(nitroJSONPath) 66 | const nitroJSON = JSON.parse(await fsp.readFile(nitroJSONPath, 'utf-8')) 67 | 68 | if (!nitroJSON.commands.preview) { 69 | logger.error('Preview is not supported for this build.') 70 | process.exit(1) 71 | } 72 | 73 | const info = [ 74 | ['Node.js:', `v${process.versions.node}`], 75 | ['Nitro Preset:', nitroJSON.preset], 76 | ['Working directory:', relative(process.cwd(), outputPath)], 77 | ] as const 78 | const _infoKeyLen = Math.max(...info.map(([label]) => label.length)) 79 | 80 | logger.log( 81 | box( 82 | [ 83 | 'You are running Nuxt production build in preview mode.', 84 | `For production deployments, please directly use ${colors.cyan( 85 | nitroJSON.commands.preview, 86 | )} command.`, 87 | '', 88 | ...info.map( 89 | ([label, value]) => 90 | `${label.padEnd(_infoKeyLen, ' ')} ${colors.cyan(value)}`, 91 | ), 92 | ].join('\n'), 93 | { 94 | title: colors.yellow('Preview Mode'), 95 | style: { 96 | borderColor: 'yellow', 97 | }, 98 | }, 99 | ), 100 | ) 101 | 102 | const envFileName = ctx.args.dotenv || '.env' 103 | 104 | const envExists = existsSync(resolve(cwd, envFileName)) 105 | 106 | if (envExists) { 107 | logger.info( 108 | `Loading \`${envFileName}\`. This will not be loaded when running the server in production.`, 109 | ) 110 | await setupDotenv({ cwd, fileName: envFileName }) 111 | } 112 | else if (ctx.args.dotenv) { 113 | logger.error(`Cannot find \`${envFileName}\`.`) 114 | } 115 | 116 | const { port } = _resolveListenOptions(ctx.args) 117 | 118 | logger.info(`Starting preview command: \`${nitroJSON.commands.preview}\``) 119 | const [command, ...commandArgs] = nitroJSON.commands.preview.split(' ') 120 | logger.log('') 121 | await x(command, commandArgs, { 122 | throwOnError: true, 123 | nodeOptions: { 124 | stdio: 'inherit', 125 | cwd: outputPath, 126 | env: { 127 | ...process.env, 128 | NUXT_PORT: port, 129 | NITRO_PORT: port, 130 | }, 131 | }, 132 | }) 133 | }, 134 | }) 135 | 136 | export default command 137 | 138 | type ArgsT = Exclude< 139 | Awaited, 140 | undefined | ((...args: unknown[]) => unknown) 141 | > 142 | 143 | function _resolveListenOptions(args: ParsedArgs) { 144 | const _port = args.port 145 | ?? args.p 146 | ?? process.env.NUXT_PORT 147 | ?? process.env.NITRO_PORT 148 | ?? process.env.PORT 149 | 150 | return { 151 | port: _port, 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/test.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | import { defineCommand } from 'citty' 4 | import { resolve } from 'pathe' 5 | 6 | import { logger } from '../utils/logger' 7 | import { cwdArgs, legacyRootDirArgs, logLevelArgs } from './_shared' 8 | 9 | export default defineCommand({ 10 | meta: { 11 | name: 'test', 12 | description: 'Run tests', 13 | }, 14 | args: { 15 | ...cwdArgs, 16 | ...logLevelArgs, 17 | ...legacyRootDirArgs, 18 | dev: { 19 | type: 'boolean', 20 | description: 'Run in dev mode', 21 | }, 22 | watch: { 23 | type: 'boolean', 24 | description: 'Watch mode', 25 | }, 26 | }, 27 | async run(ctx) { 28 | process.env.NODE_ENV = process.env.NODE_ENV || 'test' 29 | 30 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 31 | 32 | const { runTests } = await importTestUtils() 33 | await runTests({ 34 | rootDir: cwd, 35 | dev: ctx.args.dev, 36 | watch: ctx.args.watch, 37 | ...{}, 38 | }) 39 | }, 40 | }) 41 | 42 | async function importTestUtils(): Promise { 43 | let err 44 | for (const pkg of [ 45 | '@nuxt/test-utils-nightly', 46 | '@nuxt/test-utils-edge', 47 | '@nuxt/test-utils', 48 | ]) { 49 | try { 50 | const exports = await import(pkg) 51 | // Detect old @nuxt/test-utils 52 | if (!exports.runTests) { 53 | throw new Error('Invalid version of `@nuxt/test-utils` is installed!') 54 | } 55 | return exports 56 | } 57 | catch (_err) { 58 | err = _err 59 | } 60 | } 61 | logger.error(err) 62 | throw new Error('`@nuxt/test-utils` seems missing. Run `npm i -D @nuxt/test-utils` or `yarn add -D @nuxt/test-utils` to install.') 63 | } 64 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/typecheck.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | import { defineCommand } from 'citty' 5 | import { createJiti } from 'jiti' 6 | import { resolve } from 'pathe' 7 | import { isBun } from 'std-env' 8 | import { x } from 'tinyexec' 9 | 10 | import { loadKit } from '../utils/kit' 11 | import { cwdArgs, dotEnvArgs, legacyRootDirArgs, logLevelArgs } from './_shared' 12 | 13 | export default defineCommand({ 14 | meta: { 15 | name: 'typecheck', 16 | description: 'Runs `vue-tsc` to check types throughout your app.', 17 | }, 18 | args: { 19 | ...cwdArgs, 20 | ...logLevelArgs, 21 | ...dotEnvArgs, 22 | ...legacyRootDirArgs, 23 | }, 24 | async run(ctx) { 25 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 26 | 27 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 28 | 29 | const { loadNuxt, buildNuxt, writeTypes } = await loadKit(cwd) 30 | const nuxt = await loadNuxt({ 31 | cwd, 32 | dotenv: { cwd, fileName: ctx.args.dotenv }, 33 | overrides: { 34 | _prepare: true, 35 | logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose', 36 | }, 37 | }) 38 | 39 | // Generate types and build Nuxt instance 40 | await writeTypes(nuxt) 41 | await buildNuxt(nuxt) 42 | await nuxt.close() 43 | 44 | const jiti = createJiti(cwd) 45 | 46 | // Prefer local install if possible 47 | const [resolvedTypeScript, resolvedVueTsc] = await Promise.all([ 48 | jiti.esmResolve('typescript', { try: true }), 49 | jiti.esmResolve('vue-tsc/bin/vue-tsc.js', { try: true }), 50 | ]) 51 | if (resolvedTypeScript && resolvedVueTsc) { 52 | await x(fileURLToPath(resolvedVueTsc), ['--noEmit'], { 53 | throwOnError: true, 54 | nodeOptions: { 55 | stdio: 'inherit', 56 | cwd, 57 | }, 58 | }) 59 | } 60 | else { 61 | if (isBun) { 62 | await x( 63 | 'bun', 64 | 'install typescript vue-tsc --global --silent'.split(' '), 65 | { 66 | throwOnError: true, 67 | nodeOptions: { stdio: 'inherit', cwd }, 68 | }, 69 | ) 70 | 71 | await x('bunx', 'vue-tsc --noEmit'.split(' '), { 72 | throwOnError: true, 73 | nodeOptions: { 74 | stdio: 'inherit', 75 | cwd, 76 | }, 77 | }) 78 | } 79 | else { 80 | await x( 81 | 'npx', 82 | '-p vue-tsc -p typescript vue-tsc --noEmit'.split(' '), 83 | { 84 | throwOnError: true, 85 | nodeOptions: { stdio: 'inherit', cwd }, 86 | }, 87 | ) 88 | } 89 | } 90 | }, 91 | }) 92 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/upgrade.ts: -------------------------------------------------------------------------------- 1 | import type { PackageJson } from 'pkg-types' 2 | 3 | import { existsSync } from 'node:fs' 4 | import process from 'node:process' 5 | 6 | import { defineCommand } from 'citty' 7 | import { colors } from 'consola/utils' 8 | import { addDependency, dedupeDependencies, detectPackageManager } from 'nypm' 9 | import { resolve } from 'pathe' 10 | import { readPackageJSON } from 'pkg-types' 11 | 12 | import { loadKit } from '../utils/kit' 13 | import { logger } from '../utils/logger' 14 | import { cleanupNuxtDirs, nuxtVersionToGitIdentifier } from '../utils/nuxt' 15 | import { getPackageManagerVersion } from '../utils/packageManagers' 16 | import { cwdArgs, legacyRootDirArgs, logLevelArgs } from './_shared' 17 | 18 | async function getNuxtVersion(path: string): Promise { 19 | try { 20 | const pkg = await readPackageJSON('nuxt', { url: path }) 21 | if (!pkg.version) { 22 | logger.warn('Cannot find any installed Nuxt versions in ', path) 23 | } 24 | return pkg.version || null 25 | } 26 | catch { 27 | return null 28 | } 29 | } 30 | 31 | function checkNuxtDependencyType(pkg: PackageJson): 'dependencies' | 'devDependencies' { 32 | if (pkg.dependencies?.nuxt) { 33 | return 'dependencies' 34 | } 35 | if (pkg.devDependencies?.nuxt) { 36 | return 'devDependencies' 37 | } 38 | return 'dependencies' 39 | } 40 | 41 | const nuxtVersionTags = { 42 | '3.x': '3x', 43 | '4.x': 'latest', 44 | } 45 | 46 | async function getNightlyVersion(packageNames: string[]): Promise<{ npmPackages: string[], nuxtVersion: string }> { 47 | const nuxtVersion = await logger.prompt( 48 | 'Which nightly Nuxt release channel do you want to install? (3.x or 4.x)', 49 | { 50 | type: 'select', 51 | options: ['3.x', '4.x'] as const, 52 | default: '3.x', 53 | cancel: 'reject', 54 | }, 55 | ).catch(() => process.exit(1)) 56 | 57 | const npmPackages = packageNames.map(p => `${p}@npm:${p}-nightly@${nuxtVersionTags[nuxtVersion]}`) 58 | 59 | return { npmPackages, nuxtVersion } 60 | } 61 | 62 | async function getRequiredNewVersion(packageNames: string[], channel: string): Promise<{ npmPackages: string[], nuxtVersion: string }> { 63 | if (channel === 'nightly') { 64 | return getNightlyVersion(packageNames) 65 | } 66 | 67 | return { npmPackages: packageNames.map(p => `${p}@latest`), nuxtVersion: '3' } 68 | } 69 | 70 | export default defineCommand({ 71 | meta: { 72 | name: 'upgrade', 73 | description: 'Upgrade Nuxt', 74 | }, 75 | args: { 76 | ...cwdArgs, 77 | ...logLevelArgs, 78 | ...legacyRootDirArgs, 79 | dedupe: { 80 | type: 'boolean', 81 | description: 'Dedupe dependencies after upgrading', 82 | }, 83 | force: { 84 | type: 'boolean', 85 | alias: 'f', 86 | description: 'Force upgrade to recreate lockfile and node_modules', 87 | }, 88 | channel: { 89 | type: 'string', 90 | alias: 'ch', 91 | default: 'stable', 92 | description: 'Specify a channel to install from (default: stable)', 93 | valueHint: 'stable|nightly', 94 | }, 95 | }, 96 | async run(ctx) { 97 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 98 | 99 | // Check package manager 100 | const packageManager = await detectPackageManager(cwd) 101 | if (!packageManager) { 102 | logger.error( 103 | `Unable to determine the package manager used by this project.\n\nNo lock files found in \`${cwd}\`, and no \`packageManager\` field specified in \`package.json\`.\n\nPlease either add the \`packageManager\` field to \`package.json\` or execute the installation command for your package manager. For example, you can use \`pnpm i\`, \`npm i\`, \`bun i\`, or \`yarn i\`, and then try again.`, 104 | ) 105 | process.exit(1) 106 | } 107 | const { name: packageManagerName, lockFile: lockFileCandidates } = packageManager 108 | const packageManagerVersion = getPackageManagerVersion(packageManagerName) 109 | logger.info('Package manager:', packageManagerName, packageManagerVersion) 110 | 111 | // Check currently installed Nuxt version 112 | const currentVersion = (await getNuxtVersion(cwd)) || '[unknown]' 113 | logger.info('Current Nuxt version:', currentVersion) 114 | 115 | const pkg = await readPackageJSON(cwd).catch(() => null) 116 | 117 | // Check if Nuxt is a dependency or devDependency 118 | const nuxtDependencyType = pkg ? checkNuxtDependencyType(pkg) : 'dependencies' 119 | const corePackages = ['@nuxt/kit', '@nuxt/schema', '@nuxt/vite-builder', '@nuxt/webpack-builder', '@nuxt/rspack-builder'] 120 | 121 | const packagesToUpdate = pkg ? corePackages.filter(p => pkg.dependencies?.[p] || pkg.devDependencies?.[p]) : [] 122 | 123 | // Install latest version 124 | const { npmPackages, nuxtVersion } = await getRequiredNewVersion(['nuxt', ...packagesToUpdate], ctx.args.channel) 125 | 126 | // Force install 127 | const toRemove = ['node_modules'] 128 | 129 | const lockFile = normaliseLockFile(cwd, lockFileCandidates) 130 | if (lockFile) { 131 | toRemove.push(lockFile) 132 | } 133 | 134 | const forceRemovals = toRemove 135 | .map(p => colors.cyan(p)) 136 | .join(' and ') 137 | 138 | let method: 'force' | 'dedupe' | 'skip' | undefined = ctx.args.force ? 'force' : ctx.args.dedupe ? 'dedupe' : undefined 139 | 140 | method ||= await logger.prompt( 141 | `Would you like to dedupe your lockfile (recommended) or recreate ${forceRemovals}? This can fix problems with hoisted dependency versions and ensure you have the most up-to-date dependencies.`, 142 | { 143 | type: 'select', 144 | initial: 'dedupe', 145 | cancel: 'reject', 146 | options: [ 147 | { 148 | label: 'dedupe lockfile', 149 | value: 'dedupe' as const, 150 | hint: 'recommended', 151 | }, 152 | { 153 | label: `recreate ${forceRemovals}`, 154 | value: 'force' as const, 155 | }, 156 | { 157 | label: 'skip', 158 | value: 'skip' as const, 159 | }, 160 | ], 161 | }, 162 | ).catch(() => process.exit(1)) 163 | 164 | const versionType = ctx.args.channel === 'nightly' ? 'nightly' : 'latest stable' 165 | logger.info(`Installing ${versionType} Nuxt ${nuxtVersion} release...`) 166 | 167 | await addDependency(npmPackages, { 168 | cwd, 169 | packageManager, 170 | dev: nuxtDependencyType === 'devDependencies', 171 | }) 172 | 173 | if (method === 'force') { 174 | logger.info( 175 | `Recreating ${forceRemovals}. If you encounter any issues, revert the changes and try with \`--no-force\``, 176 | ) 177 | await dedupeDependencies({ recreateLockfile: true }) 178 | } 179 | 180 | if (method === 'dedupe') { 181 | logger.info('Try deduping dependencies...') 182 | await dedupeDependencies() 183 | } 184 | 185 | // Clean up after upgrade 186 | let buildDir: string = '.nuxt' 187 | try { 188 | const { loadNuxtConfig } = await loadKit(cwd) 189 | const nuxtOptions = await loadNuxtConfig({ cwd }) 190 | buildDir = nuxtOptions.buildDir 191 | } 192 | catch { 193 | // Use default buildDir (.nuxt) 194 | } 195 | await cleanupNuxtDirs(cwd, buildDir) 196 | 197 | // Check installed Nuxt version again 198 | const upgradedVersion = (await getNuxtVersion(cwd)) || '[unknown]' 199 | logger.info('Upgraded Nuxt version:', upgradedVersion) 200 | 201 | if (upgradedVersion === '[unknown]') { 202 | return 203 | } 204 | 205 | if (upgradedVersion === currentVersion) { 206 | logger.success('You\'re already using the latest version of Nuxt.') 207 | } 208 | else { 209 | logger.success( 210 | 'Successfully upgraded Nuxt from', 211 | currentVersion, 212 | 'to', 213 | upgradedVersion, 214 | ) 215 | if (currentVersion === '[unknown]') { 216 | return 217 | } 218 | const commitA = nuxtVersionToGitIdentifier(currentVersion) 219 | const commitB = nuxtVersionToGitIdentifier(upgradedVersion) 220 | if (commitA && commitB) { 221 | logger.info( 222 | 'Changelog:', 223 | `https://github.com/nuxt/nuxt/compare/${commitA}...${commitB}`, 224 | ) 225 | } 226 | } 227 | }, 228 | }) 229 | 230 | // Find which lock file is in use since `nypm.detectPackageManager` doesn't return this 231 | function normaliseLockFile(cwd: string, lockFiles: string | Array | undefined) { 232 | if (typeof lockFiles === 'string') { 233 | lockFiles = [lockFiles] 234 | } 235 | 236 | const lockFile = lockFiles?.find(file => existsSync(resolve(cwd, file))) 237 | 238 | if (lockFile === undefined) { 239 | logger.error(`Unable to find any lock files in ${cwd}`) 240 | return undefined 241 | } 242 | 243 | return lockFile 244 | } 245 | -------------------------------------------------------------------------------- /packages/nuxi/src/index.ts: -------------------------------------------------------------------------------- 1 | export { main } from './main' 2 | export { runCommand, runMain } from './run' 3 | -------------------------------------------------------------------------------- /packages/nuxi/src/main.ts: -------------------------------------------------------------------------------- 1 | import nodeCrypto from 'node:crypto' 2 | import { resolve } from 'node:path' 3 | import process from 'node:process' 4 | 5 | import { defineCommand } from 'citty' 6 | import { provider } from 'std-env' 7 | 8 | import { description, name, version } from '../package.json' 9 | import { commands } from './commands' 10 | import { cwdArgs } from './commands/_shared' 11 | import { setupGlobalConsole } from './utils/console' 12 | import { checkEngines } from './utils/engines' 13 | import { logger } from './utils/logger' 14 | 15 | // globalThis.crypto support for Node.js 18 16 | if (!globalThis.crypto) { 17 | globalThis.crypto = nodeCrypto as unknown as Crypto 18 | } 19 | 20 | export const main = defineCommand({ 21 | meta: { 22 | name: name.endsWith('nightly') ? name : 'nuxi', 23 | version, 24 | description, 25 | }, 26 | args: { 27 | ...cwdArgs, 28 | command: { 29 | type: 'positional', 30 | required: false, 31 | }, 32 | }, 33 | subCommands: commands, 34 | async setup(ctx) { 35 | const command = ctx.args._[0] 36 | const dev = command === 'dev' 37 | setupGlobalConsole({ dev }) 38 | 39 | // Check Node.js version and CLI updates in background 40 | let backgroundTasks: Promise | undefined 41 | if (command !== '_dev' && provider !== 'stackblitz') { 42 | backgroundTasks = Promise.all([ 43 | checkEngines(), 44 | ]).catch(err => logger.error(err)) 45 | } 46 | 47 | // Avoid background check to fix prompt issues 48 | if (command === 'init') { 49 | await backgroundTasks 50 | } 51 | 52 | // allow running arbitrary commands if there's a locally registered binary with `nuxt-` prefix 53 | if (ctx.args.command && !(ctx.args.command in commands)) { 54 | const cwd = resolve(ctx.args.cwd) 55 | try { 56 | const { x } = await import('tinyexec') 57 | // `tinyexec` will resolve command from local binaries 58 | await x(`nuxt-${ctx.args.command}`, ctx.rawArgs.slice(1), { 59 | nodeOptions: { stdio: 'inherit', cwd }, 60 | throwOnError: true, 61 | }) 62 | } 63 | catch (err) { 64 | // TODO: use windows err code as well 65 | if (err instanceof Error && 'code' in err && err.code === 'ENOENT') { 66 | return 67 | } 68 | } 69 | process.exit() 70 | } 71 | }, 72 | }) 73 | -------------------------------------------------------------------------------- /packages/nuxi/src/run.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | import { runCommand as _runCommand, runMain as _runMain } from 'citty' 5 | 6 | import { commands } from './commands' 7 | import { main } from './main' 8 | 9 | globalThis.__nuxt_cli__ = globalThis.__nuxt_cli__ || { 10 | // Programmatic usage fallback 11 | startTime: Date.now(), 12 | entry: fileURLToPath( 13 | new URL( 14 | import.meta.url.endsWith('.ts') 15 | ? '../bin/nuxi.mjs' 16 | : '../../bin/nuxi.mjs', 17 | import.meta.url, 18 | ), 19 | ), 20 | } 21 | 22 | export const runMain = () => _runMain(main) 23 | 24 | // To provide subcommands call it as `runCommand(, [, ...])` 25 | export async function runCommand( 26 | name: string, 27 | argv: string[] = process.argv.slice(2), 28 | data: { overrides?: Record } = {}, 29 | ) { 30 | argv.push('--no-clear') // Dev 31 | 32 | if (!(name in commands)) { 33 | throw new Error(`Invalid command ${name}`) 34 | } 35 | 36 | return await _runCommand(await commands[name as keyof typeof commands](), { 37 | rawArgs: argv, 38 | data: { 39 | overrides: data.overrides || {}, 40 | }, 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/ascii.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Thank you to IndyJoenz for this ASCII art 3 | * https://bsky.app/profile/durdraw.org/post/3liadod3gv22a 4 | */ 5 | 6 | export const themeColor = '\x1B[38;2;0;220;130m' 7 | const icon = [ 8 | ` .d$b.`, 9 | ` i$$A$$L .d$b`, 10 | ` .$$F\` \`$$L.$$A$$.`, 11 | ` j$$' \`4$$:\` \`$$.`, 12 | ` j$$' .4$: \`$$.`, 13 | ` j$$\` .$$: \`4$L`, 14 | ` :$$:____.d$$: _____.:$$:`, 15 | ` \`4$$$$$$$$P\` .i$$$$$$$$P\``, 16 | ] 17 | 18 | export const nuxtIcon = icon.map(line => line.split('').join(themeColor)).join('\n') 19 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/banner.ts: -------------------------------------------------------------------------------- 1 | import { colors } from 'consola/utils' 2 | import { readPackageJSON } from 'pkg-types' 3 | 4 | import { tryResolveNuxt } from './kit' 5 | import { logger } from './logger' 6 | 7 | export async function showVersions(cwd: string) { 8 | const { bold, gray, green } = colors 9 | const nuxtDir = await tryResolveNuxt(cwd) 10 | async function getPkgVersion(pkg: string) { 11 | for (const url of [cwd, nuxtDir]) { 12 | if (!url) { 13 | continue 14 | } 15 | const p = await readPackageJSON(pkg, { url }).catch(() => null) 16 | if (p) { 17 | return p.version! 18 | } 19 | } 20 | return '' 21 | } 22 | const nuxtVersion = await getPkgVersion('nuxt') || await getPkgVersion('nuxt-nightly') || await getPkgVersion('nuxt3') || await getPkgVersion('nuxt-edge') 23 | const nitroVersion = await getPkgVersion('nitropack') || await getPkgVersion('nitropack-nightly') || await getPkgVersion('nitropack-edge') 24 | 25 | logger.log(gray(green(`Nuxt ${bold(nuxtVersion)}`) + (nitroVersion ? ` with Nitro ${bold(nitroVersion)}` : ''))) 26 | } 27 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/console.ts: -------------------------------------------------------------------------------- 1 | import type { ConsolaReporter } from 'consola' 2 | 3 | import process from 'node:process' 4 | 5 | import { consola } from 'consola' 6 | 7 | // Filter out unwanted logs 8 | // TODO: Use better API from consola for intercepting logs 9 | function wrapReporter(reporter: ConsolaReporter) { 10 | return ({ 11 | log(logObj, ctx) { 12 | if (!logObj.args || !logObj.args.length) { 13 | return 14 | } 15 | const msg = logObj.args[0] 16 | if (typeof msg === 'string' && !process.env.DEBUG) { 17 | // Hide vue-router 404 warnings 18 | if ( 19 | msg.startsWith( 20 | '[Vue Router warn]: No match found for location with path', 21 | ) 22 | ) { 23 | return 24 | } 25 | // Suppress warning about native Node.js fetch 26 | if ( 27 | msg.includes( 28 | 'ExperimentalWarning: The Fetch API is an experimental feature', 29 | ) 30 | ) { 31 | return 32 | } 33 | // TODO: resolve upstream in Vite 34 | // Hide sourcemap warnings related to node_modules 35 | if (msg.startsWith('Sourcemap') && msg.includes('node_modules')) { 36 | return 37 | } 38 | } 39 | return reporter.log(logObj, ctx) 40 | }, 41 | }) satisfies ConsolaReporter 42 | } 43 | 44 | export function setupGlobalConsole(opts: { dev?: boolean } = {}) { 45 | consola.options.reporters = consola.options.reporters.map(wrapReporter) 46 | 47 | // Wrap all console logs with consola for better DX 48 | if (opts.dev) { 49 | consola.wrapAll() 50 | } 51 | else { 52 | consola.wrapConsole() 53 | } 54 | 55 | process.on('unhandledRejection', err => 56 | consola.error('[unhandledRejection]', err)) 57 | 58 | process.on('uncaughtException', err => 59 | consola.error('[uncaughtException]', err)) 60 | } 61 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/dev.ts: -------------------------------------------------------------------------------- 1 | import type { Nuxt, NuxtConfig } from '@nuxt/schema' 2 | import type { DotenvOptions } from 'c12' 3 | import type { FSWatcher } from 'chokidar' 4 | import type { Jiti } from 'jiti' 5 | import type { HTTPSOptions, Listener, ListenOptions, ListenURL } from 'listhen' 6 | import type { IncomingMessage, RequestListener, ServerResponse } from 'node:http' 7 | import type { AddressInfo } from 'node:net' 8 | 9 | import EventEmitter from 'node:events' 10 | import process from 'node:process' 11 | 12 | import chokidar from 'chokidar' 13 | import defu from 'defu' 14 | import { toNodeListener } from 'h3' 15 | import { createJiti } from 'jiti' 16 | import { listen } from 'listhen' 17 | import { join, relative, resolve } from 'pathe' 18 | import { debounce } from 'perfect-debounce' 19 | import { provider } from 'std-env' 20 | import { joinURL } from 'ufo' 21 | 22 | import { clearBuildDir } from '../utils/fs' 23 | import { loadKit } from '../utils/kit' 24 | import { logger } from '../utils/logger' 25 | import { loadNuxtManifest, resolveNuxtManifest, writeNuxtManifest } from '../utils/nuxt' 26 | import { renderError } from './error' 27 | 28 | export type NuxtDevIPCMessage = 29 | | { type: 'nuxt:internal:dev:ready', port: number } 30 | | { type: 'nuxt:internal:dev:loading', message: string } 31 | | { type: 'nuxt:internal:dev:restart' } 32 | | { type: 'nuxt:internal:dev:rejection', message: string } 33 | | { type: 'nuxt:internal:dev:loading:error', error: Error } 34 | 35 | export interface NuxtDevContext { 36 | public?: boolean 37 | hostname?: string 38 | publicURLs?: string[] 39 | proxy?: { 40 | url?: string 41 | urls?: ListenURL[] 42 | https?: boolean | HTTPSOptions 43 | } 44 | } 45 | 46 | interface NuxtDevServerOptions { 47 | cwd: string 48 | logLevel?: 'silent' | 'info' | 'verbose' 49 | dotenv: DotenvOptions 50 | envName?: string 51 | clear?: boolean 52 | defaults: NuxtConfig 53 | overrides: NuxtConfig 54 | port?: string | number 55 | loadingTemplate?: ({ loading }: { loading: string }) => string 56 | devContext: NuxtDevContext 57 | } 58 | 59 | export async function createNuxtDevServer(options: NuxtDevServerOptions, listenOptions?: Partial) { 60 | // Initialize dev server 61 | const devServer = new NuxtDevServer(options) 62 | 63 | // Attach internal listener 64 | devServer.listener = await listen( 65 | devServer.handler, 66 | listenOptions || { 67 | port: options.port ?? 0, 68 | hostname: '127.0.0.1', 69 | showURL: false, 70 | }, 71 | ) 72 | 73 | // Merge interface with public context 74 | // @ts-expect-error private property 75 | devServer.listener._url = devServer.listener.url 76 | if (options.devContext.proxy?.url) { 77 | devServer.listener.url = options.devContext.proxy.url 78 | } 79 | if (options.devContext.proxy?.urls) { 80 | const _getURLs = devServer.listener.getURLs.bind(devServer.listener) 81 | devServer.listener.getURLs = async () => 82 | Array.from( 83 | new Set([...options.devContext.proxy!.urls!, ...(await _getURLs())]), 84 | ) 85 | } 86 | 87 | return devServer 88 | } 89 | 90 | // https://regex101.com/r/7HkR5c/1 91 | const RESTART_RE = /^(?:nuxt\.config\.[a-z0-9]+|\.nuxtignore|\.nuxtrc|\.config\/nuxt(?:\.config)?\.[a-z0-9]+)$/ 92 | 93 | class NuxtDevServer extends EventEmitter { 94 | private _handler?: RequestListener 95 | private _distWatcher?: FSWatcher 96 | private _currentNuxt?: Nuxt 97 | private _loadingMessage?: string 98 | private _jiti: Jiti 99 | private _loadingError?: Error 100 | 101 | loadDebounced: (reload?: boolean, reason?: string) => void 102 | handler: RequestListener 103 | listener: Listener 104 | 105 | constructor(private options: NuxtDevServerOptions) { 106 | super() 107 | 108 | this.loadDebounced = debounce(this.load) 109 | 110 | let _initResolve: () => void 111 | const _initPromise = new Promise((resolve) => { 112 | _initResolve = resolve 113 | }) 114 | this.once('ready', () => { 115 | _initResolve() 116 | }) 117 | 118 | this._jiti = createJiti(options.cwd) 119 | 120 | this.handler = async (req, res) => { 121 | if (this._loadingError) { 122 | this._renderError(req, res) 123 | return 124 | } 125 | await _initPromise 126 | if (this._handler) { 127 | this._handler(req, res) 128 | } 129 | else { 130 | this._renderLoadingScreen(req, res) 131 | } 132 | } 133 | 134 | // @ts-expect-error we set it in wrapper function 135 | this.listener = undefined 136 | } 137 | 138 | _renderError(req: IncomingMessage, res: ServerResponse) { 139 | renderError(req, res, this._loadingError) 140 | } 141 | 142 | async _renderLoadingScreen(req: IncomingMessage, res: ServerResponse) { 143 | res.statusCode = 503 144 | res.setHeader('Content-Type', 'text/html') 145 | const loadingTemplate 146 | = this.options.loadingTemplate 147 | || this._currentNuxt?.options.devServer.loadingTemplate 148 | || await this._jiti.import<{ loading: () => string }>('@nuxt/ui-templates').then(r => r.loading).catch(() => {}) 149 | || ((params: { loading: string }) => `

${params.loading}

`) 150 | res.end( 151 | loadingTemplate({ 152 | loading: this._loadingMessage || 'Loading...', 153 | }), 154 | ) 155 | } 156 | 157 | async init() { 158 | await this.load() 159 | await this._watchConfig() 160 | } 161 | 162 | async load(reload?: boolean, reason?: string) { 163 | try { 164 | await this._load(reload, reason) 165 | this._loadingError = undefined 166 | } 167 | catch (error) { 168 | logger.error(`Cannot ${reload ? 'restart' : 'start'} nuxt: `, error) 169 | this._handler = undefined 170 | this._loadingError = error as Error 171 | this._loadingMessage = 'Error while loading Nuxt. Please check console and fix errors.' 172 | this.emit('loading:error', error) 173 | } 174 | } 175 | 176 | async _load(reload?: boolean, reason?: string) { 177 | const action = reload ? 'Restarting' : 'Starting' 178 | this._loadingMessage = `${reason ? `${reason}. ` : ''}${action} Nuxt...` 179 | this._handler = undefined 180 | this.emit('loading', this._loadingMessage) 181 | if (reload) { 182 | logger.info(this._loadingMessage) 183 | } 184 | 185 | if (this._currentNuxt) { 186 | await this._currentNuxt.close() 187 | } 188 | if (this._distWatcher) { 189 | await this._distWatcher.close() 190 | } 191 | 192 | const kit = await loadKit(this.options.cwd) 193 | 194 | const devServerDefaults = _getDevServerDefaults({}, await this.listener.getURLs().then(r => r.map(r => r.url))) 195 | 196 | this._currentNuxt = await kit.loadNuxt({ 197 | cwd: this.options.cwd, 198 | dev: true, 199 | ready: false, 200 | envName: this.options.envName, 201 | dotenv: { 202 | cwd: this.options.cwd, 203 | fileName: this.options.dotenv.fileName, 204 | }, 205 | defaults: defu(this.options.defaults, devServerDefaults), 206 | overrides: { 207 | logLevel: this.options.logLevel as 'silent' | 'info' | 'verbose', 208 | ...this.options.overrides, 209 | vite: { 210 | clearScreen: this.options.clear, 211 | ...this.options.overrides.vite, 212 | }, 213 | }, 214 | }) 215 | 216 | // Connect Vite HMR 217 | if (!process.env.NUXI_DISABLE_VITE_HMR) { 218 | this._currentNuxt.hooks.hook('vite:extend', ({ config }) => { 219 | if (config.server) { 220 | config.server.hmr = { 221 | protocol: undefined, 222 | ...(config.server.hmr as Exclude), 223 | port: undefined, 224 | host: undefined, 225 | server: this.listener.server, 226 | } 227 | } 228 | }) 229 | } 230 | 231 | // Remove websocket handlers on close 232 | this._currentNuxt.hooks.hookOnce('close', () => { 233 | this.listener.server.removeAllListeners('upgrade') 234 | }) 235 | 236 | // Write manifest and also check if we need cache invalidation 237 | if (!reload) { 238 | const previousManifest = await loadNuxtManifest(this._currentNuxt.options.buildDir) 239 | const newManifest = resolveNuxtManifest(this._currentNuxt) 240 | 241 | // we deliberately do not block initialising Nuxt on creation of the manifest 242 | const promise = writeNuxtManifest(this._currentNuxt, newManifest) 243 | this._currentNuxt.hooks.hookOnce('ready', async () => { 244 | await promise 245 | }) 246 | 247 | if ( 248 | previousManifest 249 | && newManifest 250 | && previousManifest._hash !== newManifest._hash 251 | ) { 252 | await clearBuildDir(this._currentNuxt.options.buildDir) 253 | } 254 | } 255 | 256 | await this._currentNuxt.ready() 257 | 258 | const unsub = this._currentNuxt.hooks.hook('restart', async (options) => { 259 | unsub() // We use this instead of `hookOnce` for Nuxt Bridge support 260 | if (options?.hard) { 261 | this.emit('restart') 262 | return 263 | } 264 | await this.load(true) 265 | }) 266 | 267 | if (this._currentNuxt.server && 'upgrade' in this._currentNuxt.server) { 268 | this.listener.server.on( 269 | 'upgrade', 270 | async (req: any, socket: any, head: any) => { 271 | const nuxt = this._currentNuxt 272 | if (!nuxt) 273 | return 274 | const viteHmrPath = joinURL( 275 | nuxt.options.app.baseURL.startsWith('./') ? nuxt.options.app.baseURL.slice(1) : nuxt.options.app.baseURL, 276 | nuxt.options.app.buildAssetsDir, 277 | ) 278 | if (req.url.startsWith(viteHmrPath)) { 279 | return // Skip for Vite HMR 280 | } 281 | await nuxt.server.upgrade(req, socket, head) 282 | }, 283 | ) 284 | } 285 | 286 | await this._currentNuxt.hooks.callHook('listen', this.listener.server, this.listener) 287 | 288 | // Sync internal server info to the internals 289 | // It is important for vite-node to use the internal URL but public proto 290 | const addr = this.listener.address 291 | this._currentNuxt.options.devServer.host = addr.address 292 | this._currentNuxt.options.devServer.port = addr.port 293 | this._currentNuxt.options.devServer.url = _getAddressURL(addr, !!this.listener.https) 294 | this._currentNuxt.options.devServer.https = this.options.devContext.proxy 295 | ?.https as boolean | { key: string, cert: string } 296 | 297 | if (this.listener.https && !process.env.NODE_TLS_REJECT_UNAUTHORIZED) { 298 | logger.warn('You might need `NODE_TLS_REJECT_UNAUTHORIZED=0` environment variable to make https work.') 299 | } 300 | 301 | await Promise.all([ 302 | kit.writeTypes(this._currentNuxt).catch(console.error), 303 | kit.buildNuxt(this._currentNuxt), 304 | ]) 305 | 306 | // Watch dist directory 307 | this._distWatcher = chokidar.watch(resolve(this._currentNuxt.options.buildDir, 'dist'), { 308 | ignoreInitial: true, 309 | depth: 0, 310 | }) 311 | this._distWatcher.on('unlinkDir', () => { 312 | this.loadDebounced(true, '.nuxt/dist directory has been removed') 313 | }) 314 | 315 | this._handler = toNodeListener(this._currentNuxt.server.app) 316 | this.emit('ready', addr) 317 | } 318 | 319 | async _watchConfig() { 320 | const configWatcher = chokidar.watch([this.options.cwd, join(this.options.cwd, '.config')], { 321 | ignoreInitial: true, 322 | depth: 0, 323 | }) 324 | configWatcher.on('all', (event, _file) => { 325 | if (event === 'all' || event === 'ready' || event === 'error' || event === 'raw') { 326 | return 327 | } 328 | const file = relative(this.options.cwd, _file) 329 | if (file === (this.options.dotenv.fileName || '.env')) { 330 | this.emit('restart') 331 | } 332 | if (RESTART_RE.test(file)) { 333 | this.loadDebounced(true, `${file} updated`) 334 | } 335 | }) 336 | } 337 | } 338 | 339 | function _getAddressURL(addr: AddressInfo, https: boolean) { 340 | const proto = https ? 'https' : 'http' 341 | let host = addr.address.includes(':') ? `[${addr.address}]` : addr.address 342 | if (host === '[::]') { 343 | host = 'localhost' // Fix issues with Docker networking 344 | } 345 | const port = addr.port || 3000 346 | return `${proto}://${host}:${port}/` 347 | } 348 | 349 | export function _getDevServerOverrides(listenOptions: Partial>) { 350 | if (listenOptions.public || provider === 'codesandbox') { 351 | return { 352 | devServer: { cors: { origin: '*' } }, 353 | vite: { server: { allowedHosts: true } }, 354 | } 355 | } 356 | 357 | return {} 358 | } 359 | 360 | export function _getDevServerDefaults(listenOptions: Partial>, urls: string[] = []) { 361 | const defaultConfig: Partial = {} 362 | 363 | if (urls) { 364 | defaultConfig.vite = { server: { allowedHosts: urls.map(u => new URL(u).hostname) } } 365 | } 366 | 367 | // defined hostname 368 | if (listenOptions.hostname) { 369 | const protocol = listenOptions.https ? 'https' : 'http' 370 | defaultConfig.devServer = { cors: { origin: [`${protocol}://${listenOptions.hostname}`, ...urls] } } 371 | defaultConfig.vite = defu(defaultConfig.vite, { server: { allowedHosts: [listenOptions.hostname] } }) 372 | } 373 | 374 | return defaultConfig 375 | } 376 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/engines.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | import { logger } from './logger' 4 | 5 | export async function checkEngines() { 6 | const satisfies = await import('semver/functions/satisfies.js').then( 7 | r => 8 | r.default || (r as any as typeof import('semver/functions/satisfies.js')), 9 | ) // npm/node-semver#381 10 | const currentNode = process.versions.node 11 | const nodeRange = '>= 18.0.0' 12 | 13 | if (!satisfies(currentNode, nodeRange)) { 14 | logger.warn( 15 | `Current version of Node.js (\`${currentNode}\`) is unsupported and might cause issues.\n Please upgrade to a compatible version \`${nodeRange}\`.`, 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/env.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | import { logger } from './logger' 4 | 5 | export function overrideEnv(targetEnv: string) { 6 | const currentEnv = process.env.NODE_ENV 7 | if (currentEnv && currentEnv !== targetEnv) { 8 | logger.warn( 9 | `Changing \`NODE_ENV\` from \`${currentEnv}\` to \`${targetEnv}\`, to avoid unintended behavior.`, 10 | ) 11 | } 12 | 13 | process.env.NODE_ENV = targetEnv 14 | } 15 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/error.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from 'node:http' 2 | import { Youch } from 'youch' 3 | 4 | export async function renderError(req: IncomingMessage, res: ServerResponse, error: unknown) { 5 | const youch = new Youch() 6 | res.statusCode = 500 7 | res.setHeader('Content-Type', 'text/html') 8 | const html = await youch.toHTML(error, { 9 | request: { 10 | url: req.url, 11 | method: req.method, 12 | headers: req.headers, 13 | }, 14 | }) 15 | res.end(html) 16 | } 17 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, promises as fsp } from 'node:fs' 2 | import { join } from 'pathe' 3 | 4 | import { logger } from '../utils/logger' 5 | 6 | export async function clearDir(path: string, exclude?: string[]) { 7 | if (!exclude) { 8 | await fsp.rm(path, { recursive: true, force: true }) 9 | } 10 | else if (existsSync(path)) { 11 | const files = await fsp.readdir(path) 12 | await Promise.all( 13 | files.map(async (name) => { 14 | if (!exclude.includes(name)) { 15 | await fsp.rm(join(path, name), { recursive: true, force: true }) 16 | } 17 | }), 18 | ) 19 | } 20 | await fsp.mkdir(path, { recursive: true }) 21 | } 22 | 23 | export function clearBuildDir(path: string) { 24 | return clearDir(path, ['cache', 'analyze', 'nuxt.json']) 25 | } 26 | 27 | export async function rmRecursive(paths: string[]) { 28 | await Promise.all( 29 | paths 30 | .filter(p => typeof p === 'string') 31 | .map(async (path) => { 32 | logger.debug('Removing recursive path', path) 33 | await fsp.rm(path, { recursive: true, force: true }).catch(() => {}) 34 | }), 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/kit.ts: -------------------------------------------------------------------------------- 1 | import { createJiti } from 'jiti' 2 | 3 | export async function loadKit(rootDir: string): Promise { 4 | const jiti = createJiti(rootDir) 5 | try { 6 | // Without PNP (or if users have a local install of kit, we bypass resolving from Nuxt) 7 | const localKit = jiti.esmResolve('@nuxt/kit', { try: true }) 8 | // Otherwise, we resolve Nuxt _first_ as it is Nuxt's kit dependency that will be used 9 | const rootURL = localKit ? rootDir : (await tryResolveNuxt(rootDir)) || rootDir 10 | let kit: typeof import('@nuxt/kit') = await jiti.import('@nuxt/kit', { parentURL: rootURL }) 11 | if (!kit.writeTypes) { 12 | kit = { 13 | ...kit, 14 | writeTypes: () => { 15 | throw new Error('`writeTypes` is not available in this version of `@nuxt/kit`. Please upgrade to v3.7 or newer.') 16 | }, 17 | } 18 | } 19 | return kit 20 | } 21 | catch (e: any) { 22 | if (e.toString().includes('Cannot find module \'@nuxt/kit\'')) { 23 | throw new Error( 24 | 'nuxi requires `@nuxt/kit` to be installed in your project. Try installing `nuxt` v3 or `@nuxt/bridge` first.', 25 | ) 26 | } 27 | throw e 28 | } 29 | } 30 | 31 | export async function tryResolveNuxt(rootDir: string) { 32 | const jiti = createJiti(rootDir) 33 | for (const pkg of ['nuxt-nightly', 'nuxt', 'nuxt3', 'nuxt-edge']) { 34 | const path = jiti.esmResolve(pkg, { try: true }) 35 | if (path) { 36 | return path 37 | } 38 | } 39 | return null 40 | } 41 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { consola } from 'consola' 2 | 3 | export const logger = consola.withTag('nuxi') 4 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/nuxt.ts: -------------------------------------------------------------------------------- 1 | import type { Nuxt } from '@nuxt/schema' 2 | 3 | import { promises as fsp } from 'node:fs' 4 | 5 | import { hash } from 'ohash' 6 | import { dirname, resolve } from 'pathe' 7 | 8 | import { logger } from '../utils/logger' 9 | import { rmRecursive } from './fs' 10 | 11 | interface NuxtProjectManifest { 12 | _hash: string | null 13 | project: { 14 | rootDir: string 15 | } 16 | versions: { 17 | nuxt: string 18 | } 19 | } 20 | 21 | export async function cleanupNuxtDirs(rootDir: string, buildDir: string) { 22 | logger.info('Cleaning up generated Nuxt files and caches...') 23 | 24 | await rmRecursive( 25 | [ 26 | buildDir, 27 | '.output', 28 | 'dist', 29 | 'node_modules/.vite', 30 | 'node_modules/.cache', 31 | ].map(dir => resolve(rootDir, dir)), 32 | ) 33 | } 34 | 35 | export function nuxtVersionToGitIdentifier(version: string) { 36 | // match the git identifier in the release, for example: 3.0.0-rc.8-27677607.a3a8706 37 | const id = /\.([0-9a-f]{7,8})$/.exec(version) 38 | if (id?.[1]) { 39 | return id[1] 40 | } 41 | // match github tag, for example 3.0.0-rc.8 42 | return `v${version}` 43 | } 44 | 45 | export function resolveNuxtManifest(nuxt: Nuxt): NuxtProjectManifest { 46 | const manifest: NuxtProjectManifest = { 47 | _hash: null, 48 | project: { 49 | rootDir: nuxt.options.rootDir, 50 | }, 51 | versions: { 52 | nuxt: nuxt._version, 53 | }, 54 | } 55 | manifest._hash = hash(manifest) 56 | return manifest 57 | } 58 | 59 | export async function writeNuxtManifest(nuxt: Nuxt, manifest = resolveNuxtManifest(nuxt)): Promise { 60 | const manifestPath = resolve(nuxt.options.buildDir, 'nuxt.json') 61 | await fsp.mkdir(dirname(manifestPath), { recursive: true }) 62 | await fsp.writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8') 63 | return manifest 64 | } 65 | 66 | export async function loadNuxtManifest(buildDir: string): Promise { 67 | const manifestPath = resolve(buildDir, 'nuxt.json') 68 | const manifest: NuxtProjectManifest | null = await fsp 69 | .readFile(manifestPath, 'utf-8') 70 | .then(data => JSON.parse(data) as NuxtProjectManifest) 71 | .catch(() => null) 72 | return manifest 73 | } 74 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/packageManagers.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process' 2 | 3 | export function getPackageManagerVersion(name: string) { 4 | return execSync(`${name} --version`).toString('utf8').trim() 5 | } 6 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/api.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '.' 2 | import { resolve } from 'pathe' 3 | import { applySuffix } from '.' 4 | 5 | const httpMethods = [ 6 | 'connect', 7 | 'delete', 8 | 'get', 9 | 'head', 10 | 'options', 11 | 'post', 12 | 'put', 13 | 'trace', 14 | 'patch', 15 | ] 16 | 17 | const api: Template = ({ name, args, nuxtOptions }) => { 18 | return { 19 | path: resolve(nuxtOptions.srcDir, nuxtOptions.serverDir, `api/${name}${applySuffix(args, httpMethods, 'method')}.ts`), 20 | contents: ` 21 | export default defineEventHandler(event => { 22 | return 'Hello ${name}' 23 | }) 24 | `, 25 | } 26 | } 27 | 28 | export { api } 29 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/app-config.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '.' 2 | import { resolve } from 'pathe' 3 | 4 | const appConfig: Template = ({ nuxtOptions }) => ({ 5 | path: resolve(nuxtOptions.srcDir, 'app.config.ts'), 6 | contents: ` 7 | export default defineAppConfig({}) 8 | `, 9 | }) 10 | 11 | export { appConfig } 12 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/app.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '.' 2 | import { resolve } from 'pathe' 3 | 4 | const app: Template = ({ args, nuxtOptions }) => ({ 5 | path: resolve(nuxtOptions.srcDir, 'app.vue'), 6 | contents: args.pages 7 | ? ` 8 | 9 | 10 | 17 | 18 | 19 | ` 20 | : ` 21 | 22 | 23 | 28 | 29 | 30 | `, 31 | }) 32 | 33 | export { app } 34 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/component.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '.' 2 | import { resolve } from 'pathe' 3 | import { applySuffix } from '.' 4 | 5 | const component: Template = ({ name, args, nuxtOptions }) => ({ 6 | path: resolve(nuxtOptions.srcDir, `components/${name}${applySuffix( 7 | args, 8 | ['client', 'server'], 9 | 'mode', 10 | )}.vue`), 11 | contents: ` 12 | 13 | 14 | 19 | 20 | 21 | `, 22 | }) 23 | 24 | export { component } 25 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/composable.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '.' 2 | import { resolve } from 'pathe' 3 | import { pascalCase } from 'scule' 4 | 5 | const composable: Template = ({ name, nuxtOptions }) => { 6 | const nameWithoutUsePrefix = name.replace(/^use-?/, '') 7 | const nameWithUsePrefix = `use${pascalCase(nameWithoutUsePrefix)}` 8 | 9 | return { 10 | path: resolve(nuxtOptions.srcDir, `composables/${name}.ts`), 11 | contents: ` 12 | export const ${nameWithUsePrefix} = () => { 13 | return ref() 14 | } 15 | `, 16 | } 17 | } 18 | 19 | export { composable } 20 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/error.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '.' 2 | import { resolve } from 'pathe' 3 | 4 | const error: Template = ({ nuxtOptions }) => ({ 5 | path: resolve(nuxtOptions.srcDir, 'error.vue'), 6 | contents: ` 7 | 14 | 15 | 21 | 22 | 23 | `, 24 | }) 25 | 26 | export { error } 27 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/index.ts: -------------------------------------------------------------------------------- 1 | import type { NuxtOptions } from '@nuxt/schema' 2 | import { api } from './api' 3 | import { app } from './app' 4 | import { appConfig } from './app-config' 5 | import { component } from './component' 6 | import { composable } from './composable' 7 | import { error } from './error' 8 | import { layer } from './layer' 9 | import { layout } from './layout' 10 | import { middleware } from './middleware' 11 | import { module } from './module' 12 | import { page } from './page' 13 | import { plugin } from './plugin' 14 | import { serverMiddleware } from './server-middleware' 15 | import { serverPlugin } from './server-plugin' 16 | import { serverRoute } from './server-route' 17 | import { serverUtil } from './server-util' 18 | 19 | interface TemplateOptions { 20 | name: string 21 | args: Record 22 | nuxtOptions: NuxtOptions 23 | } 24 | 25 | interface Template { 26 | (options: TemplateOptions): { path: string, contents: string } 27 | } 28 | 29 | const templates = { 30 | 'api': api, 31 | 'app': app, 32 | 'app-config': appConfig, 33 | 'component': component, 34 | 'composable': composable, 35 | 'error': error, 36 | 'layer': layer, 37 | 'layout': layout, 38 | 'middleware': middleware, 39 | 'module': module, 40 | 'page': page, 41 | 'plugin': plugin, 42 | 'server-middleware': serverMiddleware, 43 | 'server-plugin': serverPlugin, 44 | 'server-route': serverRoute, 45 | 'server-util': serverUtil, 46 | } satisfies Record 47 | 48 | // -- internal utils -- 49 | 50 | function applySuffix( 51 | args: TemplateOptions['args'], 52 | suffixes: string[], 53 | unwrapFrom?: string, 54 | ): string { 55 | let suffix = '' 56 | 57 | // --client 58 | for (const s of suffixes) { 59 | if (args[s]) { 60 | suffix += `.${s}` 61 | } 62 | } 63 | 64 | // --mode=server 65 | if (unwrapFrom && args[unwrapFrom] && suffixes.includes(args[unwrapFrom])) { 66 | suffix += `.${args[unwrapFrom]}` 67 | } 68 | 69 | return suffix 70 | } 71 | 72 | export { applySuffix, Template, templates } 73 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/layer.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '.' 2 | import { resolve } from 'pathe' 3 | 4 | const layer: Template = ({ name, nuxtOptions }) => { 5 | return { 6 | path: resolve(nuxtOptions.srcDir, `layers/${name}/nuxt.config.ts`), 7 | contents: ` 8 | export default defineNuxtConfig({}) 9 | `, 10 | } 11 | } 12 | 13 | export { layer } 14 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/layout.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '.' 2 | import { resolve } from 'pathe' 3 | 4 | const layout: Template = ({ name, nuxtOptions }) => ({ 5 | path: resolve(nuxtOptions.srcDir, nuxtOptions.dir.layouts, `${name}.vue`), 6 | contents: ` 7 | 8 | 9 | 15 | 16 | 17 | `, 18 | }) 19 | 20 | export { layout } 21 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/middleware.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '.' 2 | import { resolve } from 'pathe' 3 | import { applySuffix } from '.' 4 | 5 | const middleware: Template = ({ name, args, nuxtOptions }) => ({ 6 | path: resolve(nuxtOptions.srcDir, nuxtOptions.dir.middleware, `${name}${applySuffix(args, ['global'])}.ts`), 7 | contents: ` 8 | export default defineNuxtRouteMiddleware((to, from) => {}) 9 | `, 10 | }) 11 | 12 | export { middleware } 13 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/module.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '.' 2 | import { resolve } from 'pathe' 3 | 4 | const module: Template = ({ name, nuxtOptions }) => ({ 5 | path: resolve(nuxtOptions.rootDir, 'modules', `${name}.ts`), 6 | contents: ` 7 | import { defineNuxtModule } from 'nuxt/kit' 8 | 9 | export default defineNuxtModule({ 10 | meta: { 11 | name: '${name}' 12 | }, 13 | setup () {} 14 | }) 15 | `, 16 | }) 17 | 18 | export { module } 19 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/page.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '.' 2 | import { resolve } from 'pathe' 3 | 4 | const page: Template = ({ name, nuxtOptions }) => ({ 5 | path: resolve(nuxtOptions.srcDir, nuxtOptions.dir.pages, `${name}.vue`), 6 | contents: ` 7 | 8 | 9 | 14 | 15 | 16 | `, 17 | }) 18 | 19 | export { page } 20 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '.' 2 | import { resolve } from 'pathe' 3 | import { applySuffix } from '.' 4 | 5 | const plugin: Template = ({ name, args, nuxtOptions }) => ({ 6 | path: resolve(nuxtOptions.srcDir, nuxtOptions.dir.plugins, `${name}${applySuffix(args, ['client', 'server'], 'mode')}.ts`), 7 | contents: ` 8 | export default defineNuxtPlugin(nuxtApp => {}) 9 | `, 10 | }) 11 | 12 | export { plugin } 13 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/server-middleware.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '.' 2 | import { resolve } from 'pathe' 3 | 4 | const serverMiddleware: Template = ({ name, nuxtOptions }) => ({ 5 | path: resolve(nuxtOptions.srcDir, nuxtOptions.serverDir, 'middleware', `${name}.ts`), 6 | contents: ` 7 | export default defineEventHandler(event => {}) 8 | `, 9 | }) 10 | 11 | export { serverMiddleware } 12 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/server-plugin.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '.' 2 | import { resolve } from 'pathe' 3 | 4 | const serverPlugin: Template = ({ name, nuxtOptions }) => ({ 5 | path: resolve(nuxtOptions.srcDir, nuxtOptions.serverDir, 'plugins', `${name}.ts`), 6 | contents: ` 7 | export default defineNitroPlugin(nitroApp => {}) 8 | `, 9 | }) 10 | 11 | export { serverPlugin } 12 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/server-route.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '.' 2 | import { resolve } from 'pathe' 3 | 4 | const serverRoute: Template = ({ name, args, nuxtOptions }) => ({ 5 | path: resolve(nuxtOptions.srcDir, nuxtOptions.serverDir, args.api ? 'api' : 'routes', `${name}.ts`), 6 | contents: ` 7 | export default defineEventHandler(event => {}) 8 | `, 9 | }) 10 | 11 | export { serverRoute } 12 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/templates/server-util.ts: -------------------------------------------------------------------------------- 1 | import type { Template } from '.' 2 | import { resolve } from 'pathe' 3 | import { camelCase } from 'scule' 4 | 5 | const serverUtil: Template = ({ name, nuxtOptions }) => ({ 6 | path: resolve(nuxtOptions.srcDir, nuxtOptions.serverDir, 'utils', `${name}.ts`), 7 | contents: ` 8 | export function ${camelCase(name)}() {} 9 | `, 10 | }) 11 | 12 | export { serverUtil } 13 | -------------------------------------------------------------------------------- /packages/nuxi/test/unit/commands/module/add.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it, vi } from 'vitest' 2 | 3 | import commands from '../../../../src/commands/module' 4 | import * as utils from '../../../../src/commands/module/_utils' 5 | import * as runCommands from '../../../../src/run' 6 | 7 | const updateConfig = vi.fn(() => Promise.resolve()) 8 | const addDependency = vi.fn(() => Promise.resolve()) 9 | let v3 = '3.0.0' 10 | interface CommandsType { 11 | subCommands: { 12 | // biome-ignore lint/correctness/noEmptyPattern: 13 | add: () => Promise<{ setup: (args: any) => void }> 14 | } 15 | } 16 | function applyMocks() { 17 | vi.mock('c12/update', async () => { 18 | return { 19 | updateConfig, 20 | } 21 | }) 22 | vi.mock('nypm', async () => { 23 | return { 24 | addDependency, 25 | } 26 | }) 27 | vi.mock('pkg-types', async () => { 28 | return { 29 | readPackageJSON: () => { 30 | return new Promise((resolve) => { 31 | resolve({ 32 | devDependencies: { 33 | nuxt: '3.0.0', 34 | }, 35 | }) 36 | }) 37 | }, 38 | } 39 | }) 40 | vi.mock('ofetch', async () => { 41 | return { 42 | $fetch: vi.fn(() => Promise.resolve({ 43 | 'name': '@nuxt/content', 44 | 'npm': '@nuxt/content', 45 | 'devDependencies': { 46 | nuxt: v3, 47 | }, 48 | 'dist-tags': { latest: v3 }, 49 | 'versions': { 50 | [v3]: { 51 | devDependencies: { 52 | nuxt: v3, 53 | }, 54 | }, 55 | '3.1.1': { 56 | devDependencies: { 57 | nuxt: v3, 58 | }, 59 | }, 60 | '2.9.0': { 61 | devDependencies: { 62 | nuxt: v3, 63 | }, 64 | }, 65 | '2.13.1': { 66 | devDependencies: { 67 | nuxt: v3, 68 | }, 69 | }, 70 | }, 71 | })), 72 | } 73 | }) 74 | } 75 | describe('module add', () => { 76 | beforeAll(async () => { 77 | const response = await fetch('https://registry.npmjs.org/@nuxt/content') 78 | const json = await response.json() 79 | v3 = json['dist-tags'].latest 80 | }) 81 | applyMocks() 82 | vi.spyOn(runCommands, 'runCommand').mockImplementation(vi.fn()) 83 | vi.spyOn(utils, 'getNuxtVersion').mockResolvedValue('3.0.0') 84 | vi.spyOn(utils, 'fetchModules').mockResolvedValue([ 85 | { 86 | name: 'content', 87 | npm: '@nuxt/content', 88 | compatibility: { 89 | nuxt: '3.0.0', 90 | requires: {}, 91 | versionMap: {}, 92 | }, 93 | description: '', 94 | repo: '', 95 | github: '', 96 | website: '', 97 | learn_more: '', 98 | category: '', 99 | type: 'community', 100 | maintainers: [], 101 | stats: { 102 | downloads: 0, 103 | stars: 0, 104 | maintainers: 0, 105 | contributors: 0, 106 | modules: 0, 107 | }, 108 | }, 109 | ]) 110 | 111 | it('should install Nuxt module', async () => { 112 | const addCommand = await (commands as CommandsType).subCommands.add() 113 | await addCommand.setup({ 114 | args: { 115 | cwd: '/fake-dir', 116 | _: ['content'], 117 | }, 118 | }) 119 | 120 | expect(addDependency).toHaveBeenCalledWith([`@nuxt/content@${v3}`], { 121 | cwd: '/fake-dir', 122 | dev: true, 123 | installPeerDependencies: true, 124 | }) 125 | }) 126 | 127 | it('should convert versioned module to Nuxt module', async () => { 128 | const addCommand = await (commands as CommandsType).subCommands.add() 129 | await addCommand.setup({ 130 | args: { 131 | cwd: '/fake-dir', 132 | _: ['content@2.9.0'], 133 | }, 134 | }) 135 | 136 | expect(addDependency).toHaveBeenCalledWith(['@nuxt/content@2.9.0'], { 137 | cwd: '/fake-dir', 138 | dev: true, 139 | installPeerDependencies: true, 140 | }) 141 | }) 142 | 143 | it('should convert major only version to full semver', async () => { 144 | const addCommand = await (commands as CommandsType).subCommands.add() 145 | await addCommand.setup({ 146 | args: { 147 | cwd: '/fake-dir', 148 | _: ['content@2'], 149 | }, 150 | }) 151 | 152 | expect(addDependency).toHaveBeenCalledWith(['@nuxt/content@2.13.1'], { 153 | cwd: '/fake-dir', 154 | dev: true, 155 | installPeerDependencies: true, 156 | }) 157 | }) 158 | 159 | it('should convert not full version to full semver', async () => { 160 | const addCommand = await (commands as CommandsType).subCommands.add() 161 | await addCommand.setup({ 162 | args: { 163 | cwd: '/fake-dir', 164 | _: ['content@3.1'], 165 | }, 166 | }) 167 | 168 | expect(addDependency).toHaveBeenCalledWith(['@nuxt/content@3.1.1'], { 169 | cwd: '/fake-dir', 170 | dev: true, 171 | installPeerDependencies: true, 172 | }) 173 | }) 174 | }) 175 | -------------------------------------------------------------------------------- /packages/nuxi/test/unit/templates.spec.ts: -------------------------------------------------------------------------------- 1 | import type { NuxtOptions } from '@nuxt/schema' 2 | import { describe, expect, it } from 'vitest' 3 | 4 | import { templates } from '../../src/utils/templates' 5 | 6 | describe('templates', () => { 7 | it('composables', () => { 8 | for (const name of ['useSomeComposable', 'someComposable', 'use-some-composable', 'use-someComposable', 'some-composable']) { 9 | expect(templates.composable({ name, args: {}, nuxtOptions: { srcDir: '/src' } as NuxtOptions }).contents.trim().split('\n')[0]).toBe('export const useSomeComposable = () => {') 10 | } 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /packages/nuxt-cli/bin/nuxi.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { fileURLToPath } from 'node:url' 4 | import { runMain } from '../dist/index.mjs' 5 | 6 | globalThis.__nuxt_cli__ = { 7 | startTime: Date.now(), 8 | entry: fileURLToPath(import.meta.url), 9 | } 10 | 11 | runMain() 12 | -------------------------------------------------------------------------------- /packages/nuxt-cli/build.config.ts: -------------------------------------------------------------------------------- 1 | import type { InputPluginOption } from 'rollup' 2 | import process from 'node:process' 3 | import { visualizer } from 'rollup-plugin-visualizer' 4 | import { defineBuildConfig } from 'unbuild' 5 | import { purgePolyfills } from 'unplugin-purge-polyfills' 6 | 7 | const isAnalysingSize = process.env.BUNDLE_SIZE === 'true' 8 | 9 | export default defineBuildConfig({ 10 | declaration: !isAnalysingSize, 11 | failOnWarn: !isAnalysingSize, 12 | rollup: { 13 | dts: { 14 | respectExternal: false, 15 | }, 16 | }, 17 | hooks: { 18 | 'rollup:options': function (ctx, options) { 19 | const plugins = (options.plugins ||= []) as InputPluginOption[] 20 | plugins.push(purgePolyfills.rollup({ logLevel: 'verbose' })) 21 | if (isAnalysingSize) { 22 | plugins.unshift(visualizer({ template: 'raw-data' })) 23 | } 24 | }, 25 | }, 26 | entries: ['src/index'], 27 | externals: [ 28 | '@nuxt/test-utils', 29 | ], 30 | }) 31 | -------------------------------------------------------------------------------- /packages/nuxt-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxt/cli", 3 | "type": "module", 4 | "version": "3.25.1", 5 | "description": "Nuxt CLI", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/nuxt/cli.git", 10 | "directory": "packages/nuxt-cli" 11 | }, 12 | "exports": { 13 | ".": "./dist/index.mjs", 14 | "./cli": "./bin/nuxi.mjs" 15 | }, 16 | "types": "./dist/index.d.ts", 17 | "bin": { 18 | "nuxi": "bin/nuxi.mjs", 19 | "nuxi-ng": "bin/nuxi.mjs", 20 | "nuxt": "bin/nuxi.mjs", 21 | "nuxt-cli": "bin/nuxi.mjs" 22 | }, 23 | "files": [ 24 | "bin", 25 | "dist" 26 | ], 27 | "engines": { 28 | "node": "^16.10.0 || >=18.0.0" 29 | }, 30 | "scripts": { 31 | "build": "unbuild", 32 | "dev:prepare": "unbuild --stub", 33 | "prepack": "unbuild" 34 | }, 35 | "dependencies": { 36 | "c12": "^3.0.4", 37 | "chokidar": "^4.0.3", 38 | "citty": "^0.1.6", 39 | "clipboardy": "^4.0.0", 40 | "consola": "^3.4.2", 41 | "defu": "^6.1.4", 42 | "fuse.js": "^7.1.0", 43 | "giget": "^2.0.0", 44 | "h3": "^1.15.3", 45 | "httpxy": "^0.1.7", 46 | "jiti": "^2.4.2", 47 | "listhen": "^1.9.0", 48 | "nypm": "^0.6.0", 49 | "ofetch": "^1.4.1", 50 | "ohash": "^2.0.11", 51 | "pathe": "^2.0.3", 52 | "perfect-debounce": "^1.0.0", 53 | "pkg-types": "^2.1.0", 54 | "scule": "^1.3.0", 55 | "semver": "^7.7.2", 56 | "std-env": "^3.9.0", 57 | "tinyexec": "^1.0.1", 58 | "ufo": "^1.6.1", 59 | "youch": "^4.1.0-beta.8" 60 | }, 61 | "devDependencies": { 62 | "@types/node": "^22.15.29", 63 | "rollup": "^4.41.1", 64 | "rollup-plugin-visualizer": "^6.0.1", 65 | "typescript": "^5.8.3", 66 | "unbuild": "^3.5.0", 67 | "unplugin-purge-polyfills": "^0.1.0", 68 | "vitest": "^3.2.0", 69 | "youch": "^4.1.0-beta.8" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/nuxt-cli/playground/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | 10 | pnpm-lock.yaml 11 | -------------------------------------------------------------------------------- /packages/nuxt-cli/playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | compatibilityDate: '2024-09-05', 4 | nitro: { 5 | experimental: { 6 | websocket: true, 7 | }, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /packages/nuxt-cli/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-cli-playground", 3 | "version": "1.0.0", 4 | "private": true, 5 | "packageManager": "pnpm@10.11.1", 6 | "dependencies": { 7 | "nuxt": "^3.16.1" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/nuxt-cli/playground/pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | -------------------------------------------------------------------------------- /packages/nuxt-cli/playground/pages/ws.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 105 | 106 | 109 | -------------------------------------------------------------------------------- /packages/nuxt-cli/playground/server/routes/_ws.ts: -------------------------------------------------------------------------------- 1 | export default defineWebSocketHandler({ 2 | open(peer) { 3 | console.log('[ws] open', peer) 4 | }, 5 | 6 | message(peer, message) { 7 | console.log('[ws] message', peer, message) 8 | if (message.text().includes('ping')) { 9 | peer.send('pong') 10 | } 11 | }, 12 | 13 | close(peer, event) { 14 | console.log('[ws] close', peer, event) 15 | }, 16 | 17 | error(peer, error) { 18 | console.log('[ws] error', peer, error) 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /packages/nuxt-cli/playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /packages/nuxt-cli/src/index.ts: -------------------------------------------------------------------------------- 1 | export { main } from './main' 2 | export { runCommand, runMain } from './run' 3 | -------------------------------------------------------------------------------- /packages/nuxt-cli/src/main.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import process from 'node:process' 3 | 4 | import { defineCommand } from 'citty' 5 | import { provider } from 'std-env' 6 | 7 | import { commands } from '../../nuxi/src/commands' 8 | import { cwdArgs } from '../../nuxi/src/commands/_shared' 9 | import { setupGlobalConsole } from '../../nuxi/src/utils/console' 10 | import { checkEngines } from '../../nuxi/src/utils/engines' 11 | import { logger } from '../../nuxi/src/utils/logger' 12 | 13 | import { description, name, version } from '../package.json' 14 | 15 | export const main = defineCommand({ 16 | meta: { 17 | name: name.endsWith('nightly') ? name : 'nuxi', 18 | version, 19 | description, 20 | }, 21 | args: { 22 | ...cwdArgs, 23 | command: { 24 | type: 'positional', 25 | required: false, 26 | }, 27 | }, 28 | subCommands: commands, 29 | async setup(ctx) { 30 | const command = ctx.args._[0] 31 | const dev = command === 'dev' 32 | setupGlobalConsole({ dev }) 33 | 34 | // Check Node.js version and CLI updates in background 35 | let backgroundTasks: Promise | undefined 36 | if (command !== '_dev' && provider !== 'stackblitz') { 37 | backgroundTasks = Promise.all([ 38 | checkEngines(), 39 | ]).catch(err => logger.error(err)) 40 | } 41 | 42 | // Avoid background check to fix prompt issues 43 | if (command === 'init') { 44 | await backgroundTasks 45 | } 46 | 47 | // allow running arbitrary commands if there's a locally registered binary with `nuxt-` prefix 48 | if (ctx.args.command && !(ctx.args.command in commands)) { 49 | const cwd = resolve(ctx.args.cwd) 50 | try { 51 | const { x } = await import('tinyexec') 52 | // `tinyexec` will resolve command from local binaries 53 | await x(`nuxt-${ctx.args.command}`, ctx.rawArgs.slice(1), { 54 | nodeOptions: { stdio: 'inherit', cwd }, 55 | throwOnError: true, 56 | }) 57 | } 58 | catch (err) { 59 | // TODO: use windows err code as well 60 | if (err instanceof Error && 'code' in err && err.code === 'ENOENT') { 61 | return 62 | } 63 | } 64 | process.exit() 65 | } 66 | }, 67 | }) 68 | -------------------------------------------------------------------------------- /packages/nuxt-cli/src/run.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | import { runCommand as _runCommand, runMain as _runMain } from 'citty' 5 | 6 | import { commands } from '../../nuxi/src/commands' 7 | import { main } from './main' 8 | 9 | globalThis.__nuxt_cli__ = globalThis.__nuxt_cli__ || { 10 | // Programmatic usage fallback 11 | startTime: Date.now(), 12 | entry: fileURLToPath( 13 | new URL( 14 | import.meta.url.endsWith('.ts') 15 | ? '../bin/nuxi.mjs' 16 | : '../../bin/nuxi.mjs', 17 | import.meta.url, 18 | ), 19 | ), 20 | } 21 | 22 | export const runMain = () => _runMain(main) 23 | 24 | export async function runCommand( 25 | name: string, 26 | argv: string[] = process.argv.slice(2), 27 | data: { overrides?: Record } = {}, 28 | ) { 29 | argv.push('--no-clear') // Dev 30 | 31 | if (!(name in commands)) { 32 | throw new Error(`Invalid command ${name}`) 33 | } 34 | 35 | return await _runCommand(await commands[name as keyof typeof commands](), { 36 | rawArgs: argv, 37 | data: { 38 | overrides: data.overrides || {}, 39 | }, 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /packages/nuxt-cli/test/e2e/commands.spec.ts: -------------------------------------------------------------------------------- 1 | import type { TestFunction } from 'vitest' 2 | import type { commands } from '../../../nuxi/src/commands' 3 | 4 | import { existsSync } from 'node:fs' 5 | 6 | import { readdir, rm } from 'node:fs/promises' 7 | import { tmpdir } from 'node:os' 8 | import { join } from 'node:path' 9 | import { fileURLToPath } from 'node:url' 10 | import { isWindows } from 'std-env' 11 | import { x } from 'tinyexec' 12 | import { describe, expect, it } from 'vitest' 13 | 14 | const fixtureDir = fileURLToPath(new URL('../../playground', import.meta.url)) 15 | const nuxi = fileURLToPath(new URL('../../bin/nuxi.mjs', import.meta.url)) 16 | 17 | describe('commands', () => { 18 | const tests: Record> = { 19 | _dev: 'todo', 20 | add: async () => { 21 | const file = join(fixtureDir, 'server/api/test.ts') 22 | await rm(file, { force: true }) 23 | await x(nuxi, ['add', 'api', 'test'], { 24 | throwOnError: true, 25 | nodeOptions: { stdio: 'pipe', cwd: fixtureDir }, 26 | }) 27 | expect(existsSync(file)).toBeTruthy() 28 | await rm(file, { force: true }) 29 | }, 30 | analyze: 'todo', 31 | build: 'todo', 32 | cleanup: 'todo', 33 | devtools: 'todo', 34 | module: 'todo', 35 | prepare: 'todo', 36 | preview: 'todo', 37 | start: 'todo', 38 | test: 'todo', 39 | typecheck: 'todo', 40 | upgrade: 'todo', 41 | dev: 'todo', 42 | generate: 'todo', 43 | init: async () => { 44 | const dir = tmpdir() 45 | const pm = 'pnpm' 46 | const installPath = join(dir, pm) 47 | 48 | await rm(installPath, { recursive: true, force: true }) 49 | try { 50 | await x(nuxi, ['init', installPath, `--packageManager=${pm}`, '--gitInit=false', '--preferOffline', '--install=false'], { 51 | throwOnError: true, 52 | nodeOptions: { stdio: 'inherit', cwd: fixtureDir }, 53 | }) 54 | const files = await readdir(installPath).catch(() => []) 55 | expect(files).toContain('nuxt.config.ts') 56 | } 57 | finally { 58 | await rm(installPath, { recursive: true, force: true }) 59 | } 60 | }, 61 | info: 'todo', 62 | } 63 | 64 | it('throws error if no command is provided', async () => { 65 | const res = await x(nuxi, [], { 66 | nodeOptions: { stdio: 'pipe', cwd: fixtureDir }, 67 | }) 68 | expect(res.exitCode).toBe(1) 69 | expect(res.stderr).toBe('[error] No command specified.\n') 70 | }) 71 | 72 | // TODO: FIXME - windows currently throws 'nuxt-foo' is not recognized as an internal or external command, operable program or batch file. 73 | it.skipIf(isWindows)('throws error if wrong command is provided', async () => { 74 | const res = await x(nuxi, ['foo'], { 75 | nodeOptions: { stdio: 'pipe', cwd: fixtureDir }, 76 | }) 77 | expect(res.exitCode).toBe(1) 78 | expect(res.stderr).toBe('[error] Unknown command `foo`\n') 79 | }) 80 | 81 | const testsToRun = Object.entries(tests).filter(([_, value]) => value !== 'todo') 82 | it.each(testsToRun)(`%s`, (_, test) => (test as () => Promise)(), { timeout: isWindows ? 200000 : 50000 }) 83 | 84 | for (const [command, value] of Object.entries(tests)) { 85 | if (value === 'todo') { 86 | it.todo(command) 87 | } 88 | } 89 | }) 90 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - packages/nuxt-cli/playground 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>nuxt/renovate-config-nuxt"], 4 | "baseBranches": ["main"] 5 | } 6 | -------------------------------------------------------------------------------- /scripts/release.mjs: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import process from 'node:process' 3 | import { x } from 'tinyexec' 4 | 5 | const isNightly = process.env.RELEASE_TYPE === 'nightly' 6 | 7 | const dirs = ['create-nuxt', 'nuxi', 'nuxt-cli'] 8 | 9 | for (const dir of dirs) { 10 | if (isNightly) { 11 | await x('changelogen', ['--canary', 'nightly', '--publish'], { 12 | nodeOptions: { stdio: 'inherit', cwd: resolve('packages', dir) }, 13 | throwOnError: true, 14 | }) 15 | } 16 | else { 17 | await x('npm', ['publish'], { 18 | nodeOptions: { stdio: 'inherit', cwd: resolve('packages', dir) }, 19 | throwOnError: true, 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scripts/update-changelog.ts: -------------------------------------------------------------------------------- 1 | import type { ResolvedChangelogConfig } from 'changelogen' 2 | 3 | import { execSync } from 'node:child_process' 4 | import { promises as fsp } from 'node:fs' 5 | import { join, resolve } from 'node:path' 6 | import process from 'node:process' 7 | 8 | import { determineSemverChange, generateMarkDown, getCurrentGitBranch, getGitDiff, loadChangelogConfig, parseCommits } from 'changelogen' 9 | import { inc } from 'semver' 10 | 11 | const repo = `nuxt/cli` 12 | const corePackage = 'nuxi' 13 | const ignoredPackages = ['create-nuxt-app'] 14 | const user = { 15 | name: 'Daniel Roe', 16 | email: 'daniel@roe.dev', 17 | } 18 | 19 | async function main() { 20 | const releaseBranch = getCurrentGitBranch() 21 | const workspace = await loadWorkspace(process.cwd()) 22 | const config = await loadChangelogConfig(process.cwd(), {}) 23 | 24 | const commits = await getLatestCommits(config).then(commits => commits.filter(c => config.types[c.type] && !(c.type === 'chore' && c.scope === 'deps' && !c.isBreaking))) 25 | const bumpType = (await determineBumpType(config)) || 'patch' 26 | 27 | const newVersion = inc(workspace.find(corePackage).data.version, bumpType) 28 | const changelog = await generateMarkDown(commits, config) 29 | 30 | // Create and push a branch with bumped versions if it has not already been created 31 | const branchExists = execSync(`git ls-remote --heads origin v${newVersion}`).toString().trim().length > 0 32 | if (!branchExists) { 33 | for (const [key, value] of Object.entries(user)) { 34 | execSync(`git config --global user.${key} "${value}"`) 35 | execSync(`git config --global user.${key} "${value}"`) 36 | } 37 | execSync(`git checkout -b v${newVersion}`) 38 | 39 | for (const pkg of workspace.packages.filter(p => !p.data.private)) { 40 | workspace.setVersion(pkg.data.name, newVersion!) 41 | } 42 | await workspace.save() 43 | 44 | execSync(`git commit -am v${newVersion}`) 45 | execSync(`git push -u origin v${newVersion}`) 46 | } 47 | 48 | // Get the current PR for this release, if it exists 49 | const [currentPR] = await fetch(`https://api.github.com/repos/${repo}/pulls?head=nuxt:v${newVersion}`).then(r => r.json()) 50 | const contributors = await getContributors() 51 | 52 | const releaseNotes = [ 53 | currentPR?.body.replace(/## 👉 Changelog[\s\S]*$/, '') || `> ${newVersion} is the next ${bumpType} release.\n>\n> **Timetable**: to be announced.`, 54 | '## 👉 Changelog', 55 | changelog 56 | .replace(/^## v.*\n/, '') 57 | .replace(`...${releaseBranch}`, `...v${newVersion}`) 58 | .replace(/### ❤️ Contributors[\s\S]*$/, ''), 59 | '### ❤️ Contributors', 60 | contributors.map(c => `- ${c.name} (@${c.username})`).join('\n'), 61 | ].join('\n') 62 | 63 | // Create a PR with release notes if none exists 64 | if (!currentPR) { 65 | return await fetch(`https://api.github.com/repos/${repo}/pulls`, { 66 | method: 'POST', 67 | headers: { 68 | 'Authorization': `token ${process.env.GITHUB_TOKEN}`, 69 | 'content-type': 'application/json', 70 | }, 71 | body: JSON.stringify({ 72 | title: `v${newVersion}`, 73 | head: `v${newVersion}`, 74 | base: releaseBranch, 75 | body: releaseNotes, 76 | draft: true, 77 | }), 78 | }) 79 | } 80 | 81 | // Update release notes if the pull request does exist 82 | await fetch(`https://api.github.com/repos/${repo}/pulls/${currentPR.number}`, { 83 | method: 'PATCH', 84 | headers: { 85 | 'Authorization': `token ${process.env.GITHUB_TOKEN}`, 86 | 'content-type': 'application/json', 87 | }, 88 | body: JSON.stringify({ 89 | body: releaseNotes, 90 | }), 91 | }) 92 | } 93 | 94 | main().catch((err) => { 95 | console.error(err) 96 | process.exit(1) 97 | }) 98 | 99 | export interface Dep { 100 | name: string 101 | range: string 102 | type: string 103 | } 104 | 105 | type ThenArg = T extends PromiseLike ? U : T 106 | export type Package = ThenArg> 107 | 108 | export async function loadPackage(dir: string) { 109 | const pkgPath = resolve(dir, 'package.json') 110 | const data = JSON.parse(await fsp.readFile(pkgPath, 'utf-8').catch(() => '{}')) 111 | const save = () => fsp.writeFile(pkgPath, `${JSON.stringify(data, null, 2)}\n`) 112 | 113 | const updateDeps = (reviver: (dep: Dep) => Dep | void) => { 114 | for (const type of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) { 115 | if (!data[type]) { 116 | continue 117 | } 118 | for (const e of Object.entries(data[type])) { 119 | const dep: Dep = { name: e[0], range: e[1] as string, type } 120 | delete data[type][dep.name] 121 | const updated = reviver(dep) || dep 122 | data[updated.type] = data[updated.type] || {} 123 | data[updated.type][updated.name] = updated.range 124 | } 125 | } 126 | } 127 | 128 | return { 129 | dir, 130 | data, 131 | save, 132 | updateDeps, 133 | } 134 | } 135 | 136 | export async function loadWorkspace(dir: string) { 137 | const workspacePkg = await loadPackage(dir) 138 | 139 | const packages: Package[] = [] 140 | 141 | for await (const pkgDir of fsp.glob(['packages/*'], { withFileTypes: true })) { 142 | if (!pkgDir.isDirectory()) { 143 | continue 144 | } 145 | const pkg = await loadPackage(join(pkgDir.parentPath, pkgDir.name)) 146 | if (!pkg.data.name || ignoredPackages.includes(pkg.data.name)) { 147 | continue 148 | } 149 | console.log(pkg.data.name) 150 | packages.push(pkg) 151 | } 152 | 153 | const find = (name: string) => { 154 | const pkg = packages.find(pkg => pkg.data.name === name) 155 | if (!pkg) { 156 | throw new Error(`Workspace package not found: ${name}`) 157 | } 158 | return pkg 159 | } 160 | 161 | const rename = (from: string, to: string) => { 162 | find(from).data._name = find(from).data.name 163 | find(from).data.name = to 164 | for (const pkg of packages) { 165 | pkg.updateDeps((dep) => { 166 | if (dep.name === from && !dep.range.startsWith('npm:')) { 167 | dep.range = `npm:${to}@${dep.range}` 168 | } 169 | }) 170 | } 171 | } 172 | 173 | const setVersion = (name: string, newVersion: string, opts: { updateDeps?: boolean } = {}) => { 174 | find(name).data.version = newVersion 175 | if (!opts.updateDeps) { 176 | return 177 | } 178 | 179 | for (const pkg of packages) { 180 | pkg.updateDeps((dep) => { 181 | if (dep.name === name) { 182 | dep.range = newVersion 183 | } 184 | }) 185 | } 186 | } 187 | 188 | const save = () => Promise.all(packages.map(pkg => pkg.save())) 189 | 190 | return { 191 | dir, 192 | workspacePkg, 193 | packages, 194 | save, 195 | find, 196 | rename, 197 | setVersion, 198 | } 199 | } 200 | 201 | export async function determineBumpType(config: ResolvedChangelogConfig) { 202 | const commits = await getLatestCommits(config) 203 | 204 | const bumpType = determineSemverChange(commits, config) 205 | 206 | return bumpType === 'major' ? 'minor' : bumpType 207 | } 208 | 209 | export async function getLatestCommits(config: ResolvedChangelogConfig) { 210 | const latestTag = execSync('git describe --tags --abbrev=0').toString().trim() 211 | 212 | return parseCommits(await getGitDiff(latestTag), config) 213 | } 214 | 215 | export async function getContributors() { 216 | const contributors = [] as Array<{ name: string, username: string }> 217 | const emails = new Set() 218 | const latestTag = execSync('git describe --tags --abbrev=0').toString().trim() 219 | const rawCommits = await getGitDiff(latestTag) 220 | for (const commit of rawCommits) { 221 | if (emails.has(commit.author.email) || commit.author.name === 'renovate[bot]') { 222 | continue 223 | } 224 | const { author } = await fetch(`https://api.github.com/repos/${repo}/commits/${commit.shortHash}`, { 225 | headers: { 226 | 'User-Agent': `${repo} github action automation`, 227 | 'Accept': 'application/vnd.github.v3+json', 228 | 'Authorization': `token ${process.env.GITHUB_TOKEN}`, 229 | }, 230 | }).then(r => r.json() as Promise<{ author: { login: string, email: string } }>) 231 | if (!contributors.some(c => c.username === author.login)) { 232 | contributors.push({ name: commit.author.name, username: author.login }) 233 | } 234 | emails.add(author.email) 235 | } 236 | return contributors 237 | } 238 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "resolveJsonModule": true, 7 | "allowJs": true, 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "noUncheckedIndexedAccess": true, 11 | "noUnusedLocals": true, 12 | "noEmit": true, 13 | "allowSyntheticDefaultImports": true, 14 | "esModuleInterop": false, 15 | "skipLibCheck": true 16 | }, 17 | "exclude": [ 18 | "packages/nuxt-cli/playground/**/*" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | 3 | declare global { 4 | // eslint-disable-next-line vars-on-top 5 | var __nuxt_cli__: 6 | | undefined 7 | | { 8 | entry: string 9 | startTime: number 10 | } 11 | } 12 | 13 | export {} 14 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: {}, 6 | }, 7 | }) 8 | --------------------------------------------------------------------------------