1+ import { createHash } from 'node:crypto' ;
12import { PluginContext } from 'rollup' ;
23import path from 'path' ;
34import { bundleAsync , transform } from 'lightningcss' ;
45import fs from 'fs' ;
56
67import { InputAsset , InputData } from '../input/InputData' ;
7- import { toBrowserPath } from './utils.js' ;
88import { createAssetPicomatchMatcher } from '../assets/utils.js' ;
99import { RollupPluginHTMLOptions , TransformAssetFunction } from '../RollupPluginHTMLOptions' ;
10+ import { createAssetPlaceholder } from './css.js' ;
1011
1112export interface EmittedAssets {
1213 static : Map < string , string > ;
1314 hashed : Map < string , string > ;
15+ assetsInCssByHash : Record < string , { ref : string } > ;
1416}
1517
1618const allowedFileExtensions = [
@@ -56,9 +58,28 @@ export async function emitAssets(
5658 transforms . push ( options . transformAsset ) ;
5759 }
5860 }
61+
62+ async function getTransformedAsset ( content : Buffer , filePath : string ) : Promise < Buffer > {
63+ let source : Buffer = content ;
64+ for ( const transform of transforms ) {
65+ const result = await transform ( content , filePath ) ;
66+ if ( result != null ) {
67+ source = typeof result === 'string' ? Buffer . from ( result , 'utf-8' ) : result ;
68+ }
69+ }
70+ return source ;
71+ }
72+
5973 const staticAssets : InputAsset [ ] = [ ] ;
6074 const hashedAssets : InputAsset [ ] = [ ] ;
6175
76+ let assetsInCssCounter = 0 ;
77+ const assetsInCssByAbsPath : Record <
78+ string ,
79+ { tempPlaceholder : string ; ref ?: string ; outputPath ?: string ; hash ?: string }
80+ > = { } ;
81+ const assetsInCssByHash : Record < string , { ref : string } > = { } ;
82+
6283 for ( const input of inputs ) {
6384 for ( const asset of input . assets ) {
6485 if ( asset . hashed ) {
@@ -75,20 +96,10 @@ export async function emitAssets(
7596 for ( const asset of allAssets ) {
7697 const map = asset . hashed ? emittedHashedAssets : emittedStaticAssets ;
7798 if ( ! map . has ( asset . filePath ) ) {
78- let source : Buffer = asset . content ;
79-
80- // run user's transform functions
81- for ( const transform of transforms ) {
82- const result = await transform ( asset . content , asset . filePath ) ;
83- if ( result != null ) {
84- source = typeof result === 'string' ? Buffer . from ( result , 'utf-8' ) : result ;
85- }
86- }
87-
99+ let source = await getTransformedAsset ( asset . content , asset . filePath ) ;
88100 let ref : string ;
89101 let basename = path . basename ( asset . filePath ) ;
90102 const isExternal = createAssetPicomatchMatcher ( options . externalAssets ) ;
91- const emittedExternalAssets = new Map ( ) ;
92103 if ( asset . hashed ) {
93104 if ( basename . endsWith ( '.css' ) && extractAssets ) {
94105 const { code } = await ( bundleCss ? bundleAsync : transform ) ( {
@@ -99,69 +110,88 @@ export async function emitAssets(
99110 Url : url => {
100111 // Support foo.svg#bar
101112 // https://www.w3.org/TR/html4/types.html#:~:text=ID%20and%20NAME%20tokens%20must,tokens%20defined%20by%20other%20attributes.
102- const [ filePath , idRef ] = url . url . split ( '#' ) ;
103-
104- if ( shouldHandleAsset ( filePath ) && ! isExternal ( filePath ) ) {
105- // Read the asset file, get the asset from the source location on the FS using asset.filePath
106- const assetLocation = path . resolve ( path . dirname ( asset . filePath ) , filePath ) ;
107- const assetContent = fs . readFileSync ( assetLocation ) ;
108-
109- // Avoid duplicates
110- if ( ! emittedExternalAssets . has ( assetLocation ) ) {
111- const basename = path . basename ( filePath ) ;
112- const fileRef = this . emitFile ( {
113- type : 'asset' ,
114- name : extractAssetsLegacyCss ? `assets/${ basename } ` : basename ,
115- source : assetContent ,
116- } ) ;
117- const emittedAssetFilepath = this . getFileName ( fileRef ) ;
118- const emittedAssetBasename = path . basename ( emittedAssetFilepath ) ;
119- emittedExternalAssets . set ( assetLocation , emittedAssetFilepath ) ;
120- // Update the URL in the original CSS file to point to the emitted asset file
121- if ( extractAssetsLegacyCss ) {
122- url . url = `assets/${ emittedAssetBasename } ` ;
123- } else {
124- if ( options . publicPath ) {
125- url . url = toBrowserPath (
126- path . join ( options . publicPath , emittedAssetFilepath ) ,
127- ) ;
128- } else {
129- url . url = emittedAssetBasename ;
130- }
131- }
132- if ( idRef ) {
133- url . url = `${ url . url } #${ idRef } ` ;
134- }
135- } else {
136- const emittedAssetFilepath = emittedExternalAssets . get ( assetLocation ) ;
137- const emittedAssetBasename = path . basename ( emittedAssetFilepath ) ;
138- if ( extractAssetsLegacyCss ) {
139- url . url = `assets/${ emittedAssetBasename } ` ;
140- } else {
141- if ( options . publicPath ) {
142- url . url = toBrowserPath (
143- path . join ( options . publicPath , emittedAssetFilepath ) ,
144- ) ;
145- } else {
146- url . url = emittedAssetBasename ;
147- }
148- }
149- if ( idRef ) {
150- url . url = `${ url . url } #${ idRef } ` ;
151- }
113+ const [ srcAssetPath , srcAssetId ] = url . url . split ( '#' ) ;
114+
115+ if ( shouldHandleAsset ( srcAssetPath ) && ! isExternal ( srcAssetPath ) ) {
116+ const assetAbsPath = path . resolve ( path . dirname ( asset . filePath ) , srcAssetPath ) ;
117+
118+ let assetInCss = assetsInCssByAbsPath [ assetAbsPath ] ;
119+
120+ if ( ! assetInCss ) {
121+ // Avoid duplicates
122+ assetsInCssCounter ++ ;
123+ assetInCss = {
124+ tempPlaceholder : createAssetPlaceholder ( assetsInCssCounter . toString ( ) ) ,
125+ ref : undefined ,
126+ hash : undefined ,
127+ } ;
128+ assetsInCssByAbsPath [ assetAbsPath ] = assetInCss ;
152129 }
130+
131+ url . url = srcAssetId
132+ ? `${ assetInCss . tempPlaceholder } #${ srcAssetId } `
133+ : assetInCss . tempPlaceholder ;
153134 }
135+
154136 return url ;
155137 } ,
156138 } ,
157139 } ) ;
158- const codeBuffer = Buffer . from ( code ) ;
140+
141+ let codeString = code . toString ( ) ;
142+
143+ for ( const assetInCssAbsPath of Object . keys ( assetsInCssByAbsPath ) ) {
144+ const assetInCss = assetsInCssByAbsPath [ assetInCssAbsPath ] ;
145+
146+ if ( ! assetInCss . ref ) {
147+ const basename = path . basename ( assetInCssAbsPath ) ;
148+ const content = await fs . promises . readFile ( assetInCssAbsPath ) ;
149+ const transformedContent = await getTransformedAsset ( content , assetInCssAbsPath ) ;
150+ const ref = this . emitFile ( {
151+ type : 'asset' ,
152+ name : extractAssetsLegacyCss ? `assets/${ basename } ` : basename ,
153+ originalFileName : assetInCssAbsPath ,
154+ source : transformedContent ,
155+ } ) ;
156+ assetInCss . ref = ref ;
157+ assetInCss . outputPath = this . getFileName ( ref ) ;
158+ if ( ! extractAssetsLegacyCss ) {
159+ assetInCss . hash = createHash ( 'sha256' )
160+ . update ( transformedContent )
161+ . update ( '\0' )
162+ . update ( assetInCss . outputPath )
163+ . digest ( 'hex' ) ;
164+ }
165+ }
166+
167+ if ( extractAssetsLegacyCss ) {
168+ const outputName = path . basename ( assetInCss . outputPath ! ) ;
169+ codeString = codeString . replaceAll (
170+ assetInCss . tempPlaceholder ,
171+ `assets/${ outputName } ` ,
172+ ) ;
173+ } else {
174+ const hash = assetInCss . hash ! ;
175+ assetsInCssByHash [ hash ] = { ref : assetInCss . ref } ;
176+ codeString = codeString . replaceAll (
177+ assetInCss . tempPlaceholder ,
178+ createAssetPlaceholder ( hash ) ,
179+ ) ;
180+ }
181+ }
182+
183+ const codeBuffer = Buffer . from ( codeString ) ;
159184 if ( ! asset . content . equals ( codeBuffer ) ) {
160185 source = codeBuffer ;
161186 }
162187 }
163188
164- ref = this . emitFile ( { type : 'asset' , name : basename , source } ) ;
189+ ref = this . emitFile ( {
190+ type : 'asset' ,
191+ name : basename ,
192+ originalFileName : asset . filePath ,
193+ source,
194+ } ) ;
165195 } else {
166196 // ensure the output filename is unique
167197 let i = 1 ;
@@ -179,5 +209,5 @@ export async function emitAssets(
179209 }
180210 }
181211
182- return { static : emittedStaticAssets , hashed : emittedHashedAssets } ;
212+ return { static : emittedStaticAssets , hashed : emittedHashedAssets , assetsInCssByHash } ;
183213}
0 commit comments