1
1
const { createHash } = require ( 'crypto' )
2
- const { readdirSync, readFileSync, statSync, writeFileSync } = require ( 'fs' )
2
+ const { readdirSync, readFileSync, statSync, writeFileSync, existsSync , renameSync } = require ( 'fs' )
3
3
const { join } = require ( 'path' )
4
4
5
5
const getRevision = filePath => createHash ( 'md5' ) . update ( readFileSync ( filePath ) ) . digest ( 'hex' )
@@ -20,10 +20,68 @@ function formatBytes (bytes, decimals = 2) {
20
20
return `${ formattedSize } ${ sizes [ i ] } `
21
21
}
22
22
23
- function generatePrecacheManifest ( ) {
23
+ function escapeForSingleQuotedJsString ( str ) {
24
+ return str
25
+ . replace ( / \\ / g, '\\\\' )
26
+ . replace ( / ' / g, "\\'" )
27
+ . replace ( / \r / g, '\\r' )
28
+ . replace ( / \n / g, '\\n' )
29
+ . replace ( / \$ / g, '\\$' )
30
+ }
31
+
32
+ function generateDummyPrecacheManifest ( ) {
33
+ // A dummy manifest,to be easily referenced in the public/sw.js when patched
34
+ // This will be pathced with custom assets urls after Next builds
35
+ const manifest = [ {
36
+ url : '/dummy/path/test1.js' ,
37
+ revision : 'rev-123'
38
+ } ]
39
+
40
+ const output = join ( __dirname , 'precache-manifest.json' )
41
+ writeFileSync ( output , JSON . stringify ( manifest , null , 2 ) )
42
+
43
+ console . log ( `Created precache manifest at ${ output } .` )
44
+ }
45
+
46
+ function patchSwAssetsURL ( assetUrlsArray ) {
47
+ const fullPath = join ( __dirname , '../public/sw.js' )
48
+ let content = readFileSync ( fullPath , 'utf-8' )
49
+ const patchedArray = JSON . stringify ( assetUrlsArray )
50
+ const escapedPatchedArrayJson = escapeForSingleQuotedJsString ( patchedArray )
51
+
52
+ // Robust regex: matches JSON.parse('...') or JSON.parse("...") containing the dummy manifest
53
+ // Looks for the dummy manifest's url and revision keys as a marker
54
+ // This version does not use backreferences inside character classes
55
+ const regex = / J S O N \. p a r s e \( ( [ ' " ] ) \s * \[ \s * \{ \s * ( [ ' " ] ) u r l \2\s * : \s * ( [ ' " ] ) [ ^ ' " ] + \3\s * , \s * ( [ ' " ] ) r e v i s i o n \4\s * : \s * ( [ ' " ] ) [ ^ ' " ] + \5\s * \} \s * \] \s * \1\) /
56
+
57
+ if ( ! regex . test ( content ) ) {
58
+ console . warn ( '⚠️ No match found for precache manifest in sw.js. Service worker will NOT be patched.' )
59
+ return
60
+ }
61
+
62
+ content = content . replace ( regex , ( ) => {
63
+ return `JSON.parse('${ escapedPatchedArrayJson } ')`
64
+ } )
65
+
66
+ // Atomic write: write to temp file, then rename
67
+ const tempPath = fullPath + '.tmp'
68
+ try {
69
+ writeFileSync ( tempPath , content , 'utf-8' )
70
+ renameSync ( tempPath , fullPath )
71
+ console . log ( '✅ Patched service worker cached assets' )
72
+ } catch ( err ) {
73
+ console . error ( '❌ Failed to patch service worker:' , err )
74
+ // Clean up temp file if exists
75
+ if ( existsSync ( tempPath ) ) {
76
+ try { require ( 'fs' ) . unlinkSync ( tempPath ) } catch ( _ ) { }
77
+ }
78
+ throw err
79
+ }
80
+ }
81
+
82
+ async function addStaticAssetsInServiceWorker ( ) {
24
83
const manifest = [ ]
25
84
let size = 0
26
-
27
85
const addToManifest = ( filePath , url , s ) => {
28
86
const revision = getRevision ( filePath )
29
87
manifest . push ( { url, revision } )
@@ -35,7 +93,9 @@ function generatePrecacheManifest () {
35
93
const staticMatch = f => [ / \. ( g i f | j p e ? g | i c o | p n g | t t f | w o f f | w o f f 2 ) $ / ] . some ( m => m . test ( f ) )
36
94
staticFiles . filter ( staticMatch ) . forEach ( file => {
37
95
const stats = statSync ( file )
38
- addToManifest ( file , file . slice ( staticDir . length ) , stats . size )
96
+ // Normalize path separators for URLs
97
+ const url = file . slice ( staticDir . length ) . replace ( / \\ / g, '/' )
98
+ addToManifest ( file , url , stats . size )
39
99
} )
40
100
41
101
const pagesDir = join ( __dirname , '../pages' )
@@ -45,17 +105,67 @@ function generatePrecacheManifest () {
45
105
const pageMatch = f => precacheURLs . some ( url => fileToUrl ( f ) === url )
46
106
pagesFiles . filter ( pageMatch ) . forEach ( file => {
47
107
const stats = statSync ( file )
48
- // This is not ideal since dependencies of the pages may have changed
49
- // but we would still generate the same revision ...
50
- // The ideal solution would be to create a revision from the file generated by webpack
51
- // in .next/server/pages but the file may not exist yet when we run this script
52
108
addToManifest ( file , fileToUrl ( file ) , stats . size )
53
109
} )
54
110
55
- const output = 'sw/precache-manifest.json'
56
- writeFileSync ( output , JSON . stringify ( manifest , null , 2 ) )
111
+ const nextStaticDir = join ( __dirname , '../.next/static' )
112
+ // Wait until folder is emitted
113
+ console . log ( '⏳ Waiting for .next/static to be emitted...' )
114
+ let folderRetries = 0
115
+ while ( ! existsSync ( nextStaticDir ) && folderRetries < 10 ) {
116
+ // eslint-disable-next-line no-await-in-loop
117
+ await new Promise ( resolve => setTimeout ( resolve , 500 ) )
118
+ folderRetries ++
119
+ }
57
120
58
- console . log ( `Created precache manifest at ${ output } . Cache will include ${ manifest . length } URLs with a size of ${ formatBytes ( size ) } .` )
121
+ if ( ! existsSync ( nextStaticDir ) ) {
122
+ // Still write the manifest with whatever was collected from public/ and pages/
123
+ const output = join ( __dirname , 'precache-manifest.json' )
124
+ writeFileSync ( output , JSON . stringify ( manifest , null , 2 ) )
125
+ console . warn (
126
+ `⚠️ .next/static not found. Created precache manifest at ${ output } with only public/ and pages/ assets.`
127
+ )
128
+ // Patch the service worker with the available manifest
129
+ patchSwAssetsURL ( manifest )
130
+ return manifest
131
+ }
132
+
133
+ function snapshot ( files ) {
134
+ return files . map ( f => `${ f } :${ statSync ( f ) . size } ` ) . join ( ',' )
135
+ }
136
+ // Now watch for stabilization (files are emitted asynchronously)
137
+ let lastSnapshot = ''
138
+ let stableCount = 0
139
+ const maxWaitMs = 60000
140
+ const startTime = Date . now ( )
141
+ while ( stableCount < 3 && ( Date . now ( ) - startTime ) < maxWaitMs ) {
142
+ const files = walkSync ( nextStaticDir )
143
+ const currentSnapshot = snapshot ( files )
144
+ if ( currentSnapshot === lastSnapshot ) {
145
+ stableCount ++
146
+ } else {
147
+ lastSnapshot = currentSnapshot
148
+ stableCount = 0
149
+ }
150
+ await new Promise ( resolve => setTimeout ( resolve , 500 ) )
151
+ }
152
+ // finally generate manifest
153
+ const nextStaticFiles = walkSync ( nextStaticDir )
154
+ nextStaticFiles . forEach ( file => {
155
+ const stats = statSync ( file )
156
+ // Normalize path separators for URLs
157
+ const url = `/_next/static${ file . slice ( nextStaticDir . length ) . replace ( / \\ / g, '/' ) } `
158
+ addToManifest ( file , url , stats . size )
159
+ } )
160
+ // write manifest
161
+ const output = join ( __dirname , 'precache-manifest.json' )
162
+ writeFileSync ( output , JSON . stringify ( manifest , null , 2 ) )
163
+ console . log (
164
+ `✅ Created precache manifest at ${ output } . Cache will include ${ manifest . length } URLs with a size of ${ formatBytes ( size ) } .`
165
+ )
166
+ const data = readFileSync ( output , 'utf-8' )
167
+ const manifestArray = JSON . parse ( data )
168
+ patchSwAssetsURL ( manifestArray )
59
169
}
60
170
61
- module . exports = { generatePrecacheManifest }
171
+ module . exports = { generateDummyPrecacheManifest , addStaticAssetsInServiceWorker }
0 commit comments