11import { readFileSync , existsSync } from "fs" ;
22import { join , resolve , relative , dirname , isAbsolute , sep } from "path" ;
3+ import { CSS_URL_RE , isNonRelativeUrl } from "./assetPaths.js" ;
34import { transformSync } from "esbuild" ;
45import { compileHtml , type MediaDurationProber } from "./htmlCompiler" ;
56import {
@@ -72,14 +73,7 @@ function injectInterceptor(html: string, runtimeMode: "inline" | "placeholder" =
7273}
7374
7475function isRelativeUrl ( url : string ) : boolean {
75- if ( ! url ) return false ;
76- return (
77- ! url . startsWith ( "http://" ) &&
78- ! url . startsWith ( "https://" ) &&
79- ! url . startsWith ( "//" ) &&
80- ! url . startsWith ( "data:" ) &&
81- ! isAbsolute ( url )
82- ) ;
76+ return ! isNonRelativeUrl ( url ) && ! isAbsolute ( url ) ;
8377}
8478
8579function safeReadFile ( filePath : string ) : string | null {
@@ -94,8 +88,6 @@ function safeReadFile(filePath: string): string | null {
9488const CSS_IMPORT_RE =
9589 / @ i m p o r t \s + (?: u r l \( \s * ( [ " ' ] ? ) ( [ ^ ) " ' ] + ) \1\s * \) | ( [ " ' ] ) ( [ ^ " ' ] + ) \3) \s * ( [ ^ ; ] * ) ; \s * / g;
9690
97- const REBASE_URL_RE = / \b u r l \( \s * ( [ " ' ] ? ) ( [ ^ ) " ' ] + ) \1\s * \) / g;
98-
9991const CSS_COMMENT_RE = / \/ \* [ \s \S ] * ?\* \/ / g;
10092
10193function withCommentsStripped < T > (
@@ -123,7 +115,7 @@ function rebaseCssUrls(css: string, cssFileDir: string, projectDir: string): str
123115 const resolvedRoot = resolve ( projectDir ) ;
124116 const resolvedDir = resolve ( cssFileDir ) ;
125117 if ( resolvedDir === resolvedRoot ) return css ;
126- return css . replace ( REBASE_URL_RE , ( full , quote : string , urlValue : string ) => {
118+ return css . replace ( CSS_URL_RE , ( full , quote : string , urlValue : string ) => {
127119 if ( ! urlValue || ! isRelativeUrl ( urlValue ) ) return full ;
128120 const { basePath, suffix } = splitUrlSuffix ( urlValue . trim ( ) ) ;
129121 if ( ! basePath ) return full ;
@@ -205,29 +197,24 @@ function appendSuffixToUrl(baseUrl: string, suffix: string): string {
205197 return baseUrl ;
206198}
207199
208- function guessMimeType ( filePath : string ) : string {
209- const l = filePath . toLowerCase ( ) ;
210- if ( l . endsWith ( ".svg" ) ) return "image/svg+xml" ;
211- if ( l . endsWith ( ".json" ) ) return "application/json" ;
212- if ( l . endsWith ( ".txt" ) ) return "text/plain" ;
213- if ( l . endsWith ( ".xml" ) ) return "application/xml" ;
214- return "application/octet-stream" ;
215- }
216-
217- function shouldInlineAsDataUrl ( filePath : string ) : boolean {
218- const l = filePath . toLowerCase ( ) ;
219- return l . endsWith ( ".svg" ) || l . endsWith ( ".json" ) || l . endsWith ( ".txt" ) || l . endsWith ( ".xml" ) ;
220- }
200+ const INLINE_MIME : Record < string , string > = {
201+ ".svg" : "image/svg+xml" ,
202+ ".json" : "application/json" ,
203+ ".txt" : "text/plain" ,
204+ ".xml" : "application/xml" ,
205+ } ;
221206
222207function maybeInlineRelativeAssetUrl ( urlValue : string , projectDir : string ) : string | null {
223208 if ( ! urlValue || ! isRelativeUrl ( urlValue ) ) return null ;
224209 const { basePath, suffix } = splitUrlSuffix ( urlValue . trim ( ) ) ;
225210 if ( ! basePath ) return null ;
226211 const filePath = resolveWithinProject ( projectDir , basePath ) ;
227- if ( ! filePath || ! shouldInlineAsDataUrl ( filePath ) ) return null ;
212+ if ( ! filePath ) return null ;
213+ const ext = filePath . toLowerCase ( ) . match ( / \. [ ^ . ] + $ / ) ?. [ 0 ] ?? "" ;
214+ const mimeType = INLINE_MIME [ ext ] ;
215+ if ( ! mimeType ) return null ;
228216 const content = safeReadFileBuffer ( filePath ) ;
229217 if ( content == null ) return null ;
230- const mimeType = guessMimeType ( filePath ) ;
231218 const dataUrl = `data:${ mimeType } ;base64,${ content . toString ( "base64" ) } ` ;
232219 return appendSuffixToUrl ( dataUrl , suffix ) ;
233220}
@@ -479,14 +466,13 @@ function autoHealMissingCompositionIds(document: Document): void {
479466function coalesceHeadStylesAndBodyScripts ( document : Document ) : void {
480467 const headStyleEls = [ ...document . querySelectorAll ( "head style" ) ] ;
481468 if ( headStyleEls . length > 1 ) {
482- const importRe = / @ i m p o r t \s + u r l \( [ ^ ) ] * \) \s * ; | @ i m p o r t \s + [ " ' ] [ ^ " ' ] + [ " ' ] \s * ; / gi;
483469 const imports : string [ ] = [ ] ;
484470 const cssParts : string [ ] = [ ] ;
485471 const seenImports = new Set < string > ( ) ;
486472 for ( const el of headStyleEls ) {
487473 const raw = ( el . textContent || "" ) . trim ( ) ;
488474 if ( ! raw ) continue ;
489- const nonImportCss = raw . replace ( importRe , ( match ) => {
475+ const nonImportCss = raw . replace ( CSS_IMPORT_RE , ( match ) => {
490476 const cleaned = match . trim ( ) ;
491477 if ( ! seenImports . has ( cleaned ) ) {
492478 seenImports . add ( cleaned ) ;
@@ -607,6 +593,78 @@ export interface BundleOptions {
607593 * - Inlines sub-composition HTML fragments (data-composition-src)
608594 * - Inlines small textual assets as data URLs
609595 */
596+
597+ function ensureExternalScriptTag ( doc : Document , src : string ) : void {
598+ if ( doc . querySelector ( `script[src="${ src } "]` ) ) return ;
599+ const el = doc . createElement ( "script" ) ;
600+ el . setAttribute ( "src" , src ) ;
601+ doc . body . appendChild ( el ) ;
602+ }
603+
604+ function hoistExternalScript (
605+ src : string ,
606+ projectDir : string ,
607+ doc : Document ,
608+ seenSrcs : Set < string > ,
609+ chunks : string [ ] ,
610+ ) : void {
611+ if ( seenSrcs . has ( src ) ) return ;
612+ seenSrcs . add ( src ) ;
613+ if ( ! isNonRelativeUrl ( src ) && ! isAbsolute ( src ) ) {
614+ const jsPath = resolveWithinProject ( projectDir , src ) ;
615+ const js = jsPath ? safeReadFile ( jsPath ) : null ;
616+ if ( js != null ) {
617+ chunks . push ( js ) ;
618+ return ;
619+ }
620+ }
621+ ensureExternalScriptTag ( doc , src ) ;
622+ }
623+
624+ function hoistCompositionScripts (
625+ container : { querySelectorAll : ( sel : string ) => NodeListOf < Element > } ,
626+ opts : {
627+ projectDir : string ;
628+ document : Document ;
629+ compId : string | null ;
630+ runtimeScope : string | undefined ;
631+ runtimeCompId : string | undefined ;
632+ authoredRootId : string | undefined ;
633+ seenCompScriptSrcs : Set < string > ;
634+ compScriptChunks : string [ ] ;
635+ } ,
636+ ) : void {
637+ for ( const scriptEl of [ ...container . querySelectorAll ( "script" ) ] ) {
638+ const externalSrc = ( scriptEl . getAttribute ( "src" ) || "" ) . trim ( ) ;
639+ if ( externalSrc ) {
640+ hoistExternalScript (
641+ externalSrc ,
642+ opts . projectDir ,
643+ opts . document ,
644+ opts . seenCompScriptSrcs ,
645+ opts . compScriptChunks ,
646+ ) ;
647+ } else {
648+ opts . compScriptChunks . push (
649+ opts . compId
650+ ? wrapScopedCompositionScript (
651+ scriptEl . textContent || "" ,
652+ opts . compId ,
653+ "[HyperFrames] composition script error:" ,
654+ opts . runtimeScope ,
655+ opts . runtimeCompId || opts . compId ,
656+ opts . authoredRootId ,
657+ )
658+ : wrapInlineScriptWithErrorBoundary (
659+ scriptEl . textContent || "" ,
660+ "[HyperFrames] composition script error:" ,
661+ ) ,
662+ ) ;
663+ }
664+ scriptEl . remove ( ) ;
665+ }
666+ }
667+
610668export async function bundleToSingleHtml (
611669 projectDir : string ,
612670 options ?: BundleOptions ,
@@ -789,47 +847,16 @@ export async function bundleToSingleHtml(
789847 ) ;
790848 styleEl . remove ( ) ;
791849 }
792- // Hoist scripts into the collected script chunks
793- for ( const scriptEl of [ ...innerRoot . querySelectorAll ( "script" ) ] ) {
794- const externalSrc = ( scriptEl . getAttribute ( "src" ) || "" ) . trim ( ) ;
795- if ( externalSrc ) {
796- if ( ! seenCompScriptSrcs . has ( externalSrc ) ) {
797- seenCompScriptSrcs . add ( externalSrc ) ;
798- if ( isRelativeUrl ( externalSrc ) ) {
799- const jsPath = resolveWithinProject ( projectDir , externalSrc ) ;
800- const js = jsPath ? safeReadFile ( jsPath ) : null ;
801- if ( js != null ) {
802- compScriptChunks . push ( js ) ;
803- } else if ( ! document . querySelector ( `script[src="${ externalSrc } "]` ) ) {
804- const extScript = document . createElement ( "script" ) ;
805- extScript . setAttribute ( "src" , externalSrc ) ;
806- document . body . appendChild ( extScript ) ;
807- }
808- } else if ( ! document . querySelector ( `script[src="${ externalSrc } "]` ) ) {
809- const extScript = document . createElement ( "script" ) ;
810- extScript . setAttribute ( "src" , externalSrc ) ;
811- document . body . appendChild ( extScript ) ;
812- }
813- }
814- } else {
815- compScriptChunks . push (
816- compId
817- ? wrapScopedCompositionScript (
818- scriptEl . textContent || "" ,
819- compId ,
820- "[HyperFrames] composition script error:" ,
821- runtimeScope ,
822- runtimeCompId || compId ,
823- authoredRootId ,
824- )
825- : wrapInlineScriptWithErrorBoundary (
826- scriptEl . textContent || "" ,
827- "[HyperFrames] composition script error:" ,
828- ) ,
829- ) ;
830- }
831- scriptEl . remove ( ) ;
832- }
850+ hoistCompositionScripts ( innerRoot , {
851+ projectDir,
852+ document,
853+ compId,
854+ runtimeScope,
855+ runtimeCompId,
856+ authoredRootId : authoredRootId ?? undefined ,
857+ seenCompScriptSrcs,
858+ compScriptChunks,
859+ } ) ;
833860
834861 // Copy dimension attributes from inner root to host if not already set
835862 const innerW = innerRoot . getAttribute ( "data-width" ) ;
@@ -845,45 +872,16 @@ export async function bundleToSingleHtml(
845872 compStyleChunks . push ( compId ? scopeCssToComposition ( css , compId , runtimeScope ) : css ) ;
846873 styleEl . remove ( ) ;
847874 }
848- for ( const scriptEl of [ ...innerDoc . querySelectorAll ( "script" ) ] ) {
849- const externalSrc = ( scriptEl . getAttribute ( "src" ) || "" ) . trim ( ) ;
850- if ( externalSrc ) {
851- if ( ! seenCompScriptSrcs . has ( externalSrc ) ) {
852- seenCompScriptSrcs . add ( externalSrc ) ;
853- if ( isRelativeUrl ( externalSrc ) ) {
854- const jsPath = resolveWithinProject ( projectDir , externalSrc ) ;
855- const js = jsPath ? safeReadFile ( jsPath ) : null ;
856- if ( js != null ) {
857- compScriptChunks . push ( js ) ;
858- } else if ( ! document . querySelector ( `script[src="${ externalSrc } "]` ) ) {
859- const extScript = document . createElement ( "script" ) ;
860- extScript . setAttribute ( "src" , externalSrc ) ;
861- document . body . appendChild ( extScript ) ;
862- }
863- } else if ( ! document . querySelector ( `script[src="${ externalSrc } "]` ) ) {
864- const extScript = document . createElement ( "script" ) ;
865- extScript . setAttribute ( "src" , externalSrc ) ;
866- document . body . appendChild ( extScript ) ;
867- }
868- }
869- } else {
870- compScriptChunks . push (
871- compId
872- ? wrapScopedCompositionScript (
873- scriptEl . textContent || "" ,
874- compId ,
875- "[HyperFrames] composition script error:" ,
876- runtimeScope ,
877- runtimeCompId || compId ,
878- )
879- : wrapInlineScriptWithErrorBoundary (
880- scriptEl . textContent || "" ,
881- "[HyperFrames] composition script error:" ,
882- ) ,
883- ) ;
884- }
885- scriptEl . remove ( ) ;
886- }
875+ hoistCompositionScripts ( innerDoc , {
876+ projectDir,
877+ document,
878+ compId,
879+ runtimeScope,
880+ runtimeCompId,
881+ authoredRootId : undefined ,
882+ seenCompScriptSrcs,
883+ compScriptChunks,
884+ } ) ;
887885
888886 host . innerHTML = innerDoc . body . innerHTML || "" ;
889887 }
0 commit comments