@@ -123,7 +123,7 @@ export async function publish() {
123123 * Sync CodeceptJS docs from the upstream repository into src/content/docs.
124124 */
125125export async function docsSync ( ) {
126- const { diff, considered } = await stageUpstreamDocs ( ) ;
126+ const { links , diff, considered } = await stageUpstreamDocs ( ) ;
127127
128128 await task ( 'Apply curated docs' , ( ) => {
129129 for ( const { target, content } of diff . changes ) {
@@ -132,6 +132,10 @@ export async function docsSync() {
132132 }
133133 } ) ;
134134
135+ let images = 0 ;
136+ await task ( 'Copy referenced images' , ( ) => { images = syncReferencedImages ( links ) ; } ) ;
137+ say ( `copied ${ images } referenced image(s)` ) ;
138+
135139 reportSyncResult ( diff , considered ) ;
136140}
137141
@@ -231,16 +235,17 @@ async function stageUpstreamDocs() {
231235 await task ( 'Stage upstream docs' , ( ) => { stagedCount = buildStaging ( ) ; } ) ;
232236 say ( `staged ${ stagedCount } files in ${ STAGING_DIR } ` ) ;
233237
238+ let links ;
234239 let diff ;
235240 let considered = 0 ;
236241 await task ( 'Scan curated docs' , ( ) => {
237- const links = collectSidebarLinks ( ) ;
242+ links = collectSidebarLinks ( ) ;
238243 diff = diffCuratedDocs ( links ) ;
239244 considered = links . size - [ ...SKIP_LINKS ] . filter ( ( l ) => links . has ( l ) ) . length ;
240245 } ) ;
241246 say ( `scanned ${ considered } sidebar-listed docs` ) ;
242247
243- return { diff, considered } ;
248+ return { links , diff, considered } ;
244249}
245250
246251async function cloneUpstream ( ) {
@@ -280,6 +285,54 @@ function diffCuratedDocs(links) {
280285 return { changes, unchanged, missing } ;
281286}
282287
288+ function collectImagePaths ( markdown ) {
289+ const paths = new Set ( ) ;
290+ const patterns = [
291+ / ! \[ [ ^ \] ] * \] \( \s * < ? ( [ ^ ) > \s ] + ) > ? / g, // 
292+ / < i m g [ ^ > ] + s r c = [ " ' ] ( [ ^ " ' ] + ) [ " ' ] / gi, // <img src="path">
293+ ] ;
294+ for ( const re of patterns ) {
295+ let m ;
296+ while ( ( m = re . exec ( markdown ) ) !== null ) {
297+ const p = m [ 1 ] . trim ( ) ;
298+ if ( ! p || / ^ (?: [ a - z ] + : ) ? \/ \/ / i. test ( p ) || p . startsWith ( 'data:' ) || p . startsWith ( '/' ) ) continue ;
299+ paths . add ( p . split ( '#' ) [ 0 ] . split ( '?' ) [ 0 ] ) ;
300+ }
301+ }
302+ return [ ...paths ] ;
303+ }
304+
305+ // Curated docs reference upstream images by relative path; copy those images
306+ // from staging into src/content/docs so the Astro build can resolve them.
307+ function syncReferencedImages ( links ) {
308+ let copied = 0 ;
309+ for ( const link of links ) {
310+ if ( SKIP_LINKS . has ( link ) ) continue ;
311+ const staged = path . join ( STAGING_DIR , `${ link } .md` ) ;
312+ if ( ! fs . existsSync ( staged ) ) continue ;
313+ const linkDir = path . posix . dirname ( link ) ;
314+ for ( const rel of collectImagePaths ( fs . readFileSync ( staged , 'utf8' ) ) ) {
315+ const relFromRoot = path . posix . normalize (
316+ linkDir === '.' ? rel : path . posix . join ( linkDir , rel ) ,
317+ ) ;
318+ if ( relFromRoot . startsWith ( '..' ) ) continue ;
319+ const src = path . join ( STAGING_DIR , relFromRoot ) ;
320+ if ( ! fs . existsSync ( src ) ) continue ;
321+ const dest = path . join ( DOCS_DIR , relFromRoot ) ;
322+ const srcBuf = fs . readFileSync ( src ) ;
323+ let destBuf = null ;
324+ try { destBuf = fs . readFileSync ( dest ) ; } catch ( e ) {
325+ if ( e . code !== 'ENOENT' ) throw e ;
326+ }
327+ if ( destBuf && destBuf . equals ( srcBuf ) ) continue ;
328+ fs . mkdirSync ( path . dirname ( dest ) , { recursive : true } ) ;
329+ fs . writeFileSync ( dest , srcBuf ) ;
330+ copied += 1 ;
331+ }
332+ }
333+ return copied ;
334+ }
335+
283336function assertNoDocsDrift ( diff ) {
284337 if ( diff . changes . length === 0 ) return ;
285338 const names = diff . changes . map ( ( c ) => `${ c . link } .md` ) ;
0 commit comments