@@ -83,6 +83,36 @@ function copyFileWithBackup(src, dest) {
8383 return { updated : true , backup : null } ;
8484}
8585
86+ export function addMissingRuntimeDependencies ( projectPkg , templatePkg ) {
87+ const requiredDeps = templatePkg . dependencies || { } ;
88+ const existingDeps = projectPkg . dependencies || { } ;
89+ const existingDevDeps = projectPkg . devDependencies || { } ;
90+ const added = [ ] ;
91+
92+ for ( const [ name , version ] of Object . entries ( requiredDeps ) ) {
93+ if ( existingDeps [ name ] ) {
94+ continue ;
95+ }
96+
97+ existingDeps [ name ] = existingDevDeps [ name ] || version ;
98+ delete existingDevDeps [ name ] ;
99+ added . push ( name ) ;
100+ }
101+
102+ if ( added . length > 0 ) {
103+ projectPkg . dependencies = Object . fromEntries (
104+ Object . entries ( existingDeps ) . sort ( ( [ a ] , [ b ] ) => a . localeCompare ( b ) )
105+ ) ;
106+ if ( projectPkg . devDependencies ) {
107+ projectPkg . devDependencies = Object . fromEntries (
108+ Object . entries ( existingDevDeps ) . sort ( ( [ a ] , [ b ] ) => a . localeCompare ( b ) )
109+ ) ;
110+ }
111+ }
112+
113+ return added ;
114+ }
115+
86116export async function upgrade ( options = { } ) {
87117 const cwd = process . cwd ( ) ;
88118 const rcPath = path . join ( cwd , '.coursecoderc.json' ) ;
@@ -109,6 +139,7 @@ export async function upgrade(options = {}) {
109139 // Get CLI version (this is the version we'll upgrade to)
110140 const cliPkg = JSON . parse ( fs . readFileSync ( path . join ( PACKAGE_ROOT , 'package.json' ) , 'utf-8' ) ) ;
111141 const targetVersion = cliPkg . version ;
142+ const templatePkg = JSON . parse ( fs . readFileSync ( path . join ( PACKAGE_ROOT , 'template' , 'package.json' ) , 'utf-8' ) ) ;
112143
113144 console . log ( `
114145📦 CourseCode Upgrade
@@ -138,6 +169,7 @@ export async function upgrade(options = {}) {
138169 - framework/ (replace entirely)
139170 - schemas/ (replace entirely)
140171 - lib/manifest/ (replace entirely)
172+ - package.json (add missing runtime dependencies)
141173 - .coursecoderc.json (update version)` ;
142174
143175 if ( options . configs ) {
@@ -150,7 +182,6 @@ export async function upgrade(options = {}) {
150182
151183 Would NOT touch:
152184 - course/ (your content)${ ! options . configs ? '\n - vite.config.js\n - eslint.config.js' : '' }
153- - package.json
154185 - .env
155186` ;
156187 console . log ( dryRunMsg ) ;
@@ -231,13 +262,34 @@ export async function upgrade(options = {}) {
231262 rcConfig . upgradedFrom = currentVersion ;
232263 fs . writeFileSync ( rcPath , JSON . stringify ( rcConfig , null , 2 ) ) ;
233264
265+ // Add any runtime dependencies introduced by the new framework while preserving
266+ // the project's existing version ranges.
267+ const pkgPath = path . join ( cwd , 'package.json' ) ;
268+ let addedRuntimeDeps = [ ] ;
269+ if ( fs . existsSync ( pkgPath ) ) {
270+ const projectPkg = JSON . parse ( fs . readFileSync ( pkgPath , 'utf-8' ) ) ;
271+ addedRuntimeDeps = addMissingRuntimeDependencies ( projectPkg , templatePkg ) ;
272+ if ( addedRuntimeDeps . length > 0 ) {
273+ fs . writeFileSync ( pkgPath , JSON . stringify ( projectPkg , null , 2 ) + '\n' ) ;
274+ }
275+ }
276+
234277 let successMsg = `
235278✅ Upgrade complete!
236279
237280 ${ currentVersion } → ${ targetVersion }
238281
239282 Your course/ directory was not modified.` ;
240283
284+ if ( addedRuntimeDeps . length > 0 ) {
285+ successMsg += `
286+
287+ Runtime dependencies added to package.json:
288+ - ${ addedRuntimeDeps . join ( '\n - ' ) }
289+
290+ Run npm install to update node_modules and your lockfile.` ;
291+ }
292+
241293 if ( configUpdates . length > 0 ) {
242294 successMsg += `
243295
0 commit comments