1- import crypto from 'crypto' ;
21import fs from 'fs' ;
32import path from 'path' ;
43import esbuild from 'esbuild' ;
@@ -7,7 +6,7 @@ import { createServer, type Plugin } from 'vite';
76import { serializer } from '@hydrooj/framework' ;
87import {
98 Context , Handler , Logger ,
10- NotFoundError , param , size , Types ,
9+ NotFoundError , param , sha1 , size , Types ,
1110} from 'hydrooj' ;
1211
1312const logger = new Logger ( 'ui-next' ) ;
@@ -100,12 +99,25 @@ const federationPlugin: esbuild.Plugin = {
10099const vfs : Record < string , string > = { } ;
101100const hashes : Record < string , string > = { } ;
102101
102+ const applyCss = ( css : string ) => `
103+ (() => {
104+ const style = document.createElement('style');
105+ style.textContent = ${ JSON . stringify ( css ) } ;
106+ document.head.appendChild(style);
107+ })();
108+ ` ;
109+
110+ function addFile ( name : string , content : string ) {
111+ vfs [ name ] = content ;
112+ hashes [ name ] = sha1 ( content ) . substring ( 0 , 8 ) ;
113+ }
114+
103115class UiNextConstantHandler extends Handler {
104116 noCheckPermView = true ;
105117
106118 @param ( 'name' , Types . Filename )
107119 async all ( domainId : string , name : string ) {
108- if ( ! vfs [ name ] ) throw new NotFoundError ( name ) ;
120+ if ( ! ( name in vfs ) ) throw new NotFoundError ( name ) ;
109121 this . response . type = 'application/javascript' ;
110122 this . response . body = vfs [ name ] ;
111123 this . response . addHeader ( 'ETag' , hashes [ name ] ) ;
@@ -118,9 +130,23 @@ export async function buildPlugins() {
118130 let totalSize = 0 ;
119131 const entries = getAddonEntries ( ) ;
120132
133+ const newPluginFiles = new Set < string > ( ) ;
134+ const emit = ( name : string , content : string ) => {
135+ addFile ( name , content ) ;
136+ newPluginFiles . add ( name ) ;
137+ } ;
138+ const purge = ( ) => {
139+ for ( const key of Object . keys ( vfs ) ) {
140+ if ( ! newPluginFiles . has ( key ) ) {
141+ delete vfs [ key ] ;
142+ delete hashes [ key ] ;
143+ }
144+ }
145+ } ;
146+
121147 if ( ! Object . keys ( entries ) . length ) {
122- vfs [ 'plugins.js' ] = 'window.__hydroPlugins = [];' ;
123- hashes [ 'plugins.js' ] = '00000000' ;
148+ emit ( 'plugins.js' , 'window.__hydroPlugins = [];' ) ;
149+ purge ( ) ;
124150 logger . info ( 'No plugins to build' ) ;
125151 return ;
126152 }
@@ -132,11 +158,18 @@ export async function buildPlugins() {
132158 ...Object . entries ( entries ) . map ( ( [ _ , e ] , i ) => `import * as plugin${ i } from '${ e } ';` ) ,
133159 `window.__hydroPlugins = [${ Object . entries ( entries ) . map ( ( [ n ] , i ) => `{ name: '${ n } ', ...plugin${ i } }` ) . join ( ', ' ) } ];` ,
134160 ] . join ( '\n' ) ,
161+ sourcefile : 'plugins.ts' ,
135162 resolveDir : process . cwd ( ) ,
136163 loader : 'ts' ,
137164 } ,
138165 bundle : true ,
139- format : 'iife' ,
166+ format : 'esm' ,
167+ splitting : true ,
168+ outdir : 'plugins-dist' ,
169+ entryNames : 'plugins' ,
170+ chunkNames : 'chunk-[hash]' ,
171+ assetNames : 'asset-[hash]' ,
172+ metafile : true ,
140173 write : false ,
141174 target : [ 'chrome90' ] ,
142175 plugins : [ federationPlugin ] ,
@@ -145,11 +178,42 @@ export async function buildPlugins() {
145178 jsxImportSource : 'react' ,
146179 } ) ;
147180 if ( result . errors . length ) logger . error ( 'Plugin build errors: %o' , result . errors ) ;
148- const content = result . outputFiles ?. [ 0 ] ?. text || 'window.__hydroPlugins = [];' ;
149- vfs [ 'plugins.js' ] = content ;
150- hashes [ 'plugins.js' ] = crypto . createHash ( 'sha1' ) . update ( content ) . digest ( 'hex' ) . substring ( 0 , 8 ) ;
151- totalSize += content . length ;
152- logger . success ( 'Plugins built in %dms (%d entries, %s)' , Date . now ( ) - start , entries . length , size ( totalSize ) ) ;
181+
182+ const cssText = new Map < string , string > ( ) ;
183+ for ( const f of result . outputFiles ) {
184+ if ( f . path . endsWith ( '.css' ) ) cssText . set ( f . path , f . text ) ;
185+ }
186+
187+ const cssForJs = new Map < string , string > ( ) ;
188+ const claimed = new Set < string > ( ) ;
189+ for ( const [ rel , meta ] of Object . entries ( result . metafile . outputs ) ) {
190+ if ( ! meta . cssBundle ) continue ;
191+ const css = path . resolve ( meta . cssBundle ) ;
192+ cssForJs . set ( path . resolve ( rel ) , css ) ;
193+ claimed . add ( css ) ;
194+ }
195+
196+ let unclaimedCss = '' ;
197+ for ( const [ abs , text ] of cssText ) {
198+ if ( ! claimed . has ( abs ) ) unclaimedCss += text ;
199+ }
200+
201+ for ( const f of result . outputFiles ) {
202+ if ( f . path . endsWith ( '.css' ) ) continue ;
203+
204+ const name = path . basename ( f . path ) ;
205+ let content = f . text ;
206+
207+ const css = cssText . get ( cssForJs . get ( f . path ) ?? '' ) ;
208+ if ( css ) content = applyCss ( css ) + content ;
209+ if ( name === 'plugins.js' && unclaimedCss ) content = applyCss ( unclaimedCss ) + content ;
210+
211+ totalSize += content . length ;
212+ emit ( name , content ) ;
213+ }
214+
215+ purge ( ) ;
216+ logger . success ( 'Plugins built in %dms (%d entries, %s)' , Date . now ( ) - start , Object . keys ( entries ) . length , size ( totalSize ) ) ;
153217 } catch ( e ) {
154218 logger . error ( 'Plugin build failed: %o' , e ) ;
155219 }
0 commit comments