├── CODEOWNERS ├── playground ├── some-layer │ ├── nuxt.config.ts │ └── pages │ │ └── extended.vue ├── certs │ ├── pfx.dummy │ ├── cert.dummy │ └── key.dummy ├── .gitignore ├── app │ └── pages │ │ ├── index.vue │ │ └── ws.vue ├── nuxt.config.ts ├── server │ ├── api │ │ ├── hello.ts │ │ └── echo.ts │ └── routes │ │ └── _ws.ts ├── tsconfig.json ├── test │ └── e2e │ │ ├── build.spec.ts │ │ └── dev.spec.ts └── package.json ├── packages ├── nuxt-cli │ ├── src │ │ ├── dev │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── run.ts │ │ └── main.ts │ ├── test │ │ ├── fixtures │ │ │ ├── dev │ │ │ │ ├── package.json │ │ │ │ └── server │ │ │ │ │ └── api │ │ │ │ │ └── test-cookies.ts │ │ │ └── log-dev-server-options.ts │ │ ├── utils │ │ │ └── index.ts │ │ ├── bench │ │ │ └── dev.bench.ts │ │ └── e2e │ │ │ └── commands.spec.ts │ ├── tsdown.config.ts │ ├── bin │ │ └── nuxi.mjs │ └── package.json ├── create-nuxt-app │ ├── bin │ │ └── create-nuxt-app.mjs │ └── package.json ├── nuxi │ ├── src │ │ ├── index.ts │ │ ├── utils │ │ │ ├── logger.ts │ │ │ ├── packageManagers.ts │ │ │ ├── templates │ │ │ │ ├── app-config.ts │ │ │ │ ├── layer.ts │ │ │ │ ├── server-plugin.ts │ │ │ │ ├── server-middleware.ts │ │ │ │ ├── server-route.ts │ │ │ │ ├── server-util.ts │ │ │ │ ├── plugin.ts │ │ │ │ ├── middleware.ts │ │ │ │ ├── page.ts │ │ │ │ ├── module.ts │ │ │ │ ├── layout.ts │ │ │ │ ├── composable.ts │ │ │ │ ├── component.ts │ │ │ │ ├── error.ts │ │ │ │ ├── api.ts │ │ │ │ ├── app.ts │ │ │ │ └── index.ts │ │ │ ├── paths.ts │ │ │ ├── env.ts │ │ │ ├── ascii.ts │ │ │ ├── engines.ts │ │ │ ├── fs.ts │ │ │ ├── versions.ts │ │ │ ├── kit.ts │ │ │ ├── starter-templates.ts │ │ │ ├── console.ts │ │ │ ├── banner.ts │ │ │ ├── nuxt.ts │ │ │ └── formatting.ts │ │ ├── commands │ │ │ ├── module │ │ │ │ ├── index.ts │ │ │ │ ├── _utils.ts │ │ │ │ └── search.ts │ │ │ ├── _utils.ts │ │ │ ├── generate.ts │ │ │ ├── cleanup.ts │ │ │ ├── index.ts │ │ │ ├── dev-child.ts │ │ │ ├── devtools.ts │ │ │ ├── prepare.ts │ │ │ ├── test.ts │ │ │ ├── _shared.ts │ │ │ ├── typecheck.ts │ │ │ ├── build.ts │ │ │ ├── add.ts │ │ │ ├── analyze.ts │ │ │ ├── preview.ts │ │ │ ├── dev.ts │ │ │ ├── info.ts │ │ │ └── upgrade.ts │ │ ├── dev │ │ │ ├── error.ts │ │ │ ├── index.ts │ │ │ └── pool.ts │ │ ├── completions-init.ts │ │ ├── run.ts │ │ ├── completions.ts │ │ └── main.ts │ ├── test │ │ └── unit │ │ │ ├── templates.spec.ts │ │ │ ├── commands │ │ │ ├── _utils.test.ts │ │ │ └── module │ │ │ │ ├── _utils.test.ts │ │ │ │ └── add.spec.ts │ │ │ └── file-watcher.spec.ts │ ├── tsdown.config.ts │ ├── bin │ │ └── nuxi.mjs │ └── package.json └── create-nuxt │ ├── src │ ├── index.ts │ ├── run.ts │ └── main.ts │ ├── bin │ └── create-nuxt.mjs │ ├── tsdown.config.ts │ ├── package.json │ └── test │ └── init.spec.ts ├── renovate.json ├── pnpm-workspace.yaml ├── types.d.ts ├── .gitignore ├── vitest.config.ts ├── tsconfig.json ├── .github └── workflows │ ├── provenance.yml │ ├── autofix.yml │ ├── bench.yml │ ├── release.yml │ ├── changelog.yml │ ├── size.yml │ ├── size-comment.yml │ └── ci.yml ├── scripts ├── release.mjs ├── generate-data.ts ├── generate-completions-data.ts ├── parse-sizes.ts └── update-changelog.ts ├── .devcontainer └── devcontainer.json ├── eslint.config.js ├── LICENSE ├── README.md ├── knip.json └── package.json /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @danielroe 2 | -------------------------------------------------------------------------------- /playground/some-layer/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({}) 2 | -------------------------------------------------------------------------------- /packages/nuxt-cli/src/dev/index.ts: -------------------------------------------------------------------------------- 1 | export { initialize } from '../../../nuxi/src/dev' 2 | -------------------------------------------------------------------------------- /playground/certs/pfx.dummy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxt/cli/HEAD/playground/certs/pfx.dummy -------------------------------------------------------------------------------- /packages/create-nuxt-app/bin/create-nuxt-app.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'create-nuxt/cli' 3 | -------------------------------------------------------------------------------- /packages/nuxi/src/index.ts: -------------------------------------------------------------------------------- 1 | export { main, runMain } from './main' 2 | export { runCommand } from './run' 3 | -------------------------------------------------------------------------------- /packages/nuxt-cli/src/index.ts: -------------------------------------------------------------------------------- 1 | export { main } from './main' 2 | export { runCommand, runMain } from './run' 3 | -------------------------------------------------------------------------------- /packages/create-nuxt/src/index.ts: -------------------------------------------------------------------------------- 1 | export { main } from './main' 2 | export { runCommand, runMain } from './run' 3 | -------------------------------------------------------------------------------- /playground/some-layer/pages/extended.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | This is an extended page from a layer. 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>nuxt/renovate-config-nuxt"], 4 | "baseBranches": ["main"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { log } from '@clack/prompts' 2 | import createDebug from 'debug' 3 | 4 | export const logger = log 5 | export const debug = createDebug('nuxi') 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /playground/app/pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome to the Nuxt CLI playground! 6 | 7 | 8 | /ws 9 | 10 | 11 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - packages/nuxt-cli/test/fixtures/* 4 | - playground 5 | 6 | ignoredBuiltDependencies: 7 | - '@parcel/watcher' 8 | - esbuild 9 | - unrs-resolver 10 | 11 | verifyDepsBeforeRun: install 12 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | // eslint-disable-next-line vars-on-top 3 | var __nuxt_cli__: 4 | | undefined 5 | | { 6 | entry: string 7 | devEntry?: string 8 | startTime: number 9 | } 10 | } 11 | 12 | export {} 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /playground/server/api/hello.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(async (event) => { 2 | return { 3 | message: 'Hello from API!', 4 | timestamp: new Date().toISOString(), 5 | method: event.method, 6 | url: getRequestURL(event).pathname, 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /packages/nuxt-cli/test/fixtures/dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fixtures-dev", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev:prepare": "nuxt prepare", 7 | "test": "vitest" 8 | }, 9 | "dependencies": { 10 | "nuxt": "^4.0.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vscode 4 | .idea 5 | .nuxt 6 | nuxt-app 7 | .pnpm-store 8 | coverage 9 | stats.json 10 | playground-bun* 11 | playground-deno* 12 | playground-node* 13 | packages/nuxi/src/utils/completions-data.ts 14 | packages/nuxi/src/data/nitro-presets.ts 15 | packages/nuxi/src/data/templates.ts 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /playground/server/api/echo.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(async (event) => { 2 | const body = await readBody(event).catch(() => ({})) 3 | 4 | return { 5 | message: 'Echo API endpoint', 6 | echoed: body, 7 | headers: getRequestHeaders(event), 8 | method: event.method, 9 | timestamp: new Date().toISOString(), 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "./.nuxt/tsconfig.app.json" 5 | }, 6 | { 7 | "path": "./.nuxt/tsconfig.server.json" 8 | }, 9 | { 10 | "path": "./.nuxt/tsconfig.shared.json" 11 | }, 12 | { 13 | "path": "./.nuxt/tsconfig.node.json" 14 | } 15 | ], 16 | "files": [] 17 | } 18 | -------------------------------------------------------------------------------- /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.rootDir, `layers/${name}/nuxt.config.ts`), 7 | contents: ` 8 | export default defineNuxtConfig({}) 9 | `, 10 | } 11 | } 12 | 13 | export { layer } 14 | -------------------------------------------------------------------------------- /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/utils/paths.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { relative } from 'pathe' 3 | 4 | const cwd = process.cwd() 5 | 6 | export function relativeToProcess(path: string) { 7 | return relative(cwd, path) || path 8 | } 9 | 10 | export function withNodePath(path: string) { 11 | return [path, process.env.NODE_PATH].filter((i): i is NonNullable => !!i) 12 | } 13 | -------------------------------------------------------------------------------- /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-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-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/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/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/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 | 10 | 11 | Page: ${name} 12 | 13 | 14 | 15 | 16 | `, 17 | }) 18 | 19 | export { page } 20 | -------------------------------------------------------------------------------- /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/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 | 10 | 11 | Layout: ${name} 12 | 13 | 14 | 15 | 16 | 17 | `, 18 | }) 19 | 20 | export { layout } 21 | -------------------------------------------------------------------------------- /playground/test/e2e/build.spec.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { $fetch, setup } from '@nuxt/test-utils' 3 | import { describe, expect, it } from 'vitest' 4 | 5 | await setup({ 6 | rootDir: fileURLToPath(new URL('../..', import.meta.url)), 7 | }) 8 | 9 | describe('built server', () => { 10 | it('should start and return HTML', async () => { 11 | const html = await $fetch('/') 12 | 13 | expect(html).toContain('Welcome to the Nuxt CLI playground') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import type { ViteUserConfig } from 'vitest/config' 2 | import codspeed from '@codspeed/vitest-plugin' 3 | import { isCI, isWindows } from 'std-env' 4 | import { defaultExclude, defineConfig } from 'vitest/config' 5 | 6 | export default defineConfig({ 7 | plugins: isCI && !isWindows ? [codspeed()] : [], 8 | test: { 9 | coverage: {}, 10 | exclude: [ 11 | ...defaultExclude, 12 | 'playground/**', 13 | ], 14 | }, 15 | }) satisfies ViteUserConfig as ViteUserConfig 16 | -------------------------------------------------------------------------------- /playground/test/e2e/dev.spec.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { $fetch, setup } from '@nuxt/test-utils' 3 | import { describe, expect, it } from 'vitest' 4 | 5 | await setup({ 6 | rootDir: fileURLToPath(new URL('../..', import.meta.url)), 7 | dev: true, 8 | }) 9 | 10 | describe('dev server', () => { 11 | it('should start and return HTML', async () => { 12 | const html = await $fetch('/') 13 | 14 | expect(html).toContain('Welcome to the Nuxt CLI playground') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /playground/server/routes/_ws.ts: -------------------------------------------------------------------------------- 1 | export default defineWebSocketHandler({ 2 | open(peer) { 3 | console.log('[ws] open', peer?.id) 4 | }, 5 | 6 | message(peer, message) { 7 | console.log('[ws] message', peer?.id, message) 8 | if (message.text().includes('ping')) { 9 | peer.send('pong') 10 | } 11 | }, 12 | 13 | close(peer, event) { 14 | console.log('[ws] close', peer?.id, event) 15 | }, 16 | 17 | error(peer, error) { 18 | console.log('[ws] error', peer?.id, error) 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/env.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { colors } from 'consola/utils' 3 | 4 | import { logger } from './logger' 5 | 6 | export function overrideEnv(targetEnv: string) { 7 | const currentEnv = process.env.NODE_ENV 8 | if (currentEnv && currentEnv !== targetEnv) { 9 | logger.warn( 10 | `Changing ${colors.cyan('NODE_ENV')} from ${colors.cyan(currentEnv)} to ${colors.cyan(targetEnv)}, to avoid unintended behavior.`, 11 | ) 12 | } 13 | 14 | process.env.NODE_ENV = targetEnv 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "resolveJsonModule": true, 7 | "allowImportingTsExtensions": true, 8 | "allowJs": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "noUncheckedIndexedAccess": true, 12 | "noUnusedLocals": true, 13 | "noEmit": true, 14 | "allowSyntheticDefaultImports": true, 15 | "skipLibCheck": true 16 | }, 17 | "exclude": [ 18 | "playground/**/*", 19 | "**/dist/**" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/provenance.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | permissions: 11 | contents: read 12 | jobs: 13 | check-provenance: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v6 17 | with: 18 | fetch-depth: 0 19 | - name: Check provenance downgrades 20 | uses: danielroe/provenance-action@41bcc969e579d9e29af08ba44fcbfdf95cee6e6c # v0.1.1 21 | with: 22 | fail-on-provenance-change: true 23 | -------------------------------------------------------------------------------- /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/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/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 | 15 | 16 | Component: ${name} 17 | 18 | 19 | 20 | 21 | `, 22 | }) 23 | 24 | export { component } 25 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-cli-playground", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxt dev", 7 | "dev:https:certs": "nuxt dev --https --https.cert=certs/cert.dummy --https.key=certs/key.dummy", 8 | "dev:https:pfx": "nuxt dev --https --https.pfx=certs/pfx.dummy --https.passphrase=pass", 9 | "dev:prepare": "nuxt prepare", 10 | "test": "vitest" 11 | }, 12 | "dependencies": { 13 | "nuxt": "^4.2.2", 14 | "vue-router": "^4.6.4" 15 | }, 16 | "devDependencies": { 17 | "@nuxt/test-utils": "^3.21.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /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/index' 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/test/fixtures/log-dev-server-options.ts: -------------------------------------------------------------------------------- 1 | import { mkdir, writeFile } from 'node:fs/promises' 2 | import { join } from 'node:path' 3 | import { defineNuxtModule, useNuxt } from '@nuxt/kit' 4 | 5 | export default defineNuxtModule({ 6 | meta: { 7 | name: 'nuxt-cli-test-module', 8 | }, 9 | setup() { 10 | const nuxt = useNuxt() 11 | 12 | nuxt.hook('build:before', async () => { 13 | await mkdir('.nuxt', { recursive: true }) 14 | await writeFile(join(nuxt.options.rootDir, '.nuxt/dev-server.json'), JSON.stringify(nuxt.options.devServer)) 15 | await nuxt.close() 16 | }) 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /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 | 16 | 17 | {{ error.statusCode }} 18 | Go back home 19 | 20 | 21 | 22 | 23 | `, 24 | }) 25 | 26 | export { error } 27 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/_utils.ts: -------------------------------------------------------------------------------- 1 | import type { commands } from '.' 2 | 3 | // Inlined list of nuxi commands to avoid including `commands` in bundle if possible 4 | export const nuxiCommands = [ 5 | 'add', 6 | 'analyze', 7 | 'build', 8 | 'cleanup', 9 | '_dev', 10 | 'dev', 11 | 'devtools', 12 | 'generate', 13 | 'info', 14 | 'init', 15 | 'module', 16 | 'prepare', 17 | 'preview', 18 | 'start', 19 | 'test', 20 | 'typecheck', 21 | 'upgrade', 22 | ] as const satisfies (keyof typeof commands)[] 23 | 24 | export function isNuxiCommand(command: string) { 25 | return (nuxiCommands as string[]).includes(command) 26 | } 27 | -------------------------------------------------------------------------------- /packages/nuxt-cli/test/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { isCI } from 'std-env' 2 | 3 | export async function fetchWithPolling(url: string, options: RequestInit = {}, maxAttempts = 10, interval = 100): Promise { 4 | let response: Response | null = null 5 | let attempts = 0 6 | while (attempts < maxAttempts) { 7 | try { 8 | response = await fetch(url, options) 9 | if (response.ok) { 10 | return response 11 | } 12 | } 13 | catch { 14 | // Ignore errors and retry 15 | } 16 | attempts++ 17 | await new Promise(resolve => setTimeout(resolve, isCI ? interval * 10 : interval)) 18 | } 19 | return response 20 | } 21 | -------------------------------------------------------------------------------- /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.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 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ` 20 | : ` 21 | 22 | 23 | 24 | 25 | Hello World! 26 | 27 | 28 | 29 | 30 | `, 31 | }) 32 | 33 | export { app } 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // https://code.visualstudio.com/docs/devcontainers/containers 2 | // https://containers.dev/implementors/json_reference/ 3 | { 4 | "image": "node:22", 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", 18 | "mounts": [ 19 | "type=volume,target=${containerWorkspaceFolder}/node_modules", 20 | "type=volume,target=${containerWorkspaceFolder}/dist" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/create-nuxt/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from 'tsdown' 2 | import process from 'node:process' 3 | import { visualizer } from 'rollup-plugin-visualizer' 4 | import { defineConfig } from 'tsdown' 5 | import { purgePolyfills } from 'unplugin-purge-polyfills' 6 | 7 | const isAnalysingSize = process.env.BUNDLE_SIZE === 'true' 8 | 9 | export default defineConfig({ 10 | entry: ['src/index.ts'], 11 | fixedExtension: true, 12 | dts: !isAnalysingSize && { 13 | oxc: true, 14 | }, 15 | failOnWarn: !isAnalysingSize, 16 | plugins: [ 17 | purgePolyfills.rolldown({ logLevel: 'verbose' }), 18 | ...(isAnalysingSize ? [visualizer({ template: 'raw-data' })] : []), 19 | ], 20 | }) satisfies UserConfig as UserConfig 21 | -------------------------------------------------------------------------------- /packages/nuxi/src/dev/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 | if (res.headersSent) { 6 | if (!res.writableEnded) { 7 | res.end() 8 | } 9 | return 10 | } 11 | 12 | const youch = new Youch() 13 | res.statusCode = 500 14 | res.setHeader('Content-Type', 'text/html') 15 | res.setHeader('Cache-Control', 'no-store') 16 | res.setHeader('Refresh', '3') 17 | 18 | const html = await youch.toHTML(error, { 19 | request: { 20 | url: req.url, 21 | method: req.method, 22 | headers: req.headers, 23 | }, 24 | }) 25 | res.end(html) 26 | } 27 | -------------------------------------------------------------------------------- /packages/nuxt-cli/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from 'tsdown' 2 | import process from 'node:process' 3 | import { visualizer } from 'rollup-plugin-visualizer' 4 | import { defineConfig } from 'tsdown' 5 | import { purgePolyfills } from 'unplugin-purge-polyfills' 6 | 7 | const isAnalysingSize = process.env.BUNDLE_SIZE === 'true' 8 | 9 | export default defineConfig({ 10 | entry: ['src/index.ts', 'src/dev/index.ts'], 11 | fixedExtension: true, 12 | dts: !isAnalysingSize && { 13 | oxc: true, 14 | }, 15 | failOnWarn: !isAnalysingSize, 16 | plugins: [ 17 | purgePolyfills.rolldown({ logLevel: 'verbose' }), 18 | ...(isAnalysingSize ? [visualizer({ template: 'raw-data' })] : []), 19 | ], 20 | }) satisfies UserConfig as UserConfig 21 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/engines.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { colors } from 'consola/utils' 3 | 4 | import { logger } from './logger' 5 | 6 | export async function checkEngines() { 7 | const satisfies = await import('semver/functions/satisfies.js').then( 8 | r => 9 | r.default || (r as any as typeof import('semver/functions/satisfies.js')), 10 | ) // npm/node-semver#381 11 | const currentNode = process.versions.node 12 | const nodeRange = '>= 18.0.0' 13 | 14 | if (!satisfies(currentNode, nodeRange)) { 15 | logger.warn( 16 | `Current version of Node.js (${colors.cyan(currentNode)}) is unsupported and might cause issues.\n Please upgrade to a compatible version ${colors.cyan(nodeRange)}.`, 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/nuxi/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from 'tsdown' 2 | import process from 'node:process' 3 | import { visualizer } from 'rollup-plugin-visualizer' 4 | import { defineConfig } from 'tsdown' 5 | import { purgePolyfills } from 'unplugin-purge-polyfills' 6 | 7 | const isAnalysingSize = process.env.BUNDLE_SIZE === 'true' 8 | 9 | export default defineConfig({ 10 | entry: ['src/index.ts', 'src/dev/index.ts'], 11 | shims: true, 12 | fixedExtension: true, 13 | dts: !isAnalysingSize && { 14 | oxc: true, 15 | }, 16 | failOnWarn: !isAnalysingSize, 17 | plugins: [ 18 | purgePolyfills.rolldown({ logLevel: 'verbose' }), 19 | ...(isAnalysingSize ? [visualizer({ template: 'raw-data' })] : []), 20 | ], 21 | }) satisfies UserConfig as UserConfig 22 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/generate.ts: -------------------------------------------------------------------------------- 1 | import { defineCommand } from 'citty' 2 | 3 | import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, 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 | ...extendsArgs, 21 | ...legacyRootDirArgs, 22 | }, 23 | async run(ctx) { 24 | ctx.args.prerender = true 25 | await buildCommand.run!( 26 | // @ts-expect-error types do not match 27 | ctx, 28 | ) 29 | }, 30 | }) 31 | -------------------------------------------------------------------------------- /packages/nuxi/test/unit/commands/_utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { isNuxiCommand, nuxiCommands } from '../../../src/commands/_utils' 3 | 4 | describe('isNuxiCommand', () => { 5 | it('should return true for valid nuxi commands', () => { 6 | nuxiCommands.forEach((command) => { 7 | expect(isNuxiCommand(command)).toBe(true) 8 | }) 9 | }) 10 | 11 | it('should return false for invalid nuxi commands', () => { 12 | const invalidCommands = [ 13 | '', 14 | ' ', 15 | 'devv', 16 | 'Dev', 17 | 'BuilD', 18 | 'random', 19 | 'nuxi', 20 | 'install', 21 | undefined, 22 | null, 23 | ] 24 | 25 | invalidCommands.forEach((command) => { 26 | expect(isNuxiCommand(command as string)).toBe(false) 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/nuxt-cli/test/fixtures/dev/server/api/test-cookies.ts: -------------------------------------------------------------------------------- 1 | import { defineEventHandler, setCookie } from 'h3' 2 | 3 | export default defineEventHandler(async (event) => { 4 | const expiryDate = new Date(Date.now() + 7200000) // 2 hours 5 | 6 | setCookie(event, 'XSRF-TOKEN', 'eyJpdiI6IlpDZ2JlTzdIY', { 7 | expires: expiryDate, 8 | path: '/', 9 | domain: 'localhost', 10 | secure: false, 11 | sameSite: 'lax', 12 | }) 13 | 14 | setCookie(event, 'app-session', 'eyJpdiI6InpGNmxwR0t', { 15 | expires: expiryDate, 16 | path: '/', 17 | domain: 'localhost', 18 | httpOnly: true, 19 | secure: false, 20 | sameSite: 'lax', 21 | }) 22 | 23 | setCookie(event, 'user-pref', 'dark-mode', { 24 | expires: expiryDate, 25 | path: '/', 26 | }) 27 | 28 | return { ok: true, cookies: 3 } 29 | }) 30 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yml: -------------------------------------------------------------------------------- 1 | name: autofix.ci # needed to securely identify the workflow 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | autofix: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 14 | with: 15 | fetch-depth: 0 16 | - run: corepack enable 17 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 18 | with: 19 | node-version: lts/* 20 | cache: pnpm 21 | 22 | - name: 📦 Install dependencies 23 | run: pnpm install 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - name: 🔠 Lint project (+ fix) 28 | run: pnpm run lint:fix 29 | 30 | - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 31 | -------------------------------------------------------------------------------- /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 { logger } from '../utils/logger' 6 | import { cleanupNuxtDirs } from '../utils/nuxt' 7 | import { cwdArgs, legacyRootDirArgs } from './_shared' 8 | 9 | export default defineCommand({ 10 | meta: { 11 | name: 'cleanup', 12 | description: 'Clean up generated Nuxt files and caches', 13 | }, 14 | args: { 15 | ...cwdArgs, 16 | ...legacyRootDirArgs, 17 | }, 18 | async run(ctx) { 19 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 20 | const { loadNuxtConfig } = await loadKit(cwd) 21 | const nuxtOptions = await loadNuxtConfig({ cwd, overrides: { dev: true } }) 22 | await cleanupNuxtDirs(nuxtOptions.rootDir, nuxtOptions.buildDir) 23 | 24 | logger.success('Cleanup complete!') 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /packages/nuxi/src/completions-init.ts: -------------------------------------------------------------------------------- 1 | import type { ArgsDef, CommandDef } from 'citty' 2 | import tab from '@bomb.sh/tab/citty' 3 | import { templates } from './data/templates' 4 | 5 | export async function setupInitCompletions(command: CommandDef) { 6 | const completion = await tab(command) 7 | 8 | const templateOption = completion.options?.get('template') 9 | if (templateOption) { 10 | templateOption.handler = (complete) => { 11 | for (const template in templates) { 12 | complete(template, templates[template as 'content']?.description || '') 13 | } 14 | } 15 | } 16 | 17 | const logLevelOption = completion.options?.get('logLevel') 18 | if (logLevelOption) { 19 | logLevelOption.handler = (complete) => { 20 | complete('silent', 'No logs') 21 | complete('info', 'Standard logging') 22 | complete('verbose', 'Detailed logging') 23 | } 24 | } 25 | 26 | return completion 27 | } 28 | -------------------------------------------------------------------------------- /packages/nuxi/bin/nuxi.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import nodeModule from 'node:module' 4 | import process from 'node:process' 5 | import { fileURLToPath } from 'node:url' 6 | 7 | // https://nodejs.org/api/module.html#moduleenablecompilecachecachedir 8 | // https://github.com/nodejs/node/pull/54501 9 | if (nodeModule.enableCompileCache && !process.env.NODE_DISABLE_COMPILE_CACHE) { 10 | try { 11 | const { directory } = nodeModule.enableCompileCache() 12 | if (directory) { 13 | // allow child process to share the same cache directory 14 | process.env.NODE_COMPILE_CACHE ||= directory 15 | } 16 | } 17 | catch { 18 | // Ignore errors 19 | } 20 | } 21 | 22 | globalThis.__nuxt_cli__ = { 23 | startTime: Date.now(), 24 | entry: fileURLToPath(import.meta.url), 25 | devEntry: fileURLToPath(new URL('../dist/dev/index.mjs', import.meta.url)), 26 | } 27 | 28 | // eslint-disable-next-line antfu/no-top-level-await 29 | const { runMain } = await import('../dist/index.mjs') 30 | 31 | runMain() 32 | -------------------------------------------------------------------------------- /packages/nuxt-cli/bin/nuxi.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import nodeModule from 'node:module' 4 | import process from 'node:process' 5 | import { fileURLToPath } from 'node:url' 6 | 7 | // https://nodejs.org/api/module.html#moduleenablecompilecachecachedir 8 | // https://github.com/nodejs/node/pull/54501 9 | if (nodeModule.enableCompileCache && !process.env.NODE_DISABLE_COMPILE_CACHE) { 10 | try { 11 | const { directory } = nodeModule.enableCompileCache() 12 | if (directory) { 13 | // allow child process to share the same cache directory 14 | process.env.NODE_COMPILE_CACHE ||= directory 15 | } 16 | } 17 | catch { 18 | // Ignore errors 19 | } 20 | } 21 | 22 | globalThis.__nuxt_cli__ = { 23 | startTime: Date.now(), 24 | entry: fileURLToPath(import.meta.url), 25 | devEntry: fileURLToPath(new URL('../dist/dev/index.mjs', import.meta.url)), 26 | } 27 | 28 | // eslint-disable-next-line antfu/no-top-level-await 29 | const { runMain } = await import('../dist/index.mjs') 30 | 31 | runMain() 32 | -------------------------------------------------------------------------------- /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 = (): Promise => _runMain(main) 23 | 24 | export async function runCommand( 25 | name: 'init', 26 | argv: string[] = process.argv.slice(2), 27 | data: { overrides?: Record } = {}, 28 | ): Promise<{ result: unknown }> { 29 | return await _runCommand(init, { 30 | rawArgs: argv, 31 | data: { 32 | overrides: data.overrides || {}, 33 | }, 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/bench.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | permissions: {} 13 | 14 | jobs: 15 | benchmark: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 19 | with: 20 | fetch-depth: 0 21 | - run: corepack enable 22 | - uses: actions/setup-node@v6 23 | with: 24 | node-version: lts/* 25 | cache: pnpm 26 | 27 | - name: 📦 Install dependencies 28 | run: pnpm install 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: 🛠 Build project 33 | run: pnpm build 34 | 35 | - name: Run benchmarks 36 | uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1 37 | with: 38 | run: pnpm vitest bench 39 | mode: instrumentation 40 | token: ${{ secrets.CODSPEED_TOKEN }} 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | # Remove default permissions of GITHUB_TOKEN for security 9 | # https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs 10 | permissions: {} 11 | 12 | jobs: 13 | release: 14 | concurrency: 15 | group: release 16 | permissions: 17 | contents: write 18 | id-token: write 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 20 21 | steps: 22 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 23 | with: 24 | fetch-depth: 0 25 | - run: corepack enable 26 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 27 | with: 28 | node-version: latest 29 | 30 | - name: 📦 Install dependencies 31 | run: pnpm install 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: 🛠 Build project 36 | run: pnpm build 37 | 38 | - name: 📦 Release 39 | run: node ./scripts/release.mjs 40 | -------------------------------------------------------------------------------- /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 | './playground', 14 | ], 15 | }, 16 | }, await antfu()).append( 17 | { 18 | ignores: ['packages/nuxi/src/data/**'], 19 | }, 20 | { 21 | rules: { 22 | 'vue/singleline-html-element-content-newline': 'off', 23 | // TODO: remove usage of `any` throughout codebase 24 | '@typescript-eslint/no-explicit-any': 'off', 25 | 'style/indent-binary-ops': 'off', 26 | 'pnpm/json-valid-catalog': 'off', 27 | 'pnpm/json-enforce-catalog': 'off', 28 | 'pnpm/yaml-enforce-settings': 'off', 29 | }, 30 | }, 31 | { 32 | files: ['playground/**'], 33 | rules: { 34 | 'no-console': 'off', 35 | }, 36 | }, 37 | { 38 | files: ['**/*.yml'], 39 | rules: { 40 | '@stylistic/spaced-comment': 'off', 41 | }, 42 | }, 43 | ) 44 | -------------------------------------------------------------------------------- /packages/create-nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-nuxt", 3 | "type": "module", 4 | "version": "3.31.3", 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 | "build": "tsdown", 29 | "prepack": "tsdown" 30 | }, 31 | "dependencies": { 32 | "citty": "^0.1.6" 33 | }, 34 | "devDependencies": { 35 | "@bomb.sh/tab": "^0.0.10", 36 | "@types/node": "^24.10.4", 37 | "rollup": "^4.53.4", 38 | "rollup-plugin-visualizer": "^6.0.5", 39 | "tsdown": "^0.18.0", 40 | "typescript": "^5.9.3", 41 | "unplugin-purge-polyfills": "^0.1.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 23 | with: 24 | fetch-depth: 0 25 | - run: corepack enable 26 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, promises as fsp } from 'node:fs' 2 | import { join } from 'pathe' 3 | import { debug } from '../utils/logger' 4 | 5 | export async function clearDir(path: string, exclude?: string[]) { 6 | if (!exclude) { 7 | await fsp.rm(path, { recursive: true, force: true }) 8 | } 9 | else if (existsSync(path)) { 10 | const files = await fsp.readdir(path) 11 | await Promise.all( 12 | files.map(async (name) => { 13 | if (!exclude.includes(name)) { 14 | await fsp.rm(join(path, name), { recursive: true, force: true }) 15 | } 16 | }), 17 | ) 18 | } 19 | await fsp.mkdir(path, { recursive: true }) 20 | } 21 | 22 | export function clearBuildDir(path: string) { 23 | return clearDir(path, ['cache', 'analyze', 'nuxt.json']) 24 | } 25 | 26 | export async function rmRecursive(paths: string[]) { 27 | await Promise.all( 28 | paths 29 | .filter(p => typeof p === 'string') 30 | .map(async (path) => { 31 | debug(`Removing recursive path: ${path}`) 32 | await fsp.rm(path, { recursive: true, force: true }).catch(() => {}) 33 | }), 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/versions.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs' 2 | import { resolveModulePath } from 'exsolve' 3 | import { readPackageJSON } from 'pkg-types' 4 | import { coerce } from 'semver' 5 | 6 | import { tryResolveNuxt } from './kit' 7 | 8 | export async function getNuxtVersion(cwd: string, cache = true) { 9 | const nuxtPkg = await readPackageJSON('nuxt', { url: cwd, try: true, cache }) 10 | if (nuxtPkg) { 11 | return nuxtPkg.version! 12 | } 13 | const pkg = await readPackageJSON(cwd) 14 | const pkgDep = pkg?.dependencies?.nuxt || pkg?.devDependencies?.nuxt 15 | return (pkgDep && coerce(pkgDep)?.version) || '3.0.0' 16 | } 17 | 18 | export function getPkgVersion(cwd: string, pkg: string) { 19 | const pkgJSON = getPkgJSON(cwd, pkg) 20 | return pkgJSON?.version ?? '' 21 | } 22 | 23 | export function getPkgJSON(cwd: string, pkg: string) { 24 | for (const url of [cwd, tryResolveNuxt(cwd)]) { 25 | if (!url) { 26 | continue 27 | } 28 | const p = resolveModulePath(`${pkg}/package.json`, { from: url, try: true }) 29 | if (p) { 30 | return JSON.parse(readFileSync(p, 'utf-8')) 31 | } 32 | } 33 | return null 34 | } 35 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/dev-child.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { defineCommand } from 'citty' 3 | import { resolve } from 'pathe' 4 | import { isTest } from 'std-env' 5 | 6 | import { cwdArgs, dotEnvArgs, envNameArgs, legacyRootDirArgs, logLevelArgs } from './_shared' 7 | 8 | export default defineCommand({ 9 | meta: { 10 | name: '_dev', 11 | description: 'Run Nuxt development server (internal command to start child process)', 12 | }, 13 | args: { 14 | ...cwdArgs, 15 | ...logLevelArgs, 16 | ...envNameArgs, 17 | ...dotEnvArgs, 18 | ...legacyRootDirArgs, 19 | clear: { 20 | type: 'boolean', 21 | description: 'Clear console on restart', 22 | negativeDescription: 'Disable clear console on restart', 23 | }, 24 | }, 25 | async run(ctx) { 26 | if (!process.send && !isTest) { 27 | console.warn('`nuxi _dev` is an internal command and should not be used directly. Please use `nuxi dev` instead.') 28 | } 29 | 30 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 31 | 32 | const { initialize } = await import('../dev') 33 | await initialize({ cwd, args: ctx.args }, ctx) 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /packages/create-nuxt/src/main.ts: -------------------------------------------------------------------------------- 1 | import type { CommandDef } from 'citty' 2 | import process from 'node:process' 3 | import { defineCommand } from 'citty' 4 | import { provider } from 'std-env' 5 | 6 | import init from '../../nuxi/src/commands/init' 7 | import { setupInitCompletions } from '../../nuxi/src/completions-init' 8 | import { checkEngines } from '../../nuxi/src/utils/engines' 9 | import { logger } from '../../nuxi/src/utils/logger' 10 | import { description, name, version } from '../package.json' 11 | 12 | const _main = defineCommand({ 13 | meta: { 14 | name, 15 | version, 16 | description, 17 | }, 18 | args: init.args, 19 | async setup(ctx) { 20 | const isCompletionRequest = ctx.args._?.[0] === 'complete' 21 | if (isCompletionRequest) { 22 | return 23 | } 24 | 25 | // Check Node.js version and CLI updates in background 26 | if (provider !== 'stackblitz') { 27 | await checkEngines().catch(err => logger.error(err)) 28 | } 29 | 30 | await init.run?.(ctx) 31 | }, 32 | }) 33 | 34 | if (process.argv[2] === 'complete') { 35 | // eslint-disable-next-line antfu/no-top-level-await 36 | await setupInitCompletions(_main) 37 | } 38 | 39 | export const main = _main as CommandDef 40 | -------------------------------------------------------------------------------- /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 | ## Shell Autocompletions 10 | 11 | `nuxt/cli` provides shell autocompletions for commands, options, and option values – powered by [`@bomb.sh/tab`](https://github.com/bombshell-dev/tab). 12 | 13 | ### Package Manager Integration 14 | 15 | `@bomb.sh/tab` integrates with [package managers](https://github.com/bombshell-dev/tab?tab=readme-ov-file#package-manager-completions). Autocompletions work when running `nuxt` directly within a Nuxt project: 16 | 17 | ```bash 18 | pnpm nuxt 19 | npm exec nuxt 20 | yarn nuxt 21 | bun nuxt 22 | ``` 23 | 24 | For package manager autocompletions, you should install [tab's package manager completions](https://github.com/bombshell-dev/tab?tab=readme-ov-file#package-manager-completions) separately. 25 | 26 | ## Contributing 27 | 28 | ```bash 29 | # Install dependencies 30 | pnpm i 31 | 32 | # Build project and start watcher 33 | pnpm dev 34 | 35 | # Go to the playground directory 36 | cd playground 37 | 38 | # And run any commands 39 | pnpm nuxt 40 | ``` 41 | 42 | ## License 43 | 44 | [MIT](./LICENSE) 45 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/devtools.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | import { defineCommand } from 'citty' 4 | import { colors } from 'consola/utils' 5 | import { resolve } from 'pathe' 6 | import { x } from 'tinyexec' 7 | 8 | import { logger } from '../utils/logger' 9 | import { cwdArgs, legacyRootDirArgs } from './_shared' 10 | 11 | export default defineCommand({ 12 | meta: { 13 | name: 'devtools', 14 | description: 'Enable or disable devtools in a Nuxt project', 15 | }, 16 | args: { 17 | ...cwdArgs, 18 | command: { 19 | type: 'positional', 20 | description: 'Command to run', 21 | valueHint: 'enable|disable', 22 | }, 23 | ...legacyRootDirArgs, 24 | }, 25 | async run(ctx) { 26 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 27 | 28 | if (!['enable', 'disable'].includes(ctx.args.command)) { 29 | logger.error(`Unknown command ${colors.cyan(ctx.args.command)}.`) 30 | process.exit(1) 31 | } 32 | 33 | await x( 34 | 'npx', 35 | ['@nuxt/devtools-wizard@latest', ctx.args.command, cwd], 36 | { 37 | throwOnError: true, 38 | nodeOptions: { 39 | stdio: 'inherit', 40 | cwd, 41 | }, 42 | }, 43 | ) 44 | }, 45 | }) 46 | -------------------------------------------------------------------------------- /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 { initCompletions } from '../../nuxi/src/completions' 8 | import { main } from './main' 9 | 10 | globalThis.__nuxt_cli__ = globalThis.__nuxt_cli__ || { 11 | // Programmatic usage fallback 12 | startTime: Date.now(), 13 | entry: fileURLToPath( 14 | new URL('../../bin/nuxi.mjs', import.meta.url), 15 | ), 16 | devEntry: fileURLToPath( 17 | new URL('../dev/index.mjs', import.meta.url), 18 | ), 19 | } 20 | 21 | export async function runMain(): Promise { 22 | await initCompletions(main) 23 | return _runMain(main) 24 | } 25 | 26 | export async function runCommand( 27 | name: string, 28 | argv: string[] = process.argv.slice(2), 29 | data: { overrides?: Record } = {}, 30 | ): Promise<{ result: unknown }> { 31 | argv.push('--no-clear') // Dev 32 | 33 | if (!(name in commands)) { 34 | throw new Error(`Invalid command ${name}`) 35 | } 36 | 37 | return await _runCommand(await commands[name as keyof typeof commands](), { 38 | rawArgs: argv, 39 | data: { 40 | overrides: data.overrides || {}, 41 | }, 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/kit.ts: -------------------------------------------------------------------------------- 1 | import { pathToFileURL } from 'node:url' 2 | import { resolveModulePath } from 'exsolve' 3 | import { withNodePath } from './paths' 4 | 5 | export async function loadKit(rootDir: string): Promise { 6 | try { 7 | const kitPath = resolveModulePath('@nuxt/kit', { from: tryResolveNuxt(rootDir) || rootDir }) 8 | 9 | let kit: typeof import('@nuxt/kit') = await import(pathToFileURL(kitPath).href) 10 | if (!kit.writeTypes) { 11 | kit = { 12 | ...kit, 13 | writeTypes: () => { 14 | throw new Error('`writeTypes` is not available in this version of `@nuxt/kit`. Please upgrade to v3.7 or newer.') 15 | }, 16 | } 17 | } 18 | return kit 19 | } 20 | catch (e: any) { 21 | if (e.toString().includes('Cannot find module \'@nuxt/kit\'')) { 22 | throw new Error( 23 | 'nuxi requires `@nuxt/kit` to be installed in your project. Try installing `nuxt` v3+ or `@nuxt/bridge` first.', 24 | ) 25 | } 26 | throw e 27 | } 28 | } 29 | 30 | export function tryResolveNuxt(rootDir: string) { 31 | for (const pkg of ['nuxt-nightly', 'nuxt', 'nuxt3', 'nuxt-edge']) { 32 | const path = resolveModulePath(pkg, { from: withNodePath(rootDir), try: true }) 33 | if (path) { 34 | return path 35 | } 36 | } 37 | return null 38 | } 39 | -------------------------------------------------------------------------------- /packages/nuxi/src/run.ts: -------------------------------------------------------------------------------- 1 | import type { ArgsDef, CommandDef } from 'citty' 2 | import process from 'node:process' 3 | 4 | import { fileURLToPath } from 'node:url' 5 | import { runCommand as _runCommand } from 'citty' 6 | 7 | import { isNuxiCommand } from './commands/_utils' 8 | 9 | globalThis.__nuxt_cli__ = globalThis.__nuxt_cli__ || { 10 | // Programmatic usage fallback 11 | startTime: Date.now(), 12 | entry: fileURLToPath( 13 | new URL('../../bin/nuxi.mjs', import.meta.url), 14 | ), 15 | devEntry: fileURLToPath( 16 | new URL('../dev/index.mjs', import.meta.url), 17 | ), 18 | } 19 | 20 | // To provide subcommands call it as `runCommand(, [, ...])` 21 | export async function runCommand( 22 | command: CommandDef, 23 | argv: string[] = process.argv.slice(2), 24 | data: { overrides?: Record } = {}, 25 | ): Promise<{ result: unknown }> { 26 | argv.push('--no-clear') // Dev 27 | if (command.meta && 'name' in command.meta && typeof command.meta.name === 'string') { 28 | const name = command.meta.name 29 | if (!(isNuxiCommand(name))) { 30 | throw new Error(`Invalid command ${name}`) 31 | } 32 | } 33 | else { 34 | throw new Error(`Invalid command, must be named`) 35 | } 36 | 37 | return await _runCommand(command, { 38 | rawArgs: argv, 39 | data: { 40 | overrides: data.overrides || {}, 41 | }, 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /knip.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/knip@5/schema.json", 3 | "workspaces": { 4 | ".": { 5 | "ignoreDependencies": [ 6 | "@nuxt/test-utils", 7 | "nuxt", 8 | "vue", 9 | "vue-router" 10 | ], 11 | "entry": [ 12 | "scripts/*" 13 | ] 14 | }, 15 | "playground": { 16 | "entry": [ 17 | "test/**", 18 | "app/pages/**", 19 | "server/**", 20 | "some-layer/**" 21 | ], 22 | "ignoreDependencies": [ 23 | "vue-router" 24 | ] 25 | }, 26 | "packages/nuxt-cli": { 27 | "entry": [ 28 | "test/fixtures/*" 29 | ], 30 | "ignoreDependencies": [ 31 | "@bomb.sh/tab", 32 | "@clack/prompts", 33 | "c12", 34 | "confbox", 35 | "consola", 36 | "copy-paste", 37 | "debug", 38 | "defu", 39 | "exsolve", 40 | "fuse.js", 41 | "giget", 42 | "h3-next", 43 | "jiti", 44 | "nitro", 45 | "nitropack", 46 | "nypm", 47 | "ofetch", 48 | "ohash", 49 | "pathe", 50 | "perfect-debounce", 51 | "pkg-types", 52 | "scule", 53 | "semver", 54 | "srvx", 55 | "ufo", 56 | "youch" 57 | ] 58 | }, 59 | "packages/create-nuxt": { 60 | "ignoreDependencies": [ 61 | "@bomb.sh/tab" 62 | ] 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /playground/certs/cert.dummy: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID2DCCAsCgAwIBAgIFNTkzOTIwDQYJKoZIhvcNAQELBQAwdTESMBAGA1UEAxMJ 3 | bG9jYWxob3N0MQswCQYDVQQGEwJVUzERMA8GA1UECBMITWljaGlnYW4xEDAOBgNV 4 | BAcTB0JlcmtsZXkxFTATBgNVBAoTDFRlc3RpbmcgQ29ycDEWMBQGA1UECxMNSVQg 5 | ZGVwYXJ0bWVudDAgFw0yNTExMTYwMzIxNDZaGA8yMTI1MTAyMzAzMjE0NlowdTES 6 | MBAGA1UEAxMJbG9jYWxob3N0MQswCQYDVQQGEwJVUzERMA8GA1UECBMITWljaGln 7 | YW4xEDAOBgNVBAcTB0JlcmtsZXkxFTATBgNVBAoTDFRlc3RpbmcgQ29ycDEWMBQG 8 | A1UECxMNSVQgZGVwYXJ0bWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 9 | ggEBALltbdcjx7u+018fT9dOIUZbZUt9CoHpZWbWSohMz9YfVabB99SL+7AiM30/ 10 | l4XCXO5DhhUSxqwUlQ2pAcjnEHxmVpvuiXwTHwItfiF+UDxyCIlfMrj8Tc8ltB41 11 | KPpfgUuc1mb6fsISjEgRkLNW0VIXJDNenlvpx/uXf4MG+6My1T7TFVAhEulZu3wv 12 | b62Q5WR6Ud1G7pMDrEcRcPHkuh3CeQ3OW11hCrwMh2IaLPCd+PDTkwfOaKXYY3/m 13 | raLSmCb/PXVsN7qcUBd1+bwlMblteeJykoZfYawVgn8/+sVdtxBsQftAYOComz4a 14 | a4iJsuKEPoLf8VwaVUaIMpZEVtcCAwEAAaNtMGswDAYDVR0TAQH/BAIwADAOBgNV 15 | HQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMCwGA1Ud 16 | EQQlMCOCCWxvY2FsaG9zdIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG 17 | 9w0BAQsFAAOCAQEAZsnDKhb3OjHgBg1rz2yZg0FFD7oPeyNJNpxXScIOmCIsh0f0 18 | oYx8b17/4dbEg812EHpdUJeceWfvKg+vfylmL6hZ04eY4ymUjf5jrW9iuDd3mmLY 19 | +wsARPjvBvw/iA4BPYg16HUSqUEWfwG/IeQqpu+C75RCie604nt36nDoTQv8QLCx 20 | Et6wvXH9Ucz4CA5MjFJbqaM62CJU+Qeu4sVFgY/KW3UQEJboD51hutxKBMWkkZGL 21 | l5svZhTqUoPSFgdQTHeu4Tku5ZSdwgEBBE2cf8HPOET99aFhPvCVg52IysfW2UNs 22 | BKo0D/Vy+dCX8CGldS59CLx5zKta30fSLlq6vw== 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /packages/nuxt-cli/test/bench/dev.bench.ts: -------------------------------------------------------------------------------- 1 | import type { Nuxt } from '@nuxt/schema' 2 | import type { Listener } from 'listhen' 3 | 4 | import os from 'node:os' 5 | import { fileURLToPath } from 'node:url' 6 | 7 | import { runCommand } from '@nuxt/cli' 8 | import { bench, describe } from 'vitest' 9 | 10 | interface RunResult { 11 | result: { listener: Listener, close: () => Promise } 12 | } 13 | 14 | const fixtureDir = fileURLToPath(new URL('../../../../playground', import.meta.url)) 15 | 16 | describe(`dev [${os.platform()}]`, () => { 17 | bench(`starts dev server with --no-fork`, async () => { 18 | const { result } = await runCommand('dev', [fixtureDir, '--no-fork'], { 19 | overrides: { 20 | builder: { 21 | bundle: (nuxt: Nuxt) => { 22 | nuxt.hooks.removeAllHooks() 23 | return Promise.resolve() 24 | }, 25 | }, 26 | }, 27 | }) as RunResult 28 | await result.close() 29 | }) 30 | 31 | let url: string 32 | bench('makes requests to dev server', async () => { 33 | if (!url) { 34 | const { result } = await runCommand('dev', [fixtureDir, '--no-fork']) as RunResult 35 | url = result.listener.url 36 | } 37 | const html = await fetch(url).then(r => r.text()) 38 | if (!html.includes('Welcome to the Nuxt CLI playground!')) { 39 | throw new Error('Unexpected response from dev server') 40 | } 41 | await fetch(`${url}_nuxt/@vite/client`).then(r => r.text()) 42 | }, { 43 | warmupIterations: 1, 44 | time: 10_000, 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/prepare.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | import { defineCommand } from 'citty' 4 | import { colors } from 'consola/utils' 5 | import { resolve } from 'pathe' 6 | 7 | import { clearBuildDir } from '../utils/fs' 8 | import { loadKit } from '../utils/kit' 9 | import { logger } from '../utils/logger' 10 | import { relativeToProcess } from '../utils/paths' 11 | import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' 12 | 13 | export default defineCommand({ 14 | meta: { 15 | name: 'prepare', 16 | description: 'Prepare Nuxt for development/build', 17 | }, 18 | args: { 19 | ...dotEnvArgs, 20 | ...cwdArgs, 21 | ...logLevelArgs, 22 | ...envNameArgs, 23 | ...extendsArgs, 24 | ...legacyRootDirArgs, 25 | }, 26 | async run(ctx) { 27 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 28 | 29 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 30 | 31 | const { loadNuxt, buildNuxt, writeTypes } = await loadKit(cwd) 32 | const nuxt = await loadNuxt({ 33 | cwd, 34 | dotenv: { 35 | cwd, 36 | fileName: ctx.args.dotenv, 37 | }, 38 | envName: ctx.args.envName, // c12 will fall back to NODE_ENV 39 | overrides: { 40 | _prepare: true, 41 | logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose', 42 | ...(ctx.args.extends && { extends: ctx.args.extends }), 43 | ...ctx.data?.overrides, 44 | }, 45 | }) 46 | await clearBuildDir(nuxt.options.buildDir) 47 | 48 | await buildNuxt(nuxt) 49 | await writeTypes(nuxt) 50 | logger.success(`Types generated in ${colors.cyan(relativeToProcess(nuxt.options.buildDir))}.`) 51 | }, 52 | }) 53 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/starter-templates.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { $fetch } from 'ofetch' 3 | 4 | export const hiddenTemplates = [ 5 | 'doc-driven', 6 | 'v4', 7 | 'v4-compat', 8 | 'v2-bridge', 9 | 'v3', 10 | 'ui-vue', 11 | 'module-devtools', 12 | 'layer', 13 | 'hub', 14 | ] 15 | 16 | export interface TemplateData { 17 | name: string 18 | description: string 19 | defaultDir: string 20 | url: string 21 | tar: string 22 | } 23 | 24 | const fetchOptions = { 25 | timeout: 3000, 26 | responseType: 'json', 27 | headers: { 28 | 'user-agent': '@nuxt/cli', 29 | ...process.env.GITHUB_TOKEN ? { authorization: `token ${process.env.GITHUB_TOKEN}` } : {}, 30 | }, 31 | } as const 32 | 33 | let templatesCache: Promise> | null = null 34 | 35 | export async function getTemplates() { 36 | templatesCache ||= fetchTemplates() 37 | return templatesCache 38 | } 39 | 40 | export async function fetchTemplates() { 41 | const templates = {} as Record 42 | 43 | const files = await $fetch>( 44 | 'https://api.github.com/repos/nuxt/starter/contents/templates?ref=templates', 45 | fetchOptions, 46 | ) 47 | 48 | await Promise.all(files.map(async (file) => { 49 | if (!file.download_url || file.type !== 'file' || !file.name.endsWith('.json')) { 50 | return 51 | } 52 | const templateName = file.name.replace('.json', '') 53 | if (hiddenTemplates.includes(templateName)) { 54 | return 55 | } 56 | templates[templateName] = undefined as unknown as TemplateData 57 | templates[templateName] = await $fetch(file.download_url, fetchOptions) 58 | })) 59 | 60 | return templates 61 | } 62 | -------------------------------------------------------------------------------- /playground/certs/key.dummy: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAuW1t1yPHu77TXx9P104hRltlS30KgellZtZKiEzP1h9VpsH3 3 | 1Iv7sCIzfT+XhcJc7kOGFRLGrBSVDakByOcQfGZWm+6JfBMfAi1+IX5QPHIIiV8y 4 | uPxNzyW0HjUo+l+BS5zWZvp+whKMSBGQs1bRUhckM16eW+nH+5d/gwb7ozLVPtMV 5 | UCES6Vm7fC9vrZDlZHpR3UbukwOsRxFw8eS6HcJ5Dc5bXWEKvAyHYhos8J348NOT 6 | B85opdhjf+atotKYJv89dWw3upxQF3X5vCUxuW154nKShl9hrBWCfz/6xV23EGxB 7 | +0Bg4KibPhpriImy4oQ+gt/xXBpVRogylkRW1wIDAQABAoIBAB9dZLZ+7WKTATL2 8 | W216YEOD4yr1MClQXuAZwEq033UDINxPtAmGUiD1cAswDgPIoCqHTm9TGTrzUlEY 9 | tN4UQ6QfNWgz3ZqYq2aVZl/o+052JX6DFVPYDZtL798qM8/CBt9Q3K1XkshmFcd8 10 | /SJwvYBqvKtZxmSas0KZ2i5CKJ9uiHEWi/ZCs6pUJdRojxfkfeHLMujA7EDBtZic 11 | AKqH698jo7+m8hddczM7XOlBVe6MAn2fWs4VlgVK4gfw4V0xtZt5wvNhyh69YTUZ 12 | wtMTaEULPwPtmxSTdWvb0Z4kUftXLRS3jd/ibe7utQLoR579ZPoNL0Ixma8sIA9j 13 | TV+b9akCgYEA5wJufOg7uFXD1bL/TsnU9ciNs7rof6SK+Hc7IrLFVk7a5SeAjcAJ 14 | oar70pnaGE0TgLLMXysi/2XZL0y45a3ihgl3CoLGJBGOv4QcgccmFrZqYUGCV0Cv 15 | oz7PskhcmcneyWnoI/twbbyYxkQNww+5LSTN8iAaVcb+3G1YZPaQ2AkCgYEAzXym 16 | btesxvDXHurQdviBKEjp7yhvSo44Q54ahEW8n1RiGfjgwFcjF1jZXxqGml/bIiu4 17 | AYsSeUqBrFXAXq9AlA6DThFJtC5EK385do1WUhcaU53FlvWt2atmRXuUl+rgaBrS 18 | b43uvLyFJZezQILzvYkaHJjh8ioKOkPzLW2ar98CgYB0oJ6lgx27d9lSB3esEGvq 19 | 1qDrz35YCvt6a7+4SeclJtSOgr39UqnKLCfM8I3SXP9up1ZU6dNWe9YFckea9Yn6 20 | v8aQ0Os2BIM8H3fA8YlCSEA277rdUDQcR7bWPIA7yFYo+8YOfIALdv7ugicshsCn 21 | kQBEsH57Necv5CiPeIgx+QKBgG+BY6MkX/p4eJOrYkIc6aFdp6wCqhmwATIYGlWK 22 | ridbl/x2BCf7YOxrZ1FnSIF+4J+zT59uwzCUULeetMvsl8N/+JqlYPRoYs+jsx/0 23 | 5FGZfczAAZfAa32Bt/aeb+zcJLf5ThYA0/sQ5cOXhUrNhMxmGIhKIdnSHEiv1Mbj 24 | AhzLAoGBAKDYjXW1FNIR3le4Ngs8adCPN18iJ7rXSYPnen5e40g3i1vQGKwxMQpg 25 | aEKByb1RWSH4fZDToiC+CPqrimgu24mgYJNMD9W7M8iizR2/5vKNJ/ABqL1/SV0L 26 | OsvTc2QFx005TGOLteaw1mhqjSGptNHzHhPQ01v2gcIkobBk0N5y 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /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 as string) 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/_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 extendsArgs = { 35 | extends: { 36 | type: 'string', 37 | description: 'Extend from a Nuxt layer', 38 | valueHint: 'layer-name', 39 | alias: ['e'], 40 | }, 41 | } as const satisfies Record 42 | 43 | export const legacyRootDirArgs = { 44 | // cwd falls back to rootDir's default (indirect default) 45 | cwd: { 46 | ...cwdArgs.cwd, 47 | description: 'Specify the working directory, this takes precedence over ROOTDIR (default: `.`)', 48 | default: undefined, 49 | }, 50 | rootDir: { 51 | type: 'positional', 52 | description: 'Specifies the working directory (default: `.`)', 53 | required: false, 54 | default: '.', 55 | }, 56 | } as const satisfies Record 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/generate-data.ts: -------------------------------------------------------------------------------- 1 | /** generate completion data from nitropack and Nuxt starter repo */ 2 | 3 | import { mkdir, writeFile } from 'node:fs/promises' 4 | import { dirname, join } from 'node:path' 5 | import process from 'node:process' 6 | import { pathToFileURL } from 'node:url' 7 | import { resolveModulePath } from 'exsolve' 8 | 9 | import { fetchTemplates } from '../packages/nuxi/src/utils/starter-templates.ts' 10 | 11 | interface PresetMeta { 12 | _meta?: { name: string } 13 | } 14 | 15 | const dataDir = new URL('../packages/nuxi/src/data/', import.meta.url) 16 | 17 | export async function generateCompletionData() { 18 | const [nitroPresets, templates] = await Promise.all([ 19 | getNitroPresets(), 20 | fetchTemplates(), 21 | ]) 22 | 23 | await mkdir(dataDir, { recursive: true }) 24 | await writeFile( 25 | new URL('nitro-presets.ts', dataDir), 26 | `export const nitroPresets = ${JSON.stringify(nitroPresets, null, 2)} as const`, 27 | ) 28 | await writeFile( 29 | new URL('templates.ts', dataDir), 30 | `export const templates = ${JSON.stringify(templates, null, 2)} as const`, 31 | ) 32 | } 33 | 34 | async function getNitroPresets() { 35 | const nitropackPath = dirname(resolveModulePath('nitropack/package.json', { from: dataDir })) 36 | const presetsPath = join(nitropackPath, 'dist/presets/_all.gen.mjs') 37 | const { default: allPresets } = await import(pathToFileURL(presetsPath).toString()) as { default: PresetMeta[] } 38 | 39 | return allPresets 40 | .map(preset => preset._meta?.name) 41 | .filter((name): name is string => Boolean(name)) 42 | .filter(name => !['base-worker', 'nitro-dev', 'nitro-prerender'].includes(name)) 43 | .filter((name, index, array) => array.indexOf(name) === index) 44 | .sort() 45 | } 46 | 47 | generateCompletionData().catch((error) => { 48 | console.error('Failed to generate completion data:', error) 49 | process.exit(1) 50 | }) 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxi-workspace", 3 | "type": "module", 4 | "private": true, 5 | "packageManager": "pnpm@10.26.0", 6 | "description": "⚡️ Nuxt Generation CLI Experience", 7 | "license": "MIT", 8 | "repository": "nuxt/cli", 9 | "scripts": { 10 | "build": "pnpm -r build", 11 | "dev": "pnpm -r build --watch", 12 | "lint": "eslint .", 13 | "lint:fix": "eslint --fix .", 14 | "nuxi": "node ./packages/nuxi/bin/nuxi.mjs", 15 | "nuxt": "node ./packages/nuxt-cli/bin/nuxi.mjs", 16 | "nuxi-bun": "bun --bun ./packages/nuxt-cli/bin/nuxi.mjs", 17 | "postinstall": "node --experimental-strip-types ./scripts/generate-data.ts && pnpm build", 18 | "test:types": "tsc --noEmit", 19 | "test:knip": "knip", 20 | "test:dist": "pnpm -r test:dist", 21 | "test:unit": "vitest --coverage --run && pnpm --filter nuxt-cli-playground test --run" 22 | }, 23 | "devDependencies": { 24 | "@antfu/eslint-config": "^6.7.1", 25 | "@codspeed/vitest-plugin": "^5.0.1", 26 | "@nuxt/eslint-config": "^1.12.1", 27 | "@nuxt/test-utils": "^3.21.0", 28 | "@types/node": "^24.10.4", 29 | "@types/semver": "^7.7.1", 30 | "@vitest/coverage-v8": "^3.2.4", 31 | "changelogen": "^0.6.2", 32 | "eslint": "^9.39.2", 33 | "exsolve": "^1.0.8", 34 | "knip": "^5.73.4", 35 | "nuxt": "^4.2.2", 36 | "pkg-pr-new": "^0.0.62", 37 | "semver": "^7.7.3", 38 | "std-env": "^3.10.0", 39 | "tinyexec": "^1.0.2", 40 | "typescript": "^5.9.3", 41 | "vitest": "^3.2.4", 42 | "vue": "^3.5.25", 43 | "vue-router": "^4.6.4" 44 | }, 45 | "resolutions": { 46 | "@nuxt/cli": "workspace:*", 47 | "@nuxt/schema": "4.2.2", 48 | "create-nuxt": "workspace:*", 49 | "create-nuxt-app": "workspace:*", 50 | "eslint-plugin-jsdoc": "61.5.0", 51 | "eslint-plugin-unicorn": "62.0.0", 52 | "h3": "^1.15.4", 53 | "nitropack": "latest", 54 | "nuxi": "workspace:*" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /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/banner.ts: -------------------------------------------------------------------------------- 1 | import type { NuxtBuilder, NuxtConfig, NuxtOptions } from '@nuxt/schema' 2 | 3 | import { colors } from 'consola/utils' 4 | 5 | import { logger } from './logger' 6 | import { getPkgJSON, getPkgVersion } from './versions' 7 | 8 | export function getBuilder(cwd: string, builder: Exclude): { name: string, version: string } { 9 | switch (builder) { 10 | case 'rspack': 11 | case '@nuxt/rspack-builder': 12 | return { name: 'Rspack', version: getPkgVersion(cwd, '@rspack/core') } 13 | case 'webpack': 14 | case '@nuxt/webpack-builder': 15 | return { name: 'Webpack', version: getPkgVersion(cwd, 'webpack') } 16 | case 'vite': 17 | case '@nuxt/vite-builder': 18 | default: { 19 | const pkgJSON = getPkgJSON(cwd, 'vite') 20 | const isRolldown = pkgJSON.name.includes('rolldown') 21 | return { name: isRolldown ? 'Rolldown-Vite' : 'Vite', version: pkgJSON.version } 22 | } 23 | } 24 | } 25 | 26 | export function showVersionsFromConfig(cwd: string, config: NuxtOptions) { 27 | const { bold, gray, green } = colors 28 | 29 | const nuxtVersion = getPkgVersion(cwd, 'nuxt') || getPkgVersion(cwd, 'nuxt-nightly') || getPkgVersion(cwd, 'nuxt3') || getPkgVersion(cwd, 'nuxt-edge') 30 | const nitroVersion = getPkgVersion(cwd, 'nitropack') || getPkgVersion(cwd, 'nitro') || getPkgVersion(cwd, 'nitropack-nightly') || getPkgVersion(cwd, 'nitropack-edge') 31 | const builder = getBuilder(cwd, config.builder) 32 | const vueVersion = getPkgVersion(cwd, 'vue') || null 33 | 34 | logger.info( 35 | green(`Nuxt ${bold(nuxtVersion)}`) 36 | + gray(' (with ') 37 | + (nitroVersion ? gray(`Nitro ${bold(nitroVersion)}`) : '') 38 | + gray(`, ${builder.name} ${bold(builder.version)}`) 39 | + (vueVersion ? gray(` and Vue ${bold(vueVersion)}`) : '') 40 | + gray(')'), 41 | ) 42 | } 43 | 44 | export async function showVersions(cwd: string, kit: typeof import('@nuxt/kit')) { 45 | const config = await kit.loadNuxtConfig({ cwd }) 46 | 47 | return showVersionsFromConfig(cwd, config) 48 | } 49 | -------------------------------------------------------------------------------- /packages/nuxi/test/unit/commands/module/_utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { getRegistryFromContent } from '../../../../src/commands/module/_utils' 3 | 4 | describe('getRegistryFromContent', () => { 5 | it('extracts scoped registry when scope is provided', () => { 6 | const content = ` 7 | registry=https://registry.npmjs.org/ 8 | @myorg:registry=https://my-registry.org/ 9 | @another:registry=https://another-registry.org/ 10 | ` 11 | 12 | expect(getRegistryFromContent(content, '@myorg')).toBe('https://my-registry.org/') 13 | expect(getRegistryFromContent(content, '@another')).toBe('https://another-registry.org/') 14 | }) 15 | 16 | it('extracts default registry when scope is not provided', () => { 17 | const content = ` 18 | registry=https://registry.npmjs.org/ 19 | @myorg:registry=https://my-registry.org/ 20 | ` 21 | 22 | expect(getRegistryFromContent(content, null)).toBe('https://registry.npmjs.org/') 23 | }) 24 | 25 | it('extracts default registry when scope is provided but not found', () => { 26 | const content = ` 27 | registry=https://registry.npmjs.org/ 28 | @myorg:registry=https://my-registry.org/ 29 | ` 30 | 31 | expect(getRegistryFromContent(content, '@notfound')).toBe('https://registry.npmjs.org/') 32 | }) 33 | 34 | it('returns null when no registry is found', () => { 35 | const content = ` 36 | # some npmrc content without registry 37 | some-other-setting=value 38 | ` 39 | 40 | expect(getRegistryFromContent(content, null)).toBeNull() 41 | expect(getRegistryFromContent(content, '@myorg')).toBeNull() 42 | }) 43 | 44 | it('handles empty content', () => { 45 | expect(getRegistryFromContent('', null)).toBeNull() 46 | expect(getRegistryFromContent('', '@myorg')).toBeNull() 47 | }) 48 | 49 | it('extracts registry from line with comments', () => { 50 | const content = ` 51 | registry=https://registry.npmjs.org/ # with comment 52 | @myorg:registry=https://my-registry.org/ # another comment 53 | ` 54 | 55 | expect(getRegistryFromContent(content, null)).toBe('https://registry.npmjs.org/') 56 | expect(getRegistryFromContent(content, '@myorg')).toBe('https://my-registry.org/') 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /packages/nuxi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxi", 3 | "type": "module", 4 | "version": "3.31.3", 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 | "build": "tsdown", 32 | "prepack": "pnpm build" 33 | }, 34 | "devDependencies": { 35 | "@bomb.sh/tab": "^0.0.10", 36 | "@clack/prompts": "1.0.0-alpha.8", 37 | "@nuxt/kit": "^4.2.2", 38 | "@nuxt/schema": "^4.2.2", 39 | "@nuxt/test-utils": "^3.21.0", 40 | "@types/copy-paste": "^2.1.0", 41 | "@types/debug": "^4.1.12", 42 | "@types/node": "^24.10.4", 43 | "@types/semver": "^7.7.1", 44 | "c12": "^3.3.2", 45 | "citty": "^0.1.6", 46 | "confbox": "^0.2.2", 47 | "consola": "^3.4.2", 48 | "copy-paste": "^2.2.0", 49 | "debug": "^4.4.3", 50 | "defu": "^6.1.4", 51 | "exsolve": "^1.0.8", 52 | "fuse.js": "^7.1.0", 53 | "giget": "^2.0.0", 54 | "h3": "^1.15.4", 55 | "h3-next": "npm:h3@^2.0.1-rc.6", 56 | "jiti": "^2.6.1", 57 | "listhen": "^1.9.0", 58 | "magicast": "^0.5.1", 59 | "nitro": "^3.0.1-alpha.1", 60 | "nitropack": "^2.12.9", 61 | "nypm": "^0.6.2", 62 | "ofetch": "^1.5.1", 63 | "ohash": "^2.0.11", 64 | "pathe": "^2.0.3", 65 | "perfect-debounce": "^2.0.0", 66 | "pkg-types": "^2.3.0", 67 | "rollup": "^4.53.4", 68 | "rollup-plugin-visualizer": "^6.0.5", 69 | "scule": "^1.3.0", 70 | "semver": "^7.7.3", 71 | "srvx": "^0.9.8", 72 | "std-env": "^3.10.0", 73 | "tinyexec": "^1.0.2", 74 | "tsdown": "^0.18.0", 75 | "typescript": "^5.9.3", 76 | "ufo": "^1.6.1", 77 | "unplugin-purge-polyfills": "^0.1.0", 78 | "vitest": "^3.2.4", 79 | "youch": "^4.1.0-beta.13" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /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/nuxt-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxt/cli", 3 | "type": "module", 4 | "version": "3.31.3", 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": "tsdown", 32 | "dev:prepare": "tsdown --watch", 33 | "prepack": "tsdown" 34 | }, 35 | "dependencies": { 36 | "@bomb.sh/tab": "^0.0.10", 37 | "@clack/prompts": "1.0.0-alpha.8", 38 | "c12": "^3.3.2", 39 | "citty": "^0.1.6", 40 | "confbox": "^0.2.2", 41 | "consola": "^3.4.2", 42 | "copy-paste": "^2.2.0", 43 | "debug": "^4.4.3", 44 | "defu": "^6.1.4", 45 | "exsolve": "^1.0.8", 46 | "fuse.js": "^7.1.0", 47 | "giget": "^2.0.0", 48 | "jiti": "^2.6.1", 49 | "listhen": "^1.9.0", 50 | "nypm": "^0.6.2", 51 | "ofetch": "^1.5.1", 52 | "ohash": "^2.0.11", 53 | "pathe": "^2.0.3", 54 | "perfect-debounce": "^2.0.0", 55 | "pkg-types": "^2.3.0", 56 | "scule": "^1.3.0", 57 | "semver": "^7.7.3", 58 | "srvx": "^0.9.8", 59 | "std-env": "^3.10.0", 60 | "tinyexec": "^1.0.2", 61 | "ufo": "^1.6.1", 62 | "youch": "^4.1.0-beta.13" 63 | }, 64 | "devDependencies": { 65 | "@nuxt/kit": "^4.2.2", 66 | "@nuxt/schema": "^4.2.2", 67 | "@types/debug": "^4.1.12", 68 | "@types/node": "^24.10.4", 69 | "get-port-please": "^3.2.0", 70 | "h3": "^1.15.4", 71 | "h3-next": "npm:h3@^2.0.1-rc.6", 72 | "nitro": "^3.0.1-alpha.1", 73 | "nitropack": "^2.12.9", 74 | "rollup": "^4.53.4", 75 | "rollup-plugin-visualizer": "^6.0.5", 76 | "tsdown": "^0.18.0", 77 | "typescript": "^5.9.3", 78 | "undici": "^7.16.0", 79 | "unplugin-purge-polyfills": "^0.1.0", 80 | "vitest": "^3.2.4", 81 | "youch": "^4.1.0-beta.13" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/nuxt-cli/src/main.ts: -------------------------------------------------------------------------------- 1 | import type { CommandDef } from 'citty' 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 { commands } from '../../nuxi/src/commands' 9 | import { cwdArgs } from '../../nuxi/src/commands/_shared' 10 | import { setupGlobalConsole } from '../../nuxi/src/utils/console' 11 | import { checkEngines } from '../../nuxi/src/utils/engines' 12 | 13 | import { logger } from '../../nuxi/src/utils/logger' 14 | import { description, name, version } from '../package.json' 15 | 16 | const _main = defineCommand({ 17 | meta: { 18 | name: name.endsWith('nightly') ? name : 'nuxi', 19 | version, 20 | description, 21 | }, 22 | args: { 23 | ...cwdArgs, 24 | command: { 25 | type: 'positional', 26 | required: false, 27 | }, 28 | }, 29 | subCommands: commands, 30 | async setup(ctx) { 31 | const command = ctx.args._[0] 32 | setupGlobalConsole({ dev: command === '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 | 69 | export const main = _main as CommandDef 70 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | env: 9 | BUNDLE_SIZE: true 10 | 11 | permissions: {} 12 | 13 | jobs: 14 | # Build current and upload stats.json 15 | # You may replace this with your own build method. All that 16 | # is required is that the stats.json be an artifact 17 | build-head: 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: read 21 | steps: 22 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 23 | with: 24 | ref: ${{ github.event.pull_request.head.sha }} 25 | - run: corepack enable 26 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 27 | with: 28 | node-version: lts/* 29 | 30 | - name: 📦 Install dependencies 31 | run: pnpm install 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: 🛠 Build project 36 | run: pnpm build 37 | 38 | - name: 💾 Save PR number 39 | run: echo "${{ github.event.pull_request.number }}" > pr-number.txt 40 | 41 | - name: ⏫ Upload stats.json 42 | uses: actions/upload-artifact@v6 43 | with: 44 | name: head-stats 45 | path: ./packages/*/stats.json 46 | 47 | - name: ⏫ Upload PR number 48 | uses: actions/upload-artifact@v6 49 | with: 50 | name: pr-number 51 | path: pr-number.txt 52 | 53 | # Build base for comparison and upload stats.json 54 | # You may replace this with your own build method. All that 55 | # is required is that the stats.json be an artifact 56 | build-base: 57 | runs-on: ubuntu-latest 58 | permissions: 59 | contents: read 60 | steps: 61 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 62 | with: 63 | ref: ${{ github.base_ref }} 64 | - run: corepack enable 65 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 66 | with: 67 | node-version: lts/* 68 | 69 | - name: 📦 Install dependencies 70 | run: 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | 74 | - name: 🛠 Build project 75 | run: pnpm build 76 | 77 | - name: ⏫ Upload stats.json 78 | uses: actions/upload-artifact@v6 79 | with: 80 | name: base-stats 81 | path: ./packages/*/stats.json 82 | -------------------------------------------------------------------------------- /packages/nuxi/src/completions.ts: -------------------------------------------------------------------------------- 1 | import type { ArgsDef, CommandDef } from 'citty' 2 | import tab from '@bomb.sh/tab/citty' 3 | import { nitroPresets } from './data/nitro-presets' 4 | import { templates } from './data/templates' 5 | 6 | export async function initCompletions(command: CommandDef) { 7 | const completion = await tab(command) 8 | 9 | const devCommand = completion.commands.get('dev') 10 | if (devCommand) { 11 | const portOption = devCommand.options.get('port') 12 | if (portOption) { 13 | portOption.handler = (complete) => { 14 | complete('3000', 'Default development port') 15 | complete('3001', 'Alternative port') 16 | complete('8080', 'Common alternative port') 17 | } 18 | } 19 | 20 | const hostOption = devCommand.options.get('host') 21 | if (hostOption) { 22 | hostOption.handler = (complete) => { 23 | complete('localhost', 'Local development') 24 | complete('0.0.0.0', 'Listen on all interfaces') 25 | complete('127.0.0.1', 'Loopback address') 26 | } 27 | } 28 | } 29 | 30 | const buildCommand = completion.commands.get('build') 31 | if (buildCommand) { 32 | const presetOption = buildCommand.options.get('preset') 33 | if (presetOption) { 34 | presetOption.handler = (complete) => { 35 | for (const preset of nitroPresets) { 36 | complete(preset, '') 37 | } 38 | } 39 | } 40 | } 41 | 42 | const initCommand = completion.commands.get('init') 43 | if (initCommand) { 44 | const templateOption = initCommand.options.get('template') 45 | if (templateOption) { 46 | templateOption.handler = (complete) => { 47 | for (const template in templates) { 48 | complete(template, templates[template as 'content']?.description || '') 49 | } 50 | } 51 | } 52 | } 53 | 54 | const addCommand = completion.commands.get('add') 55 | if (addCommand) { 56 | const cwdOption = addCommand.options.get('cwd') 57 | if (cwdOption) { 58 | cwdOption.handler = (complete) => { 59 | complete('.', 'Current directory') 60 | } 61 | } 62 | } 63 | 64 | const logLevelCommands = ['dev', 'build', 'generate', 'preview', 'prepare', 'init'] 65 | for (const cmdName of logLevelCommands) { 66 | const cmd = completion.commands.get(cmdName) 67 | if (cmd) { 68 | const logLevelOption = cmd.options.get('logLevel') 69 | if (logLevelOption) { 70 | logLevelOption.handler = (complete) => { 71 | complete('silent', 'No logs') 72 | complete('info', 'Standard logging') 73 | complete('verbose', 'Detailed logging') 74 | } 75 | } 76 | } 77 | } 78 | 79 | return completion 80 | } 81 | -------------------------------------------------------------------------------- /playground/app/pages/ws.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 60 | 64 | Nuxt WebSocket Test Page 65 | 66 | 67 | Send Ping 68 | Reconnect 69 | Clear 70 | 71 | 72 | 73 | 74 | 75 | 83 | 84 | 85 | 89 | Send 90 | 91 | 92 | 93 | 94 | 95 | 96 | 100 | {{ l }} 101 | 102 | 103 | 104 | 105 | 106 | 109 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/typecheck.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | import { defineCommand } from 'citty' 4 | import { resolveModulePath } from 'exsolve' 5 | import { resolve } from 'pathe' 6 | import { readTSConfig } from 'pkg-types' 7 | import { isBun } from 'std-env' 8 | import { x } from 'tinyexec' 9 | 10 | import { loadKit } from '../utils/kit' 11 | import { cwdArgs, dotEnvArgs, extendsArgs, 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 | ...extendsArgs, 23 | ...legacyRootDirArgs, 24 | }, 25 | async run(ctx) { 26 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 27 | 28 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 29 | 30 | const [supportsProjects, resolvedTypeScript, resolvedVueTsc] = await Promise.all([ 31 | readTSConfig(cwd).then(r => !!(r.references?.length)), 32 | // Prefer local install if possible 33 | resolveModulePath('typescript', { try: true }), 34 | resolveModulePath('vue-tsc/bin/vue-tsc.js', { try: true }), 35 | writeTypes(cwd, ctx.args.dotenv, ctx.args.logLevel as 'silent' | 'info' | 'verbose', { 36 | ...ctx.data?.overrides, 37 | ...(ctx.args.extends && { extends: ctx.args.extends }), 38 | }), 39 | ]) 40 | 41 | const typeCheckArgs = supportsProjects ? ['-b', '--noEmit'] : ['--noEmit'] 42 | if (resolvedTypeScript && resolvedVueTsc) { 43 | return await x(resolvedVueTsc, typeCheckArgs, { 44 | throwOnError: true, 45 | nodeOptions: { 46 | stdio: 'inherit', 47 | cwd, 48 | }, 49 | }) 50 | } 51 | 52 | if (isBun) { 53 | await x('bun', ['install', 'typescript', 'vue-tsc', '--global', '--silent'], { 54 | throwOnError: true, 55 | nodeOptions: { stdio: 'inherit', cwd }, 56 | }) 57 | 58 | return await x('bunx', ['vue-tsc', ...typeCheckArgs], { 59 | throwOnError: true, 60 | nodeOptions: { 61 | stdio: 'inherit', 62 | cwd, 63 | }, 64 | }) 65 | } 66 | 67 | await x('npx', ['-p', 'vue-tsc', '-p', 'typescript', 'vue-tsc', ...typeCheckArgs], { 68 | throwOnError: true, 69 | nodeOptions: { stdio: 'inherit', cwd }, 70 | }) 71 | }, 72 | }) 73 | 74 | async function writeTypes(cwd: string, dotenv?: string, logLevel?: 'silent' | 'info' | 'verbose', overrides?: Record) { 75 | const { loadNuxt, buildNuxt, writeTypes } = await loadKit(cwd) 76 | const nuxt = await loadNuxt({ 77 | cwd, 78 | dotenv: { cwd, fileName: dotenv }, 79 | overrides: { 80 | _prepare: true, 81 | logLevel, 82 | ...overrides, 83 | }, 84 | }) 85 | 86 | // Generate types and build Nuxt instance 87 | await writeTypes(nuxt) 88 | await buildNuxt(nuxt) 89 | await nuxt.close() 90 | } 91 | -------------------------------------------------------------------------------- /packages/nuxi/src/main.ts: -------------------------------------------------------------------------------- 1 | import type { CommandDef } from 'citty' 2 | import nodeCrypto from 'node:crypto' 3 | import { builtinModules, createRequire } from 'node:module' 4 | import { resolve } from 'node:path' 5 | import process from 'node:process' 6 | 7 | import { runMain as _runMain, defineCommand } from 'citty' 8 | import { provider } from 'std-env' 9 | 10 | import { description, name, version } from '../package.json' 11 | import { commands } from './commands' 12 | import { cwdArgs } from './commands/_shared' 13 | import { initCompletions } from './completions' 14 | import { setupGlobalConsole } from './utils/console' 15 | import { checkEngines } from './utils/engines' 16 | import { debug, logger } from './utils/logger' 17 | 18 | // globalThis.crypto support for Node.js 18 19 | if (!globalThis.crypto) { 20 | globalThis.crypto = nodeCrypto.webcrypto as unknown as Crypto 21 | } 22 | 23 | // Node.js below v22.3.0, v20.16.0 24 | if (!process.getBuiltinModule) { 25 | const _require = createRequire(import.meta.url) 26 | // @ts-expect-error we are overriding with inferior types 27 | process.getBuiltinModule = (name: string) => { 28 | if (name.startsWith('node:') || builtinModules.includes(name)) { 29 | return _require.resolve(name) 30 | } 31 | } 32 | } 33 | 34 | const _main = defineCommand({ 35 | meta: { 36 | name: name.endsWith('nightly') ? name : 'nuxi', 37 | version, 38 | description, 39 | }, 40 | args: { 41 | ...cwdArgs, 42 | command: { 43 | type: 'positional', 44 | required: false, 45 | }, 46 | }, 47 | subCommands: commands, 48 | async setup(ctx) { 49 | const command = ctx.args._[0] 50 | setupGlobalConsole({ dev: command === 'dev' }) 51 | debug(`Running \`nuxt ${command}\` command`) 52 | 53 | // Check Node.js version and CLI updates in background 54 | let backgroundTasks: Promise | undefined 55 | if (command !== '_dev' && provider !== 'stackblitz') { 56 | backgroundTasks = Promise.all([ 57 | checkEngines(), 58 | ]).catch(err => logger.error(err)) 59 | } 60 | 61 | // Avoid background check to fix prompt issues 62 | if (command === 'init') { 63 | await backgroundTasks 64 | } 65 | 66 | // allow running arbitrary commands if there's a locally registered binary with `nuxt-` prefix 67 | if (ctx.args.command && !(ctx.args.command in commands)) { 68 | const cwd = resolve(ctx.args.cwd) 69 | try { 70 | const { x } = await import('tinyexec') 71 | // `tinyexec` will resolve command from local binaries 72 | await x(`nuxt-${ctx.args.command}`, ctx.rawArgs.slice(1), { 73 | nodeOptions: { stdio: 'inherit', cwd }, 74 | throwOnError: true, 75 | }) 76 | } 77 | catch (err) { 78 | // TODO: use windows err code as well 79 | if (err instanceof Error && 'code' in err && err.code === 'ENOENT') { 80 | return 81 | } 82 | } 83 | process.exit() 84 | } 85 | }, 86 | }) 87 | 88 | export const main = _main as CommandDef 89 | 90 | export async function runMain(): Promise { 91 | await initCompletions(main) 92 | 93 | return _runMain(main) 94 | } 95 | -------------------------------------------------------------------------------- /.github/workflows/size-comment.yml: -------------------------------------------------------------------------------- 1 | name: size-comment 2 | 3 | on: 4 | workflow_run: 5 | workflows: [size] 6 | types: [completed] 7 | 8 | permissions: 9 | pull-requests: write 10 | actions: read 11 | 12 | jobs: 13 | comment: 14 | # Only run if the build workflow succeeded 15 | if: github.event.workflow_run.conclusion == 'success' 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 19 | with: 20 | node-version: latest 21 | 22 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 23 | with: 24 | sparse-checkout: scripts/parse-sizes.ts 25 | sparse-checkout-cone-mode: false 26 | 27 | - name: ⏬ Download artifacts from workflow run 28 | uses: actions/download-artifact@v7 29 | with: 30 | github-token: ${{ secrets.GITHUB_TOKEN }} 31 | run-id: ${{ github.event.workflow_run.id }} 32 | path: . 33 | 34 | - name: 🔍 Get PR number from artifact 35 | id: pr 36 | run: | 37 | if [ -f pr-number/pr-number.txt ]; then 38 | PR_NUMBER=$(cat pr-number/pr-number.txt) 39 | echo "result=$PR_NUMBER" >> $GITHUB_OUTPUT 40 | echo "Found PR number: $PR_NUMBER" 41 | else 42 | echo "PR number file not found" 43 | echo "result=" >> $GITHUB_OUTPUT 44 | fi 45 | 46 | - name: 📊 Generate size comparison 47 | id: sizes 48 | run: | 49 | COMMENT=$(node scripts/parse-sizes.ts nuxi nuxt-cli create-nuxt) 50 | echo "comment<> $GITHUB_OUTPUT 51 | echo "$COMMENT" >> $GITHUB_OUTPUT 52 | echo "EOF" >> $GITHUB_OUTPUT 53 | 54 | - name: 💬 Post or update comment 55 | uses: actions/github-script@v8 56 | with: 57 | script: | 58 | const prNumber = ${{ steps.pr.outputs.result }}; 59 | const commentBody = `${{ steps.sizes.outputs.comment }}`; 60 | 61 | // Find existing comment 62 | const comments = await github.rest.issues.listComments({ 63 | owner: context.repo.owner, 64 | repo: context.repo.repo, 65 | issue_number: prNumber, 66 | }); 67 | 68 | const botComment = comments.data.find(comment => 69 | comment.user.type === 'Bot' && 70 | comment.body.includes('Bundle Size Comparison') 71 | ); 72 | 73 | if (botComment) { 74 | console.log(`Updating existing comment ${botComment.id}`); 75 | await github.rest.issues.updateComment({ 76 | owner: context.repo.owner, 77 | repo: context.repo.repo, 78 | comment_id: botComment.id, 79 | body: commentBody 80 | }); 81 | } else { 82 | console.log('Creating new comment'); 83 | await github.rest.issues.createComment({ 84 | owner: context.repo.owner, 85 | repo: context.repo.repo, 86 | issue_number: prNumber, 87 | body: commentBody 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /scripts/generate-completions-data.ts: -------------------------------------------------------------------------------- 1 | /** generate completion data from nitropack and Nuxt starter repo */ 2 | 3 | import { writeFile } from 'node:fs/promises' 4 | import { dirname, join } from 'node:path' 5 | import process from 'node:process' 6 | import { pathToFileURL } from 'node:url' 7 | import { resolveModulePath } from 'exsolve' 8 | 9 | import { hiddenTemplates } from '../packages/nuxi/src/utils/starter-templates.ts' 10 | 11 | interface PresetMeta { 12 | _meta?: { name: string } 13 | } 14 | 15 | const outputPath = new URL('../packages/nuxi/src/utils/completions-data.ts', import.meta.url) 16 | 17 | export async function generateCompletionData() { 18 | const data = { 19 | nitroPresets: [] as string[], 20 | templates: {} as Record, 21 | templateDefaultDirs: {} as Record, 22 | } 23 | 24 | const nitropackPath = dirname(resolveModulePath('nitropack/package.json', { from: outputPath })) 25 | const presetsPath = join(nitropackPath, 'dist/presets/_all.gen.mjs') 26 | const { default: allPresets } = await import(pathToFileURL(presetsPath).toString()) as { default: PresetMeta[] } 27 | 28 | data.nitroPresets = allPresets 29 | .map(preset => preset._meta?.name) 30 | .filter((name): name is string => Boolean(name)) 31 | .filter(name => !['base-worker', 'nitro-dev', 'nitro-prerender'].includes(name)) 32 | .filter((name, index, array) => array.indexOf(name) === index) 33 | .sort() 34 | 35 | const response = await fetch( 36 | 'https://api.github.com/repos/nuxt/starter/contents/templates?ref=templates', 37 | ) 38 | 39 | if (!response.ok) { 40 | throw new Error(`GitHub API error: ${response.status}`) 41 | } 42 | 43 | const files = await response.json() as Array<{ name: string, type: string, download_url?: string }> 44 | 45 | const jsonFiles = files.filter(file => file.type === 'file' && file.name.endsWith('.json')) 46 | 47 | for (const file of jsonFiles) { 48 | try { 49 | const templateName = file.name.replace('.json', '') 50 | if (hiddenTemplates.includes(templateName)) { 51 | continue 52 | } 53 | data.templates[templateName] = '' 54 | const fileResponse = await fetch(file.download_url!) 55 | if (fileResponse.ok) { 56 | const json = await fileResponse.json() as { description?: string, defaultDir?: string } 57 | data.templates[templateName] = json.description || '' 58 | if (json.defaultDir) { 59 | data.templateDefaultDirs[templateName] = json.defaultDir 60 | } 61 | } 62 | } 63 | catch (error) { 64 | // Skip if we can't fetch the file 65 | console.warn(`Could not fetch description for ${file.name}:`, error) 66 | } 67 | } 68 | 69 | const content = `/** Auto-generated file */ 70 | 71 | export const nitroPresets = ${JSON.stringify(data.nitroPresets, null, 2)} as const 72 | 73 | export const templates = ${JSON.stringify(data.templates, null, 2)} as const 74 | 75 | export const templateDefaultDirs = ${JSON.stringify(data.templateDefaultDirs, null, 2)} as const 76 | ` 77 | 78 | await writeFile(outputPath, content, 'utf-8') 79 | } 80 | 81 | generateCompletionData().catch((error) => { 82 | console.error('Failed to generate completion data:', error) 83 | process.exit(1) 84 | }) 85 | -------------------------------------------------------------------------------- /packages/nuxi/src/utils/formatting.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { stripVTControlCharacters } from 'node:util' 3 | import { colors } from 'consola/utils' 4 | 5 | function getStringWidth(str: string): number { 6 | const stripped = stripVTControlCharacters(str) 7 | let width = 0 8 | 9 | for (const char of stripped) { 10 | const code = char.codePointAt(0) 11 | if (!code) { 12 | continue 13 | } 14 | 15 | // Variation selectors don't add width 16 | if (code >= 0xFE00 && code <= 0xFE0F) { 17 | continue 18 | } 19 | 20 | // Emoji and wide characters (simplified heuristic) 21 | // Most emojis are in these ranges 22 | if ( 23 | (code >= 0x1F300 && code <= 0x1F9FF) // Emoticons, symbols, pictographs 24 | || (code >= 0x1F600 && code <= 0x1F64F) // Emoticons 25 | || (code >= 0x1F680 && code <= 0x1F6FF) // Transport and map symbols 26 | || (code >= 0x2600 && code <= 0x26FF) // Miscellaneous symbols (includes ❤) 27 | || (code >= 0x2700 && code <= 0x27BF) // Dingbats 28 | || (code >= 0x1F900 && code <= 0x1F9FF) // Supplemental symbols and pictographs 29 | || (code >= 0x1FA70 && code <= 0x1FAFF) // Symbols and Pictographs Extended-A 30 | ) { 31 | width += 2 32 | } 33 | else { 34 | width += 1 35 | } 36 | } 37 | 38 | return width 39 | } 40 | 41 | export function formatInfoBox(infoObj: Record): string { 42 | let firstColumnLength = 0 43 | let ansiFirstColumnLength = 0 44 | const entries = Object.entries(infoObj).map(([label, val]) => { 45 | if (label.length > firstColumnLength) { 46 | ansiFirstColumnLength = colors.bold(colors.whiteBright(label)).length + 6 47 | firstColumnLength = label.length + 6 48 | } 49 | return [label, val || '-'] as const 50 | }) 51 | 52 | // get maximum width of terminal 53 | const terminalWidth = Math.max(process.stdout.columns || 80, firstColumnLength) - 8 /* box padding + extra margin */ 54 | 55 | let boxStr = '' 56 | for (const [label, value] of entries) { 57 | const formattedValue = value 58 | .replace(/\b@([^, ]+)/g, (_, r) => colors.gray(` ${r}`)) 59 | .replace(/`([^`]*)`/g, (_, r) => r) 60 | 61 | boxStr += (`${colors.bold(colors.whiteBright(label))}`).padEnd(ansiFirstColumnLength) 62 | 63 | let boxRowLength = firstColumnLength 64 | 65 | // Split by spaces and wrap as needed 66 | const words = formattedValue.split(' ') 67 | let currentLine = '' 68 | 69 | for (const word of words) { 70 | const wordLength = getStringWidth(word) 71 | const spaceLength = currentLine ? 1 : 0 72 | 73 | if (boxRowLength + wordLength + spaceLength > terminalWidth) { 74 | // Wrap to next line 75 | if (currentLine) { 76 | boxStr += colors.cyan(currentLine) 77 | } 78 | boxStr += `\n${' '.repeat(firstColumnLength)}` 79 | currentLine = word 80 | boxRowLength = firstColumnLength + wordLength 81 | } 82 | else { 83 | currentLine += (currentLine ? ' ' : '') + word 84 | boxRowLength += wordLength + spaceLength 85 | } 86 | } 87 | 88 | if (currentLine) { 89 | boxStr += colors.cyan(currentLine) 90 | } 91 | 92 | boxStr += '\n' 93 | } 94 | 95 | return boxStr 96 | } 97 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/module/_utils.ts: -------------------------------------------------------------------------------- 1 | import { parseINI } from 'confbox' 2 | import { $fetch } from 'ofetch' 3 | import { 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 function getRegistryFromContent(content: string, scope: string | null) { 119 | try { 120 | const npmConfig = parseINI>(content) 121 | 122 | if (scope) { 123 | const scopeKey = `${scope}:registry` 124 | if (npmConfig[scopeKey]) { 125 | return npmConfig[scopeKey].trim() 126 | } 127 | } 128 | 129 | if (npmConfig.registry) { 130 | return npmConfig.registry.trim() 131 | } 132 | 133 | return null 134 | } 135 | catch { 136 | return null 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /.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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 16 | with: 17 | fetch-depth: 0 18 | - run: corepack enable 19 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 20 | with: 21 | node-version: lts/* 22 | cache: pnpm 23 | 24 | - name: 📦 Install dependencies 25 | run: pnpm install 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - name: 🔠 Lint project 30 | run: pnpm lint 31 | 32 | - name: ✂️ Knip project 33 | run: pnpm test:knip 34 | 35 | # - name: ⚙️ Check package engines 36 | # run: pnpm test:engines 37 | 38 | ci: 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | os: [ubuntu-latest, macos-latest, windows-latest] 43 | runs-on: ${{ matrix.os }} 44 | steps: 45 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 46 | with: 47 | fetch-depth: 0 48 | - run: corepack enable 49 | - uses: actions/setup-node@v6 50 | with: 51 | node-version: lts/-1 52 | cache: pnpm 53 | - uses: oven-sh/setup-bun@v2 54 | with: 55 | bun-version: latest 56 | - uses: denoland/setup-deno@v2 57 | with: 58 | deno-version: 2.6.0 59 | 60 | - name: 📦 Install dependencies 61 | run: pnpm install 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | 65 | - name: 🛠 Build project 66 | run: pnpm build 67 | 68 | - name: 💪 Test types 69 | run: pnpm test:types 70 | 71 | - name: 🧪 Test built `nuxi` 72 | run: pnpm test:dist 73 | 74 | - name: 👷♂️ Prepare playground 75 | run: pnpm nuxt prepare playground 76 | 77 | - name: 🧪 Test (unit) 78 | run: pnpm test:unit 79 | 80 | - if: matrix.os != 'windows-latest' 81 | uses: codecov/codecov-action@v5 82 | 83 | release: 84 | runs-on: ubuntu-latest 85 | permissions: 86 | id-token: write 87 | if: github.repository_owner == 'nuxt' 88 | steps: 89 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 90 | with: 91 | fetch-depth: 0 92 | - run: corepack enable 93 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 94 | with: 95 | node-version: latest 96 | cache: pnpm 97 | 98 | - name: 📦 Install dependencies 99 | run: pnpm install 100 | env: 101 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 102 | 103 | - name: 🛠 Build project 104 | run: pnpm build 105 | 106 | - name: 📦 release pkg.pr.new 107 | run: pnpm pkg-pr-new publish --compact --template './playground' ./packages/create-nuxt ./packages/nuxi ./packages/nuxt-cli 108 | 109 | - name: 📦 release nightly 110 | if: github.event_name == 'push' 111 | run: node ./scripts/release.mjs 112 | env: 113 | RELEASE_TYPE: nightly 114 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/build.ts: -------------------------------------------------------------------------------- 1 | import type { Nitro } from 'nitropack' 2 | 3 | import process from 'node:process' 4 | 5 | import { intro, outro } from '@clack/prompts' 6 | import { defineCommand } from 'citty' 7 | import { colors } from 'consola/utils' 8 | import { relative, resolve } from 'pathe' 9 | 10 | import { showVersions } from '../utils/banner' 11 | import { overrideEnv } from '../utils/env' 12 | import { clearBuildDir } from '../utils/fs' 13 | import { loadKit } from '../utils/kit' 14 | import { logger } from '../utils/logger' 15 | import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' 16 | 17 | export default defineCommand({ 18 | meta: { 19 | name: 'build', 20 | description: 'Build Nuxt for production deployment', 21 | }, 22 | args: { 23 | ...cwdArgs, 24 | ...logLevelArgs, 25 | prerender: { 26 | type: 'boolean', 27 | description: 'Build Nuxt and prerender static routes', 28 | }, 29 | preset: { 30 | type: 'string', 31 | description: 'Nitro server preset', 32 | }, 33 | ...dotEnvArgs, 34 | ...envNameArgs, 35 | ...extendsArgs, 36 | ...legacyRootDirArgs, 37 | }, 38 | async run(ctx) { 39 | overrideEnv('production') 40 | 41 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 42 | 43 | intro(colors.cyan('Building Nuxt for production...')) 44 | 45 | const kit = await loadKit(cwd) 46 | 47 | await showVersions(cwd, kit) 48 | const nuxt = await kit.loadNuxt({ 49 | cwd, 50 | dotenv: { 51 | cwd, 52 | fileName: ctx.args.dotenv, 53 | }, 54 | envName: ctx.args.envName, // c12 will fall back to NODE_ENV 55 | overrides: { 56 | logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose', 57 | // TODO: remove in 3.8 58 | _generate: ctx.args.prerender, 59 | nitro: { 60 | static: ctx.args.prerender, 61 | preset: ctx.args.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET, 62 | }, 63 | ...(ctx.args.extends && { extends: ctx.args.extends }), 64 | ...ctx.data?.overrides, 65 | }, 66 | }) 67 | 68 | let nitro: Nitro | undefined 69 | // In Bridge, if Nitro is not enabled, useNitro will throw an error 70 | try { 71 | // Use ? for backward compatibility for Nuxt <= RC.10 72 | nitro = kit.useNitro?.() 73 | if (nitro) { 74 | logger.info(`Nitro preset: ${colors.cyan(nitro.options.preset)}`) 75 | } 76 | } 77 | catch { 78 | // 79 | } 80 | 81 | await clearBuildDir(nuxt.options.buildDir) 82 | 83 | await kit.writeTypes(nuxt) 84 | 85 | nuxt.hook('build:error', (err) => { 86 | logger.error(`Nuxt build error: ${err}`) 87 | process.exit(1) 88 | }) 89 | 90 | await kit.buildNuxt(nuxt) 91 | 92 | if (ctx.args.prerender) { 93 | if (!nuxt.options.ssr) { 94 | logger.warn(`HTML content not prerendered because ${colors.cyan('ssr: false')} was set.`) 95 | logger.info(`You can read more in ${colors.cyan('https://nuxt.com/docs/getting-started/deployment#static-hosting')}.`) 96 | } 97 | // TODO: revisit later if/when nuxt build --prerender will output hybrid 98 | const dir = nitro?.options.output.publicDir 99 | const publicDir = dir ? relative(process.cwd(), dir) : '.output/public' 100 | outro(`✨ You can now deploy ${colors.cyan(publicDir)} to any static hosting!`) 101 | } 102 | else { 103 | outro('✨ Build complete!') 104 | } 105 | }, 106 | }) 107 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/add.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, promises as fsp } from 'node:fs' 2 | import process from 'node:process' 3 | 4 | import { cancel, intro, outro } from '@clack/prompts' 5 | import { defineCommand } from 'citty' 6 | import { colors } from 'consola/utils' 7 | import { dirname, extname, resolve } from 'pathe' 8 | 9 | import { loadKit } from '../utils/kit' 10 | import { logger } from '../utils/logger' 11 | import { relativeToProcess } from '../utils/paths' 12 | import { templates } from '../utils/templates/index' 13 | import { cwdArgs, logLevelArgs } from './_shared' 14 | 15 | const templateNames = Object.keys(templates) 16 | 17 | export default defineCommand({ 18 | meta: { 19 | name: 'add', 20 | description: 'Create a new template file.', 21 | }, 22 | args: { 23 | ...cwdArgs, 24 | ...logLevelArgs, 25 | force: { 26 | type: 'boolean', 27 | description: 'Force override file if it already exists', 28 | default: false, 29 | }, 30 | template: { 31 | type: 'positional', 32 | required: true, 33 | valueHint: templateNames.join('|'), 34 | description: `Specify which template to generate`, 35 | }, 36 | name: { 37 | type: 'positional', 38 | required: true, 39 | description: 'Specify name of the generated file', 40 | }, 41 | }, 42 | async run(ctx) { 43 | const cwd = resolve(ctx.args.cwd) 44 | 45 | intro(colors.cyan('Adding template...')) 46 | 47 | const templateName = ctx.args.template 48 | 49 | // Validate template name 50 | if (!templateNames.includes(templateName)) { 51 | const templateNames = Object.keys(templates).map(name => colors.cyan(name)) 52 | const lastTemplateName = templateNames.pop() 53 | logger.error(`Template ${colors.cyan(templateName)} is not supported.`) 54 | logger.info(`Possible values are ${templateNames.join(', ')} or ${lastTemplateName}.`) 55 | process.exit(1) 56 | } 57 | 58 | // Validate options 59 | const ext = extname(ctx.args.name) 60 | const name 61 | = ext === '.vue' || ext === '.ts' 62 | ? ctx.args.name.replace(ext, '') 63 | : ctx.args.name 64 | 65 | if (!name) { 66 | cancel('name argument is missing!') 67 | process.exit(1) 68 | } 69 | 70 | // Load config in order to respect srcDir 71 | const kit = await loadKit(cwd) 72 | const config = await kit.loadNuxtConfig({ cwd }) 73 | 74 | // Resolve template 75 | const template = templates[templateName as keyof typeof templates] 76 | 77 | const res = template({ name, args: ctx.args, nuxtOptions: config }) 78 | 79 | // Ensure not overriding user code 80 | if (!ctx.args.force && existsSync(res.path)) { 81 | logger.error(`File exists at ${colors.cyan(relativeToProcess(res.path))}.`) 82 | logger.info(`Use ${colors.cyan('--force')} to override or use a different name.`) 83 | process.exit(1) 84 | } 85 | 86 | // Ensure parent directory exists 87 | const parentDir = dirname(res.path) 88 | if (!existsSync(parentDir)) { 89 | logger.step(`Creating directory ${colors.cyan(relativeToProcess(parentDir))}.`) 90 | if (templateName === 'page') { 91 | logger.info('This enables vue-router functionality!') 92 | } 93 | await fsp.mkdir(parentDir, { recursive: true }) 94 | } 95 | 96 | // Write file 97 | await fsp.writeFile(res.path, `${res.contents.trim()}\n`) 98 | logger.success(`Created ${colors.cyan(relativeToProcess(res.path))}.`) 99 | outro(`Generated a new ${colors.cyan(templateName)}!`) 100 | }, 101 | }) 102 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/module/search.ts: -------------------------------------------------------------------------------- 1 | import { box } from '@clack/prompts' 2 | import { defineCommand } from 'citty' 3 | import { colors } from 'consola/utils' 4 | import Fuse from 'fuse.js' 5 | import { kebabCase, upperFirst } from 'scule' 6 | 7 | import { formatInfoBox } from '../../utils/formatting' 8 | import { logger } from '../../utils/logger' 9 | import { getNuxtVersion } from '../../utils/versions' 10 | import { cwdArgs } from '../_shared' 11 | import { checkNuxtCompatibility, fetchModules } from './_utils' 12 | 13 | const { format: formatNumber } = Intl.NumberFormat('en-GB', { 14 | notation: 'compact', 15 | maximumFractionDigits: 1, 16 | }) 17 | 18 | export default defineCommand({ 19 | meta: { 20 | name: 'search', 21 | description: 'Search in Nuxt modules', 22 | }, 23 | args: { 24 | ...cwdArgs, 25 | query: { 26 | type: 'positional', 27 | description: 'keywords to search for', 28 | required: true, 29 | }, 30 | nuxtVersion: { 31 | type: 'string', 32 | description: 33 | 'Filter by Nuxt version and list compatible modules only (auto detected by default)', 34 | required: false, 35 | valueHint: '2|3', 36 | }, 37 | }, 38 | async setup(ctx) { 39 | const nuxtVersion = await getNuxtVersion(ctx.args.cwd) 40 | return findModuleByKeywords(ctx.args._.join(' '), nuxtVersion) 41 | }, 42 | }) 43 | 44 | async function findModuleByKeywords(query: string, nuxtVersion: string) { 45 | const allModules = await fetchModules() 46 | const compatibleModules = allModules.filter(m => 47 | checkNuxtCompatibility(m, nuxtVersion), 48 | ) 49 | const fuse = new Fuse(compatibleModules, { 50 | threshold: 0.1, 51 | keys: [ 52 | { name: 'name', weight: 1 }, 53 | { name: 'npm', weight: 1 }, 54 | { name: 'repo', weight: 1 }, 55 | { name: 'tags', weight: 1 }, 56 | { name: 'category', weight: 1 }, 57 | { name: 'description', weight: 0.5 }, 58 | { name: 'maintainers.name', weight: 0.5 }, 59 | { name: 'maintainers.github', weight: 0.5 }, 60 | ], 61 | }) 62 | 63 | const results = fuse.search(query).map((result) => { 64 | const res: Record = { 65 | name: result.item.name, 66 | package: result.item.npm, 67 | homepage: colors.cyan(result.item.website), 68 | compatibility: `nuxt: ${result.item.compatibility?.nuxt || '*'}`, 69 | repository: result.item.github, 70 | description: result.item.description, 71 | install: `npx nuxt module add ${result.item.name}`, 72 | stars: colors.yellow(formatNumber(result.item.stats.stars)), 73 | monthlyDownloads: colors.yellow(formatNumber(result.item.stats.downloads)), 74 | } 75 | if (result.item.github === result.item.website) { 76 | delete res.homepage 77 | } 78 | if (result.item.name === result.item.npm) { 79 | delete res.packageName 80 | } 81 | return res 82 | }) 83 | 84 | if (!results.length) { 85 | logger.info( 86 | `No Nuxt modules found matching query ${colors.magenta(query)} for Nuxt ${colors.cyan(nuxtVersion)}`, 87 | ) 88 | return 89 | } 90 | 91 | logger.success( 92 | `Found ${results.length} Nuxt ${results.length > 1 ? 'modules' : 'module'} matching ${colors.cyan(query)} ${nuxtVersion ? `for Nuxt ${colors.cyan(nuxtVersion)}` : ''}:\n`, 93 | ) 94 | for (const foundModule of results) { 95 | const formattedModule: Record = {} 96 | for (const [key, val] of Object.entries(foundModule)) { 97 | const label = upperFirst(kebabCase(key)).replace(/-/g, ' ') 98 | formattedModule[label] = val 99 | } 100 | const title = formattedModule.Name || formattedModule.Package 101 | delete formattedModule.Name 102 | const boxContent = formatInfoBox(formattedModule) 103 | box( 104 | `\n${boxContent}`, 105 | ` ${title} `, 106 | { 107 | contentAlign: 'left', 108 | titleAlign: 'left', 109 | width: 'auto', 110 | titlePadding: 2, 111 | contentPadding: 2, 112 | rounded: true, 113 | }, 114 | ) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /packages/nuxi/src/dev/index.ts: -------------------------------------------------------------------------------- 1 | import type { NuxtConfig } from '@nuxt/schema' 2 | import type { Listener, ListenOptions } from 'listhen' 3 | import type { NuxtDevContext, NuxtDevIPCMessage, NuxtParentIPCMessage } from './utils' 4 | 5 | import process from 'node:process' 6 | import defu from 'defu' 7 | import { overrideEnv } from '../utils/env.ts' 8 | import { NuxtDevServer } from './utils' 9 | 10 | const start = Date.now() 11 | 12 | interface InitializeOptions { 13 | data?: { 14 | overrides?: NuxtConfig 15 | } 16 | listenOverrides?: Partial 17 | showBanner?: boolean 18 | } 19 | 20 | // IPC Hooks 21 | class IPC { 22 | enabled = !!process.send && !process.title?.includes('vitest') && process.env.__NUXT__FORK 23 | constructor() { 24 | // only kill process if it is a fork 25 | if (this.enabled) { 26 | process.once('unhandledRejection', (reason) => { 27 | this.send({ type: 'nuxt:internal:dev:rejection', message: reason instanceof Error ? reason.toString() : 'Unhandled Rejection' }) 28 | process.exit() 29 | }) 30 | } 31 | process.on('message', (message: NuxtParentIPCMessage) => { 32 | if (message.type === 'nuxt:internal:dev:context') { 33 | initialize(message.context, { listenOverrides: message.listenOverrides }) 34 | } 35 | }) 36 | this.send({ type: 'nuxt:internal:dev:fork-ready' }) 37 | } 38 | 39 | send(message: T) { 40 | if (this.enabled) { 41 | process.send?.(message) 42 | } 43 | } 44 | } 45 | 46 | const ipc = new IPC() 47 | 48 | interface InitializeReturn { 49 | listener: Listener 50 | close: () => Promise 51 | onReady: (callback: (address: string) => void) => void 52 | onRestart: (callback: (devServer: NuxtDevServer) => void) => void 53 | } 54 | 55 | export async function initialize(devContext: NuxtDevContext, ctx: InitializeOptions = {}): Promise { 56 | overrideEnv('development') 57 | 58 | const devServer = new NuxtDevServer({ 59 | cwd: devContext.cwd, 60 | overrides: defu( 61 | ctx.data?.overrides, 62 | ({ extends: devContext.args.extends } satisfies NuxtConfig) as NuxtConfig, 63 | ), 64 | logLevel: devContext.args.logLevel as 'silent' | 'info' | 'verbose', 65 | clear: devContext.args.clear, 66 | dotenv: { cwd: devContext.cwd, fileName: devContext.args.dotenv }, 67 | envName: devContext.args.envName, 68 | showBanner: ctx.showBanner !== false && !ipc.enabled, 69 | listenOverrides: ctx.listenOverrides, 70 | }) 71 | 72 | let address: string 73 | 74 | if (ipc.enabled) { 75 | devServer.on('loading:error', (_error) => { 76 | ipc.send({ 77 | type: 'nuxt:internal:dev:loading:error', 78 | error: { 79 | message: _error.message, 80 | stack: _error.stack, 81 | name: _error.name, 82 | code: 'code' in _error ? _error.code : undefined, 83 | }, 84 | }) 85 | }) 86 | devServer.on('loading', (message) => { 87 | ipc.send({ type: 'nuxt:internal:dev:loading', message }) 88 | }) 89 | devServer.on('restart', () => { 90 | ipc.send({ type: 'nuxt:internal:dev:restart' }) 91 | }) 92 | devServer.on('ready', (payload) => { 93 | ipc.send({ type: 'nuxt:internal:dev:ready', address: payload }) 94 | }) 95 | } 96 | else { 97 | devServer.on('ready', (payload) => { 98 | address = payload 99 | }) 100 | } 101 | 102 | // Init server 103 | await devServer.init() 104 | 105 | if (process.env.DEBUG) { 106 | // eslint-disable-next-line no-console 107 | console.debug(`Dev server (internal) initialized in ${Date.now() - start}ms`) 108 | } 109 | 110 | return { 111 | listener: devServer.listener, 112 | close: async () => { 113 | devServer.closeWatchers() 114 | await Promise.all([ 115 | devServer.listener.close(), 116 | devServer.close(), 117 | ]) 118 | }, 119 | onReady: (callback: (address: string) => void) => { 120 | if (address) { 121 | callback(address) 122 | } 123 | else { 124 | devServer.once('ready', payload => callback(payload)) 125 | } 126 | }, 127 | onRestart: (callback: (devServer: NuxtDevServer) => void) => { 128 | let restarted = false 129 | function restart() { 130 | if (!restarted) { 131 | restarted = true 132 | callback(devServer) 133 | } 134 | } 135 | devServer.once('restart', restart) 136 | process.once('uncaughtException', restart) 137 | process.once('unhandledRejection', restart) 138 | }, 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /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 { intro, note, outro, taskLog } from '@clack/prompts' 7 | import { defineCommand } from 'citty' 8 | import { colors } from 'consola/utils' 9 | import { defu } from 'defu' 10 | import { H3, lazyEventHandler } from 'h3-next' 11 | import { join, resolve } from 'pathe' 12 | import { serve } from 'srvx' 13 | 14 | import { overrideEnv } from '../utils/env' 15 | import { clearDir } from '../utils/fs' 16 | import { loadKit } from '../utils/kit' 17 | import { logger } from '../utils/logger' 18 | import { relativeToProcess } from '../utils/paths' 19 | import { cwdArgs, dotEnvArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' 20 | 21 | const indexHtml = ` 22 | 23 | 24 | 25 | 26 | Nuxt Bundle Stats (experimental) 27 | 28 | Nuxt Bundle Stats (experimental) 29 | 30 | 31 | Nitro server bundle stats 32 | 33 | 34 | Client bundle stats 35 | 36 | 37 | 38 | `.trim() 39 | 40 | export default defineCommand({ 41 | meta: { 42 | name: 'analyze', 43 | description: 'Build nuxt and analyze production bundle (experimental)', 44 | }, 45 | args: { 46 | ...cwdArgs, 47 | ...logLevelArgs, 48 | ...legacyRootDirArgs, 49 | ...dotEnvArgs, 50 | ...extendsArgs, 51 | name: { 52 | type: 'string', 53 | description: 'Name of the analysis', 54 | default: 'default', 55 | valueHint: 'name', 56 | }, 57 | serve: { 58 | type: 'boolean', 59 | description: 'Serve the analysis results', 60 | negativeDescription: 'Skip serving the analysis results', 61 | default: true, 62 | }, 63 | }, 64 | async run(ctx) { 65 | overrideEnv('production') 66 | 67 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 68 | const name = ctx.args.name || 'default' 69 | const slug = name.trim().replace(/[^\w-]/g, '_') 70 | 71 | intro(colors.cyan('Analyzing bundle size...')) 72 | 73 | const startTime = Date.now() 74 | 75 | const { loadNuxt, buildNuxt } = await loadKit(cwd) 76 | 77 | const nuxt = await loadNuxt({ 78 | cwd, 79 | dotenv: { 80 | cwd, 81 | fileName: ctx.args.dotenv, 82 | }, 83 | overrides: defu(ctx.data?.overrides, { 84 | ...(ctx.args.extends && { extends: ctx.args.extends }), 85 | build: { 86 | analyze: { 87 | enabled: true, 88 | }, 89 | }, 90 | vite: { 91 | build: { 92 | rollupOptions: { 93 | output: { 94 | chunkFileNames: '_nuxt/[name].js', 95 | entryFileNames: '_nuxt/[name].js', 96 | }, 97 | }, 98 | }, 99 | }, 100 | logLevel: ctx.args.logLevel, 101 | }), 102 | }) 103 | 104 | const analyzeDir = nuxt.options.analyzeDir 105 | const buildDir = nuxt.options.buildDir 106 | const outDir 107 | = nuxt.options.nitro.output?.dir || join(nuxt.options.rootDir, '.output') 108 | 109 | nuxt.options.build.analyze = defu(nuxt.options.build.analyze, { 110 | filename: join(analyzeDir, 'client.html'), 111 | }) 112 | 113 | const tasklog = taskLog({ 114 | title: 'Building Nuxt with analysis enabled', 115 | retainLog: false, 116 | limit: 1, 117 | }) 118 | 119 | tasklog.message('Clearing analyze directory...') 120 | await clearDir(analyzeDir) 121 | tasklog.message('Building Nuxt...') 122 | await buildNuxt(nuxt) 123 | tasklog.success('Build complete') 124 | 125 | const endTime = Date.now() 126 | 127 | const meta: NuxtAnalyzeMeta = { 128 | name, 129 | slug, 130 | startTime, 131 | endTime, 132 | analyzeDir, 133 | buildDir, 134 | outDir, 135 | } 136 | 137 | await nuxt.callHook('build:analyze:done', meta) 138 | await fsp.writeFile(join(analyzeDir, 'meta.json'), JSON.stringify(meta, null, 2), 'utf-8') 139 | 140 | note(`${relativeToProcess(analyzeDir)}\n\nDo not deploy analyze results! Use ${colors.cyan('nuxt build')} before deploying.`, 'Build location') 141 | 142 | if (ctx.args.serve !== false && !process.env.CI) { 143 | const app = new H3() 144 | 145 | const opts = { headers: { 'content-type': 'text/html' } } 146 | const serveFile = (filePath: string) => lazyEventHandler(async () => { 147 | const contents = await fsp.readFile(filePath, 'utf-8') 148 | return () => new Response(contents, opts) 149 | }) 150 | 151 | logger.step('Starting stats server...') 152 | 153 | app.use('/client', serveFile(join(analyzeDir, 'client.html'))) 154 | app.use('/nitro', serveFile(join(analyzeDir, 'nitro.html'))) 155 | app.use(() => new Response(indexHtml, opts)) 156 | 157 | await serve(app).serve() 158 | } 159 | else { 160 | outro('✨ Analysis build complete!') 161 | } 162 | }, 163 | }) 164 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/preview.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, promises as fsp } from 'node:fs' 2 | import { dirname } from 'node:path' 3 | import process from 'node:process' 4 | 5 | import { box, outro } from '@clack/prompts' 6 | import { setupDotenv } from 'c12' 7 | import { defineCommand } from 'citty' 8 | import { colors } from 'consola/utils' 9 | import { resolve } from 'pathe' 10 | import { x } from 'tinyexec' 11 | 12 | import { loadKit } from '../utils/kit' 13 | import { logger } from '../utils/logger' 14 | import { relativeToProcess } from '../utils/paths' 15 | import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, 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 | ...extendsArgs, 27 | ...legacyRootDirArgs, 28 | port: { 29 | type: 'string', 30 | description: 'Port to listen on', 31 | alias: ['p'], 32 | }, 33 | ...dotEnvArgs, 34 | }, 35 | async run(ctx) { 36 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 37 | 38 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 39 | 40 | const { loadNuxt } = await loadKit(cwd) 41 | 42 | const resolvedOutputDir = await new Promise((res) => { 43 | loadNuxt({ 44 | cwd, 45 | dotenv: { 46 | cwd, 47 | fileName: ctx.args.dotenv, 48 | }, 49 | envName: ctx.args.envName, // c12 will fall back to NODE_ENV 50 | ready: true, 51 | overrides: { 52 | ...(ctx.args.extends && { extends: ctx.args.extends }), 53 | modules: [ 54 | function (_, nuxt) { 55 | nuxt.hook('nitro:init', (nitro) => { 56 | res(resolve(nuxt.options.srcDir || cwd, nitro.options.output.dir || '.output', 'nitro.json')) 57 | }) 58 | }, 59 | ], 60 | }, 61 | }).then(nuxt => nuxt.close()).catch(() => '') 62 | }) 63 | 64 | const defaultOutput = resolve(cwd, '.output', 'nitro.json') // for backwards compatibility 65 | 66 | const nitroJSONPaths = [resolvedOutputDir, defaultOutput].filter(Boolean) 67 | const nitroJSONPath = nitroJSONPaths.find(p => existsSync(p)) 68 | if (!nitroJSONPath) { 69 | logger.error( 70 | `Cannot find ${colors.cyan('nitro.json')}. Did you run ${colors.cyan('nuxi build')} first? Search path:\n${nitroJSONPaths.join('\n')}`, 71 | ) 72 | process.exit(1) 73 | } 74 | const outputPath = dirname(nitroJSONPath) 75 | const nitroJSON = JSON.parse(await fsp.readFile(nitroJSONPath, 'utf-8')) 76 | 77 | if (!nitroJSON.commands.preview) { 78 | logger.error('Preview is not supported for this build.') 79 | process.exit(1) 80 | } 81 | 82 | const info = [ 83 | ['Node.js:', `v${process.versions.node}`], 84 | ['Nitro preset:', nitroJSON.preset], 85 | ['Working directory:', relativeToProcess(outputPath)], 86 | ] as const 87 | const _infoKeyLen = Math.max(...info.map(([label]) => label.length)) 88 | 89 | logger.message('') 90 | box( 91 | [ 92 | '', 93 | 'You are previewing a Nuxt app. In production, do not use this CLI. ', 94 | `Instead, run ${colors.cyan(nitroJSON.commands.preview)} directly.`, 95 | '', 96 | ...info.map( 97 | ([label, value]) => 98 | `${label.padEnd(_infoKeyLen, ' ')} ${colors.cyan(value)}`, 99 | ), 100 | '', 101 | ].join('\n'), 102 | colors.yellow(' Previewing Nuxt app '), 103 | { 104 | contentAlign: 'left', 105 | titleAlign: 'left', 106 | width: 'auto', 107 | titlePadding: 2, 108 | contentPadding: 2, 109 | rounded: true, 110 | withGuide: true, 111 | formatBorder: (text: string) => colors.yellow(text), 112 | }, 113 | ) 114 | 115 | const envFileName = ctx.args.dotenv || '.env' 116 | 117 | const envExists = existsSync(resolve(cwd, envFileName)) 118 | 119 | if (envExists) { 120 | logger.info( 121 | `Loading ${colors.cyan(envFileName)}. This will not be loaded when running the server in production.`, 122 | ) 123 | await setupDotenv({ cwd, fileName: envFileName }) 124 | } 125 | else if (ctx.args.dotenv) { 126 | logger.error(`Cannot find ${colors.cyan(envFileName)}.`) 127 | } 128 | 129 | const port = ctx.args.port 130 | ?? process.env.NUXT_PORT 131 | ?? process.env.NITRO_PORT 132 | ?? process.env.PORT 133 | 134 | outro(`Running ${colors.cyan(nitroJSON.commands.preview)} in ${colors.cyan(relativeToProcess(outputPath))}`) 135 | 136 | const [command, ...commandArgs] = nitroJSON.commands.preview.split(' ') 137 | await x(command, commandArgs, { 138 | throwOnError: true, 139 | nodeOptions: { 140 | stdio: 'inherit', 141 | cwd: outputPath, 142 | env: { 143 | ...process.env, 144 | NUXT_PORT: port, 145 | NITRO_PORT: port, 146 | }, 147 | }, 148 | }) 149 | }, 150 | }) 151 | 152 | export default command 153 | -------------------------------------------------------------------------------- /scripts/parse-sizes.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs' 2 | import process from 'node:process' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | interface RollupStatsNode { 6 | renderedLength?: number 7 | gzipLength?: number 8 | brotliLength?: number 9 | } 10 | 11 | interface RollupStats { 12 | nodeParts?: Record 13 | } 14 | 15 | interface BundleSize { 16 | rendered: number 17 | gzip: number 18 | brotli: number 19 | } 20 | 21 | interface PackageComparison { 22 | name: string 23 | base: BundleSize 24 | head: BundleSize 25 | diff: { 26 | rendered: number 27 | gzip: number 28 | brotli: number 29 | } 30 | error?: string 31 | } 32 | 33 | /** 34 | * Calculate total size from Rollup stats 35 | */ 36 | function calculateTotalSize(stats: RollupStats): BundleSize { 37 | if (!stats.nodeParts) { 38 | return { rendered: 0, gzip: 0, brotli: 0 } 39 | } 40 | 41 | let totalRendered = 0 42 | let totalGzip = 0 43 | let totalBrotli = 0 44 | 45 | for (const node of Object.values(stats.nodeParts)) { 46 | totalRendered += node.renderedLength || 0 47 | totalGzip += node.gzipLength || 0 48 | totalBrotli += node.brotliLength || 0 49 | } 50 | 51 | return { rendered: totalRendered, gzip: totalGzip, brotli: totalBrotli } 52 | } 53 | 54 | /** 55 | * Format bytes to KB with 2 decimal places 56 | */ 57 | function formatBytes(bytes: number): string { 58 | return (bytes / 1024).toFixed(2) 59 | } 60 | 61 | /** 62 | * Format diff with sign and percentage 63 | */ 64 | function formatDiff(diff: number, base: number): { icon: string, sign: string, percent: string } { 65 | const percent = base ? ((diff / base) * 100).toFixed(2) : '0.00' 66 | const sign = diff > 0 ? '+' : '' 67 | const icon = diff > 0 ? '📈' : diff < 0 ? '📉' : '➡️' 68 | return { icon, sign, percent } 69 | } 70 | 71 | /** 72 | * Compare sizes for a single package 73 | */ 74 | function comparePackage(name: string, headPath: string, basePath: string): PackageComparison { 75 | try { 76 | const headStats: RollupStats = JSON.parse(readFileSync(headPath, 'utf8')) 77 | const baseStats: RollupStats = JSON.parse(readFileSync(basePath, 'utf8')) 78 | 79 | const head = calculateTotalSize(headStats) 80 | const base = calculateTotalSize(baseStats) 81 | 82 | return { 83 | name, 84 | base, 85 | head, 86 | diff: { 87 | rendered: head.rendered - base.rendered, 88 | gzip: head.gzip - base.gzip, 89 | brotli: head.brotli - base.brotli, 90 | }, 91 | } 92 | } 93 | catch (error) { 94 | return { 95 | name, 96 | base: { rendered: 0, gzip: 0, brotli: 0 }, 97 | head: { rendered: 0, gzip: 0, brotli: 0 }, 98 | diff: { rendered: 0, gzip: 0, brotli: 0 }, 99 | error: error instanceof Error ? error.message : String(error), 100 | } 101 | } 102 | } 103 | 104 | /** 105 | * Generate markdown comment for size comparison 106 | */ 107 | export function generateSizeComment(packages: string[], statsDir = process.cwd()): string { 108 | let commentBody = '## 📦 Bundle Size Comparison\n\n' 109 | 110 | for (const pkg of packages) { 111 | const headPath = `${statsDir}/head-stats/${pkg}/stats.json` 112 | const basePath = `${statsDir}/base-stats/${pkg}/stats.json` 113 | 114 | const comparison = comparePackage(pkg, headPath, basePath) 115 | 116 | if (comparison.error) { 117 | console.error(`Error processing ${pkg}:`, comparison.error) 118 | commentBody += `### ⚠️ **${pkg}**\n\nCould not compare sizes: ${comparison.error}\n\n` 119 | continue 120 | } 121 | 122 | const { icon, sign, percent } = formatDiff(comparison.diff.rendered, comparison.base.rendered) 123 | 124 | commentBody += `### ${icon} **${pkg}**\n\n` 125 | commentBody += `| Metric | Base | Head | Diff |\n` 126 | commentBody += `|--------|------|------|------|\n` 127 | commentBody += `| Rendered | ${formatBytes(comparison.base.rendered)} KB | ${formatBytes(comparison.head.rendered)} KB | ${sign}${formatBytes(comparison.diff.rendered)} KB (${sign}${percent}%) |\n` 128 | 129 | if (comparison.base.gzip > 0 || comparison.head.gzip > 0) { 130 | const gzipFmt = formatDiff(comparison.diff.gzip, comparison.base.gzip) 131 | commentBody += `| Gzip | ${formatBytes(comparison.base.gzip)} KB | ${formatBytes(comparison.head.gzip)} KB | ${gzipFmt.sign}${formatBytes(comparison.diff.gzip)} KB (${gzipFmt.sign}${gzipFmt.percent}%) |\n` 132 | } 133 | 134 | commentBody += '\n' 135 | } 136 | 137 | return commentBody 138 | } 139 | 140 | // CLI usage 141 | const isMainModule = process.argv[1] && ( 142 | import.meta.url === `file://${process.argv[1]}` 143 | || import.meta.url.endsWith(process.argv[1]) 144 | ) 145 | 146 | if (isMainModule) { 147 | const packages = process.argv.slice(2) 148 | if (packages.length === 0) { 149 | console.error('Usage: node scripts/parse-sizes.ts ...') 150 | console.error('') 151 | console.error('Example: node scripts/parse-sizes.ts nuxi nuxt-cli create-nuxt') 152 | process.exit(1) 153 | } 154 | 155 | const rootDir = fileURLToPath(new URL('..', import.meta.url)) 156 | const comment = generateSizeComment(packages, rootDir) 157 | console.log(comment) 158 | } 159 | -------------------------------------------------------------------------------- /packages/create-nuxt/test/init.spec.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'node:fs' 2 | 3 | import { readFile, rm } from 'node:fs/promises' 4 | import { tmpdir } from 'node:os' 5 | import { join } from 'node:path' 6 | import { fileURLToPath } from 'node:url' 7 | import { isWindows } from 'std-env' 8 | import { x } from 'tinyexec' 9 | import { describe, expect, it } from 'vitest' 10 | 11 | const fixtureDir = fileURLToPath(new URL('../../../playground', import.meta.url)) 12 | const createNuxt = fileURLToPath(new URL('../bin/create-nuxt.mjs', import.meta.url)) 13 | 14 | describe('init command package name slugification', () => { 15 | it('should slugify directory names with special characters', { timeout: isWindows ? 200000 : 50000 }, async () => { 16 | const dir = tmpdir() 17 | const specialDirName = 'my@special#project!' 18 | const installPath = join(dir, specialDirName) 19 | 20 | await rm(installPath, { recursive: true, force: true }) 21 | try { 22 | await x(createNuxt, [installPath, '--packageManager=pnpm', '--template=minimal', '--gitInit=false', '--preferOffline', '--install=false'], { 23 | throwOnError: true, 24 | nodeOptions: { stdio: 'inherit', cwd: fixtureDir }, 25 | }) 26 | 27 | // Check that package.json was created 28 | const packageJsonPath = join(installPath, 'package.json') 29 | expect(existsSync(packageJsonPath)).toBeTruthy() 30 | 31 | // Read package.json and verify the name was slugified 32 | const packageJsonContent = await readFile(packageJsonPath, 'utf-8') 33 | const packageJson = JSON.parse(packageJsonContent) 34 | 35 | // The name should be slugified: my@special#project! -> my-special-project 36 | expect(packageJson.name).toBe('my-special-project') 37 | } 38 | finally { 39 | await rm(installPath, { recursive: true, force: true }) 40 | } 41 | }) 42 | 43 | it('should handle consecutive special characters', { timeout: isWindows ? 200000 : 50000 }, async () => { 44 | const dir = tmpdir() 45 | const specialDirName = 'test___project@@@name!!!' 46 | const installPath = join(dir, specialDirName) 47 | 48 | await rm(installPath, { recursive: true, force: true }) 49 | try { 50 | await x(createNuxt, [installPath, '--packageManager=pnpm', '--template=minimal', '--gitInit=false', '--preferOffline', '--install=false'], { 51 | throwOnError: true, 52 | nodeOptions: { stdio: 'inherit', cwd: fixtureDir }, 53 | }) 54 | 55 | const packageJsonPath = join(installPath, 'package.json') 56 | expect(existsSync(packageJsonPath)).toBeTruthy() 57 | 58 | const packageJsonContent = await readFile(packageJsonPath, 'utf-8') 59 | const packageJson = JSON.parse(packageJsonContent) 60 | 61 | // Note: underscores are word characters (\w) so they are preserved 62 | // Only @@@!!! are replaced with hyphens, then consecutive hyphens are collapsed 63 | expect(packageJson.name).toBe('test___project-name') 64 | } 65 | finally { 66 | await rm(installPath, { recursive: true, force: true }) 67 | } 68 | }) 69 | 70 | it('should handle leading and trailing special characters', { timeout: isWindows ? 200000 : 50000 }, async () => { 71 | const dir = tmpdir() 72 | const specialDirName = '---project-name---' 73 | const installPath = join(dir, specialDirName) 74 | 75 | await rm(installPath, { recursive: true, force: true }) 76 | try { 77 | await x(createNuxt, [installPath, '--packageManager=pnpm', '--template=minimal', '--gitInit=false', '--preferOffline', '--install=false'], { 78 | throwOnError: true, 79 | nodeOptions: { stdio: 'inherit', cwd: fixtureDir }, 80 | }) 81 | 82 | const packageJsonPath = join(installPath, 'package.json') 83 | expect(existsSync(packageJsonPath)).toBeTruthy() 84 | 85 | const packageJsonContent = await readFile(packageJsonPath, 'utf-8') 86 | const packageJson = JSON.parse(packageJsonContent) 87 | 88 | // Should remove leading and trailing hyphens 89 | expect(packageJson.name).toBe('project-name') 90 | } 91 | finally { 92 | await rm(installPath, { recursive: true, force: true }) 93 | } 94 | }) 95 | 96 | it('should preserve valid package names without modification', { timeout: isWindows ? 200000 : 50000 }, async () => { 97 | const dir = tmpdir() 98 | const validDirName = 'my-valid-project-name' 99 | const installPath = join(dir, validDirName) 100 | 101 | await rm(installPath, { recursive: true, force: true }) 102 | try { 103 | await x(createNuxt, [installPath, '--packageManager=pnpm', '--template=minimal', '--gitInit=false', '--preferOffline', '--install=false'], { 104 | throwOnError: true, 105 | nodeOptions: { stdio: 'inherit', cwd: fixtureDir }, 106 | }) 107 | 108 | const packageJsonPath = join(installPath, 'package.json') 109 | expect(existsSync(packageJsonPath)).toBeTruthy() 110 | 111 | const packageJsonContent = await readFile(packageJsonPath, 'utf-8') 112 | const packageJson = JSON.parse(packageJsonContent) 113 | 114 | // Valid names should remain unchanged 115 | expect(packageJson.name).toBe('my-valid-project-name') 116 | } 117 | finally { 118 | await rm(installPath, { recursive: true, force: true }) 119 | } 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /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 | import * as versions from '../../../../src/utils/versions' 7 | 8 | const updateConfig = vi.fn(() => Promise.resolve()) 9 | const addDependency = vi.fn(() => Promise.resolve()) 10 | const detectPackageManager = vi.fn(() => Promise.resolve({ name: 'npm' })) 11 | let v3 = '3.0.0' 12 | interface CommandsType { 13 | subCommands: { 14 | // biome-ignore lint/correctness/noEmptyPattern: 15 | add: () => Promise<{ setup: (args: any) => void }> 16 | } 17 | } 18 | function applyMocks() { 19 | vi.mock('c12/update', async () => { 20 | return { 21 | updateConfig, 22 | } 23 | }) 24 | vi.mock('nypm', async () => { 25 | return { 26 | addDependency, 27 | detectPackageManager, 28 | } 29 | }) 30 | vi.mock('pkg-types', async () => { 31 | return { 32 | readPackageJSON: () => { 33 | return new Promise((resolve) => { 34 | resolve({ 35 | devDependencies: { 36 | nuxt: '3.0.0', 37 | }, 38 | }) 39 | }) 40 | }, 41 | } 42 | }) 43 | vi.mock('ofetch', async () => { 44 | return { 45 | $fetch: vi.fn(() => Promise.resolve({ 46 | 'name': '@nuxt/content', 47 | 'npm': '@nuxt/content', 48 | 'devDependencies': { 49 | nuxt: v3, 50 | }, 51 | 'dist-tags': { latest: v3 }, 52 | 'versions': { 53 | [v3]: { 54 | devDependencies: { 55 | nuxt: v3, 56 | }, 57 | }, 58 | '3.1.1': { 59 | devDependencies: { 60 | nuxt: v3, 61 | }, 62 | }, 63 | '2.9.0': { 64 | devDependencies: { 65 | nuxt: v3, 66 | }, 67 | }, 68 | '2.13.1': { 69 | devDependencies: { 70 | nuxt: v3, 71 | }, 72 | }, 73 | }, 74 | })), 75 | } 76 | }) 77 | } 78 | describe('module add', () => { 79 | beforeAll(async () => { 80 | const response = await fetch('https://registry.npmjs.org/@nuxt/content') 81 | const json = await response.json() 82 | v3 = json['dist-tags'].latest 83 | }) 84 | applyMocks() 85 | vi.spyOn(runCommands, 'runCommand').mockImplementation(vi.fn()) 86 | vi.spyOn(versions, 'getNuxtVersion').mockResolvedValue('3.0.0') 87 | vi.spyOn(utils, 'fetchModules').mockResolvedValue([ 88 | { 89 | name: 'content', 90 | npm: '@nuxt/content', 91 | compatibility: { 92 | nuxt: '3.0.0', 93 | requires: {}, 94 | versionMap: {}, 95 | }, 96 | description: '', 97 | repo: '', 98 | github: '', 99 | website: '', 100 | learn_more: '', 101 | category: '', 102 | type: 'community', 103 | maintainers: [], 104 | stats: { 105 | downloads: 0, 106 | stars: 0, 107 | maintainers: 0, 108 | contributors: 0, 109 | modules: 0, 110 | }, 111 | }, 112 | ]) 113 | 114 | it('should install Nuxt module', async () => { 115 | const addCommand = await (commands as CommandsType).subCommands.add() 116 | await addCommand.setup({ 117 | args: { 118 | cwd: '/fake-dir', 119 | _: ['content'], 120 | }, 121 | }) 122 | 123 | expect(addDependency).toHaveBeenCalledWith([`@nuxt/content@${v3}`], { 124 | cwd: '/fake-dir', 125 | dev: true, 126 | installPeerDependencies: true, 127 | packageManager: { 128 | name: 'npm', 129 | }, 130 | workspace: false, 131 | }) 132 | }) 133 | 134 | it('should convert versioned module to Nuxt module', async () => { 135 | const addCommand = await (commands as CommandsType).subCommands.add() 136 | await addCommand.setup({ 137 | args: { 138 | cwd: '/fake-dir', 139 | _: ['content@2.9.0'], 140 | }, 141 | }) 142 | 143 | expect(addDependency).toHaveBeenCalledWith(['@nuxt/content@2.9.0'], { 144 | cwd: '/fake-dir', 145 | dev: true, 146 | installPeerDependencies: true, 147 | packageManager: { 148 | name: 'npm', 149 | }, 150 | workspace: false, 151 | }) 152 | }) 153 | 154 | it('should convert major only version to full semver', async () => { 155 | const addCommand = await (commands as CommandsType).subCommands.add() 156 | await addCommand.setup({ 157 | args: { 158 | cwd: '/fake-dir', 159 | _: ['content@2'], 160 | }, 161 | }) 162 | 163 | expect(addDependency).toHaveBeenCalledWith(['@nuxt/content@2.13.1'], { 164 | cwd: '/fake-dir', 165 | dev: true, 166 | installPeerDependencies: true, 167 | packageManager: { 168 | name: 'npm', 169 | }, 170 | workspace: false, 171 | }) 172 | }) 173 | 174 | it('should convert not full version to full semver', async () => { 175 | const addCommand = await (commands as CommandsType).subCommands.add() 176 | await addCommand.setup({ 177 | args: { 178 | cwd: '/fake-dir', 179 | _: ['content@3.1'], 180 | }, 181 | }) 182 | 183 | expect(addDependency).toHaveBeenCalledWith(['@nuxt/content@3.1.1'], { 184 | cwd: '/fake-dir', 185 | dev: true, 186 | installPeerDependencies: true, 187 | packageManager: { 188 | name: 'npm', 189 | }, 190 | workspace: false, 191 | }) 192 | }) 193 | }) 194 | -------------------------------------------------------------------------------- /packages/nuxi/test/unit/file-watcher.spec.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'node:fs' 2 | import { mkdir, mkdtemp, rm, utimes, writeFile } from 'node:fs/promises' 3 | import { tmpdir } from 'node:os' 4 | import { join } from 'node:path' 5 | import { afterEach, beforeEach, describe, expect, it } from 'vitest' 6 | 7 | import { FileChangeTracker } from '../../src/dev/utils' 8 | 9 | describe('fileWatcher', () => { 10 | let tempDir: string 11 | let tempSubdir: string 12 | let testFile: string 13 | let testSubdirFile: string 14 | let fileWatcher: FileChangeTracker 15 | 16 | beforeEach(async () => { 17 | tempDir = await mkdtemp(join(tmpdir(), 'nuxt-cli-test-')) 18 | tempSubdir = join(tempDir, 'subdir') 19 | await mkdir(tempSubdir) 20 | testFile = join(tempDir, 'test-config.js') 21 | testSubdirFile = join(tempSubdir, 'test-subdir-config.js') 22 | fileWatcher = new FileChangeTracker() 23 | }) 24 | 25 | afterEach(async () => { 26 | if (existsSync(tempDir)) { 27 | await rm(tempDir, { recursive: true, force: true }) 28 | } 29 | }) 30 | 31 | it('should return true for first check of a file', async () => { 32 | await writeFile(testFile, 'initial content') 33 | 34 | const shouldEmit = fileWatcher.shouldEmitChange(testFile) 35 | expect(shouldEmit).toBe(true) 36 | }) 37 | 38 | it('should return false for first check of a file if primed', async () => { 39 | await writeFile(testFile, 'initial content') 40 | 41 | fileWatcher.prime(testFile) 42 | const shouldEmit = fileWatcher.shouldEmitChange(testFile) 43 | expect(shouldEmit).toBe(false) 44 | }) 45 | 46 | it('should return false for first check of nested files if primed in recursive mode', async () => { 47 | await writeFile(testFile, 'initial content') 48 | await writeFile(testSubdirFile, 'initial content in subdir') 49 | 50 | // Prime the directory in recursive mode 51 | fileWatcher.prime(tempDir, true) 52 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(false) 53 | expect(fileWatcher.shouldEmitChange(testSubdirFile)).toBe(false) 54 | expect(fileWatcher.shouldEmitChange(tempDir)).toBe(false) 55 | expect(fileWatcher.shouldEmitChange(tempSubdir)).toBe(false) 56 | }) 57 | 58 | it('should return true for first check of nested files if primed in non-recursive mode', async () => { 59 | await writeFile(testFile, 'initial content') 60 | await writeFile(testSubdirFile, 'initial content in subdir') 61 | 62 | // Prime the directory in recursive mode 63 | fileWatcher.prime(tempDir) 64 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(false) 65 | expect(fileWatcher.shouldEmitChange(testSubdirFile)).toBe(true) 66 | expect(fileWatcher.shouldEmitChange(tempDir)).toBe(false) 67 | expect(fileWatcher.shouldEmitChange(tempSubdir)).toBe(false) 68 | }) 69 | 70 | it('should return false for first check of a file if directory is primed', async () => { 71 | await writeFile(testFile, 'initial content') 72 | 73 | fileWatcher.prime(tempDir) 74 | const shouldEmit = fileWatcher.shouldEmitChange(testFile) 75 | expect(shouldEmit).toBe(false) 76 | // Also test the directory itself 77 | const dirShouldEmit = fileWatcher.shouldEmitChange(tempDir) 78 | expect(dirShouldEmit).toBe(false) 79 | }) 80 | 81 | it('should return false when file has not been modified', async () => { 82 | await writeFile(testFile, 'initial content') 83 | 84 | // First call should return true (new file) 85 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(true) 86 | 87 | // Second call without modification should return false 88 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(false) 89 | 90 | // Third call still should return false 91 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(false) 92 | }) 93 | 94 | it('should return true when file has been modified', async () => { 95 | await writeFile(testFile, 'initial content') 96 | 97 | // First check 98 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(true) 99 | 100 | // No modification - should return false 101 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(false) 102 | 103 | // Wait a bit and modify the file 104 | await new Promise(resolve => setTimeout(resolve, 10)) 105 | await writeFile(testFile, 'modified content') 106 | 107 | // Should return true because file was modified 108 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(true) 109 | 110 | // Subsequent check without modification should return false 111 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(false) 112 | }) 113 | 114 | it('should return true only when file has been modified, if primed', async () => { 115 | await writeFile(testFile, 'initial content') 116 | fileWatcher.prime(testFile) 117 | 118 | // First check 119 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(false) 120 | 121 | // No modification - should return false 122 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(false) 123 | 124 | // Wait a bit and modify the file 125 | await new Promise(resolve => setTimeout(resolve, 10)) 126 | await writeFile(testFile, 'modified content') 127 | 128 | // Should return true because file was modified 129 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(true) 130 | 131 | // Subsequent check without modification should return false 132 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(false) 133 | }) 134 | 135 | it('should handle file deletion gracefully', async () => { 136 | await writeFile(testFile, 'content') 137 | 138 | // First check 139 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(true) 140 | 141 | // Delete the file 142 | await rm(testFile) 143 | 144 | // Should return true when file is deleted (indicates change) 145 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(true) 146 | }) 147 | 148 | it('should detect mtime changes even with same content', async () => { 149 | await writeFile(testFile, 'same content') 150 | 151 | // First check 152 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(true) 153 | 154 | // No change 155 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(false) 156 | 157 | // Manually update mtime to simulate file modification 158 | const now = Date.now() 159 | await utimes(testFile, new Date(now), new Date(now + 1000)) 160 | 161 | // Should detect the mtime change 162 | expect(fileWatcher.shouldEmitChange(testFile)).toBe(true) 163 | }) 164 | }) 165 | -------------------------------------------------------------------------------- /packages/nuxi/src/commands/dev.ts: -------------------------------------------------------------------------------- 1 | import type { ParsedArgs } from 'citty' 2 | import type { NuxtDevContext } from '../dev/utils' 3 | 4 | import process from 'node:process' 5 | 6 | import { defineCommand } from 'citty' 7 | import { colors } from 'consola/utils' 8 | import { getArgs as getListhenArgs, parseArgs as parseListhenArgs } from 'listhen/cli' 9 | import { resolve } from 'pathe' 10 | import { satisfies } from 'semver' 11 | import { isBun, isTest } from 'std-env' 12 | 13 | import { initialize } from '../dev' 14 | import { ForkPool } from '../dev/pool' 15 | import { debug, logger } from '../utils/logger' 16 | import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared' 17 | 18 | const startTime: number | undefined = Date.now() 19 | const forkSupported = !isTest && (!isBun || isBunForkSupported()) 20 | const listhenArgs = getListhenArgs() 21 | 22 | const command = defineCommand({ 23 | meta: { 24 | name: 'dev', 25 | description: 'Run Nuxt development server', 26 | }, 27 | args: { 28 | ...cwdArgs, 29 | ...logLevelArgs, 30 | ...dotEnvArgs, 31 | ...legacyRootDirArgs, 32 | ...envNameArgs, 33 | ...extendsArgs, 34 | clear: { 35 | type: 'boolean', 36 | description: 'Clear console on restart', 37 | default: false, 38 | }, 39 | fork: { 40 | type: 'boolean', 41 | description: forkSupported ? 'Disable forked mode' : 'Enable forked mode', 42 | negativeDescription: 'Disable forked mode', 43 | default: forkSupported, 44 | alias: ['f'], 45 | }, 46 | ...{ 47 | ...listhenArgs, 48 | port: { 49 | ...listhenArgs.port, 50 | description: 'Port to listen on (default: `NUXT_PORT || NITRO_PORT || PORT || nuxtOptions.devServer.port`)', 51 | alias: ['p'], 52 | }, 53 | open: { 54 | ...listhenArgs.open, 55 | alias: ['o'], 56 | default: false, 57 | }, 58 | host: { 59 | ...listhenArgs.host, 60 | alias: ['h'], 61 | description: 'Host to listen on (default: `NUXT_HOST || NITRO_HOST || HOST || nuxtOptions.devServer?.host`)', 62 | }, 63 | clipboard: { ...listhenArgs.clipboard, default: false }, 64 | }, 65 | sslCert: { 66 | type: 'string', 67 | description: '(DEPRECATED) Use `--https.cert` instead.', 68 | }, 69 | sslKey: { 70 | type: 'string', 71 | description: '(DEPRECATED) Use `--https.key` instead.', 72 | }, 73 | }, 74 | async run(ctx) { 75 | // Prepare 76 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 77 | 78 | const listenOverrides = resolveListenOverrides(ctx.args) 79 | 80 | // Start the initial dev server in-process with listener 81 | const { listener, close, onRestart, onReady } = await initialize({ cwd, args: ctx.args }, { 82 | data: ctx.data, 83 | listenOverrides, 84 | showBanner: true, 85 | }) 86 | 87 | if (!ctx.args.fork) { 88 | return { 89 | listener, 90 | close, 91 | } 92 | } 93 | 94 | const pool = new ForkPool({ 95 | rawArgs: ctx.rawArgs, 96 | poolSize: 2, 97 | listenOverrides, 98 | }) 99 | 100 | // When ready, start warming up the fork pool 101 | onReady((_address) => { 102 | pool.startWarming() 103 | if (startTime) { 104 | debug(`Dev server ready for connections in ${Date.now() - startTime}ms`) 105 | } 106 | }) 107 | 108 | // On hard restart, use a fork from the pool 109 | let cleanupCurrentFork: (() => void) | undefined 110 | 111 | async function restartWithFork() { 112 | // Get a fork from the pool (warm if available, cold otherwise) 113 | const context: NuxtDevContext = { cwd, args: ctx.args } 114 | 115 | // Clean up previous fork if any 116 | cleanupCurrentFork?.() 117 | 118 | cleanupCurrentFork = await pool.getFork(context, (message) => { 119 | // Handle IPC messages from the fork 120 | if (message.type === 'nuxt:internal:dev:ready') { 121 | if (startTime) { 122 | debug(`Dev server ready for connections in ${Date.now() - startTime}ms`) 123 | } 124 | } 125 | else if (message.type === 'nuxt:internal:dev:restart') { 126 | // Fork is requesting another restart 127 | void restartWithFork() 128 | } 129 | else if (message.type === 'nuxt:internal:dev:rejection') { 130 | logger.info(`Restarting Nuxt due to error: ${colors.cyan(message.message)}`) 131 | void restartWithFork() 132 | } 133 | }) 134 | } 135 | 136 | onRestart(async () => { 137 | // Close the in-process dev server 138 | await close() 139 | await restartWithFork() 140 | }) 141 | 142 | return { 143 | async close() { 144 | cleanupCurrentFork?.() 145 | await Promise.all([ 146 | listener.close(), 147 | close(), 148 | ]) 149 | }, 150 | } 151 | }, 152 | }) 153 | 154 | export default command 155 | 156 | // --- Internal --- 157 | 158 | type ArgsT = Exclude< 159 | Awaited, 160 | undefined | ((...args: unknown[]) => unknown) 161 | > 162 | 163 | function resolveListenOverrides(args: ParsedArgs) { 164 | // _PORT is used by `@nuxt/test-utils` to launch the dev server on a specific port 165 | if (process.env._PORT) { 166 | return { 167 | port: process.env._PORT || 0, 168 | hostname: '127.0.0.1', 169 | showURL: false, 170 | } as const 171 | } 172 | 173 | const options = parseListhenArgs({ 174 | ...args, 175 | 'host': args.host 176 | || process.env.NUXT_HOST 177 | || process.env.NITRO_HOST 178 | || process.env.HOST!, 179 | 'port': args.port 180 | || process.env.NUXT_PORT 181 | || process.env.NITRO_PORT 182 | || process.env.PORT!, 183 | 'https': args.https !== false, 184 | 'https.cert': args['https.cert'] 185 | || args.sslCert 186 | || process.env.NUXT_SSL_CERT 187 | || process.env.NITRO_SSL_CERT!, 188 | 'https.key': args['https.key'] 189 | || args.sslKey 190 | || process.env.NUXT_SSL_KEY 191 | || process.env.NITRO_SSL_KEY!, 192 | }) 193 | 194 | return { 195 | ...options, 196 | // if the https flag is not present, https.xxx arguments are ignored. 197 | // override if https is enabled in devServer config. 198 | _https: args.https, 199 | get https(): typeof options['https'] { 200 | return this._https ? options.https : false 201 | }, 202 | } as const 203 | } 204 | 205 | function isBunForkSupported() { 206 | const bunVersion: string = (globalThis as any).Bun.version 207 | return satisfies(bunVersion, '>=1.2') 208 | } 209 | -------------------------------------------------------------------------------- /packages/nuxi/src/dev/pool.ts: -------------------------------------------------------------------------------- 1 | import type { ListenOptions } from 'listhen' 2 | import type { ChildProcess } from 'node:child_process' 3 | import type { NuxtDevContext, NuxtDevIPCMessage } from './utils' 4 | 5 | import { fork } from 'node:child_process' 6 | import process from 'node:process' 7 | import { isDeno } from 'std-env' 8 | import { debug } from '../utils/logger' 9 | 10 | interface ForkPoolOptions { 11 | rawArgs: string[] 12 | poolSize?: number 13 | listenOverrides: Partial 14 | } 15 | 16 | interface PooledFork { 17 | process: ChildProcess 18 | ready: Promise 19 | state: 'warming' | 'ready' | 'active' | 'dead' 20 | } 21 | 22 | export class ForkPool { 23 | private pool: PooledFork[] = [] 24 | private poolSize: number 25 | private rawArgs: string[] 26 | private listenOverrides: Partial 27 | private warming = false 28 | 29 | constructor(options: ForkPoolOptions) { 30 | this.rawArgs = options.rawArgs 31 | this.poolSize = options.poolSize ?? 2 32 | this.listenOverrides = options.listenOverrides 33 | 34 | // Graceful shutdown 35 | for (const signal of [ 36 | 'exit', 37 | 'SIGTERM' /* Graceful shutdown */, 38 | 'SIGINT' /* Ctrl-C */, 39 | 'SIGQUIT' /* Ctrl-\ */, 40 | ] as const) { 41 | process.once(signal, () => { 42 | this.killAll(signal === 'exit' ? 0 : signal) 43 | }) 44 | } 45 | } 46 | 47 | startWarming(): void { 48 | if (this.warming) { 49 | return 50 | } 51 | this.warming = true 52 | 53 | // Start warming forks up to pool size 54 | for (let i = 0; i < this.poolSize; i++) { 55 | this.warmFork() 56 | } 57 | } 58 | 59 | async getFork(context: NuxtDevContext, onMessage?: (message: NuxtDevIPCMessage) => void): Promise<() => void> { 60 | // Try to get a ready fork from the pool 61 | const readyFork = this.pool.find(f => f.state === 'ready') 62 | 63 | if (readyFork) { 64 | readyFork.state = 'active' 65 | if (onMessage) { 66 | this.attachMessageHandler(readyFork.process, onMessage) 67 | } 68 | await this.sendContext(readyFork.process, context) 69 | 70 | // Start warming a replacement fork 71 | if (this.warming) { 72 | this.warmFork() 73 | } 74 | 75 | return () => this.killFork(readyFork) 76 | } 77 | 78 | // No ready fork available, try a warming fork 79 | const warmingFork = this.pool.find(f => f.state === 'warming') 80 | if (warmingFork) { 81 | await warmingFork.ready 82 | warmingFork.state = 'active' 83 | if (onMessage) { 84 | this.attachMessageHandler(warmingFork.process, onMessage) 85 | } 86 | await this.sendContext(warmingFork.process, context) 87 | 88 | // Start warming a replacement fork 89 | if (this.warming) { 90 | this.warmFork() 91 | } 92 | 93 | return () => this.killFork(warmingFork) 94 | } 95 | 96 | // No forks in pool, create a cold fork 97 | debug('No pre-warmed forks available, starting cold fork') 98 | const coldFork = this.createFork() 99 | await coldFork.ready 100 | coldFork.state = 'active' 101 | if (onMessage) { 102 | this.attachMessageHandler(coldFork.process, onMessage) 103 | } 104 | await this.sendContext(coldFork.process, context) 105 | 106 | return () => this.killFork(coldFork) 107 | } 108 | 109 | private attachMessageHandler(childProc: ChildProcess, onMessage: (message: NuxtDevIPCMessage) => void): void { 110 | childProc.on('message', (message: NuxtDevIPCMessage) => { 111 | // Don't forward fork-ready messages as those are internal 112 | if (message.type !== 'nuxt:internal:dev:fork-ready') { 113 | onMessage(message) 114 | } 115 | }) 116 | } 117 | 118 | private warmFork(): void { 119 | const fork = this.createFork() 120 | fork.ready.then(() => { 121 | if (fork.state === 'warming') { 122 | fork.state = 'ready' 123 | } 124 | }).catch(() => { 125 | // Fork failed to warm, remove from pool 126 | this.removeFork(fork) 127 | }) 128 | this.pool.push(fork) 129 | } 130 | 131 | private createFork(): PooledFork { 132 | const childProc = fork(globalThis.__nuxt_cli__.devEntry!, this.rawArgs, { 133 | execArgv: ['--enable-source-maps', process.argv.find((a: string) => a.includes('--inspect'))].filter(Boolean) as string[], 134 | env: { 135 | ...process.env, 136 | __NUXT__FORK: 'true', 137 | }, 138 | }) 139 | 140 | let readyResolve: () => void 141 | let readyReject: (err: Error) => void 142 | const ready = new Promise((resolve, reject) => { 143 | readyResolve = resolve 144 | readyReject = reject 145 | }) 146 | 147 | const pooledFork: PooledFork = { 148 | process: childProc, 149 | ready, 150 | state: 'warming', 151 | } 152 | 153 | // Listen for fork-ready message 154 | childProc.on('message', (message: NuxtDevIPCMessage) => { 155 | if (message.type === 'nuxt:internal:dev:fork-ready') { 156 | readyResolve() 157 | } 158 | }) 159 | 160 | // Handle errors 161 | childProc.on('error', (err) => { 162 | readyReject(err) 163 | this.removeFork(pooledFork) 164 | }) 165 | 166 | // Handle unexpected exit 167 | childProc.on('close', (errorCode) => { 168 | if (pooledFork.state === 'active' && errorCode) { 169 | // Active fork crashed 170 | process.exit(errorCode) 171 | } 172 | this.removeFork(pooledFork) 173 | }) 174 | 175 | return pooledFork 176 | } 177 | 178 | private async sendContext(childProc: ChildProcess, context: NuxtDevContext): Promise { 179 | childProc.send({ 180 | type: 'nuxt:internal:dev:context', 181 | listenOverrides: this.listenOverrides, 182 | context, 183 | }) 184 | } 185 | 186 | private killFork(fork: PooledFork, signal: NodeJS.Signals | number = 'SIGTERM'): void { 187 | fork.state = 'dead' 188 | if (fork.process) { 189 | fork.process.kill(signal === 0 && isDeno ? 'SIGTERM' : signal) 190 | } 191 | this.removeFork(fork) 192 | } 193 | 194 | private removeFork(fork: PooledFork): void { 195 | const index = this.pool.indexOf(fork) 196 | if (index > -1) { 197 | this.pool.splice(index, 1) 198 | } 199 | } 200 | 201 | private killAll(signal: NodeJS.Signals | number): void { 202 | for (const fork of this.pool) { 203 | this.killFork(fork, signal) 204 | } 205 | } 206 | 207 | getStats() { 208 | return { 209 | total: this.pool.length, 210 | warming: this.pool.filter(f => f.state === 'warming').length, 211 | ready: this.pool.filter(f => f.state === 'ready').length, 212 | active: this.pool.filter(f => f.state === 'active').length, 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /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 { getPort } from 'get-port-please' 11 | import { isWindows } from 'std-env' 12 | import { x } from 'tinyexec' 13 | import { describe, expect, it } from 'vitest' 14 | import { fetchWithPolling } from '../utils' 15 | 16 | const fixtureDir = fileURLToPath(new URL('../../../../playground', import.meta.url)) 17 | const nuxi = fileURLToPath(new URL('../../bin/nuxi.mjs', import.meta.url)) 18 | 19 | describe('commands', () => { 20 | const tests: Record> = { 21 | _dev: 'todo', 22 | add: async () => { 23 | const file = join(fixtureDir, 'server/api/test.ts') 24 | await rm(file, { force: true }) 25 | await x(nuxi, ['add', 'api', 'test'], { 26 | throwOnError: true, 27 | nodeOptions: { stdio: 'pipe', cwd: fixtureDir }, 28 | }) 29 | expect(existsSync(file)).toBeTruthy() 30 | await rm(file, { force: true }) 31 | }, 32 | analyze: 'todo', 33 | build: async () => { 34 | const res = await x(nuxi, ['build'], { 35 | throwOnError: true, 36 | nodeOptions: { stdio: 'pipe', cwd: fixtureDir }, 37 | }) 38 | expect(res.exitCode).toBe(0) 39 | expect(existsSync(join(fixtureDir, '.output'))).toBeTruthy() 40 | expect(existsSync(join(fixtureDir, '.output/server'))).toBeTruthy() 41 | expect(existsSync(join(fixtureDir, '.output/public'))).toBeTruthy() 42 | }, 43 | cleanup: async () => { 44 | const res = await x(nuxi, ['cleanup'], { 45 | throwOnError: true, 46 | nodeOptions: { stdio: 'pipe', cwd: fixtureDir }, 47 | }) 48 | expect(res.exitCode).toBe(0) 49 | }, 50 | devtools: 'todo', 51 | module: 'todo', 52 | prepare: async () => { 53 | const res = await x(nuxi, ['prepare'], { 54 | throwOnError: true, 55 | nodeOptions: { stdio: 'pipe', cwd: fixtureDir }, 56 | }) 57 | expect(res.exitCode).toBe(0) 58 | expect(existsSync(join(fixtureDir, '.nuxt'))).toBeTruthy() 59 | expect(existsSync(join(fixtureDir, '.nuxt/types'))).toBeTruthy() 60 | }, 61 | preview: async () => { 62 | await x(nuxi, ['build'], { 63 | throwOnError: true, 64 | nodeOptions: { stdio: 'pipe', cwd: fixtureDir }, 65 | }) 66 | 67 | const port = await getPort({ host: '127.0.0.1', port: 3002 }) 68 | const previewProcess = x(nuxi, ['preview', `--host=127.0.0.1`, `--port=${port}`], { 69 | throwOnError: true, 70 | nodeOptions: { stdio: 'pipe', cwd: fixtureDir }, 71 | }) 72 | 73 | // Test that server responds 74 | const response = await fetchWithPolling(`http://127.0.0.1:${port}`) 75 | expect.soft(response?.status).toBe(200) 76 | 77 | previewProcess.kill() 78 | }, 79 | start: 'todo', 80 | test: 'todo', 81 | typecheck: async () => { 82 | const res = await x(nuxi, ['typecheck'], { 83 | throwOnError: true, 84 | nodeOptions: { stdio: 'pipe', cwd: fixtureDir }, 85 | }) 86 | expect(res.exitCode).toBe(0) 87 | }, 88 | upgrade: 'todo', 89 | dev: async () => { 90 | const controller = new AbortController() 91 | const port = await getPort({ host: '127.0.0.1', port: 3001 }) 92 | const devProcess = x(nuxi, ['dev', `--host=127.0.0.1`, `--port=${port}`], { 93 | nodeOptions: { stdio: 'pipe', cwd: fixtureDir }, 94 | signal: controller.signal, 95 | }) 96 | 97 | // Test that server responds 98 | const response = await fetchWithPolling(`http://127.0.0.1:${port}`, {}, 30, 300) 99 | expect.soft(response?.status).toBe(200) 100 | 101 | controller.abort() 102 | try { 103 | await devProcess 104 | } 105 | catch {} 106 | }, 107 | generate: async () => { 108 | const res = await x(nuxi, ['generate'], { 109 | throwOnError: true, 110 | nodeOptions: { stdio: 'pipe', cwd: fixtureDir }, 111 | }) 112 | expect(res.exitCode).toBe(0) 113 | expect(existsSync(join(fixtureDir, 'dist'))).toBeTruthy() 114 | expect(existsSync(join(fixtureDir, 'dist/index.html'))).toBeTruthy() 115 | }, 116 | init: async () => { 117 | const dir = tmpdir() 118 | const pm = 'pnpm' 119 | const installPath = join(dir, pm) 120 | 121 | await rm(installPath, { recursive: true, force: true }) 122 | try { 123 | await x(nuxi, ['init', installPath, `--packageManager=${pm}`, '--template=minimal', '--gitInit=false', '--preferOffline', '--install=false'], { 124 | throwOnError: true, 125 | nodeOptions: { stdio: 'inherit', cwd: fixtureDir }, 126 | }) 127 | const files = await readdir(installPath).catch(() => []) 128 | expect(files).toContain('nuxt.config.ts') 129 | } 130 | finally { 131 | await rm(installPath, { recursive: true, force: true }) 132 | } 133 | }, 134 | info: 'todo', 135 | } 136 | 137 | it('throws error if no command is provided', async () => { 138 | const res = await x(nuxi, [], { 139 | nodeOptions: { stdio: 'pipe', cwd: fixtureDir }, 140 | }) 141 | expect(res.exitCode).toBe(1) 142 | expect(res.stderr).toBe('[error] No command specified.\n') 143 | }) 144 | 145 | // TODO: FIXME - windows currently throws 'nuxt-foo' is not recognized as an internal or external command, operable program or batch file. 146 | it.skipIf(isWindows)('throws error if wrong command is provided', async () => { 147 | const res = await x(nuxi, ['foo'], { 148 | nodeOptions: { stdio: 'pipe', cwd: fixtureDir }, 149 | }) 150 | expect(res.exitCode).toBe(1) 151 | expect(res.stderr).toBe('[error] Unknown command `foo`\n') 152 | }) 153 | 154 | const testsToRun = Object.entries(tests).filter(([_, value]) => value !== 'todo') 155 | it.each(testsToRun)(`%s`, { timeout: isWindows ? 200000 : 50000 }, (_, test) => (test as () => Promise)()) 156 | 157 | for (const [command, value] of Object.entries(tests)) { 158 | if (value === 'todo') { 159 | it.todo(command) 160 | } 161 | } 162 | }) 163 | 164 | describe('extends support', () => { 165 | it('works with dev server', { timeout: isWindows ? 200000 : 50000 }, async () => { 166 | const controller = new AbortController() 167 | const port = await getPort({ host: '127.0.0.1', port: 3003 }) 168 | const devProcess = x(nuxi, ['dev', `--host=127.0.0.1`, `--port=${port}`, '--extends=some-layer'], { 169 | nodeOptions: { stdio: 'pipe', cwd: fixtureDir }, 170 | signal: controller.signal, 171 | }) 172 | 173 | // Test that server responds 174 | const response = await fetchWithPolling(`http://127.0.0.1:${port}/extended`, {}, 30, 300) 175 | expect.soft(response?.status).toBe(200) 176 | expect(await response?.text()).toContain('This is an extended page from a layer.') 177 | 178 | controller.abort() 179 | try { 180 | await devProcess 181 | } 182 | catch {} 183 | }) 184 | }) 185 | -------------------------------------------------------------------------------- /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 } | null }>) 231 | if (!author) { 232 | continue 233 | } 234 | if (!contributors.some(c => c.username === author.login)) { 235 | contributors.push({ name: commit.author.name, username: author.login }) 236 | } 237 | emails.add(author.email) 238 | } 239 | return contributors 240 | } 241 | -------------------------------------------------------------------------------- /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 { box } from '@clack/prompts' 8 | import { defineCommand } from 'citty' 9 | import { colors } from 'consola/utils' 10 | import { copy as copyToClipboard } from 'copy-paste' 11 | import { detectPackageManager } from 'nypm' 12 | import { resolve } from 'pathe' 13 | import { readPackageJSON } from 'pkg-types' 14 | 15 | import { isBun, isDeno, isMinimal } from 'std-env' 16 | 17 | import { version as nuxiVersion } from '../../package.json' 18 | import { getBuilder } from '../utils/banner' 19 | import { formatInfoBox } from '../utils/formatting' 20 | import { tryResolveNuxt } from '../utils/kit' 21 | import { logger } from '../utils/logger' 22 | import { getPackageManagerVersion } from '../utils/packageManagers' 23 | import { cwdArgs, legacyRootDirArgs } from './_shared' 24 | 25 | export default defineCommand({ 26 | meta: { 27 | name: 'info', 28 | description: 'Get information about Nuxt project', 29 | }, 30 | args: { 31 | ...cwdArgs, 32 | ...legacyRootDirArgs, 33 | }, 34 | async run(ctx) { 35 | // Resolve rootDir 36 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 37 | 38 | // Load Nuxt config 39 | const nuxtConfig = await getNuxtConfig(cwd) 40 | 41 | // Find nearest package.json 42 | const { dependencies = {}, devDependencies = {} } = await readPackageJSON(cwd).catch(() => ({} as PackageJson)) 43 | 44 | // Utils to query a dependency version 45 | const nuxtPath = tryResolveNuxt(cwd) 46 | async function getDepVersion(name: string) { 47 | for (const url of [cwd, nuxtPath]) { 48 | if (!url) { 49 | continue 50 | } 51 | const pkg = await readPackageJSON(name, { url }).catch(() => null) 52 | if (pkg) { 53 | return pkg.version! 54 | } 55 | } 56 | return dependencies[name] || devDependencies[name] 57 | } 58 | 59 | async function listModules(arr: NonNullable = []) { 60 | const info: string[] = [] 61 | for (let m of arr) { 62 | if (Array.isArray(m)) { 63 | m = m[0] 64 | } 65 | const name = normalizeConfigModule(m, cwd) 66 | if (name) { 67 | const npmName = name!.split('/').splice(0, 2).join('/') // @foo/bar/baz => @foo/bar 68 | const v = await getDepVersion(npmName) 69 | info.push(`\`${v ? `${name}@${v}` : name}\``) 70 | } 71 | } 72 | return info.join(', ') 73 | } 74 | 75 | // Check Nuxt version 76 | const nuxtVersion = await getDepVersion('nuxt') || await getDepVersion('nuxt-nightly') || await getDepVersion('nuxt-edge') || await getDepVersion('nuxt3') || '-' 77 | const isLegacy = nuxtVersion.startsWith('2') 78 | const builder = !isLegacy 79 | ? nuxtConfig.builder /* latest schema */ || 'vite' 80 | : (nuxtConfig as any /* nuxt v2 */).bridge?.vite 81 | ? 'vite' /* bridge vite implementation */ 82 | : (nuxtConfig as any /* nuxt v2 */).buildModules?.includes('nuxt-vite') 83 | ? 'vite' /* nuxt-vite */ 84 | : 'webpack' 85 | 86 | let packageManager = (await detectPackageManager(cwd))?.name 87 | 88 | if (packageManager) { 89 | packageManager += `@${getPackageManagerVersion(packageManager)}` 90 | } 91 | 92 | const osType = os.type() 93 | const builderInfo = typeof builder === 'string' 94 | ? getBuilder(cwd, builder) 95 | : { name: 'custom', version: '0.0.0' } 96 | 97 | const infoObj = { 98 | 'Operating system': osType === 'Darwin' ? `macOS ${os.release()}` : osType === 'Windows_NT' ? `Windows ${os.release()}` : `${osType} ${os.release()}`, 99 | 'CPU': `${os.cpus()[0]?.model || 'unknown'} (${os.cpus().length} cores)`, 100 | ...isBun 101 | // @ts-expect-error Bun global 102 | ? { 'Bun version': Bun?.version as string } 103 | : isDeno 104 | // @ts-expect-error Deno global 105 | ? { 'Deno version': Deno?.version.deno as string } 106 | : { 'Node.js version': process.version as string }, 107 | 'nuxt/cli version': nuxiVersion, 108 | 'Package manager': packageManager ?? 'unknown', 109 | 'Nuxt version': nuxtVersion, 110 | 'Nitro version': await getDepVersion('nitropack') || await getDepVersion('nitro'), 111 | 'Builder': builderInfo.name === 'custom' ? 'custom' : `${builderInfo.name.toLowerCase()}@${builderInfo.version}`, 112 | 'Config': Object.keys(nuxtConfig) 113 | .map(key => `\`${key}\``) 114 | .sort() 115 | .join(', '), 116 | 'Modules': await listModules(nuxtConfig.modules), 117 | ...isLegacy 118 | ? { 'Build modules': await listModules((nuxtConfig as any /* nuxt v2 */).buildModules || []) } 119 | : {}, 120 | } 121 | 122 | logger.info(`Nuxt root directory: ${colors.cyan(nuxtConfig.rootDir || cwd)}\n`) 123 | 124 | const boxStr = formatInfoBox(infoObj) 125 | 126 | let firstColumnLength = 0 127 | let secondColumnLength = 0 128 | const entries = Object.entries(infoObj).map(([label, val]) => { 129 | if (label.length > firstColumnLength) { 130 | firstColumnLength = label.length + 4 131 | } 132 | if ((val || '').length > secondColumnLength) { 133 | secondColumnLength = (val || '').length + 2 134 | } 135 | return [label, val || '-'] as const 136 | }) 137 | 138 | // formatted for copy-pasting into an issue 139 | let copyStr = `| ${' '.repeat(firstColumnLength)} | ${' '.repeat(secondColumnLength)} |\n| ${'-'.repeat(firstColumnLength)} | ${'-'.repeat(secondColumnLength)} |\n` 140 | for (const [label, value] of entries) { 141 | if (!isMinimal) { 142 | copyStr += `| ${`**${label}**`.padEnd(firstColumnLength)} | ${(value.includes('`') ? value : `\`${value}\``).padEnd(secondColumnLength)} |\n` 143 | } 144 | } 145 | 146 | const copied = !isMinimal && await new Promise(resolve => copyToClipboard(copyStr, err => resolve(!err))) 147 | 148 | if (copied) { 149 | box( 150 | `\n${boxStr}`, 151 | ` Nuxt project info ${colors.gray('(copied to clipboard) ')}`, 152 | { 153 | contentAlign: 'left', 154 | titleAlign: 'left', 155 | width: 'auto', 156 | titlePadding: 2, 157 | contentPadding: 2, 158 | rounded: true, 159 | }, 160 | ) 161 | } 162 | else { 163 | logger.info(`Nuxt project info:\n${copyStr}`, { withGuide: false }) 164 | } 165 | 166 | const isNuxt3 = !isLegacy 167 | const isBridge = !isNuxt3 && infoObj['Build modules']?.includes('bridge') 168 | const repo = isBridge ? 'nuxt/bridge' : 'nuxt/nuxt' 169 | const docsURL = (isNuxt3 || isBridge) ? 'https://nuxt.com' : 'https://v2.nuxt.com' 170 | logger.info(`👉 Read documentation: ${colors.cyan(docsURL)}`) 171 | if (isNuxt3 || isBridge) { 172 | logger.info(`👉 Report an issue: ${colors.cyan(`https://github.com/${repo}/issues/new?template=bug-report.yml`)}`, { 173 | spacing: 0, 174 | }) 175 | logger.info(`👉 Suggest an improvement: ${colors.cyan(`https://github.com/${repo}/discussions/new`)}`, { 176 | spacing: 0, 177 | }) 178 | } 179 | }, 180 | }) 181 | 182 | function normalizeConfigModule( 183 | module: NuxtModule | string | false | null | undefined, 184 | rootDir: string, 185 | ): string | null { 186 | if (!module) { 187 | return null 188 | } 189 | if (typeof module === 'string') { 190 | return module 191 | .split(rootDir) 192 | .pop()! // Strip rootDir 193 | .split('node_modules') 194 | .pop()! // Strip node_modules 195 | .replace(/^\//, '') 196 | } 197 | if (typeof module === 'function') { 198 | return `${module.name}()` 199 | } 200 | if (Array.isArray(module)) { 201 | return normalizeConfigModule(module[0], rootDir) 202 | } 203 | return null 204 | } 205 | 206 | async function getNuxtConfig(rootDir: string) { 207 | try { 208 | const { createJiti } = await import('jiti') 209 | const jiti = createJiti(rootDir, { 210 | interopDefault: true, 211 | // allow using `~` and `@` in `nuxt.config` 212 | alias: { 213 | '~': rootDir, 214 | '@': rootDir, 215 | }, 216 | }) 217 | ;(globalThis as any).defineNuxtConfig = (c: any) => c 218 | const result = await jiti.import('./nuxt.config', { default: true }) as NuxtConfig 219 | delete (globalThis as any).defineNuxtConfig 220 | return result 221 | } 222 | catch { 223 | // TODO: Show error as warning if it is not 404 224 | return {} 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /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 { cancel, intro, isCancel, note, outro, select, spinner, tasks } from '@clack/prompts' 7 | import { defineCommand } from 'citty' 8 | import { colors } from 'consola/utils' 9 | import { addDependency, dedupeDependencies, detectPackageManager } from 'nypm' 10 | import { resolve } from 'pathe' 11 | import { findWorkspaceDir, readPackageJSON } from 'pkg-types' 12 | 13 | import { loadKit } from '../utils/kit' 14 | import { logger } from '../utils/logger' 15 | import { cleanupNuxtDirs, nuxtVersionToGitIdentifier } from '../utils/nuxt' 16 | import { getPackageManagerVersion } from '../utils/packageManagers' 17 | import { relativeToProcess } from '../utils/paths' 18 | import { getNuxtVersion } from '../utils/versions' 19 | import { cwdArgs, legacyRootDirArgs, logLevelArgs } from './_shared' 20 | 21 | function checkNuxtDependencyType(pkg: PackageJson): 'dependencies' | 'devDependencies' { 22 | if (pkg.dependencies?.nuxt) { 23 | return 'dependencies' 24 | } 25 | if (pkg.devDependencies?.nuxt) { 26 | return 'devDependencies' 27 | } 28 | return 'dependencies' 29 | } 30 | 31 | const nuxtVersionTags = { 32 | '3.x': '3x', 33 | '4.x': 'latest', 34 | } 35 | 36 | type NuxtVersionTag = keyof typeof nuxtVersionTags 37 | 38 | function getNightlyDependency(dep: string, nuxtVersion: NuxtVersionTag) { 39 | return `${dep}@npm:${dep}-nightly@${nuxtVersionTags[nuxtVersion]}` 40 | } 41 | 42 | async function getNightlyVersion(packageNames: string[]): Promise<{ npmPackages: string[], nuxtVersion: NuxtVersionTag }> { 43 | const nuxtVersion = await select({ 44 | message: 'Which nightly Nuxt release channel do you want to install?', 45 | options: [ 46 | { value: '3.x' as const, label: '3.x' }, 47 | { value: '4.x' as const, label: '4.x' }, 48 | ], 49 | initialValue: '4.x' as const, 50 | }) 51 | 52 | if (isCancel(nuxtVersion)) { 53 | cancel('Operation cancelled.') 54 | process.exit(1) 55 | } 56 | 57 | const npmPackages = packageNames.map(p => getNightlyDependency(p, nuxtVersion)) 58 | 59 | return { npmPackages, nuxtVersion } 60 | } 61 | 62 | async function getRequiredNewVersion(packageNames: string[], channel: string): Promise<{ npmPackages: string[], nuxtVersion: NuxtVersionTag }> { 63 | switch (channel) { 64 | case 'nightly': 65 | return getNightlyVersion(packageNames) 66 | case 'v3': 67 | return { npmPackages: packageNames.map(p => `${p}@3`), nuxtVersion: '3.x' } 68 | case 'v3-nightly': 69 | return { npmPackages: packageNames.map(p => getNightlyDependency(p, '3.x')), nuxtVersion: '3.x' } 70 | case 'v4': 71 | return { npmPackages: packageNames.map(p => `${p}@4`), nuxtVersion: '4.x' } 72 | case 'v4-nightly': 73 | return { npmPackages: packageNames.map(p => getNightlyDependency(p, '4.x')), nuxtVersion: '4.x' } 74 | case 'stable': 75 | default: 76 | return { npmPackages: packageNames.map(p => `${p}@latest`), nuxtVersion: '4.x' } 77 | } 78 | } 79 | 80 | export default defineCommand({ 81 | meta: { 82 | name: 'upgrade', 83 | description: 'Upgrade Nuxt', 84 | }, 85 | args: { 86 | ...cwdArgs, 87 | ...logLevelArgs, 88 | ...legacyRootDirArgs, 89 | dedupe: { 90 | type: 'boolean', 91 | description: 'Dedupe dependencies after upgrading', 92 | }, 93 | force: { 94 | type: 'boolean', 95 | alias: 'f', 96 | description: 'Force upgrade to recreate lockfile and node_modules', 97 | }, 98 | channel: { 99 | type: 'string', 100 | alias: 'ch', 101 | default: 'stable', 102 | description: 'Specify a channel to install from (default: stable)', 103 | valueHint: 'stable|nightly|v3|v4|v4-nightly|v3-nightly', 104 | }, 105 | }, 106 | async run(ctx) { 107 | const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) 108 | 109 | intro(colors.cyan('Upgrading Nuxt ...')) 110 | 111 | // Check package manager 112 | const [packageManager, workspaceDir = cwd] = await Promise.all([detectPackageManager(cwd), findWorkspaceDir(cwd, { try: true })]) 113 | if (!packageManager) { 114 | logger.error( 115 | `Unable to determine the package manager used by this project.\n\nNo lock files found in ${colors.cyan(relativeToProcess(cwd))}, and no ${colors.cyan('packageManager')} field specified in ${colors.cyan('package.json')}.`, 116 | ) 117 | logger.info(`Please either add the ${colors.cyan('packageManager')} field to ${colors.cyan('package.json')} or execute the installation command for your package manager. For example, you can use ${colors.cyan('pnpm i')}, ${colors.cyan('npm i')}, ${colors.cyan('bun i')}, or ${colors.cyan('yarn i')}, and then try again.`) 118 | process.exit(1) 119 | } 120 | const { name: packageManagerName, lockFile: lockFileCandidates } = packageManager 121 | const packageManagerVersion = getPackageManagerVersion(packageManagerName) 122 | logger.step(`Package manager: ${colors.cyan(packageManagerName)} ${packageManagerVersion}`) 123 | 124 | // Check currently installed Nuxt version 125 | const currentVersion = (await getNuxtVersion(cwd, false)) || '[unknown]' 126 | logger.step(`Current Nuxt version: ${colors.cyan(currentVersion)}`) 127 | 128 | const pkg = await readPackageJSON(cwd).catch(() => null) 129 | 130 | // Check if Nuxt is a dependency or devDependency 131 | const nuxtDependencyType = pkg ? checkNuxtDependencyType(pkg) : 'dependencies' 132 | const corePackages = ['@nuxt/kit', '@nuxt/schema', '@nuxt/vite-builder', '@nuxt/webpack-builder', '@nuxt/rspack-builder'] 133 | 134 | const packagesToUpdate = pkg ? corePackages.filter(p => pkg.dependencies?.[p] || pkg.devDependencies?.[p]) : [] 135 | 136 | // Install latest version 137 | const { npmPackages, nuxtVersion } = await getRequiredNewVersion(['nuxt', ...packagesToUpdate], ctx.args.channel) 138 | 139 | // Force install 140 | const toRemove = ['node_modules'] 141 | 142 | const lockFile = normaliseLockFile(workspaceDir, lockFileCandidates) 143 | if (lockFile) { 144 | toRemove.push(lockFile) 145 | } 146 | 147 | const forceRemovals = toRemove 148 | .map(p => colors.cyan(p)) 149 | .join(' and ') 150 | 151 | let method: 'force' | 'dedupe' | 'skip' | undefined = ctx.args.force ? 'force' : ctx.args.dedupe ? 'dedupe' : undefined 152 | 153 | if (!method) { 154 | const result = await select({ 155 | message: `Would you like to dedupe your lockfile, or recreate ${forceRemovals}? This can fix problems with hoisted dependency versions and ensure you have the most up-to-date dependencies.`, 156 | options: [ 157 | { 158 | label: 'dedupe lockfile', 159 | value: 'dedupe' as const, 160 | hint: 'recommended', 161 | }, 162 | { 163 | label: `recreate ${forceRemovals}`, 164 | value: 'force' as const, 165 | }, 166 | { 167 | label: 'skip', 168 | value: 'skip' as const, 169 | }, 170 | ], 171 | initialValue: 'dedupe' as const, 172 | }) 173 | 174 | if (isCancel(result)) { 175 | cancel('Operation cancelled.') 176 | process.exit(1) 177 | } 178 | 179 | method = result 180 | } 181 | 182 | const versionType = ctx.args.channel === 'nightly' ? 'nightly' : `latest ${ctx.args.channel}` 183 | 184 | const spin = spinner() 185 | spin.start('Upgrading Nuxt') 186 | 187 | await tasks([ 188 | { 189 | title: `Installing ${versionType} Nuxt ${nuxtVersion} release`, 190 | task: async () => { 191 | await addDependency(npmPackages, { 192 | cwd, 193 | packageManager, 194 | dev: nuxtDependencyType === 'devDependencies', 195 | workspace: packageManager?.name === 'pnpm' && existsSync(resolve(cwd, 'pnpm-workspace.yaml')), 196 | }) 197 | return 'Nuxt packages installed' 198 | }, 199 | }, 200 | ...(method === 'force' 201 | ? [{ 202 | title: `Recreating ${forceRemovals}`, 203 | task: async () => { 204 | await dedupeDependencies({ recreateLockfile: true }) 205 | return 'Lockfile recreated' 206 | }, 207 | }] 208 | : []), 209 | ...(method === 'dedupe' 210 | ? [{ 211 | title: 'Deduping dependencies', 212 | task: async () => { 213 | await dedupeDependencies() 214 | return 'Dependencies deduped' 215 | }, 216 | }] 217 | : []), 218 | { 219 | title: 'Cleaning up build directories', 220 | task: async () => { 221 | let buildDir: string = '.nuxt' 222 | try { 223 | const { loadNuxtConfig } = await loadKit(cwd) 224 | const nuxtOptions = await loadNuxtConfig({ cwd }) 225 | buildDir = nuxtOptions.buildDir 226 | } 227 | catch { 228 | // Use default buildDir (.nuxt) 229 | } 230 | await cleanupNuxtDirs(cwd, buildDir) 231 | return 'Build directories cleaned' 232 | }, 233 | }, 234 | ]) 235 | 236 | spin.stop() 237 | 238 | if (method === 'force') { 239 | logger.info(`If you encounter any issues, revert the changes and try with ${colors.cyan('--no-force')}`) 240 | } 241 | 242 | // Check installed Nuxt version again 243 | const upgradedVersion = (await getNuxtVersion(cwd, false)) || '[unknown]' 244 | 245 | if (upgradedVersion === '[unknown]') { 246 | return 247 | } 248 | 249 | if (upgradedVersion === currentVersion) { 250 | outro(`You were already using the latest version of Nuxt (${colors.green(currentVersion)})`) 251 | } 252 | else { 253 | logger.success( 254 | `Successfully upgraded Nuxt from ${colors.cyan(currentVersion)} to ${colors.green(upgradedVersion)}`, 255 | ) 256 | if (currentVersion === '[unknown]') { 257 | return 258 | } 259 | const commitA = nuxtVersionToGitIdentifier(currentVersion) 260 | const commitB = nuxtVersionToGitIdentifier(upgradedVersion) 261 | if (commitA && commitB) { 262 | note( 263 | `https://github.com/nuxt/nuxt/compare/${commitA}...${commitB}`, 264 | 'Changelog', 265 | ) 266 | } 267 | outro('✨ Upgrade complete!') 268 | } 269 | }, 270 | }) 271 | 272 | // Find which lock file is in use since `nypm.detectPackageManager` doesn't return this 273 | function normaliseLockFile(cwd: string, lockFiles: string | Array | undefined) { 274 | if (typeof lockFiles === 'string') { 275 | lockFiles = [lockFiles] 276 | } 277 | 278 | const lockFile = lockFiles?.find(file => existsSync(resolve(cwd, file))) 279 | 280 | if (lockFile === undefined) { 281 | logger.error(`Unable to find any lock files in ${colors.cyan(relativeToProcess(cwd))}.`) 282 | return undefined 283 | } 284 | 285 | return lockFile 286 | } 287 | --------------------------------------------------------------------------------
96 | 100 | {{ l }} 101 | 102 |