11import crypto from 'node:crypto' ;
22import fs from 'node:fs' ;
33import path from 'node:path' ;
4- import util from 'node:util' ;
54import type { Compiler as RspackCompiler } from '@rspack/core' ;
65import jwt from 'jsonwebtoken' ;
76import type { Compiler as WebpackCompiler } from 'webpack' ;
87import { type CodeSigningPluginConfig , validateConfig } from './config.js' ;
98
109export class CodeSigningPlugin {
11- private chunkFilenames : Set < string > ;
1210 /**
1311 * Constructs new `RepackPlugin`.
1412 *
@@ -17,7 +15,6 @@ export class CodeSigningPlugin {
1715 constructor ( private config : CodeSigningPluginConfig ) {
1816 validateConfig ( config ) ;
1917 this . config . excludeChunks = this . config . excludeChunks ?? [ ] ;
20- this . chunkFilenames = new Set ( ) ;
2118 }
2219
2320 private shouldSignFile (
@@ -26,7 +23,7 @@ export class CodeSigningPlugin {
2623 excludedChunks : string [ ] | RegExp [ ]
2724 ) : boolean {
2825 /** Exclude non-chunks & main chunk as it's always local */
29- if ( ! this . chunkFilenames . has ( file ) || file === mainOutputFilename ) {
26+ if ( file === mainOutputFilename ) {
3027 return false ;
3128 }
3229
@@ -38,6 +35,26 @@ export class CodeSigningPlugin {
3835 } ) ;
3936 }
4037
38+ private signAsset (
39+ asset : { source : { source ( ) : string | Buffer } } ,
40+ privateKey : Buffer ,
41+ beginMark : string ,
42+ tokenBufferSize : number
43+ ) : Buffer {
44+ const source = asset . source . source ( ) ;
45+ const content = Buffer . isBuffer ( source ) ? source : Buffer . from ( source ) ;
46+
47+ const hash = crypto . createHash ( 'sha256' ) . update ( content ) . digest ( 'hex' ) ;
48+ const token = jwt . sign ( { hash } , privateKey , {
49+ algorithm : 'RS256' ,
50+ } ) ;
51+
52+ return Buffer . concat (
53+ [ content , Buffer . from ( beginMark ) , Buffer . from ( token ) ] ,
54+ content . length + tokenBufferSize
55+ ) ;
56+ }
57+
4158 apply ( compiler : RspackCompiler ) : void ;
4259 apply ( compiler : WebpackCompiler ) : void ;
4360
@@ -75,40 +92,49 @@ export class CodeSigningPlugin {
7592 ? this . config . excludeChunks
7693 : [ this . config . excludeChunks as RegExp ] ;
7794
78- compiler . hooks . emit . tap ( 'RepackCodeSigningPlugin' , ( compilation ) => {
79- compilation . chunks . forEach ( ( chunk ) => {
80- chunk . files . forEach ( ( file ) => this . chunkFilenames . add ( file ) ) ;
81- } ) ;
82- } ) ;
83-
84- compiler . hooks . assetEmitted . tapPromise (
85- { name : 'RepackCodeSigningPlugin' , stage : 20 } ,
86- async ( file , { outputPath, compilation } ) => {
87- const outputFilepath = path . join ( outputPath , file ) ;
88- const readFileAsync = util . promisify (
89- compiler . outputFileSystem ! . readFile
90- ) ;
91- const content = ( await readFileAsync ( outputFilepath ) ) as Buffer ;
95+ compiler . hooks . thisCompilation . tap (
96+ 'RepackCodeSigningPlugin' ,
97+ ( compilation ) => {
98+ const { sources } = compiler . webpack ;
9299 const mainBundleName = compilation . outputOptions . filename as string ;
93- if ( ! this . shouldSignFile ( file , mainBundleName , excludedChunks ) ) {
94- return ;
95- }
96- logger . debug ( `Signing ${ file } ` ) ;
97- /** generate bundle hash */
98- const hash = crypto . createHash ( 'sha256' ) . update ( content ) . digest ( 'hex' ) ;
99- /** generate token */
100- const token = jwt . sign ( { hash } , privateKey , { algorithm : 'RS256' } ) ;
101- /** combine the bundle and the token */
102- const signedBundle = Buffer . concat (
103- [ content , Buffer . from ( BEGIN_CS_MARK ) , Buffer . from ( token ) ] ,
104- content . length + TOKEN_BUFFER_SIZE
105- ) ;
106100
107- const writeFileAsync = util . promisify (
108- compiler . outputFileSystem ! . writeFile
101+ compilation . hooks . processAssets . tap (
102+ {
103+ name : 'RepackCodeSigningPlugin' ,
104+ // Sign at ANALYSE (2000) so later processAssets consumers,
105+ // such as Zephyr at REPORT (5000), receive already-signed assets
106+ stage : compiler . webpack . Compilation . PROCESS_ASSETS_STAGE_ANALYSE ,
107+ } ,
108+ ( ) => {
109+ for ( const chunk of compilation . chunks ) {
110+ for ( const file of chunk . files ) {
111+ if (
112+ ! this . shouldSignFile ( file , mainBundleName , excludedChunks )
113+ ) {
114+ continue ;
115+ }
116+
117+ const asset = compilation . getAsset ( file ) ;
118+ if ( ! asset ) continue ;
119+
120+ logger . debug ( `Signing ${ file } ` ) ;
121+ const signedBundle = this . signAsset (
122+ asset ,
123+ privateKey ,
124+ BEGIN_CS_MARK ,
125+ TOKEN_BUFFER_SIZE
126+ ) ;
127+
128+ compilation . updateAsset (
129+ file ,
130+ new sources . RawSource ( signedBundle )
131+ ) ;
132+
133+ logger . debug ( `Signed ${ file } ` ) ;
134+ }
135+ }
136+ }
109137 ) ;
110- await writeFileAsync ( outputFilepath , signedBundle ) ;
111- logger . debug ( `Signed ${ file } ` ) ;
112138 }
113139 ) ;
114140 }
0 commit comments