From 0cca67e434f4cac4ad80b536c29f183862f57b37 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 3 Dec 2025 16:34:52 +0000 Subject: [PATCH 1/7] feat(init): prompt user to select a template --- .gitignore | 2 + eslint.config.js | 37 +-- package.json | 2 +- packages/nuxi/src/commands/add.ts | 2 +- packages/nuxi/src/commands/init.ts | 261 +++++++++++-------- packages/nuxi/src/commands/module/add.ts | 1 + packages/nuxi/src/completions-init.ts | 6 +- packages/nuxi/src/completions.ts | 7 +- packages/nuxi/src/utils/starter-templates.ts | 59 +++++ packages/nuxi/test/unit/templates.spec.ts | 2 +- scripts/generate-completions-data.ts | 47 ++-- scripts/generate-data.ts | 50 ++++ tsconfig.json | 2 +- 13 files changed, 334 insertions(+), 144 deletions(-) create mode 100644 packages/nuxi/src/utils/starter-templates.ts create mode 100644 scripts/generate-data.ts diff --git a/.gitignore b/.gitignore index 806dc20d6..affa8acf9 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ playground-bun* playground-deno* playground-node* packages/nuxi/src/utils/completions-data.ts +packages/nuxi/src/data/nitro-presets.ts +packages/nuxi/src/data/templates.ts diff --git a/eslint.config.js b/eslint.config.js index 2317a532e..c16c5955e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -13,21 +13,28 @@ export default createConfigForNuxt({ './playground', ], }, -}, await antfu()).append({ - rules: { - 'vue/singleline-html-element-content-newline': 'off', - // TODO: remove usage of `any` throughout codebase - '@typescript-eslint/no-explicit-any': 'off', - 'style/indent-binary-ops': 'off', +}, await antfu()).append( + { + ignores: ['packages/nuxi/src/data/**'] }, -}, { - files: ['playground/**'], - rules: { - 'no-console': 'off', + { + rules: { + 'vue/singleline-html-element-content-newline': 'off', + // TODO: remove usage of `any` throughout codebase + '@typescript-eslint/no-explicit-any': 'off', + 'style/indent-binary-ops': 'off', + }, }, -}, { - files: ['**/*.yml'], - rules: { - '@stylistic/spaced-comment': 'off', + { + files: ['playground/**'], + rules: { + 'no-console': 'off', + }, }, -}) + { + files: ['**/*.yml'], + rules: { + '@stylistic/spaced-comment': 'off', + }, + }, +) diff --git a/package.json b/package.json index 1f3399f0e..df6a114f1 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "nuxi": "node ./packages/nuxi/bin/nuxi.mjs", "nuxt": "node ./packages/nuxt-cli/bin/nuxi.mjs", "nuxi-bun": "bun --bun ./packages/nuxt-cli/bin/nuxi.mjs", - "postinstall": "node --experimental-strip-types ./scripts/generate-completions-data.ts && pnpm build", + "postinstall": "node --experimental-strip-types ./scripts/generate-data.ts && pnpm build", "test:types": "tsc --noEmit", "test:knip": "knip", "test:dist": "pnpm -r test:dist", diff --git a/packages/nuxi/src/commands/add.ts b/packages/nuxi/src/commands/add.ts index 215d70114..65cfdb25d 100644 --- a/packages/nuxi/src/commands/add.ts +++ b/packages/nuxi/src/commands/add.ts @@ -9,7 +9,7 @@ import { dirname, extname, resolve } from 'pathe' import { loadKit } from '../utils/kit' import { logger } from '../utils/logger' import { relativeToProcess } from '../utils/paths' -import { templates } from '../utils/templates' +import { templates } from '../utils/templates/index' import { cwdArgs, logLevelArgs } from './_shared' const templateNames = Object.keys(templates) diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index 86f280508..bc41c6269 100644 --- a/packages/nuxi/src/commands/init.ts +++ b/packages/nuxi/src/commands/init.ts @@ -19,11 +19,13 @@ import { runCommand } from '../run' import { nuxtIcon, themeColor } from '../utils/ascii' import { logger } from '../utils/logger' import { relativeToProcess } from '../utils/paths' +import { getTemplates, TemplateData } from '../utils/starter-templates' import { cwdArgs, logLevelArgs } from './_shared' +import { fetchModules } from './module/_utils' import addModuleCommand from './module/add' const DEFAULT_REGISTRY = 'https://raw.githubusercontent.com/nuxt/starter/templates/templates' -const DEFAULT_TEMPLATE_NAME = 'v4' +const DEFAULT_TEMPLATE_NAME = 'minimal' const pms: Record = { npm: undefined, @@ -36,73 +38,6 @@ const pms: Record = { // this is for type safety to prompt updating code in nuxi when nypm adds a new package manager const packageManagerOptions = Object.keys(pms) as PackageManagerName[] -async function getModuleDependencies(moduleName: string) { - try { - const response = await $fetch(`https://registry.npmjs.org/${moduleName}/latest`) - const dependencies = response.dependencies || {} - return Object.keys(dependencies) - } - catch (err) { - logger.warn(`Could not get dependencies for ${colors.cyan(moduleName)}: ${err}`) - return [] - } -} - -function filterModules(modules: string[], allDependencies: Record) { - const result = { - toInstall: [] as string[], - skipped: [] as string[], - } - - for (const module of modules) { - const isDependency = modules.some((otherModule) => { - if (otherModule === module) - return false - const deps = allDependencies[otherModule] || [] - return deps.includes(module) - }) - - if (isDependency) { - result.skipped.push(module) - } - else { - result.toInstall.push(module) - } - } - - return result -} - -async function getTemplateDependencies(templateDir: string) { - try { - const packageJsonPath = join(templateDir, 'package.json') - if (!existsSync(packageJsonPath)) { - return [] - } - const packageJson = await readPackageJSON(packageJsonPath) - const directDeps = { - ...packageJson.dependencies, - ...packageJson.devDependencies, - } - const directDepNames = Object.keys(directDeps) - const allDeps = new Set(directDepNames) - - const transitiveDepsResults = await Promise.all( - directDepNames.map(dep => getModuleDependencies(dep)), - ) - - transitiveDepsResults.forEach((deps) => { - deps.forEach(dep => allDeps.add(dep)) - }) - - return Array.from(allDeps) - } - catch (err) { - logger.warn(`Could not read template dependencies: ${err}`) - return [] - } -} - export default defineCommand({ meta: { name: 'init', @@ -164,17 +99,69 @@ export default defineCommand({ }, }, async run(ctx) { + if (!ctx.args.offline && !ctx.args.preferOffline && !ctx.args.template) { + getTemplates() + } + if (hasTTY) { process.stdout.write(`\n${nuxtIcon}\n\n`) } intro(colors.bold(`Welcome to Nuxt!`.split('').map(m => `${themeColor}${m}`).join(''))) + let availableTemplates: Record = {} + + if (!ctx.args.template || !ctx.args.dir) { + if (ctx.args.offline || ctx.args.preferOffline) { + // In offline mode, use static templates directly + availableTemplates = await import('../data/templates').then(r => r.templates) + } + else { + const templatesSpinner = spinner() + templatesSpinner.start('Loading available templates') + + availableTemplates = await getTemplates() + templatesSpinner.stop('Templates loaded') + } + } + + let templateName = ctx.args.template + if (!templateName) { + const result = await select({ + message: 'Which template would you like to use?', + options: Object.entries(availableTemplates).map(([name, data]) => { + return { + value: name, + label: data ? `${colors.whiteBright(name)} – ${data.description}` : name, + hint: name === DEFAULT_TEMPLATE_NAME ? 'recommended' : undefined, + } + }), + initialValue: DEFAULT_TEMPLATE_NAME, + }) + + if (isCancel(result)) { + cancel('Operation cancelled.') + process.exit(1) + } + + templateName = result + } + + // Fallback to default if still not set + templateName ||= DEFAULT_TEMPLATE_NAME + + if (typeof templateName !== 'string') { + logger.error('Please specify a template!') + process.exit(1) + } + + if (ctx.args.dir === '') { + const defaultDir = availableTemplates[templateName]?.defaultDir || 'nuxt-app' const result = await text({ message: 'Where would you like to create your project?', - placeholder: './nuxt-app', - defaultValue: 'nuxt-app', + placeholder: `./${defaultDir}`, + defaultValue: defaultDir, }) if (isCancel(result)) { @@ -189,14 +176,6 @@ export default defineCommand({ let templateDownloadPath = resolve(cwd, ctx.args.dir) logger.step(`Creating project in ${colors.cyan(relativeToProcess(templateDownloadPath))}`) - // Get template name - const templateName = ctx.args.template || DEFAULT_TEMPLATE_NAME - - if (typeof templateName !== 'string') { - logger.error('Please specify a template!') - process.exit(1) - } - let shouldForce = Boolean(ctx.args.force) // Prompt the user if the template download directory already exists @@ -290,10 +269,14 @@ export default defineCommand({ } if (ctx.args.nightly !== undefined && !ctx.args.offline && !ctx.args.preferOffline) { + const nightlySpinner = spinner() + nightlySpinner.start('Fetching nightly version info') + const response = await $fetch<{ 'dist-tags': Record }>('https://registry.npmjs.org/nuxt-nightly') const nightlyChannelTag = ctx.args.nightly || 'latest' if (!nightlyChannelTag) { + nightlySpinner.stop('Failed to get nightly channel tag', 1) logger.error(`Error getting nightly channel tag.`) process.exit(1) } @@ -301,6 +284,7 @@ export default defineCommand({ const nightlyChannelVersion = response['dist-tags'][nightlyChannelTag] if (!nightlyChannelVersion) { + nightlySpinner.stop('Nightly version not found', 1) logger.error(`Nightly channel version for tag ${colors.cyan(nightlyChannelTag)} not found.`) process.exit(1) } @@ -318,17 +302,7 @@ export default defineCommand({ } await writePackageJSON(join(packageJsonPath, 'package.json'), packageJson) - } - - function detectCurrentPackageManager() { - const userAgent = process.env.npm_config_user_agent - if (!userAgent) { - return - } - const [name] = userAgent.split('/') - if (packageManagerOptions.includes(name as PackageManagerName)) { - return name as PackageManagerName - } + nightlySpinner.stop(`Updated to nightly version ${colors.cyan(nightlyChannelVersion)}`) } const currentPackageManager = detectCurrentPackageManager() @@ -431,21 +405,18 @@ export default defineCommand({ // Get modules from arg (if provided) if (ctx.args.modules !== undefined) { - modulesToAdd.push( - // ctx.args.modules is false when --no-modules is used - ...(ctx.args.modules || '').split(',').map(module => module.trim()).filter(Boolean), - ) + // ctx.args.modules is false when --no-modules is used + for (const segment of (ctx.args.modules || '').split(',')) { + const mod = segment.trim() + if (mod) { + modulesToAdd.push(mod) + } + } } + // ...or offer to install official modules (if not offline) else if (!ctx.args.offline && !ctx.args.preferOffline) { - const modulesPromise = $fetch<{ - modules: { - npm: string - type: 'community' | 'official' - description: string - }[] - }>('https://api.nuxt.com/modules') - + const modulesPromise = fetchModules() const wantsUserModules = await confirm({ message: `Would you like to install any of the official modules?`, initialValue: false, @@ -457,14 +428,18 @@ export default defineCommand({ } if (wantsUserModules) { + const modulesSpinner = spinner() + modulesSpinner.start('Fetching available modules') + const [response, templateDeps] = await Promise.all([ modulesPromise, getTemplateDependencies(template.dir), ]) - const officialModules = response.modules - .filter(module => module.type === 'official' && module.npm !== '@nuxt/devtools') - .filter(module => !templateDeps.includes(module.npm)) + modulesSpinner.stop('Modules loaded') + + const officialModules = response + .filter(module => module.type === 'official' && module.npm !== '@nuxt/devtools' && !templateDeps.includes(module.npm)) if (officialModules.length === 0) { logger.info('All official modules are already included in this template.') @@ -543,3 +518,81 @@ export default defineCommand({ } }, }) + +async function getModuleDependencies(moduleName: string) { + try { + const response = await $fetch(`https://registry.npmjs.org/${moduleName}/latest`) + const dependencies = response.dependencies || {} + return Object.keys(dependencies) + } + catch (err) { + logger.warn(`Could not get dependencies for ${colors.cyan(moduleName)}: ${err}`) + return [] + } +} + +function filterModules(modules: string[], allDependencies: Record) { + const result = { + toInstall: [] as string[], + skipped: [] as string[], + } + + for (const module of modules) { + const isDependency = modules.some((otherModule) => { + if (otherModule === module) + return false + const deps = allDependencies[otherModule] || [] + return deps.includes(module) + }) + + if (isDependency) { + result.skipped.push(module) + } + else { + result.toInstall.push(module) + } + } + + return result +} + +async function getTemplateDependencies(templateDir: string) { + try { + const packageJsonPath = join(templateDir, 'package.json') + if (!existsSync(packageJsonPath)) { + return [] + } + const packageJson = await readPackageJSON(packageJsonPath) + const directDeps = { + ...packageJson.dependencies, + ...packageJson.devDependencies, + } + const directDepNames = Object.keys(directDeps) + const allDeps = new Set(directDepNames) + + const transitiveDepsResults = await Promise.all( + directDepNames.map(dep => getModuleDependencies(dep)), + ) + + transitiveDepsResults.forEach((deps) => { + deps.forEach(dep => allDeps.add(dep)) + }) + + return Array.from(allDeps) + } + catch (err) { + logger.warn(`Could not read template dependencies: ${err}`) + return [] + } +} + +function detectCurrentPackageManager() { + const userAgent = process.env.npm_config_user_agent + if (!userAgent) { + return + } + const [name] = userAgent.split('/') + if (packageManagerOptions.includes(name as PackageManagerName)) { + return name as PackageManagerName + } +} diff --git a/packages/nuxi/src/commands/module/add.ts b/packages/nuxi/src/commands/module/add.ts index 34b8df67a..ab4cd0037 100644 --- a/packages/nuxi/src/commands/module/add.ts +++ b/packages/nuxi/src/commands/module/add.ts @@ -307,6 +307,7 @@ async function resolveModule(moduleName: string, cwd: string): Promise(command: CommandDef) { const completion = await tab(command) @@ -8,8 +8,8 @@ export async function setupInitCompletions(command: const templateOption = completion.options?.get('template') if (templateOption) { templateOption.handler = (complete) => { - for (const template of templates) { - complete(template, '') + for (const template in templates) { + complete(template, templates[template as 'content']?.description || '') } } } diff --git a/packages/nuxi/src/completions.ts b/packages/nuxi/src/completions.ts index f593b0320..9c6dc4d35 100644 --- a/packages/nuxi/src/completions.ts +++ b/packages/nuxi/src/completions.ts @@ -1,6 +1,7 @@ import type { ArgsDef, CommandDef } from 'citty' import tab from '@bomb.sh/tab/citty' -import { nitroPresets, templates } from './utils/completions-data' +import { nitroPresets } from './data/nitro-presets' +import { templates } from './data/templates' export async function initCompletions(command: CommandDef) { const completion = await tab(command) @@ -43,8 +44,8 @@ export async function initCompletions(command: Comm const templateOption = initCommand.options.get('template') if (templateOption) { templateOption.handler = (complete) => { - for (const template of templates) { - complete(template, '') + for (const template in templates) { + complete(template, templates[template as 'content']?.description || '') } } } diff --git a/packages/nuxi/src/utils/starter-templates.ts b/packages/nuxi/src/utils/starter-templates.ts new file mode 100644 index 000000000..3f8b72c47 --- /dev/null +++ b/packages/nuxi/src/utils/starter-templates.ts @@ -0,0 +1,59 @@ +import { $fetch } from 'ofetch' + +export const hiddenTemplates = [ + 'doc-driven', + 'v4', + 'v4-compat', + 'v2-bridge', + 'v3', + 'ui-vue', + 'module-devtools', + 'layer', + 'hub', +] + +export interface TemplateData { + name: string + description: string + defaultDir: string + url: string + tar: string +} + +const fetchOptions = { + timeout: 3000, + responseType: 'json', + headers: { + 'user-agent': '@nuxt/cli', + }, +} as const + +let templatesCache: Promise> | null = null + +export async function getTemplates() { + templatesCache ||= fetchTemplates() + return templatesCache +} + +export async function fetchTemplates() { + const templates = {} as Record + + const files = await $fetch>( + 'https://api.github.com/repos/nuxt/starter/contents/templates?ref=templates', + fetchOptions, + ) + + await Promise.all(files.map(async (file) => { + if (!file.download_url || file.type !== 'file' || !file.name.endsWith('.json')) { + return + } + const templateName = file.name.replace('.json', '') + if (hiddenTemplates.includes(templateName)) { + return + } + templates[templateName] = undefined as unknown as TemplateData + templates[templateName] = await $fetch(file.download_url, fetchOptions) + })) + + return templates +} diff --git a/packages/nuxi/test/unit/templates.spec.ts b/packages/nuxi/test/unit/templates.spec.ts index 1aee837ab..8af95ff07 100644 --- a/packages/nuxi/test/unit/templates.spec.ts +++ b/packages/nuxi/test/unit/templates.spec.ts @@ -1,7 +1,7 @@ import type { NuxtOptions } from '@nuxt/schema' import { describe, expect, it } from 'vitest' -import { templates } from '../../src/utils/templates' +import { templates } from '../../src/utils/templates/index' describe('templates', () => { it('composables', () => { diff --git a/scripts/generate-completions-data.ts b/scripts/generate-completions-data.ts index 4cc56214a..ba24f5878 100644 --- a/scripts/generate-completions-data.ts +++ b/scripts/generate-completions-data.ts @@ -6,6 +6,8 @@ import process from 'node:process' import { pathToFileURL } from 'node:url' import { resolveModulePath } from 'exsolve' +import { hiddenTemplates } from '../packages/nuxi/src/utils/starter-templates.ts' + interface PresetMeta { _meta?: { name: string } } @@ -13,7 +15,11 @@ interface PresetMeta { const outputPath = new URL('../packages/nuxi/src/utils/completions-data.ts', import.meta.url) export async function generateCompletionData() { - const data = { nitroPresets: [] as string[], templates: [] as string[] } + const data = { + nitroPresets: [] as string[], + templates: {} as Record, + templateDefaultDirs: {} as Record, + } const nitropackPath = dirname(resolveModulePath('nitropack/package.json', { from: outputPath })) const presetsPath = join(nitropackPath, 'dist/presets/_all.gen.mjs') @@ -34,28 +40,39 @@ export async function generateCompletionData() { throw new Error(`GitHub API error: ${response.status}`) } - const files = await response.json() as Array<{ name: string, type: string }> + const files = await response.json() as Array<{ name: string, type: string, download_url?: string }> - const templateEntries = files - .filter((file) => { - if (file.type === 'dir') - return true - if (file.type === 'file' && file.name.endsWith('.json') && file.name !== 'content.json') { - return true - } - return false - }) - .map(file => file.name.replace('.json', '')) + const jsonFiles = files.filter(file => file.type === 'file' && file.name.endsWith('.json')) - data.templates = Array.from(new Set(templateEntries)) - .filter(name => name !== 'community') - .sort() + for (const file of jsonFiles) { + try { + const templateName = file.name.replace('.json', '') + if (hiddenTemplates.includes(templateName)) { + continue + } + data.templates[templateName] = '' + const fileResponse = await fetch(file.download_url!) + if (fileResponse.ok) { + const json = await fileResponse.json() as { description?: string, defaultDir?: string } + data.templates[templateName] = json.description || '' + if (json.defaultDir) { + data.templateDefaultDirs[templateName] = json.defaultDir + } + } + } + catch (error) { + // Skip if we can't fetch the file + console.warn(`Could not fetch description for ${file.name}:`, error) + } + } const content = `/** Auto-generated file */ export const nitroPresets = ${JSON.stringify(data.nitroPresets, null, 2)} as const export const templates = ${JSON.stringify(data.templates, null, 2)} as const + +export const templateDefaultDirs = ${JSON.stringify(data.templateDefaultDirs, null, 2)} as const ` await writeFile(outputPath, content, 'utf-8') diff --git a/scripts/generate-data.ts b/scripts/generate-data.ts new file mode 100644 index 000000000..0929730f1 --- /dev/null +++ b/scripts/generate-data.ts @@ -0,0 +1,50 @@ +/** generate completion data from nitropack and Nuxt starter repo */ + +import { mkdir, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import process from 'node:process' +import { pathToFileURL } from 'node:url' +import { resolveModulePath } from 'exsolve' + +import { fetchTemplates } from '../packages/nuxi/src/utils/starter-templates.ts' + +interface PresetMeta { + _meta?: { name: string } +} + +const dataDir = new URL('../packages/nuxi/src/data/', import.meta.url) + +export async function generateCompletionData() { + const [nitroPresets, templates] = await Promise.all([ + getNitroPresets(), + fetchTemplates(), + ]) + + await mkdir(dataDir, { recursive: true }) + await writeFile( + new URL('nitro-presets.ts', dataDir), + `export const nitroPresets = ${JSON.stringify(nitroPresets, null, 2)} as const`, + ) + await writeFile( + new URL('templates.ts', dataDir), + `export const templates = ${JSON.stringify(templates, null, 2)} as const`, + ) +} + +async function getNitroPresets() { + const nitropackPath = dirname(resolveModulePath('nitropack/package.json', { from: dataDir })) + const presetsPath = join(nitropackPath, 'dist/presets/_all.gen.mjs') + const { default: allPresets } = await import(pathToFileURL(presetsPath).toString()) as { default: PresetMeta[] } + + return allPresets + .map(preset => preset._meta?.name) + .filter((name): name is string => Boolean(name)) + .filter(name => !['base-worker', 'nitro-dev', 'nitro-prerender'].includes(name)) + .filter((name, index, array) => array.indexOf(name) === index) + .sort() +} + +generateCompletionData().catch((error) => { + console.error('Failed to generate completion data:', error) + process.exit(1) +}) diff --git a/tsconfig.json b/tsconfig.json index e97dc0203..0ad2e37d7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "module": "ESNext", "moduleResolution": "Bundler", "resolveJsonModule": true, + "allowImportingTsExtensions": true, "allowJs": true, "strict": true, "noImplicitAny": true, @@ -11,7 +12,6 @@ "noUnusedLocals": true, "noEmit": true, "allowSyntheticDefaultImports": true, - "esModuleInterop": false, "skipLibCheck": true }, "exclude": [ From 39d404f2c7e4d9dcd90a6b066a223e0a22b625b0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 16:40:49 +0000 Subject: [PATCH 2/7] [autofix.ci] apply automated fixes --- eslint.config.js | 2 +- packages/nuxi/src/commands/init.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index c16c5955e..9b034c013 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -15,7 +15,7 @@ export default createConfigForNuxt({ }, }, await antfu()).append( { - ignores: ['packages/nuxi/src/data/**'] + ignores: ['packages/nuxi/src/data/**'], }, { rules: { diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index bc41c6269..c11f6ff6d 100644 --- a/packages/nuxi/src/commands/init.ts +++ b/packages/nuxi/src/commands/init.ts @@ -1,9 +1,10 @@ import type { DownloadTemplateResult } from 'giget' import type { PackageManagerName } from 'nypm' +import type { TemplateData } from '../utils/starter-templates' import { existsSync } from 'node:fs' -import process from 'node:process' +import process from 'node:process' import { box, cancel, confirm, intro, isCancel, multiselect, outro, select, spinner, tasks, text } from '@clack/prompts' import { defineCommand } from 'citty' import { colors } from 'consola/utils' @@ -12,14 +13,14 @@ import { installDependencies } from 'nypm' import { $fetch } from 'ofetch' import { basename, join, relative, resolve } from 'pathe' import { findFile, readPackageJSON, writePackageJSON } from 'pkg-types' -import { hasTTY } from 'std-env' +import { hasTTY } from 'std-env' import { x } from 'tinyexec' import { runCommand } from '../run' import { nuxtIcon, themeColor } from '../utils/ascii' import { logger } from '../utils/logger' import { relativeToProcess } from '../utils/paths' -import { getTemplates, TemplateData } from '../utils/starter-templates' +import { getTemplates } from '../utils/starter-templates' import { cwdArgs, logLevelArgs } from './_shared' import { fetchModules } from './module/_utils' import addModuleCommand from './module/add' @@ -155,7 +156,6 @@ export default defineCommand({ process.exit(1) } - if (ctx.args.dir === '') { const defaultDir = availableTemplates[templateName]?.defaultDir || 'nuxt-app' const result = await text({ From ee2e5d8e496427c52e8c1345ff0659e3cd6668f7 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 3 Dec 2025 16:47:35 +0000 Subject: [PATCH 3/7] test: update e2e test to pass template name --- packages/nuxt-cli/test/e2e/init.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/nuxt-cli/test/e2e/init.spec.ts b/packages/nuxt-cli/test/e2e/init.spec.ts index d1f7b61bc..77361174c 100644 --- a/packages/nuxt-cli/test/e2e/init.spec.ts +++ b/packages/nuxt-cli/test/e2e/init.spec.ts @@ -19,7 +19,7 @@ describe('init command package name slugification', () => { await rm(installPath, { recursive: true, force: true }) try { - await x(nuxi, ['init', installPath, '--packageManager=pnpm', '--gitInit=false', '--preferOffline', '--install=false'], { + await x(nuxi, ['init', installPath, '--packageManager=pnpm', '--template=minimal', '--gitInit=false', '--preferOffline', '--install=false'], { throwOnError: true, nodeOptions: { stdio: 'inherit', cwd: fixtureDir }, }) @@ -47,7 +47,7 @@ describe('init command package name slugification', () => { await rm(installPath, { recursive: true, force: true }) try { - await x(nuxi, ['init', installPath, '--packageManager=pnpm', '--gitInit=false', '--preferOffline', '--install=false'], { + await x(nuxi, ['init', installPath, '--packageManager=pnpm', '--template=minimal', '--gitInit=false', '--preferOffline', '--install=false'], { throwOnError: true, nodeOptions: { stdio: 'inherit', cwd: fixtureDir }, }) @@ -74,7 +74,7 @@ describe('init command package name slugification', () => { await rm(installPath, { recursive: true, force: true }) try { - await x(nuxi, ['init', installPath, '--packageManager=pnpm', '--gitInit=false', '--preferOffline', '--install=false'], { + await x(nuxi, ['init', installPath, '--packageManager=pnpm', '--template=minimal', '--gitInit=false', '--preferOffline', '--install=false'], { throwOnError: true, nodeOptions: { stdio: 'inherit', cwd: fixtureDir }, }) @@ -100,7 +100,7 @@ describe('init command package name slugification', () => { await rm(installPath, { recursive: true, force: true }) try { - await x(nuxi, ['init', installPath, '--packageManager=pnpm', '--gitInit=false', '--preferOffline', '--install=false'], { + await x(nuxi, ['init', installPath, '--packageManager=pnpm', '--template=minimal', '--gitInit=false', '--preferOffline', '--install=false'], { throwOnError: true, nodeOptions: { stdio: 'inherit', cwd: fixtureDir }, }) From b5149965d26637eaf81cb3f290938fda2a56ff89 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 3 Dec 2025 16:48:04 +0000 Subject: [PATCH 4/7] chore: update line breaks --- packages/nuxi/src/commands/init.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index c11f6ff6d..8c3cabdf0 100644 --- a/packages/nuxi/src/commands/init.ts +++ b/packages/nuxi/src/commands/init.ts @@ -1,10 +1,10 @@ import type { DownloadTemplateResult } from 'giget' import type { PackageManagerName } from 'nypm' - import type { TemplateData } from '../utils/starter-templates' -import { existsSync } from 'node:fs' +import { existsSync } from 'node:fs' import process from 'node:process' + import { box, cancel, confirm, intro, isCancel, multiselect, outro, select, spinner, tasks, text } from '@clack/prompts' import { defineCommand } from 'citty' import { colors } from 'consola/utils' @@ -13,9 +13,9 @@ import { installDependencies } from 'nypm' import { $fetch } from 'ofetch' import { basename, join, relative, resolve } from 'pathe' import { findFile, readPackageJSON, writePackageJSON } from 'pkg-types' - import { hasTTY } from 'std-env' import { x } from 'tinyexec' + import { runCommand } from '../run' import { nuxtIcon, themeColor } from '../utils/ascii' import { logger } from '../utils/logger' From 70dd8acd731d16ed916beb2c96c40ceba30bdeb1 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 3 Dec 2025 16:51:43 +0000 Subject: [PATCH 5/7] test: update one more e2e test to pass template name --- packages/nuxt-cli/test/e2e/commands.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxt-cli/test/e2e/commands.spec.ts b/packages/nuxt-cli/test/e2e/commands.spec.ts index 77e0e9184..560a3a003 100644 --- a/packages/nuxt-cli/test/e2e/commands.spec.ts +++ b/packages/nuxt-cli/test/e2e/commands.spec.ts @@ -120,7 +120,7 @@ describe('commands', () => { await rm(installPath, { recursive: true, force: true }) try { - await x(nuxi, ['init', installPath, `--packageManager=${pm}`, '--gitInit=false', '--preferOffline', '--install=false'], { + await x(nuxi, ['init', installPath, `--packageManager=${pm}`, '--template=minimal', '--gitInit=false', '--preferOffline', '--install=false'], { throwOnError: true, nodeOptions: { stdio: 'inherit', cwd: fixtureDir }, }) From 3018d28954157b78d31e428f9bac70ed152bb769 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Wed, 3 Dec 2025 17:04:42 +0000 Subject: [PATCH 6/7] fix: handle offline correctly --- packages/nuxi/src/commands/init.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index 8c3cabdf0..53683aef1 100644 --- a/packages/nuxi/src/commands/init.ts +++ b/packages/nuxi/src/commands/init.ts @@ -101,7 +101,7 @@ export default defineCommand({ }, async run(ctx) { if (!ctx.args.offline && !ctx.args.preferOffline && !ctx.args.template) { - getTemplates() + getTemplates().catch(() => null) } if (hasTTY) { @@ -113,16 +113,22 @@ export default defineCommand({ let availableTemplates: Record = {} if (!ctx.args.template || !ctx.args.dir) { + const defaultTemplates = await import('../data/templates').then(r => r.templates) if (ctx.args.offline || ctx.args.preferOffline) { // In offline mode, use static templates directly - availableTemplates = await import('../data/templates').then(r => r.templates) + availableTemplates = defaultTemplates } else { const templatesSpinner = spinner() templatesSpinner.start('Loading available templates') + try { availableTemplates = await getTemplates() - templatesSpinner.stop('Templates loaded') + templatesSpinner.stop('Templates loaded') + } catch { + availableTemplates = defaultTemplates + templatesSpinner.stop('Templates loaded from cache') + } } } From 4799eac097da1f41191988759c05eb8f533fe2f1 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 17:05:50 +0000 Subject: [PATCH 7/7] [autofix.ci] apply automated fixes --- packages/nuxi/src/commands/init.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index 53683aef1..eb0725a25 100644 --- a/packages/nuxi/src/commands/init.ts +++ b/packages/nuxi/src/commands/init.ts @@ -123,9 +123,10 @@ export default defineCommand({ templatesSpinner.start('Loading available templates') try { - availableTemplates = await getTemplates() + availableTemplates = await getTemplates() templatesSpinner.stop('Templates loaded') - } catch { + } + catch { availableTemplates = defaultTemplates templatesSpinner.stop('Templates loaded from cache') }