99 * - package.json — "version" field
1010 * - package-lock.json — top-level "version" and packages[""].version fields
1111 * - src/index.html — ?v= cache-busting hashes for changed public/*.js files
12+ * - public/**\/*.js — ?v= cache-busting hashes in dynamic import() calls
1213 *
1314 * Usage:
1415 * node scripts/bump-version.js # interactive prompt
@@ -156,9 +157,7 @@ function updatePackageLockJson(newVersion, dry) {
156157 console . log ( ` package-lock.json ${ oldVersion } → ${ newVersion } ` ) ;
157158}
158159
159- function updateIndexHtmlHashes ( newVersion , dry ) {
160- const changedFiles = getChangedPublicJsFiles ( ) ;
161-
160+ function updateIndexHtmlHashes ( changedFiles , newVersion , dry ) {
162161 if ( changedFiles . length === 0 ) {
163162 console . log ( " src/index.html (no changed public/*.js files detected)" ) ;
164163 return ;
@@ -181,11 +180,102 @@ function updateIndexHtmlHashes(newVersion, dry) {
181180 console . log ( ` src/index.html hashes updated for:\n - ${ updated . join ( "\n - " ) } ` ) ;
182181 } else {
183182 console . log (
184- ` src/index.html (changed files not referenced: ${ changedFiles . map ( f => f . replace ( "public/" , "" ) ) . join ( ", " ) } )`
183+ const publicRoot = path . join ( repoRoot , "public" ) ;
184+
185+ function walk ( dir ) {
186+ for ( const entry of fs . readdirSync ( dir , { withFileTypes : true } ) ) {
187+ const full = path . join ( dir , entry . name ) ;
188+ if ( entry . isDirectory ( ) ) {
189+ // Skip third-party vendor bundles under public/libs/**
190+ const relFromPublic = path
191+ . relative ( publicRoot , full )
192+ . replace ( / \\ / g, "/" ) ;
193+ if ( relFromPublic === "libs" || relFromPublic . startsWith ( "libs/" ) ) {
194+ continue ;
195+ }
196+ walk ( full ) ;
197+ } else if ( entry . isFile ( ) && entry . name . endsWith ( ".js" ) ) {
198+ results . push ( path . relative ( repoRoot , full ) . replace ( / \\ / g, "/" ) ) ;
199+ }
200+ }
201+ }
202+
203+ walk ( publicRoot ) ;
185204 ) ;
186205 }
187206}
188207
208+ /** Returns all .js file paths (relative to repo root, with forward slashes) under public/. */
209+ function getAllPublicJsFiles ( ) {
210+ const results = [ ] ;
211+ function walk ( dir ) {
212+ for ( const entry of fs . readdirSync ( dir , { withFileTypes : true } ) ) {
213+ const full = path . join ( dir , entry . name ) ;
214+ if ( entry . isDirectory ( ) ) {
215+ walk ( full ) ;
216+ } else if ( entry . isFile ( ) && entry . name . endsWith ( ".js" ) ) {
217+ results . push ( path . relative ( repoRoot , full ) . replace ( / \\ / g, "/" ) ) ;
218+ }
219+ }
220+ }
221+ walk ( path . join ( repoRoot , "public" ) ) ;
222+ return results ;
223+ }
224+
225+ /**
226+ * Scans all public/**\/*.js files for relative dynamic-import ?v= references
227+ * (e.g. import("../dynamic/supporters.js?v=1.97.14")) and updates any that
228+ * point to one of the changed files.
229+ */
230+ function updatePublicJsDynamicImportHashes ( changedFiles , newVersion , dry ) {
231+ if ( changedFiles . length === 0 ) {
232+ console . log ( " public/**/*.js (no changed public/*.js files detected)" ) ;
233+ return ;
234+ const replacement = `${ quote } ${ relImportPath } ?v=${ newVersion } ${ quote } ` ;
235+ // Only record and apply an update if the version actually changes
236+ if ( match === replacement ) {
237+ return match ;
238+ }
239+ if ( ! updatedMap [ relJsFile ] ) updatedMap [ relJsFile ] = [ ] ;
240+ updatedMap [ relJsFile ] . push ( relImportPath ) ;
241+ return replacement ;
242+ const publicJsFiles = getAllPublicJsFiles ( ) ;
243+ const updatedMap = { } ;
244+
245+ for ( const relJsFile of publicJsFiles ) {
246+ const absJsFile = path . join ( repoRoot , relJsFile ) ;
247+ const content = readFile ( absJsFile ) ;
248+ const dir = path . dirname ( absJsFile ) ;
249+
250+ const pattern = / ( [ ' " ] ) ( \. \. ? \/ [ ^ ' " ] * ) \? v = [ 0 - 9 . ] + \1/ g;
251+ const newContent = content . replace ( pattern , ( match , quote , relImportPath ) => {
252+ // Strip any query string defensively before resolving the path
253+ const cleanImportPath = relImportPath . split ( "?" ) [ 0 ] ;
254+ const absImport = path . resolve ( dir , cleanImportPath ) ;
255+ const repoRelImport = path . relative ( repoRoot , absImport ) . replace ( / \\ / g, "/" ) ;
256+ if ( changedSet . has ( repoRelImport ) ) {
257+ if ( ! updatedMap [ relJsFile ] ) updatedMap [ relJsFile ] = [ ] ;
258+ updatedMap [ relJsFile ] . push ( relImportPath ) ;
259+ return `${ quote } ${ relImportPath } ?v=${ newVersion } ${ quote } ` ;
260+ }
261+ return match ;
262+ } ) ;
263+
264+ if ( updatedMap [ relJsFile ] && ! dry ) {
265+ writeFile ( absJsFile , newContent ) ;
266+ }
267+ }
268+
269+ if ( Object . keys ( updatedMap ) . length > 0 ) {
270+ const lines = Object . entries ( updatedMap )
271+ . map ( ( [ file , refs ] ) => ` ${ file } :\n - ${ refs . join ( "\n - " ) } ` )
272+ . join ( "\n" ) ;
273+ console . log ( ` public/**/*.js hashes updated:\n${ lines } ` ) ;
274+ } else {
275+ console . log ( " public/**/*.js (no dynamic import ?v= hashes needed updating)" ) ;
276+ }
277+ }
278+
189279// ---------------------------------------------------------------------------
190280// Prompt
191281// ---------------------------------------------------------------------------
@@ -230,7 +320,9 @@ async function main() {
230320 `\n[bump-version] Version already updated manually: ${ baseVersion } → ${ currentVersion } (base was ${ baseVersion } )\n`
231321 ) ;
232322 console . log ( " Skipping version increment — updating ?v= hashes only.\n" ) ;
233- updateIndexHtmlHashes ( currentVersion , dry ) ;
323+ const changedFiles = getChangedPublicJsFiles ( ) ;
324+ updateIndexHtmlHashes ( changedFiles , currentVersion , dry ) ;
325+ updatePublicJsDynamicImportHashes ( changedFiles , currentVersion , dry ) ;
234326 console . log ( `\n[bump-version] ${ dry ? "(dry run) " : "" } done.\n` ) ;
235327 return ;
236328 }
@@ -247,10 +339,12 @@ async function main() {
247339
248340 console . log ( `\n[bump-version] ${ bumpType } : ${ currentVersion } → ${ newVersion } \n` ) ;
249341
342+ const changedFiles = getChangedPublicJsFiles ( ) ;
250343 updateVersioningJs ( newVersion , dry ) ;
251344 updatePackageJson ( newVersion , dry ) ;
252345 updatePackageLockJson ( newVersion , dry ) ;
253- updateIndexHtmlHashes ( newVersion , dry ) ;
346+ updateIndexHtmlHashes ( changedFiles , newVersion , dry ) ;
347+ updatePublicJsDynamicImportHashes ( changedFiles , newVersion , dry ) ;
254348
255349 console . log ( `\n[bump-version] ${ dry ? "(dry run) " : "" } done.\n` ) ;
256350}
0 commit comments