@@ -34,12 +34,8 @@ export interface TemplateConfig {
3434 defaultProjectName : string
3535 // The template directory name (e.g. "next-app", "vite-app").
3636 templateDir : string
37- // Force a specific package manager for this template.
38- packageManager ?: string
3937 // Framework names that map to this template.
4038 frameworks ?: string [ ]
41- // Custom args passed to `packageManager install`.
42- installArgs ?: string [ ]
4339 scaffold ?: ( options : TemplateOptions ) => Promise < void >
4440 create : ( options : TemplateOptions ) => Promise < void >
4541 init ?: ( options : TemplateInitOptions ) => Promise < Config >
@@ -50,8 +46,6 @@ export interface TemplateConfig {
5046 monorepo ?: {
5147 templateDir : string
5248 defaultProjectName ?: string
53- packageManager ?: string
54- installArgs ?: string [ ]
5549 init ?: ( options : TemplateInitOptions ) => Promise < Config >
5650 files ?: RegistryItem [ "files" ]
5751 }
@@ -66,7 +60,6 @@ export function createTemplate(config: TemplateConfig) {
6660 defaultScaffold ( {
6761 title : config . title ,
6862 templateDir : config . templateDir ,
69- installArgs : config . installArgs ,
7063 } ) ,
7164 postInit : config . postInit ?? defaultPostInit ,
7265 }
@@ -86,8 +79,6 @@ export function resolveTemplate(
8679 ...template ,
8780 templateDir : m . templateDir ,
8881 defaultProjectName : m . defaultProjectName ?? m . templateDir ,
89- packageManager : m . packageManager ?? template . packageManager ,
90- installArgs : m . installArgs ?? template . installArgs ,
9182 init : m . init ?? template . init ,
9283 files : m . files ?? template . files ,
9384 }
@@ -96,21 +87,122 @@ export function resolveTemplate(
9687 resolved . scaffold = defaultScaffold ( {
9788 title : template . title ,
9889 templateDir : m . templateDir ,
99- installArgs : resolved . installArgs ,
10090 } )
10191
10292 return resolved
10393}
10494
95+ // Get the appropriate install args for the given package manager.
96+ function getInstallArgs ( packageManager : string ) : string [ ] {
97+ switch ( packageManager ) {
98+ case "pnpm" :
99+ // pnpm enables frozen lockfile in CI by default.
100+ // The template lockfile may drift, so force-disable it explicitly.
101+ return [ "--no-frozen-lockfile" ]
102+ default :
103+ return [ ]
104+ }
105+ }
106+
107+ // Adapt a pnpm-based monorepo template to the target package manager.
108+ async function adaptWorkspaceConfig (
109+ projectPath : string ,
110+ packageManager : string
111+ ) {
112+ if ( packageManager === "pnpm" ) {
113+ return
114+ }
115+
116+ const pnpmWorkspacePath = path . join ( projectPath , "pnpm-workspace.yaml" )
117+ const packageJsonPath = path . join ( projectPath , "package.json" )
118+
119+ // Remove pnpm-lock.yaml.
120+ const lockFilePath = path . join ( projectPath , "pnpm-lock.yaml" )
121+ if ( fs . existsSync ( lockFilePath ) ) {
122+ await fs . remove ( lockFilePath )
123+ }
124+
125+ const isMonorepo = fs . existsSync ( pnpmWorkspacePath )
126+
127+ // Update root package.json: strip "packageManager" field to avoid
128+ // triggering Corepack, and add "workspaces" for npm/bun/yarn.
129+ if ( fs . existsSync ( packageJsonPath ) ) {
130+ const packageJsonContent = await fs . readFile ( packageJsonPath , "utf8" )
131+ const packageJson = JSON . parse ( packageJsonContent )
132+ delete packageJson . packageManager
133+
134+ if ( isMonorepo ) {
135+ // Read workspace patterns from pnpm-workspace.yaml.
136+ const workspaceContent = await fs . readFile ( pnpmWorkspacePath , "utf8" )
137+ const patterns : string [ ] = [ ]
138+ for ( const line of workspaceContent . split ( "\n" ) ) {
139+ const match = line . match ( / ^ \s * - \s * [ " ' ] ? ( .+ ?) [ " ' ] ? \s * $ / )
140+ if ( match ) {
141+ patterns . push ( match [ 1 ] )
142+ }
143+ }
144+
145+ packageJson . workspaces = patterns
146+ await fs . remove ( pnpmWorkspacePath )
147+ }
148+
149+ await fs . writeFile (
150+ packageJsonPath ,
151+ JSON . stringify ( packageJson , null , 2 ) + "\n"
152+ )
153+ }
154+
155+ // Rewrite workspace: protocol references in nested package.json files.
156+ // npm does not support workspace: protocol; bun and yarn do, so only
157+ // rewrite for npm monorepo templates.
158+ if ( isMonorepo && packageManager === "npm" ) {
159+ await rewriteWorkspaceProtocol ( projectPath )
160+ }
161+ }
162+
163+ // Recursively find all package.json files and replace workspace: protocol
164+ // version specifiers with "*", which npm understands.
165+ async function rewriteWorkspaceProtocol ( dir : string ) {
166+ const entries = await fs . readdir ( dir , { withFileTypes : true } )
167+ for ( const entry of entries ) {
168+ if ( entry . name === "node_modules" ) continue
169+ const fullPath = path . join ( dir , entry . name )
170+ if ( entry . isDirectory ( ) ) {
171+ await rewriteWorkspaceProtocol ( fullPath )
172+ } else if ( entry . name === "package.json" ) {
173+ const content = await fs . readFile ( fullPath , "utf8" )
174+ if ( ! content . includes ( "workspace:" ) ) continue
175+ const pkg = JSON . parse ( content )
176+ let changed = false
177+ for ( const depKey of [
178+ "dependencies" ,
179+ "devDependencies" ,
180+ "peerDependencies" ,
181+ "optionalDependencies" ,
182+ ] ) {
183+ const deps = pkg [ depKey ]
184+ if ( ! deps ) continue
185+ for ( const [ name , version ] of Object . entries ( deps ) ) {
186+ if ( typeof version === "string" && version . startsWith ( "workspace:" ) ) {
187+ deps [ name ] = "*"
188+ changed = true
189+ }
190+ }
191+ }
192+ if ( changed ) {
193+ await fs . writeFile ( fullPath , JSON . stringify ( pkg , null , 2 ) + "\n" )
194+ }
195+ }
196+ }
197+ }
198+
105199// Default scaffold that downloads a template from GitHub.
106200function defaultScaffold ( {
107201 title,
108202 templateDir,
109- installArgs,
110203} : {
111204 title : string
112205 templateDir : string
113- installArgs ?: string [ ]
114206} ) {
115207 return async ( { projectPath, packageManager } : TemplateOptions ) => {
116208 const createSpinner = spinner (
@@ -157,16 +249,12 @@ function defaultScaffold({
157249 await fs . remove ( templatePath )
158250 }
159251
160- // Remove pnpm-lock.yaml if using a different package manager.
161- if ( packageManager !== "pnpm" ) {
162- const lockFilePath = path . join ( projectPath , "pnpm-lock.yaml" )
163- if ( fs . existsSync ( lockFilePath ) ) {
164- await fs . remove ( lockFilePath )
165- }
166- }
252+ // Adapt workspace config and lockfiles for the target package manager.
253+ await adaptWorkspaceConfig ( projectPath , packageManager )
167254
168255 // Run install.
169- const args = [ "install" , ...( installArgs ?? [ ] ) ]
256+ const installArgs = getInstallArgs ( packageManager )
257+ const args = [ "install" , ...installArgs ]
170258 await execa ( packageManager , args , {
171259 cwd : projectPath ,
172260 } )
@@ -179,7 +267,7 @@ function defaultScaffold({
179267 packageJson . name = path . basename ( projectPath )
180268 await fs . writeFile (
181269 packageJsonPath ,
182- JSON . stringify ( packageJson , null , 2 )
270+ JSON . stringify ( packageJson , null , 2 ) + "\n"
183271 )
184272 }
185273
0 commit comments