11import fs from 'node:fs' ;
2+ import { createRequire } from 'node:module' ;
23import path from 'node:path' ;
34import type { Plugin } from 'vite' ;
45
56/**
67 * Post-build CSS steps for the multi-entry library bundle. See css-layers.md.
78 *
89 * Emits `index.css` (all component styles, prefixed with the @layer order)
9- * and `styles.css` (the global layer order, reset and typography ), then
10+ * and `styles.css` (the global layer order, reset, typography and fonts ), then
1011 * stamps the @layer order statement into every per-component stylesheet in
1112 * `dist/assets/`. Duplicate statements are no-ops, so whichever stylesheet
1213 * loads first establishes the correct order. Do not rely on import order
@@ -22,13 +23,131 @@ export function combineCssBundle(rootDirectory: string): Plugin {
2223 name : 'wb-ui:combine-css-bundle' ,
2324 apply : 'build' ,
2425 closeBundle ( ) {
25- writeCombinedStylesheet ( distributionDirectory , stylesDirectory ) ;
26- writeGlobalStylesheet ( distributionDirectory , stylesDirectory ) ;
26+ const fontStyles = emitFontAssets ( distributionDirectory ) ;
27+ const layerOrder = readLayerOrder ( stylesDirectory ) ;
28+ fs . writeFileSync ( path . resolve ( distributionDirectory , 'fonts.css' ) , `${ layerOrder } \n${ fontStyles } \n` ) ;
29+ writeCombinedStylesheet ( distributionDirectory , stylesDirectory , fontStyles ) ;
30+ writeGlobalStylesheet ( distributionDirectory , stylesDirectory , fontStyles ) ;
2731 prependLayerOrderToAssets ( distributionDirectory , stylesDirectory ) ;
2832 } ,
2933 } ;
3034}
3135
36+ type FontFaceDefinition = {
37+ family : 'Inter' | 'Poppins' ;
38+ packageName : '@fontsource/inter' | '@fontsource/poppins' ;
39+ subset : 'latin' | 'latin-ext' ;
40+ weight : 300 | 400 | 500 | 600 | 700 ;
41+ inline : boolean ;
42+ } ;
43+
44+ const FONT_FACES : FontFaceDefinition [ ] = [
45+ { family : 'Poppins' , packageName : '@fontsource/poppins' , subset : 'latin' , weight : 300 , inline : false } ,
46+ { family : 'Poppins' , packageName : '@fontsource/poppins' , subset : 'latin' , weight : 400 , inline : true } ,
47+ { family : 'Poppins' , packageName : '@fontsource/poppins' , subset : 'latin' , weight : 500 , inline : false } ,
48+ { family : 'Poppins' , packageName : '@fontsource/poppins' , subset : 'latin' , weight : 600 , inline : true } ,
49+ { family : 'Poppins' , packageName : '@fontsource/poppins' , subset : 'latin' , weight : 700 , inline : false } ,
50+ {
51+ family : 'Poppins' ,
52+ packageName : '@fontsource/poppins' ,
53+ subset : 'latin-ext' ,
54+ weight : 300 ,
55+ inline : false ,
56+ } ,
57+ {
58+ family : 'Poppins' ,
59+ packageName : '@fontsource/poppins' ,
60+ subset : 'latin-ext' ,
61+ weight : 400 ,
62+ inline : false ,
63+ } ,
64+ {
65+ family : 'Poppins' ,
66+ packageName : '@fontsource/poppins' ,
67+ subset : 'latin-ext' ,
68+ weight : 500 ,
69+ inline : false ,
70+ } ,
71+ {
72+ family : 'Poppins' ,
73+ packageName : '@fontsource/poppins' ,
74+ subset : 'latin-ext' ,
75+ weight : 600 ,
76+ inline : false ,
77+ } ,
78+ {
79+ family : 'Poppins' ,
80+ packageName : '@fontsource/poppins' ,
81+ subset : 'latin-ext' ,
82+ weight : 700 ,
83+ inline : false ,
84+ } ,
85+ { family : 'Inter' , packageName : '@fontsource/inter' , subset : 'latin' , weight : 400 , inline : false } ,
86+ {
87+ family : 'Inter' ,
88+ packageName : '@fontsource/inter' ,
89+ subset : 'latin-ext' ,
90+ weight : 400 ,
91+ inline : false ,
92+ } ,
93+ ] ;
94+
95+ const require = createRequire ( import . meta. url ) ;
96+
97+ export function emitFontAssets ( distributionDirectory : string ) : string {
98+ const assetsDirectory = path . resolve ( distributionDirectory , 'assets' ) ;
99+ fs . mkdirSync ( assetsDirectory , { recursive : true } ) ;
100+
101+ const packageDirectories = new Map < string , string > ( ) ;
102+ const unicodeRanges = new Map < string , Record < string , string > > ( ) ;
103+ const rules = FONT_FACES . map ( ( face ) => {
104+ let packageDirectory = packageDirectories . get ( face . packageName ) ;
105+ if ( ! packageDirectory ) {
106+ packageDirectory = path . dirname ( require . resolve ( `${ face . packageName } /package.json` ) ) ;
107+ packageDirectories . set ( face . packageName , packageDirectory ) ;
108+ }
109+
110+ let packageUnicodeRanges = unicodeRanges . get ( face . packageName ) ;
111+ if ( ! packageUnicodeRanges ) {
112+ packageUnicodeRanges = JSON . parse (
113+ fs . readFileSync ( path . resolve ( packageDirectory , 'unicode.json' ) , 'utf8' ) ,
114+ ) as Record < string , string > ;
115+ unicodeRanges . set ( face . packageName , packageUnicodeRanges ) ;
116+ }
117+
118+ const unicodeRange = packageUnicodeRanges [ face . subset ] ;
119+ if ( ! unicodeRange ) {
120+ throw new Error ( `wb-ui:combine-css-bundle: ${ face . packageName } has no ${ face . subset } unicode range` ) ;
121+ }
122+
123+ const familySlug = face . family . toLowerCase ( ) ;
124+ const fileName = `${ familySlug } -${ face . subset } -${ face . weight } -normal.woff2` ;
125+ const sourcePath = path . resolve ( packageDirectory , 'files' , fileName ) ;
126+ if ( ! fs . existsSync ( sourcePath ) ) {
127+ throw new Error ( `wb-ui:combine-css-bundle: ${ sourcePath } is missing` ) ;
128+ }
129+
130+ const source = face . inline
131+ ? `url(data:font/woff2;base64,${ fs . readFileSync ( sourcePath ) . toString ( 'base64' ) } ) format('woff2')`
132+ : `url(./assets/${ fileName } ) format('woff2')` ;
133+
134+ if ( ! face . inline ) fs . copyFileSync ( sourcePath , path . resolve ( assetsDirectory , fileName ) ) ;
135+
136+ return [
137+ ' @font-face {' ,
138+ ` font-family: '${ face . family } ';` ,
139+ ' font-style: normal;' ,
140+ ' font-display: swap;' ,
141+ ` font-weight: ${ face . weight } ;` ,
142+ ` src: ${ source } ;` ,
143+ ` unicode-range: ${ unicodeRange } ;` ,
144+ ' }' ,
145+ ] . join ( '\n' ) ;
146+ } ) ;
147+
148+ return `@layer ui.base {\n${ rules . join ( '\n\n' ) } \n}` ;
149+ }
150+
32151function readLayerOrder ( stylesDirectory : string ) : string {
33152 return fs . readFileSync ( path . resolve ( stylesDirectory , 'layers.css' ) , 'utf8' ) . trim ( ) ;
34153}
@@ -62,24 +181,24 @@ function cssFilesIn(assetsDirectory: string): string[] {
62181 return files ;
63182}
64183
65- function writeCombinedStylesheet ( distributionDirectory : string , stylesDirectory : string ) {
184+ function writeCombinedStylesheet ( distributionDirectory : string , stylesDirectory : string , fontStyles : string ) {
66185 const assetsDirectory = assetsDirectoryOf ( distributionDirectory ) ;
67186
68187 // Within a layer, file order only breaks ties between equal-specificity rules.
69188 const styles = cssFilesIn ( assetsDirectory )
70189 . map ( ( file ) => fs . readFileSync ( path . resolve ( assetsDirectory , file ) , 'utf8' ) )
71190 . join ( '\n' ) ;
72191
73- const combined = `${ readLayerOrder ( stylesDirectory ) } \n${ styles } ` ;
192+ const combined = `${ readLayerOrder ( stylesDirectory ) } \n${ styles } \n ${ fontStyles } ` ;
74193 fs . writeFileSync ( path . resolve ( distributionDirectory , 'index.css' ) , combined ) ;
75194}
76195
77- function writeGlobalStylesheet ( distributionDirectory : string , stylesDirectory : string ) {
196+ function writeGlobalStylesheet ( distributionDirectory : string , stylesDirectory : string , fontStyles : string ) {
78197 const globals = [ 'layers.css' , 'globals.css' , 'typography.css' ]
79198 . map ( ( file ) => fs . readFileSync ( path . resolve ( stylesDirectory , file ) , 'utf8' ) )
80199 . join ( '\n' ) ;
81200
82- fs . writeFileSync ( path . resolve ( distributionDirectory , 'styles.css' ) , globals ) ;
201+ fs . writeFileSync ( path . resolve ( distributionDirectory , 'styles.css' ) , ` ${ globals } \n ${ fontStyles } ` ) ;
83202}
84203
85204function prependLayerOrderToAssets ( distributionDirectory : string , stylesDirectory : string ) {
0 commit comments