From 8c167db91e2104395b0148c4bdb5525d268065dc Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 2 Mar 2026 15:03:02 +0100 Subject: [PATCH 1/4] feat: add makeStaticStyles AOT/CSS extraction support --- .../__fixtures__/static-styles-array/code.ts | 10 ++ .../static-styles-array/output.ts | 4 + .../__fixtures__/static-styles-string/code.ts | 3 + .../static-styles-string/output.ts | 4 + .../__fixtures__/static-styles/code.ts | 7 ++ .../__fixtures__/static-styles/output.ts | 4 + .../src/assets/replaceAssetsWithImports.ts | 3 +- packages/babel-preset/src/schema.ts | 3 + .../babel-preset/src/transformPlugin.test.ts | 22 ++++ packages/babel-preset/src/transformPlugin.ts | 115 +++++++++++++++++- packages/babel-preset/src/types.ts | 2 + packages/core/src/__staticCSS.ts | 12 ++ packages/core/src/__staticStyles.ts | 18 +++ packages/core/src/index.ts | 3 + packages/react/src/__staticCSS.ts | 17 +++ packages/react/src/__staticStyles.ts | 23 ++++ packages/react/src/index.ts | 2 + .../__fixtures__/babel/static-mixed/code.ts | 17 +++ .../__fixtures__/babel/static-mixed/output.ts | 8 ++ .../__fixtures__/babel/static/code.ts | 5 + .../__fixtures__/babel/static/output.ts | 2 + .../webpack/static-assets/blank.jpg | Bin 0 -> 631 bytes .../webpack/static-assets/code.ts | 6 + .../webpack/static-assets/fs.json | 1 + .../webpack/static-assets/output.css | 5 + .../webpack/static-assets/output.ts | 5 + .../src/GriffelCSSExtractionPlugin.test.ts | 1 + .../babelPluginStripGriffelRuntime.test.ts | 11 ++ .../src/babelPluginStripGriffelRuntime.ts | 37 +++++- .../src/generateCSSRules.ts | 10 +- .../src/webpackLoader.ts | 6 +- 31 files changed, 351 insertions(+), 15 deletions(-) create mode 100644 packages/babel-preset/__fixtures__/static-styles-array/code.ts create mode 100644 packages/babel-preset/__fixtures__/static-styles-array/output.ts create mode 100644 packages/babel-preset/__fixtures__/static-styles-string/code.ts create mode 100644 packages/babel-preset/__fixtures__/static-styles-string/output.ts create mode 100644 packages/babel-preset/__fixtures__/static-styles/code.ts create mode 100644 packages/babel-preset/__fixtures__/static-styles/output.ts create mode 100644 packages/core/src/__staticCSS.ts create mode 100644 packages/core/src/__staticStyles.ts create mode 100644 packages/react/src/__staticCSS.ts create mode 100644 packages/react/src/__staticStyles.ts create mode 100644 packages/webpack-extraction-plugin/__fixtures__/babel/static-mixed/code.ts create mode 100644 packages/webpack-extraction-plugin/__fixtures__/babel/static-mixed/output.ts create mode 100644 packages/webpack-extraction-plugin/__fixtures__/babel/static/code.ts create mode 100644 packages/webpack-extraction-plugin/__fixtures__/babel/static/output.ts create mode 100644 packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/blank.jpg create mode 100644 packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/code.ts create mode 100644 packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/fs.json create mode 100644 packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/output.css create mode 100644 packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/output.ts diff --git a/packages/babel-preset/__fixtures__/static-styles-array/code.ts b/packages/babel-preset/__fixtures__/static-styles-array/code.ts new file mode 100644 index 0000000000..93bc4e0934 --- /dev/null +++ b/packages/babel-preset/__fixtures__/static-styles-array/code.ts @@ -0,0 +1,10 @@ +import { makeStaticStyles } from '@griffel/react'; + +export const useStaticStyles = makeStaticStyles([ + { + body: { + background: 'red', + }, + }, + 'html { margin: 0; }', +]); diff --git a/packages/babel-preset/__fixtures__/static-styles-array/output.ts b/packages/babel-preset/__fixtures__/static-styles-array/output.ts new file mode 100644 index 0000000000..ee6879a6e6 --- /dev/null +++ b/packages/babel-preset/__fixtures__/static-styles-array/output.ts @@ -0,0 +1,4 @@ +import { __staticStyles } from '@griffel/react'; +export const useStaticStyles = __staticStyles({ + d: ['body{background:red;}', 'html{margin:0;}'], +}); diff --git a/packages/babel-preset/__fixtures__/static-styles-string/code.ts b/packages/babel-preset/__fixtures__/static-styles-string/code.ts new file mode 100644 index 0000000000..e63de5cbd0 --- /dev/null +++ b/packages/babel-preset/__fixtures__/static-styles-string/code.ts @@ -0,0 +1,3 @@ +import { makeStaticStyles } from '@griffel/react'; + +export const useStaticStyles = makeStaticStyles('body { background: red; }'); diff --git a/packages/babel-preset/__fixtures__/static-styles-string/output.ts b/packages/babel-preset/__fixtures__/static-styles-string/output.ts new file mode 100644 index 0000000000..577c3c774e --- /dev/null +++ b/packages/babel-preset/__fixtures__/static-styles-string/output.ts @@ -0,0 +1,4 @@ +import { __staticStyles } from '@griffel/react'; +export const useStaticStyles = __staticStyles({ + d: ['body{background:red;}'], +}); diff --git a/packages/babel-preset/__fixtures__/static-styles/code.ts b/packages/babel-preset/__fixtures__/static-styles/code.ts new file mode 100644 index 0000000000..47e99cdf88 --- /dev/null +++ b/packages/babel-preset/__fixtures__/static-styles/code.ts @@ -0,0 +1,7 @@ +import { makeStaticStyles } from '@griffel/react'; + +export const useStaticStyles = makeStaticStyles({ + body: { + background: 'red', + }, +}); diff --git a/packages/babel-preset/__fixtures__/static-styles/output.ts b/packages/babel-preset/__fixtures__/static-styles/output.ts new file mode 100644 index 0000000000..577c3c774e --- /dev/null +++ b/packages/babel-preset/__fixtures__/static-styles/output.ts @@ -0,0 +1,4 @@ +import { __staticStyles } from '@griffel/react'; +export const useStaticStyles = __staticStyles({ + d: ['body{background:red;}'], +}); diff --git a/packages/babel-preset/src/assets/replaceAssetsWithImports.ts b/packages/babel-preset/src/assets/replaceAssetsWithImports.ts index f8e46721f6..cce5abd655 100644 --- a/packages/babel-preset/src/assets/replaceAssetsWithImports.ts +++ b/packages/babel-preset/src/assets/replaceAssetsWithImports.ts @@ -43,7 +43,8 @@ export function replaceAssetsWithImports( acc += tokens[i]; if (tokens[i] === 'url') { - const url = tokens[i + 1].slice(1, -1); + // Strip outer parentheses and optional CSS string quotes from url() values + const url = tokens[i + 1].slice(1, -1).replace(/^["']|["']$/g, ''); if (isAssetUrl(url)) { // Handle `filter: url(./a.svg#id)` diff --git a/packages/babel-preset/src/schema.ts b/packages/babel-preset/src/schema.ts index d3ee43e67e..c855aa8deb 100644 --- a/packages/babel-preset/src/schema.ts +++ b/packages/babel-preset/src/schema.ts @@ -24,6 +24,9 @@ export const configSchema: JSONSchema7 = { importName: { type: 'string', }, + staticImportName: { + type: 'string', + }, }, }, }, diff --git a/packages/babel-preset/src/transformPlugin.test.ts b/packages/babel-preset/src/transformPlugin.test.ts index 460a1d4d54..713e94c10f 100644 --- a/packages/babel-preset/src/transformPlugin.test.ts +++ b/packages/babel-preset/src/transformPlugin.test.ts @@ -207,6 +207,25 @@ pluginTester({ outputFixture: path.resolve(fixturesDir, 'reset-styles-at-rules', 'output.ts'), }, + // Static styles + // + // + { + title: 'static: object styles', + fixture: path.resolve(fixturesDir, 'static-styles', 'code.ts'), + outputFixture: path.resolve(fixturesDir, 'static-styles', 'output.ts'), + }, + { + title: 'static: string styles', + fixture: path.resolve(fixturesDir, 'static-styles-string', 'code.ts'), + outputFixture: path.resolve(fixturesDir, 'static-styles-string', 'output.ts'), + }, + { + title: 'static: array of mixed styles', + fixture: path.resolve(fixturesDir, 'static-styles-array', 'code.ts'), + outputFixture: path.resolve(fixturesDir, 'static-styles-array', 'output.ts'), + }, + // Imports // // @@ -353,6 +372,7 @@ describe('babel preset', () => { }, }, cssResetEntries: Object {}, + cssStaticEntries: Object {}, } `); }); @@ -378,6 +398,7 @@ describe('babel preset', () => { .r7z97ji{color:red;padding-right:4px;}, ], }, + cssStaticEntries: Object {}, } `); }); @@ -413,6 +434,7 @@ export const useStyles = makeStyles({ }, }, cssResetEntries: Object {}, + cssStaticEntries: Object {}, } `); }); diff --git a/packages/babel-preset/src/transformPlugin.ts b/packages/babel-preset/src/transformPlugin.ts index a897f4ffdb..ac92b07490 100644 --- a/packages/babel-preset/src/transformPlugin.ts +++ b/packages/babel-preset/src/transformPlugin.ts @@ -3,18 +3,29 @@ import { types as t } from '@babel/core'; import { declare } from '@babel/helper-plugin-utils'; import { Module } from '@linaria/babel-preset'; import shakerEvaluator from '@linaria/shaker'; -import type { GriffelStyle, CSSRulesByBucket, CSSClassesMapBySlot } from '@griffel/core'; -import { resolveStyleRulesForSlots, resolveResetStyleRules, normalizeCSSBucketEntry } from '@griffel/core'; +import type { + GriffelStyle, + GriffelStaticStyles, + CSSRulesByBucket, + CSSBucketEntry, + CSSClassesMapBySlot, +} from '@griffel/core'; +import { + resolveStyleRulesForSlots, + resolveResetStyleRules, + resolveStaticStyleRules, + normalizeCSSBucketEntry, +} from '@griffel/core'; import * as path from 'path'; -import { normalizeStyleRules } from './assets/normalizeStyleRules'; +import { normalizeStyleRule, normalizeStyleRules } from './assets/normalizeStyleRules'; import { replaceAssetsWithImports } from './assets/replaceAssetsWithImports'; import { dedupeCSSRules } from './utils/dedupeCSSRules'; import { evaluatePaths } from './utils/evaluatePaths'; import type { BabelPluginOptions, BabelPluginMetadata } from './types'; import { validateOptions } from './validateOptions'; -type FunctionKinds = 'makeStyles' | 'makeResetStyles'; +type FunctionKinds = 'makeStyles' | 'makeResetStyles' | 'makeStaticStyles'; type BabelPluginState = PluginPass & { importDeclarationPaths?: NodePath[]; @@ -31,6 +42,7 @@ type BabelPluginState = PluginPass & { calleePaths?: NodePath[]; cssEntries?: BabelPluginMetadata['cssEntries']; cssResetEntries?: BabelPluginMetadata['cssResetEntries']; + cssStaticEntries?: BabelPluginMetadata['cssStaticEntries']; }; function getDefinitionPathFromCallExpression( @@ -68,6 +80,10 @@ function getCalleeFunctionKind( if (path.referencesImport(module.moduleSource, module.resetImportName || 'makeResetStyles')) { return 'makeResetStyles'; } + + if (path.referencesImport(module.moduleSource, module.staticImportName || 'makeStaticStyles')) { + return 'makeStaticStyles'; + } } return null; @@ -113,6 +129,23 @@ function buildCSSResetEntriesMetadata( }); } +/** + * Extracts CSS rules from evaluated static styles to metadata + */ +function buildCSSStaticEntriesMetadata( + state: Required, + cssRulesByBucket: CSSRulesByBucket, + declaratorId: string, +) { + state.cssStaticEntries[declaratorId] ??= []; + state.cssStaticEntries[declaratorId] = Object.values(cssRulesByBucket).flatMap(bucketEntries => { + return bucketEntries.map(bucketEntry => { + const [cssRule] = normalizeCSSBucketEntry(bucketEntry); + return cssRule; + }); + }); +} + /** * Extracts CSS rules from evaluated styles to metadata */ @@ -229,11 +262,13 @@ export const transformPlugin = declare, PluginObj, PluginObj 0 && state.definitionPaths.length > 0) { programPath.addComment('trailing', `@griffel:classNameHashSalt "${pluginOptions.classNameHashSalt}"`); } + // Separate makeStaticStyles paths from makeStyles/makeResetStyles paths. + // Static styles are evaluated independently so that failures (e.g. unresolvable asset imports like .ttf + // files) gracefully fall back to runtime without breaking the transform for other calls in the same file. + const regularDefinitionPaths = state.definitionPaths.filter(p => p.functionKind !== 'makeStaticStyles'); + const staticDefinitionPaths = state.definitionPaths.filter(p => p.functionKind === 'makeStaticStyles'); + // Runs Babel AST processing or module evaluation for Node once for all arguments of makeStyles() calls once evaluatePaths( programPath, state.file.opts.filename!, - state.definitionPaths.map(p => p.path), + regularDefinitionPaths.map(p => p.path), pluginOptions, ); - state.definitionPaths.forEach(definitionPath => { + if (staticDefinitionPaths.length > 0) { + try { + evaluatePaths( + programPath, + state.file.opts.filename!, + staticDefinitionPaths.map(p => p.path), + pluginOptions, + ); + staticPathsEvaluated = true; + } catch { + // If evaluation fails (e.g. due to unresolvable asset imports), skip the transform for + // makeStaticStyles calls and let them execute at runtime. + } + } + + const pathsToProcess = staticPathsEvaluated + ? [...regularDefinitionPaths, ...staticDefinitionPaths] + : regularDefinitionPaths; + + pathsToProcess.forEach(definitionPath => { const callExpressionPath = definitionPath.path.findParent(parentPath => parentPath.isCallExpression(), ) as NodePath; @@ -353,6 +415,32 @@ export const transformPlugin = declare, PluginObj + typeof rule === 'string' + ? (normalizeStyleRule(path, pluginOptions.projectRoot, state.filename as string, rule) as string) + : rule, + ); + const cssRulesByBucket: CSSRulesByBucket = { d: normalizedRules }; + + if (pluginOptions.generateMetadata) { + buildCSSStaticEntriesMetadata( + state as Required, + cssRulesByBucket, + definitionPath.declaratorId, + ); + } + + (callExpressionPath.get('arguments.0') as NodePath).remove(); + callExpressionPath.pushContainer('arguments', [t.valueToNode(cssRulesByBucket)]); + } + replaceAssetsWithImports(pluginOptions.projectRoot, state.filename!, programPath, callExpressionPath); }); @@ -360,6 +448,7 @@ export const transformPlugin = declare, PluginObj, PluginObj, PluginObj, PluginObj>; cssResetEntries: Record; + cssStaticEntries: Record; }; diff --git a/packages/core/src/__staticCSS.ts b/packages/core/src/__staticCSS.ts new file mode 100644 index 0000000000..239ec00591 --- /dev/null +++ b/packages/core/src/__staticCSS.ts @@ -0,0 +1,12 @@ +/** + * A version of makeStaticStyles() that accepts build output as an input and skips all runtime transforms & DOM insertion. + * + * @internal + */ +// eslint-disable-next-line @typescript-eslint/no-empty-function +export function __staticCSS() { + // eslint-disable-next-line @typescript-eslint/no-empty-function + function useStaticStyles(): void {} + + return useStaticStyles; +} diff --git a/packages/core/src/__staticStyles.ts b/packages/core/src/__staticStyles.ts new file mode 100644 index 0000000000..7f122bc6c3 --- /dev/null +++ b/packages/core/src/__staticStyles.ts @@ -0,0 +1,18 @@ +import { insertionFactory } from './insertionFactory'; +import type { CSSRulesByBucket, GriffelInsertionFactory } from './types'; +import type { MakeStaticStylesOptions } from './makeStaticStyles'; + +/** + * A version of makeStaticStyles() that accepts build output as an input and skips all runtime transforms. + * + * @internal + */ +export function __staticStyles(cssRules: CSSRulesByBucket, factory: GriffelInsertionFactory = insertionFactory) { + const insertStyles = factory(); + + function useStaticStyles(options: MakeStaticStylesOptions): void { + insertStyles(options.renderer, cssRules); + } + + return useStaticStyles; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index bf4e99e9aa..58b9db94b4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -71,6 +71,8 @@ export { __css } from './__css'; export { __styles } from './__styles'; export { __resetCSS } from './__resetCSS'; export { __resetStyles } from './__resetStyles'; +export { __staticCSS } from './__staticCSS'; +export { __staticStyles } from './__staticStyles'; export { normalizeCSSBucketEntry } from './runtime/utils/normalizeCSSBucketEntry'; export { styleBucketOrdering, getStyleSheetKey } from './renderer/getStyleSheetForBucket'; @@ -79,6 +81,7 @@ export { getStyleBucketName } from './runtime/getStyleBucketName'; export { reduceToClassNameForSlots } from './runtime/reduceToClassNameForSlots'; export { resolveStyleRules } from './runtime/resolveStyleRules'; export { resolveResetStyleRules } from './runtime/resolveResetStyleRules'; +export { resolveStaticStyleRules } from './runtime/resolveStaticStyleRules'; export * from './constants'; diff --git a/packages/react/src/__staticCSS.ts b/packages/react/src/__staticCSS.ts new file mode 100644 index 0000000000..bc2426d5f0 --- /dev/null +++ b/packages/react/src/__staticCSS.ts @@ -0,0 +1,17 @@ +'use client'; + +import { __staticCSS as vanillaStaticCSS } from '@griffel/core'; + +/** + * A version of makeStaticStyles() that accepts build output as an input and skips all runtime transforms & DOM insertion. + * + * @internal + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export function __staticCSS() { + const getStyles = vanillaStaticCSS(); + + return function useStaticStyles(): void { + return getStyles(); + }; +} diff --git a/packages/react/src/__staticStyles.ts b/packages/react/src/__staticStyles.ts new file mode 100644 index 0000000000..8a4074eaf4 --- /dev/null +++ b/packages/react/src/__staticStyles.ts @@ -0,0 +1,23 @@ +'use client'; + +import { __staticStyles as vanillaStaticStyles } from '@griffel/core'; +import type { CSSRulesByBucket } from '@griffel/core'; + +import { insertionFactory } from './insertionFactory'; +import { useRenderer } from './RendererContext'; + +/** + * A version of makeStaticStyles() that accepts build output as an input and skips all runtime transforms. + * + * @internal + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export function __staticStyles(cssRules: CSSRulesByBucket) { + const getStyles = vanillaStaticStyles(cssRules, insertionFactory); + + return function useStaticStyles(): void { + const renderer = useRenderer(); + + return getStyles({ renderer }); + }; +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 151df3297e..dfa23d343e 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -16,3 +16,5 @@ export { __css } from './__css'; export { __styles } from './__styles'; export { __resetCSS } from './__resetCSS'; export { __resetStyles } from './__resetStyles'; +export { __staticCSS } from './__staticCSS'; +export { __staticStyles } from './__staticStyles'; diff --git a/packages/webpack-extraction-plugin/__fixtures__/babel/static-mixed/code.ts b/packages/webpack-extraction-plugin/__fixtures__/babel/static-mixed/code.ts new file mode 100644 index 0000000000..5bba123994 --- /dev/null +++ b/packages/webpack-extraction-plugin/__fixtures__/babel/static-mixed/code.ts @@ -0,0 +1,17 @@ +import { __styles } from '@griffel/react'; +import { __staticStyles } from '@griffel/react'; + +export const styles = __styles( + { + root: { + sj55zd: 'fe3e8s9', + }, + }, + { + d: ['.fe3e8s9{color:red;}'], + }, +); + +export const useStaticStyles = __staticStyles({ + d: ['body{background:red;}'], +}); diff --git a/packages/webpack-extraction-plugin/__fixtures__/babel/static-mixed/output.ts b/packages/webpack-extraction-plugin/__fixtures__/babel/static-mixed/output.ts new file mode 100644 index 0000000000..a27ec62246 --- /dev/null +++ b/packages/webpack-extraction-plugin/__fixtures__/babel/static-mixed/output.ts @@ -0,0 +1,8 @@ +import { __styles, __css as _css, __staticCSS as _staticCSS } from '@griffel/react'; +import { __staticStyles } from '@griffel/react'; +export const styles = _css({ + root: { + sj55zd: 'fe3e8s9', + }, +}); +export const useStaticStyles = _staticCSS(); diff --git a/packages/webpack-extraction-plugin/__fixtures__/babel/static/code.ts b/packages/webpack-extraction-plugin/__fixtures__/babel/static/code.ts new file mode 100644 index 0000000000..1132f046c2 --- /dev/null +++ b/packages/webpack-extraction-plugin/__fixtures__/babel/static/code.ts @@ -0,0 +1,5 @@ +import { __staticStyles } from '@griffel/react'; + +export const useStaticStyles = __staticStyles({ + d: ['body{background:red;}'], +}); diff --git a/packages/webpack-extraction-plugin/__fixtures__/babel/static/output.ts b/packages/webpack-extraction-plugin/__fixtures__/babel/static/output.ts new file mode 100644 index 0000000000..3af7a1eaf3 --- /dev/null +++ b/packages/webpack-extraction-plugin/__fixtures__/babel/static/output.ts @@ -0,0 +1,2 @@ +import { __staticStyles, __staticCSS as _staticCSS } from '@griffel/react'; +export const useStaticStyles = _staticCSS(); diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/blank.jpg b/packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/blank.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1cda9a53dc357ce07d3c67051b7615ebf7dc2f64 GIT binary patch literal 631 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<ECr+Na zbot8FYu9hwy!G(W<0ns_J%91?)yGetzkL1n{m0K=Ab&A3Fhjfr_ZgbM1cClyVqsxs zVF&q(k*OSrnFU!`6%E;h90S=C3x$=88aYIqCNA7~kW<+>=!0ld(M2vX6_bamA3L7B$%azX<@d&d)*s literal 0 HcmV?d00001 diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/code.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/code.ts new file mode 100644 index 0000000000..0a30b6f41c --- /dev/null +++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/code.ts @@ -0,0 +1,6 @@ +import _asset from './blank.jpg'; +import { __staticStyles } from '@griffel/react'; + +export const useStaticStyles = __staticStyles({ + d: [`@font-face{font-family:TestFont;src:url(${_asset}) format("woff2");}`], +}); diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/fs.json b/packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/fs.json new file mode 100644 index 0000000000..5e549a74a8 --- /dev/null +++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/fs.json @@ -0,0 +1 @@ +["blank.jpg", "bundle.js", "griffel.css"] diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/output.css b/packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/output.css new file mode 100644 index 0000000000..0c1cef6984 --- /dev/null +++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/output.css @@ -0,0 +1,5 @@ +/** griffel.css **/ +@font-face { + font-family: TestFont; + src: url(blank.jpg) format('woff2'); +} diff --git a/packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/output.ts b/packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/output.ts new file mode 100644 index 0000000000..4c8ec805c1 --- /dev/null +++ b/packages/webpack-extraction-plugin/__fixtures__/webpack/static-assets/output.ts @@ -0,0 +1,5 @@ +import { __staticCSS as _staticCSS } from '@griffel/react'; +import { __staticStyles } from '@griffel/react'; +export const useStaticStyles = _staticCSS(); + +import './code.griffel.css!=!../../../virtual-loader/index.js!./code.ts'; diff --git a/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.test.ts b/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.test.ts index 82e1f09def..76d562017a 100644 --- a/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.test.ts +++ b/packages/webpack-extraction-plugin/src/GriffelCSSExtractionPlugin.test.ts @@ -292,6 +292,7 @@ describe('GriffelCSSExtractionPlugin', () => { testFixture('assets-flip'); testFixture('assets-multiple'); testFixture('reset-assets'); + testFixture('static-assets'); // Config // -------------------- diff --git a/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.test.ts b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.test.ts index 7857a30763..73d1e9f981 100644 --- a/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.test.ts +++ b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.test.ts @@ -55,6 +55,17 @@ pluginTester({ fixture: path.resolve(fixturesDir, 'alias', 'code.ts'), outputFixture: path.resolve(fixturesDir, 'alias', 'output.ts'), }, + + { + title: 'basic (makeStaticStyles)', + fixture: path.resolve(fixturesDir, 'static', 'code.ts'), + outputFixture: path.resolve(fixturesDir, 'static', 'output.ts'), + }, + { + title: 'mixed (makeStyles + makeStaticStyles)', + fixture: path.resolve(fixturesDir, 'static-mixed', 'code.ts'), + outputFixture: path.resolve(fixturesDir, 'static-mixed', 'output.ts'), + }, ], plugin: babelPluginStripGriffelRuntime, diff --git a/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts index 21e0195f3b..f50ca93d4a 100644 --- a/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts +++ b/packages/webpack-extraction-plugin/src/babelPluginStripGriffelRuntime.ts @@ -6,7 +6,7 @@ import type { CSSRulesByBucket } from '@griffel/core'; type StripRuntimeBabelPluginOptions = Record; -type FunctionKinds = '__styles' | '__resetStyles'; +type FunctionKinds = '__styles' | '__resetStyles' | '__staticStyles'; type StripRuntimeBabelPluginState = PluginPass & { cssRulesByBucket?: CSSRulesByBucket; @@ -54,6 +54,10 @@ function evaluateAndUpdateArgument( state.cssRulesByBucket, Array.isArray(cssRules) ? { r: cssRules } : cssRules, ); + } else if (functionKind === '__staticStyles') { + const cssRulesByBucket = evaluationResult.value as CSSRulesByBucket; + + state.cssRulesByBucket = concatCSSRulesByBucket(state.cssRulesByBucket, cssRulesByBucket); } argumentPath.remove(); @@ -77,6 +81,12 @@ function getFunctionArgumentPath( } } + if (functionKind === '__staticStyles') { + if (argumentPaths.length === 1 && argumentPaths[0].isObjectExpression()) { + return argumentPaths[0]; + } + } + return null; } @@ -166,7 +176,8 @@ function updateReferences( importSource: string, functionKind: FunctionKinds, ) { - const importName = functionKind === '__styles' ? '__css' : '__resetCSS'; + const importName = + functionKind === '__styles' ? '__css' : functionKind === '__resetStyles' ? '__resetCSS' : '__staticCSS'; const importIdentifier = addNamed(importSpecifierPath, importName, importSource); const referencePaths = getReferencePaths(importSpecifierPath, functionKind); @@ -213,8 +224,16 @@ export const babelPluginStripGriffelRuntime = declare< } }, - exit(path, state) { - path.traverse({ + exit(programPath, state) { + // Collect all matching imports first to avoid AST modification during traversal + // (addNamed modifies import declarations which can cause skipped specifiers) + const importsToProcess: Array<{ + specifierPath: NodePath; + importSource: string; + kind: FunctionKinds; + }> = []; + + programPath.traverse({ ImportSpecifier(path) { const importedPath = path.get('imported'); @@ -222,12 +241,18 @@ export const babelPluginStripGriffelRuntime = declare< const importSource = importSourcePath.node.value; if (importedPath.isIdentifier({ name: '__styles' })) { - updateReferences(state, path, importSource, '__styles'); + importsToProcess.push({ specifierPath: path, importSource, kind: '__styles' }); } else if (importedPath.isIdentifier({ name: '__resetStyles' })) { - updateReferences(state, path, importSource, '__resetStyles'); + importsToProcess.push({ specifierPath: path, importSource, kind: '__resetStyles' }); + } else if (importedPath.isIdentifier({ name: '__staticStyles' })) { + importsToProcess.push({ specifierPath: path, importSource, kind: '__staticStyles' }); } }, }); + + for (const { specifierPath, importSource, kind } of importsToProcess) { + updateReferences(state, specifierPath, importSource, kind); + } }, }, }, diff --git a/packages/webpack-extraction-plugin/src/generateCSSRules.ts b/packages/webpack-extraction-plugin/src/generateCSSRules.ts index 53cb9f1a37..77e74ea216 100644 --- a/packages/webpack-extraction-plugin/src/generateCSSRules.ts +++ b/packages/webpack-extraction-plugin/src/generateCSSRules.ts @@ -1,5 +1,13 @@ import { type CSSRulesByBucket, normalizeCSSBucketEntry } from '@griffel/core'; +/** + * Strips CSS string quotes from url() values so that css-loader can properly resolve asset paths. + * Converts url("./path") and url('./path') to url(./path). + */ +function stripCSSUrlQuotes(cssRule: string): string { + return cssRule.replace(/url\(["']([^"')]+)["']\)/g, 'url($1)'); +} + export function generateCSSRules(cssRulesByBucket: CSSRulesByBucket): string { const entries = Object.entries(cssRulesByBucket); @@ -26,7 +34,7 @@ export function generateCSSRules(cssRulesByBucket: CSSRulesByBucket): string { lastEntryKey = entryKey; } - cssLines.push(cssRule); + cssLines.push(stripCSSUrlQuotes(cssRule)); } } diff --git a/packages/webpack-extraction-plugin/src/webpackLoader.ts b/packages/webpack-extraction-plugin/src/webpackLoader.ts index b5a93d7204..a957be090f 100644 --- a/packages/webpack-extraction-plugin/src/webpackLoader.ts +++ b/packages/webpack-extraction-plugin/src/webpackLoader.ts @@ -87,7 +87,11 @@ function webpackLoader( this.cacheable(); // Early return to handle cases when __styles() calls are not present, allows skipping expensive invocation of Babel - if (sourceCode.indexOf('__styles') === -1 && sourceCode.indexOf('__resetStyles') === -1) { + if ( + sourceCode.indexOf('__styles') === -1 && + sourceCode.indexOf('__resetStyles') === -1 && + sourceCode.indexOf('__staticStyles') === -1 + ) { this.callback(null, sourceCode, inputSourceMap); return; } From 22db8b24f3e50ab25fdb566eeedb138389cbc201 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 2 Mar 2026 17:40:25 +0100 Subject: [PATCH 2/4] Change files --- ...-babel-preset-74562825-5479-4fc4-a589-df0a244ee73b.json | 7 +++++++ ...@griffel-core-4657ec9a-0702-463c-acea-6ddfbb95ad1e.json | 7 +++++++ ...griffel-react-c2e3d63b-8b49-4c67-95f4-8c27e6af5f50.json | 7 +++++++ ...action-plugin-81e6b586-e23c-48ca-9349-7010f4583dd3.json | 7 +++++++ 4 files changed, 28 insertions(+) create mode 100644 change/@griffel-babel-preset-74562825-5479-4fc4-a589-df0a244ee73b.json create mode 100644 change/@griffel-core-4657ec9a-0702-463c-acea-6ddfbb95ad1e.json create mode 100644 change/@griffel-react-c2e3d63b-8b49-4c67-95f4-8c27e6af5f50.json create mode 100644 change/@griffel-webpack-extraction-plugin-81e6b586-e23c-48ca-9349-7010f4583dd3.json diff --git a/change/@griffel-babel-preset-74562825-5479-4fc4-a589-df0a244ee73b.json b/change/@griffel-babel-preset-74562825-5479-4fc4-a589-df0a244ee73b.json new file mode 100644 index 0000000000..b9e453035d --- /dev/null +++ b/change/@griffel-babel-preset-74562825-5479-4fc4-a589-df0a244ee73b.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: add makeStaticStyles AOT/CSS extraction support", + "packageName": "@griffel/babel-preset", + "email": "hochelmartin@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@griffel-core-4657ec9a-0702-463c-acea-6ddfbb95ad1e.json b/change/@griffel-core-4657ec9a-0702-463c-acea-6ddfbb95ad1e.json new file mode 100644 index 0000000000..839ec10ff1 --- /dev/null +++ b/change/@griffel-core-4657ec9a-0702-463c-acea-6ddfbb95ad1e.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: add makeStaticStyles AOT/CSS extraction support", + "packageName": "@griffel/core", + "email": "hochelmartin@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@griffel-react-c2e3d63b-8b49-4c67-95f4-8c27e6af5f50.json b/change/@griffel-react-c2e3d63b-8b49-4c67-95f4-8c27e6af5f50.json new file mode 100644 index 0000000000..00a8ecedc3 --- /dev/null +++ b/change/@griffel-react-c2e3d63b-8b49-4c67-95f4-8c27e6af5f50.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: add makeStaticStyles AOT/CSS extraction support", + "packageName": "@griffel/react", + "email": "hochelmartin@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@griffel-webpack-extraction-plugin-81e6b586-e23c-48ca-9349-7010f4583dd3.json b/change/@griffel-webpack-extraction-plugin-81e6b586-e23c-48ca-9349-7010f4583dd3.json new file mode 100644 index 0000000000..35603fe638 --- /dev/null +++ b/change/@griffel-webpack-extraction-plugin-81e6b586-e23c-48ca-9349-7010f4583dd3.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "feat: add makeStaticStyles AOT/CSS extraction support", + "packageName": "@griffel/webpack-extraction-plugin", + "email": "hochelmartin@gmail.com", + "dependentChangeType": "patch" +} From ce1145075fbb54e09749792b6e091632732dfc07 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 2 Mar 2026 18:12:21 +0100 Subject: [PATCH 3/4] refactor: clean up unnecessary parts --- .../src/assets/replaceAssetsWithImports.ts | 10 ++++- packages/babel-preset/src/transformPlugin.ts | 40 ++----------------- packages/core/src/__staticCSS.ts | 2 - .../__fixtures__/babel/static-mixed/code.ts | 3 +- .../__fixtures__/babel/static-mixed/output.ts | 3 +- .../src/babelPluginStripGriffelRuntime.ts | 18 ++------- .../src/generateCSSRules.ts | 10 +---- 7 files changed, 19 insertions(+), 67 deletions(-) diff --git a/packages/babel-preset/src/assets/replaceAssetsWithImports.ts b/packages/babel-preset/src/assets/replaceAssetsWithImports.ts index cce5abd655..1d8c5ebb18 100644 --- a/packages/babel-preset/src/assets/replaceAssetsWithImports.ts +++ b/packages/babel-preset/src/assets/replaceAssetsWithImports.ts @@ -44,7 +44,7 @@ export function replaceAssetsWithImports( if (tokens[i] === 'url') { // Strip outer parentheses and optional CSS string quotes from url() values - const url = tokens[i + 1].slice(1, -1).replace(/^["']|["']$/g, ''); + const url = stripUrlTokenWrapper(tokens[i + 1]); if (isAssetUrl(url)) { // Handle `filter: url(./a.svg#id)` @@ -90,3 +90,11 @@ export function replaceAssetsWithImports( ); } } + +/** + * Strips outer parentheses and optional CSS string quotes from a url() token value. + * @example `(url.png)` → `url.png`, `('url.png')` → `url.png` + */ +function stripUrlTokenWrapper(token: string): string { + return token.replace(/^\(["']?|["']?\)$/g, ''); +} diff --git a/packages/babel-preset/src/transformPlugin.ts b/packages/babel-preset/src/transformPlugin.ts index ac92b07490..b33699823e 100644 --- a/packages/babel-preset/src/transformPlugin.ts +++ b/packages/babel-preset/src/transformPlugin.ts @@ -296,47 +296,20 @@ export const transformPlugin = declare, PluginObj 0 && state.definitionPaths.length > 0) { programPath.addComment('trailing', `@griffel:classNameHashSalt "${pluginOptions.classNameHashSalt}"`); } - // Separate makeStaticStyles paths from makeStyles/makeResetStyles paths. - // Static styles are evaluated independently so that failures (e.g. unresolvable asset imports like .ttf - // files) gracefully fall back to runtime without breaking the transform for other calls in the same file. - const regularDefinitionPaths = state.definitionPaths.filter(p => p.functionKind !== 'makeStaticStyles'); - const staticDefinitionPaths = state.definitionPaths.filter(p => p.functionKind === 'makeStaticStyles'); - // Runs Babel AST processing or module evaluation for Node once for all arguments of makeStyles() calls once evaluatePaths( programPath, state.file.opts.filename!, - regularDefinitionPaths.map(p => p.path), + state.definitionPaths.map(p => p.path), pluginOptions, ); - if (staticDefinitionPaths.length > 0) { - try { - evaluatePaths( - programPath, - state.file.opts.filename!, - staticDefinitionPaths.map(p => p.path), - pluginOptions, - ); - staticPathsEvaluated = true; - } catch { - // If evaluation fails (e.g. due to unresolvable asset imports), skip the transform for - // makeStaticStyles calls and let them execute at runtime. - } - } - - const pathsToProcess = staticPathsEvaluated - ? [...regularDefinitionPaths, ...staticDefinitionPaths] - : regularDefinitionPaths; - - pathsToProcess.forEach(definitionPath => { + state.definitionPaths.forEach(definitionPath => { const callExpressionPath = definitionPath.path.findParent(parentPath => parentPath.isCallExpression(), ) as NodePath; @@ -471,10 +444,7 @@ export const transformPlugin = declare, PluginObj, PluginObj; - importSource: string; - kind: FunctionKinds; - }> = []; - programPath.traverse({ ImportSpecifier(path) { const importedPath = path.get('imported'); @@ -241,18 +233,14 @@ export const babelPluginStripGriffelRuntime = declare< const importSource = importSourcePath.node.value; if (importedPath.isIdentifier({ name: '__styles' })) { - importsToProcess.push({ specifierPath: path, importSource, kind: '__styles' }); + updateReferences(state, path, importSource, '__styles'); } else if (importedPath.isIdentifier({ name: '__resetStyles' })) { - importsToProcess.push({ specifierPath: path, importSource, kind: '__resetStyles' }); + updateReferences(state, path, importSource, '__resetStyles'); } else if (importedPath.isIdentifier({ name: '__staticStyles' })) { - importsToProcess.push({ specifierPath: path, importSource, kind: '__staticStyles' }); + updateReferences(state, path, importSource, '__staticStyles'); } }, }); - - for (const { specifierPath, importSource, kind } of importsToProcess) { - updateReferences(state, specifierPath, importSource, kind); - } }, }, }, diff --git a/packages/webpack-extraction-plugin/src/generateCSSRules.ts b/packages/webpack-extraction-plugin/src/generateCSSRules.ts index 77e74ea216..53cb9f1a37 100644 --- a/packages/webpack-extraction-plugin/src/generateCSSRules.ts +++ b/packages/webpack-extraction-plugin/src/generateCSSRules.ts @@ -1,13 +1,5 @@ import { type CSSRulesByBucket, normalizeCSSBucketEntry } from '@griffel/core'; -/** - * Strips CSS string quotes from url() values so that css-loader can properly resolve asset paths. - * Converts url("./path") and url('./path') to url(./path). - */ -function stripCSSUrlQuotes(cssRule: string): string { - return cssRule.replace(/url\(["']([^"')]+)["']\)/g, 'url($1)'); -} - export function generateCSSRules(cssRulesByBucket: CSSRulesByBucket): string { const entries = Object.entries(cssRulesByBucket); @@ -34,7 +26,7 @@ export function generateCSSRules(cssRulesByBucket: CSSRulesByBucket): string { lastEntryKey = entryKey; } - cssLines.push(stripCSSUrlQuotes(cssRule)); + cssLines.push(cssRule); } } From b064288cdc9d2495cdec38d61f63528f877f5dd8 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Mon, 2 Mar 2026 18:19:10 +0100 Subject: [PATCH 4/4] fixup! refactor: clean up unnecessary parts --- packages/core/src/__staticCSS.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/__staticCSS.ts b/packages/core/src/__staticCSS.ts index f73f0180a0..45eb1b5f11 100644 --- a/packages/core/src/__staticCSS.ts +++ b/packages/core/src/__staticCSS.ts @@ -4,6 +4,7 @@ * @internal */ export function __staticCSS() { + // eslint-disable-next-line @typescript-eslint/no-empty-function function useStaticStyles(): void {} return useStaticStyles;