diff --git a/index.ts b/index.ts index 7c62dd049..b36009728 100755 --- a/index.ts +++ b/index.ts @@ -21,6 +21,12 @@ import generateReadme from './utils/generateReadme' import getCommand from './utils/getCommand' import getLanguage from './utils/getLanguage' import { trimBoilerplate, removeCSSImport, emptyRouterConfig } from './utils/trimBoilerplate' +import applyVueBeta from './utils/applyVueBeta' +import { + inferPackageManager, + getPackageManagerOptions, + type PackageManager, +} from './utils/packageManager' import cliPackageJson from './package.json' with { type: 'json' } @@ -45,6 +51,7 @@ const FEATURE_FLAGS = [ 'eslint-with-prettier', 'oxlint', 'vite-beta', + 'vue-beta', ] as const const FEATURE_OPTIONS = [ @@ -90,6 +97,10 @@ const EXPERIMENTAL_FEATURE_OPTIONS = [ value: 'vite-beta', label: language.needsViteBeta.message, }, + { + value: 'vue-beta', + label: language.needsVueBeta.message, + }, ] as const type PromptResult = { @@ -100,6 +111,7 @@ type PromptResult = { e2eFramework?: 'cypress' | 'nightwatch' | 'playwright' experimentFeatures?: (typeof EXPERIMENTAL_FEATURE_OPTIONS)[number]['value'][] needsBareboneTemplates?: boolean + packageManager?: PackageManager } function isValidPackageName(projectName) { @@ -199,6 +211,8 @@ Available feature flags: Add Oxfmt for code formatting. --vite-beta Use Vite 8 Beta instead of Vite for building the project. + --vue-beta + Use Vue 3.6 Beta. Requires specifying a package manager in interactive mode. Unstable feature flags: --tests, --with-tests @@ -250,6 +264,9 @@ async function init() { const forceOverwrite = argv.force + // Infer package manager from user agent early so we can use it in prompts + const inferredPackageManager = inferPackageManager() + const result: PromptResult = { projectName: defaultProjectName, shouldOverwrite: forceOverwrite, @@ -359,6 +376,21 @@ async function init() { required: false, }), ) + + // Ask for package manager if Vue 3.6 beta is selected (needed for correct overrides) + if (result.experimentFeatures.includes('vue-beta')) { + const packageManagerOptions = getPackageManagerOptions(inferredPackageManager).map((pm) => ({ + value: pm, + label: pm, + })) + + result.packageManager = await unwrapPrompt( + select({ + message: `${language.packageManagerSelection.message} ${dim(language.packageManagerSelection.hint)}`, + options: packageManagerOptions, + }), + ) + } } if (argv.bare) { @@ -386,6 +418,7 @@ async function init() { const needsOxfmt = experimentFeatures.includes('oxfmt') || argv['oxfmt'] const needsViteBeta = experimentFeatures.includes('vite-beta') || argv['vite-beta'] || argv['rolldown-vite'] // keep `rolldown-vite` for backward compatibility + const needsVueBeta = experimentFeatures.includes('vue-beta') || argv['vue-beta'] const { e2eFramework } = result const needsCypress = argv.cypress || argv.tests || e2eFramework === 'cypress' @@ -672,16 +705,16 @@ async function init() { } } - // Instructions: - // Supported package managers: pnpm > yarn > bun > npm - const userAgent = process.env.npm_config_user_agent ?? '' - const packageManager = /pnpm/.test(userAgent) - ? 'pnpm' - : /yarn/.test(userAgent) - ? 'yarn' - : /bun/.test(userAgent) - ? 'bun' - : 'npm' + // Use the package manager selected by user for Vue 3.6 beta, or inferred from user agent + const packageManager = result.packageManager ?? inferredPackageManager + + // Apply Vue 3.6 Beta overrides if the feature is enabled + if (needsVueBeta) { + const pkgPath = path.resolve(root, 'package.json') + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) + applyVueBeta(root, packageManager, pkg) + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n') + } // README generation fs.writeFileSync( diff --git a/locales/en-US.json b/locales/en-US.json index 5a8b3bd34..16b939d1a 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -69,12 +69,19 @@ "needsViteBeta": { "message": "Vite 8 (beta)" }, + "needsVueBeta": { + "message": "Vue 3.6 (beta)" + }, "needsOxfmt": { "message": "Replace Prettier with Oxfmt" }, "needsBareboneTemplates": { "message": "Skip all example code and start with a blank Vue project?" }, + "packageManagerSelection": { + "message": "Which package manager will you use?", + "hint": "(Vue 3.6 beta requires version overrides that differ per package manager)" + }, "errors": { "operationCancelled": "Operation cancelled" }, diff --git a/locales/fr-FR.json b/locales/fr-FR.json index f27db3563..f369a370a 100644 --- a/locales/fr-FR.json +++ b/locales/fr-FR.json @@ -69,12 +69,19 @@ "needsViteBeta": { "message": "Vite 8 (beta)" }, + "needsVueBeta": { + "message": "Vue 3.6 (beta)" + }, "needsOxfmt": { "message": "Remplacer Prettier par Oxfmt" }, "needsBareboneTemplates": { "message": "Ignorer tout le code d'exemple et commencer avec un projet Vue vierge\u00a0?" }, + "packageManagerSelection": { + "message": "Quel gestionnaire de paquets allez-vous utiliser\u00a0?", + "hint": "(Vue 3.6 beta nécessite des surcharges de version spécifiques au gestionnaire de paquets)" + }, "errors": { "operationCancelled": "Operation annulée" }, diff --git a/locales/tr-TR.json b/locales/tr-TR.json index 48a3eb2ac..e5b8ceb9e 100644 --- a/locales/tr-TR.json +++ b/locales/tr-TR.json @@ -69,12 +69,19 @@ "needsViteBeta": { "message": "Vite 8 (beta)" }, + "needsVueBeta": { + "message": "Vue 3.6 (beta)" + }, "needsOxfmt": { "message": "Prettier'ı Oxfmt ile değiştir" }, "needsBareboneTemplates": { "message": "Tüm örnek kodları atlayıp boş bir Vue projesi ile başlansın mı?" }, + "packageManagerSelection": { + "message": "Hangi paket yöneticisini kullanacaksınız?", + "hint": "(Vue 3.6 beta, paket yöneticisine özgü sürüm geçersiz kılmaları gerektirir)" + }, "errors": { "operationCancelled": "İşlem iptal edildi" }, diff --git a/locales/zh-Hans.json b/locales/zh-Hans.json index b22215bfb..16b9d7d76 100644 --- a/locales/zh-Hans.json +++ b/locales/zh-Hans.json @@ -69,12 +69,19 @@ "needsViteBeta": { "message": "Vite 8(测试版)" }, + "needsVueBeta": { + "message": "Vue 3.6(测试版)" + }, "needsOxfmt": { "message": "使用 Oxfmt 替代 Prettier" }, "needsBareboneTemplates": { "message": "跳过所有示例代码,创建一个空白的 Vue 项目?" }, + "packageManagerSelection": { + "message": "项目将使用哪个包管理器?", + "hint": "(需要根据包管理器生成对应的 Vue 3.6 测试版配置)" + }, "errors": { "operationCancelled": "操作取消" }, diff --git a/locales/zh-Hant.json b/locales/zh-Hant.json index d3bb4e1c7..25dd41cf1 100644 --- a/locales/zh-Hant.json +++ b/locales/zh-Hant.json @@ -69,12 +69,19 @@ "needsViteBeta": { "message": "Vite 8(測試版)" }, + "needsVueBeta": { + "message": "Vue 3.6(測試版)" + }, "needsOxfmt": { "message": "使用 Oxfmt 替代 Prettier" }, "needsBareboneTemplates": { "message": "跳過所有範例程式碼,建立一個空白的 Vue 專案?" }, + "packageManagerSelection": { + "message": "專案將使用哪個套件管理器?", + "hint": "(需要根據套件管理器生成對應的 Vue 3.6 測試版配置)" + }, "errors": { "operationCancelled": "操作取消" }, diff --git a/utils/applyVueBeta.ts b/utils/applyVueBeta.ts new file mode 100644 index 000000000..5cadf1dfb --- /dev/null +++ b/utils/applyVueBeta.ts @@ -0,0 +1,79 @@ +import * as fs from 'node:fs' +import * as path from 'node:path' +import type { PackageManager } from './packageManager' + +// Core Vue packages that need to be overridden +// Based on https://github.com/haoqunjiang/install-vue/blob/main/src/constants.ts +const CORE_VUE_PACKAGES = [ + 'vue', + '@vue/compiler-core', + '@vue/compiler-dom', + '@vue/compiler-sfc', + '@vue/compiler-ssr', + '@vue/compiler-vapor', + '@vue/reactivity', + '@vue/runtime-core', + '@vue/runtime-dom', + '@vue/runtime-vapor', + '@vue/server-renderer', + '@vue/shared', + '@vue/compat', +] as const + +function generateOverridesMap(): Record { + return Object.fromEntries(CORE_VUE_PACKAGES.map((name) => [name, 'beta'])) +} + +/** + * Apply Vue 3.6 beta overrides to the project based on the package manager. + * Different package managers have different mechanisms for version overrides: + * - npm/bun: uses `overrides` field in package.json + * - yarn: uses `resolutions` field in package.json + * - pnpm: uses `overrides` and `peerDependencyRules` in pnpm-workspace.yaml + */ +export default function applyVueBeta( + root: string, + packageManager: PackageManager, + pkg: Record, +): void { + const overrides = generateOverridesMap() + + if (packageManager === 'npm' || packageManager === 'bun') { + // https://github.com/npm/rfcs/blob/main/accepted/0036-overrides.md + // NPM overrides require exact versions for resolution, but "beta" dist-tag works too + // Bun also supports the same `overrides` field + pkg.overrides = { + ...pkg.overrides, + ...overrides, + } + + // NPM requires direct dependencies to be rewritten to match overrides + for (const dependencyName of CORE_VUE_PACKAGES) { + for (const dependencyType of ['dependencies', 'devDependencies', 'optionalDependencies']) { + if (pkg[dependencyType]?.[dependencyName]) { + pkg[dependencyType][dependencyName] = overrides[dependencyName] + } + } + } + } else if (packageManager === 'yarn') { + // https://github.com/yarnpkg/rfcs/blob/master/implemented/0000-selective-versions-resolutions.md + pkg.resolutions = { + ...pkg.resolutions, + ...overrides, + } + } else if (packageManager === 'pnpm') { + // pnpm now recommends putting overrides in pnpm-workspace.yaml + // https://pnpm.io/pnpm-workspace_yaml + const yamlContent = `overrides: +${Object.entries(overrides) + .map(([key, value]) => ` '${key}': '${value}'`) + .join('\n')} + +peerDependencyRules: + allowAny: + - 'vue' +` + + fs.writeFileSync(path.resolve(root, 'pnpm-workspace.yaml'), yamlContent, 'utf-8') + } +} diff --git a/utils/getCommand.ts b/utils/getCommand.ts index 40b907d8e..103140dd7 100644 --- a/utils/getCommand.ts +++ b/utils/getCommand.ts @@ -1,4 +1,10 @@ -export default function getCommand(packageManager: string, scriptName: string, args?: string) { +import type { PackageManager } from './packageManager' + +export default function getCommand( + packageManager: PackageManager, + scriptName: string, + args?: string, +) { if (scriptName === 'install') { return packageManager === 'yarn' ? 'yarn' : `${packageManager} install` } diff --git a/utils/getLanguage.ts b/utils/getLanguage.ts index 301c171e2..d9da12693 100644 --- a/utils/getLanguage.ts +++ b/utils/getLanguage.ts @@ -39,8 +39,10 @@ interface Language { needsExperimental: LanguageItem needsExperimentalFeatures: LanguageItem needsViteBeta: LanguageItem + needsVueBeta: LanguageItem needsOxfmt: LanguageItem needsBareboneTemplates: LanguageItem + packageManagerSelection: LanguageItem errors: { operationCancelled: string } diff --git a/utils/packageManager.ts b/utils/packageManager.ts new file mode 100644 index 000000000..a651bf985 --- /dev/null +++ b/utils/packageManager.ts @@ -0,0 +1,23 @@ +export type PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun' + +/** + * Infers the package manager from the user agent string. + * Falls back to npm if unable to detect. + */ +export function inferPackageManager(): PackageManager { + const userAgent = process.env.npm_config_user_agent ?? '' + + if (/pnpm/.test(userAgent)) return 'pnpm' + if (/yarn/.test(userAgent)) return 'yarn' + if (/bun/.test(userAgent)) return 'bun' + + return 'npm' +} + +/** + * Creates an ordered list of package managers with the preferred one first. + */ +export function getPackageManagerOptions(preferred: PackageManager) { + const all: PackageManager[] = ['npm', 'pnpm', 'yarn', 'bun'] + return [preferred, ...all.filter((pm) => pm !== preferred)] +}