@@ -2,6 +2,7 @@ import { PluginContext } from 'rollup';
22import path from 'path' ;
33import { bundleAsync , transform } from 'lightningcss' ;
44import fs from 'fs' ;
5+ import xxhash from 'xxhash-wasm' ;
56
67import { InputAsset , InputData } from '../input/InputData' ;
78import { createAssetPicomatchMatcher } from '../assets/utils.js' ;
@@ -11,6 +12,7 @@ import { createAssetPlaceholder } from './css.js';
1112export interface EmittedAssets {
1213 static : Map < string , string > ;
1314 hashed : Map < string , string > ;
15+ assetsInCssByHash : Record < string , { ref : string } > ;
1416}
1517
1618const allowedFileExtensions = [
@@ -40,6 +42,8 @@ export async function emitAssets(
4042 inputs : InputData [ ] ,
4143 options : RollupPluginHTMLOptions ,
4244) {
45+ const { create64 } = await xxhash ( ) ;
46+
4347 const extractAssets = options . extractAssets ?? true ;
4448 const bundleCss = options . bundleCss ?? true ;
4549 const minifyCss = options . minifyCss ?? false ;
@@ -56,9 +60,28 @@ export async function emitAssets(
5660 transforms . push ( options . transformAsset ) ;
5761 }
5862 }
63+
64+ async function getTransformedAsset ( content : Buffer , filePath : string ) : Promise < Buffer > {
65+ let source : Buffer = content ;
66+ for ( const transform of transforms ) {
67+ const result = await transform ( content , filePath ) ;
68+ if ( result != null ) {
69+ source = typeof result === 'string' ? Buffer . from ( result , 'utf-8' ) : result ;
70+ }
71+ }
72+ return source ;
73+ }
74+
5975 const staticAssets : InputAsset [ ] = [ ] ;
6076 const hashedAssets : InputAsset [ ] = [ ] ;
6177
78+ let assetsInCssCounter = 0 ;
79+ const assetsInCssByAbsPath : Record <
80+ string ,
81+ { tempPlaceholder : string ; ref ?: string ; outputPath ?: string ; hash ?: string }
82+ > = { } ;
83+ const assetsInCssByHash : Record < string , { ref : string } > = { } ;
84+
6285 for ( const input of inputs ) {
6386 for ( const asset of input . assets ) {
6487 if ( asset . hashed ) {
@@ -75,20 +98,10 @@ export async function emitAssets(
7598 for ( const asset of allAssets ) {
7699 const map = asset . hashed ? emittedHashedAssets : emittedStaticAssets ;
77100 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-
101+ let source = await getTransformedAsset ( asset . content , asset . filePath ) ;
88102 let ref : string ;
89103 let basename = path . basename ( asset . filePath ) ;
90104 const isExternal = createAssetPicomatchMatcher ( options . externalAssets ) ;
91- const emittedAssets = new Map < string , { filePath : string ; refId : string } > ( ) ;
92105 if ( asset . hashed ) {
93106 if ( basename . endsWith ( '.css' ) && extractAssets ) {
94107 const { code } = await ( bundleCss ? bundleAsync : transform ) ( {
@@ -99,48 +112,79 @@ export async function emitAssets(
99112 Url : url => {
100113 // Support foo.svg#bar
101114 // 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 ( '#' ) ;
115+ const [ srcAssetPath , srcAssetId ] = url . url . split ( '#' ) ;
103116
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 ) ;
117+ if ( shouldHandleAsset ( srcAssetPath ) && ! isExternal ( srcAssetPath ) ) {
118+ const assetAbsPath = path . resolve ( path . dirname ( asset . filePath ) , srcAssetPath ) ;
108119
109- let emittedAsset = emittedAssets . get ( assetLocation ) ;
120+ let assetInCss = assetsInCssByAbsPath [ assetAbsPath ] ;
110121
111- if ( ! emittedAsset ) {
122+ if ( ! assetInCss ) {
112123 // Avoid duplicates
113- const basename = path . basename ( filePath ) ;
114- const fileRef = this . emitFile ( {
115- type : 'asset' ,
116- name : extractAssetsLegacyCss ? `assets/${ basename } ` : basename ,
117- originalFileName : assetLocation ,
118- source : assetContent ,
119- } ) ;
120- const emittedAssetFilepath = this . getFileName ( fileRef ) ;
121- emittedAsset = {
122- filePath : emittedAssetFilepath ,
123- refId : fileRef ,
124+ assetsInCssCounter ++ ;
125+ assetInCss = {
126+ tempPlaceholder : createAssetPlaceholder ( assetsInCssCounter . toString ( ) ) ,
127+ ref : undefined ,
128+ hash : undefined ,
124129 } ;
125- emittedAssets . set ( assetLocation , emittedAsset ) ;
130+ assetsInCssByAbsPath [ assetAbsPath ] = assetInCss ;
126131 }
127132
128- if ( extractAssetsLegacyCss ) {
129- const emittedAssetBasename = path . basename ( emittedAsset . filePath ) ;
130- url . url = `assets/${ emittedAssetBasename } ` ;
131- } else {
132- url . url = createAssetPlaceholder ( emittedAsset . refId ) ;
133- }
134-
135- if ( idRef ) {
136- url . url = `${ url . url } #${ idRef } ` ;
137- }
133+ url . url = srcAssetId
134+ ? `${ assetInCss . tempPlaceholder } #${ srcAssetId } `
135+ : assetInCss . tempPlaceholder ;
138136 }
137+
139138 return url ;
140139 } ,
141140 } ,
142141 } ) ;
143- const codeBuffer = Buffer . from ( code ) ;
142+
143+ let codeString = code . toString ( ) ;
144+
145+ for ( const assetInCssAbsPath of Object . keys ( assetsInCssByAbsPath ) ) {
146+ const assetInCss = assetsInCssByAbsPath [ assetInCssAbsPath ] ;
147+
148+ if ( ! assetInCss . ref ) {
149+ const basename = path . basename ( assetInCssAbsPath ) ;
150+ const content = await fs . promises . readFile ( assetInCssAbsPath ) ;
151+ const transformedContent = await getTransformedAsset ( content , assetInCssAbsPath ) ;
152+ const ref = this . emitFile ( {
153+ type : 'asset' ,
154+ name : extractAssetsLegacyCss ? `assets/${ basename } ` : basename ,
155+ originalFileName : assetInCssAbsPath ,
156+ source : transformedContent ,
157+ } ) ;
158+ assetInCss . ref = ref ;
159+ assetInCss . outputPath = this . getFileName ( ref ) ;
160+ if ( ! extractAssetsLegacyCss ) {
161+ assetInCss . hash = create64 ( )
162+ . update ( transformedContent )
163+ . update ( '\0' )
164+ . update ( assetInCss . outputPath )
165+ . digest ( )
166+ . toString ( 16 )
167+ . padStart ( 16 , '0' ) ;
168+ }
169+ }
170+
171+ if ( extractAssetsLegacyCss ) {
172+ const outputName = path . basename ( assetInCss . outputPath ! ) ;
173+ codeString = codeString . replaceAll (
174+ assetInCss . tempPlaceholder ,
175+ `assets/${ outputName } ` ,
176+ ) ;
177+ } else {
178+ const hash = assetInCss . hash ! ;
179+ assetsInCssByHash [ hash ] = { ref : assetInCss . ref } ;
180+ codeString = codeString . replaceAll (
181+ assetInCss . tempPlaceholder ,
182+ createAssetPlaceholder ( hash ) ,
183+ ) ;
184+ }
185+ }
186+
187+ const codeBuffer = Buffer . from ( codeString ) ;
144188 if ( ! asset . content . equals ( codeBuffer ) ) {
145189 source = codeBuffer ;
146190 }
@@ -169,5 +213,5 @@ export async function emitAssets(
169213 }
170214 }
171215
172- return { static : emittedStaticAssets , hashed : emittedHashedAssets } ;
216+ return { static : emittedStaticAssets , hashed : emittedHashedAssets , assetsInCssByHash } ;
173217}
0 commit comments