From ec88b59b740e45c49964378a382469d2f2bdd353 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 17 Feb 2026 04:54:12 +0900 Subject: [PATCH 1/7] Add build plugins to inject design token fallbacks --- package-lock.json | 1 + packages/theme/package.json | 15 +++++++ .../esbuild-ds-token-fallbacks.d.mts | 4 ++ .../esbuild-ds-token-fallbacks.mjs | 44 +++++++++++++++++++ .../postcss-ds-token-fallbacks.d.mts | 4 ++ .../postcss-ds-token-fallbacks.mjs | 16 +++++++ .../vite-ds-token-fallbacks.d.mts | 4 ++ .../vite-plugins/vite-ds-token-fallbacks.mjs | 23 ++++++++++ packages/wp-build/lib/build.mjs | 24 +++++++++- storybook/main.ts | 10 +++++ storybook/package.json | 1 + 11 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 packages/theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.d.mts create mode 100644 packages/theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.mjs create mode 100644 packages/theme/src/postcss-plugins/postcss-ds-token-fallbacks.d.mts create mode 100644 packages/theme/src/postcss-plugins/postcss-ds-token-fallbacks.mjs create mode 100644 packages/theme/src/vite-plugins/vite-ds-token-fallbacks.d.mts create mode 100644 packages/theme/src/vite-plugins/vite-ds-token-fallbacks.mjs diff --git a/package-lock.json b/package-lock.json index 84805c72ba8dc0..1186806b1381e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63241,6 +63241,7 @@ "@storybook/addon-docs": "^10.1.11", "@storybook/icons": "^2.0.1", "@storybook/react-vite": "^10.1.11", + "@wordpress/theme": "file:../packages/theme", "storybook": "^10.1.11", "storybook-addon-tag-badges": "^3.0.4", "terser": "^5.37.0" diff --git a/packages/theme/package.json b/packages/theme/package.json index ed605e0e28ce29..6c98a837925753 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -43,6 +43,21 @@ "import": "./build-module/prebuilt/js/design-tokens.mjs", "default": "./build/prebuilt/js/design-tokens.cjs" }, + "./postcss-plugins/postcss-ds-token-fallbacks": { + "types": "./src/postcss-plugins/postcss-ds-token-fallbacks.d.mts", + "import": "./src/postcss-plugins/postcss-ds-token-fallbacks.mjs", + "default": "./src/postcss-plugins/postcss-ds-token-fallbacks.mjs" + }, + "./esbuild-plugins/esbuild-ds-token-fallbacks": { + "types": "./src/esbuild-plugins/esbuild-ds-token-fallbacks.d.mts", + "import": "./src/esbuild-plugins/esbuild-ds-token-fallbacks.mjs", + "default": "./src/esbuild-plugins/esbuild-ds-token-fallbacks.mjs" + }, + "./vite-plugins/vite-ds-token-fallbacks": { + "types": "./src/vite-plugins/vite-ds-token-fallbacks.d.mts", + "import": "./src/vite-plugins/vite-ds-token-fallbacks.mjs", + "default": "./src/vite-plugins/vite-ds-token-fallbacks.mjs" + }, "./stylelint-plugins/no-unknown-ds-tokens": { "types": "./build-types/stylelint-plugins/no-unknown-ds-tokens.d.ts", "import": "./src/stylelint-plugins/no-unknown-ds-tokens.mjs", diff --git a/packages/theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.d.mts b/packages/theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.d.mts new file mode 100644 index 00000000000000..40ead2ca892881 --- /dev/null +++ b/packages/theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.d.mts @@ -0,0 +1,4 @@ +import type { Plugin } from 'esbuild'; + +declare const plugin: Plugin; +export default plugin; diff --git a/packages/theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.mjs b/packages/theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.mjs new file mode 100644 index 00000000000000..cde59bc40dda29 --- /dev/null +++ b/packages/theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.mjs @@ -0,0 +1,44 @@ +import { readFile } from 'fs/promises'; +import { addFallbackToVar } from '../postcss-plugins/ds-token-fallbacks.mjs'; + +const LOADER_MAP = { + '.js': 'jsx', + '.jsx': 'jsx', + '.ts': 'tsx', + '.tsx': 'tsx', + '.mjs': 'jsx', +}; + +/** + * esbuild plugin that injects design-system token fallbacks into JS/TS files. + * + * Replaces bare `var(--wpds-*)` references in string literals with + * `var(--wpds-*, )` so components render correctly without + * a ThemeProvider. + */ +const plugin = { + name: 'ds-token-fallbacks-js', + setup( build ) { + build.onLoad( { filter: /\.[mc]?[jt]sx?$/ }, async ( args ) => { + // Skip node_modules. + if ( args.path.includes( 'node_modules' ) ) { + return undefined; + } + + const source = await readFile( args.path, 'utf8' ); + + if ( ! source.includes( '--wpds-' ) ) { + return undefined; + } + + const ext = args.path.match( /(\.[^.]+)$/ )?.[ 1 ] || '.js'; + + return { + contents: addFallbackToVar( source ), + loader: LOADER_MAP[ ext ] || 'jsx', + }; + } ); + }, +}; + +export default plugin; diff --git a/packages/theme/src/postcss-plugins/postcss-ds-token-fallbacks.d.mts b/packages/theme/src/postcss-plugins/postcss-ds-token-fallbacks.d.mts new file mode 100644 index 00000000000000..f44a48ef090258 --- /dev/null +++ b/packages/theme/src/postcss-plugins/postcss-ds-token-fallbacks.d.mts @@ -0,0 +1,4 @@ +import type { PluginCreator } from 'postcss'; + +declare const plugin: PluginCreator; +export default plugin; diff --git a/packages/theme/src/postcss-plugins/postcss-ds-token-fallbacks.mjs b/packages/theme/src/postcss-plugins/postcss-ds-token-fallbacks.mjs new file mode 100644 index 00000000000000..0936b8eac2ceb9 --- /dev/null +++ b/packages/theme/src/postcss-plugins/postcss-ds-token-fallbacks.mjs @@ -0,0 +1,16 @@ +import { addFallbackToVar } from './ds-token-fallbacks.mjs'; + +const plugin = () => ( { + postcssPlugin: 'postcss-ds-token-fallbacks', + /** @param {import('postcss').Declaration} decl */ + Declaration( decl ) { + const updated = addFallbackToVar( decl.value ); + if ( updated !== decl.value ) { + decl.value = updated; + } + }, +} ); + +plugin.postcss = true; + +export default plugin; diff --git a/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.d.mts b/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.d.mts new file mode 100644 index 00000000000000..f4ae4b5cff849e --- /dev/null +++ b/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.d.mts @@ -0,0 +1,4 @@ +import type { Plugin } from 'vite'; + +declare const plugin: () => Plugin; +export default plugin; diff --git a/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.mjs b/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.mjs new file mode 100644 index 00000000000000..0f04a06f0b5168 --- /dev/null +++ b/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.mjs @@ -0,0 +1,23 @@ +import { addFallbackToVar } from '../postcss-plugins/ds-token-fallbacks.mjs'; + +/** + * Vite plugin that injects design-system token fallbacks into JS/TS files. + * + * Replaces bare `var(--wpds-*)` references in string literals with + * `var(--wpds-*, )` so components render correctly without + * a ThemeProvider. + */ +const plugin = () => ( { + name: 'ds-token-fallbacks-js', + transform( code, id ) { + if ( ! /\.[mc]?[jt]sx?$/.test( id ) ) { + return null; + } + if ( ! code.includes( '--wpds-' ) ) { + return null; + } + return addFallbackToVar( code ); + }, +} ); + +export default plugin; diff --git a/packages/wp-build/lib/build.mjs b/packages/wp-build/lib/build.mjs index b928ee7e2bfe0a..92bb1361784560 100755 --- a/packages/wp-build/lib/build.mjs +++ b/packages/wp-build/lib/build.mjs @@ -21,6 +21,26 @@ import babel from 'esbuild-plugin-babel'; import { camelCase } from 'change-case'; import { NodePackageImporter } from 'sass-embedded'; +// Optional dependency: @wordpress/theme provides plugins that inject fallback +// values for design system tokens. Fails gracefully when the package is not +// installed (it is an optional peerDependency). +let dsTokenFallbacks; +let dsTokenFallbacksJs; +try { + const { default: postcssPlugin } = await import( + // eslint-disable-next-line import/no-unresolved + '@wordpress/theme/postcss-plugins/postcss-ds-token-fallbacks' + ); + const { default: esbuildPlugin } = await import( + // eslint-disable-next-line import/no-unresolved + '@wordpress/theme/esbuild-plugins/esbuild-ds-token-fallbacks' + ); + dsTokenFallbacks = postcssPlugin; + dsTokenFallbacksJs = esbuildPlugin; +} catch { + // @wordpress/theme is optional; skip token fallbacks if not available. +} + /** * Internal dependencies */ @@ -150,8 +170,9 @@ function compileInlineStyle( { cssModules = false, minify = true } = {} ) { let moduleExports = null; - // Transform the code: CSS modules and minification. + // Transform the code: token fallbacks, CSS modules and minification. const plugins = [ + dsTokenFallbacks, cssModules && postcssModules( { generateScopedName: '[contenthash]__[local]', @@ -1251,6 +1272,7 @@ async function transpilePackage( packageName ) { }, }; const plugins = [ + dsTokenFallbacksJs, needsEmotionPlugin && emotionPlugin, wasmInlinePlugin, externalizeAllExceptCssPlugin, diff --git a/storybook/main.ts b/storybook/main.ts index 9568efc2a8828b..e1cf8b226677e6 100644 --- a/storybook/main.ts +++ b/storybook/main.ts @@ -6,6 +6,8 @@ import { } from 'vite'; import react from '@vitejs/plugin-react'; import type { StorybookConfig } from '@storybook/react-vite'; +import dsTokenFallbacks from '@wordpress/theme/postcss-plugins/postcss-ds-token-fallbacks'; +import dsTokenFallbacksJs from '@wordpress/theme/vite-plugins/vite-ds-token-fallbacks'; const { NODE_ENV = 'development' } = process.env; @@ -77,6 +79,7 @@ const config: StorybookConfig = { viteFinal: async ( viteConfig ) => { return mergeConfig( viteConfig, { plugins: [ + dsTokenFallbacksJs(), react( { jsxImportSource: '@emotion/react', babel: { @@ -174,6 +177,13 @@ const config: StorybookConfig = { NODE_ENV === 'development' ), }, + css: { + postcss: { + // Vite bundles its own PostCSS, creating a deep + // type incompatibility with the top-level PostCSS. + plugins: [ dsTokenFallbacks as any ], + }, + }, optimizeDeps: { esbuildOptions: { loader: { diff --git a/storybook/package.json b/storybook/package.json index b4e7a07b3cb876..f2d434e295580a 100644 --- a/storybook/package.json +++ b/storybook/package.json @@ -24,6 +24,7 @@ "@storybook/addon-docs": "^10.1.11", "@storybook/icons": "^2.0.1", "@storybook/react-vite": "^10.1.11", + "@wordpress/theme": "file:../packages/theme", "storybook": "^10.1.11", "storybook-addon-tag-badges": "^3.0.4", "terser": "^5.37.0" From 034c014aea60bcb5025294584b43b938bfdbc7a3 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 17 Feb 2026 07:13:43 +0900 Subject: [PATCH 2/7] Add changelog --- packages/theme/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index 357ee51ab3ee2f..0a16fdcb2681eb 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -4,6 +4,7 @@ ### New Features +- Added PostCSS, esbuild, and Vite build plugins that inject fallback values for design system tokens (`--wpds-*`). Available as package exports: `@wordpress/theme/postcss-plugins/postcss-ds-token-fallbacks`, `@wordpress/theme/esbuild-plugins/esbuild-ds-token-fallbacks`, `@wordpress/theme/vite-plugins/vite-ds-token-fallbacks` ([#75589](https://github.com/WordPress/gutenberg/pull/75589)). - Add `--wpds-cursor-control` design token for interactive non-link elements ([#75697](https://github.com/WordPress/gutenberg/pull/75697)). ## 0.7.0 (2026-02-18) From f0aaec55587334550a7f7aed8c8ebc2902dd309b Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 17 Feb 2026 07:26:44 +0900 Subject: [PATCH 3/7] Harden esbuild and Vite token fallback plugins --- .../theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.mjs | 3 +++ packages/theme/src/vite-plugins/vite-ds-token-fallbacks.mjs | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.mjs b/packages/theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.mjs index cde59bc40dda29..20e7f264ac8d91 100644 --- a/packages/theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.mjs +++ b/packages/theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.mjs @@ -7,6 +7,9 @@ const LOADER_MAP = { '.ts': 'tsx', '.tsx': 'tsx', '.mjs': 'jsx', + '.mts': 'tsx', + '.cjs': 'jsx', + '.cts': 'tsx', }; /** diff --git a/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.mjs b/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.mjs index 0f04a06f0b5168..de4d836e19493c 100644 --- a/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.mjs +++ b/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.mjs @@ -13,10 +13,13 @@ const plugin = () => ( { if ( ! /\.[mc]?[jt]sx?$/.test( id ) ) { return null; } + if ( id.includes( 'node_modules' ) ) { + return null; + } if ( ! code.includes( '--wpds-' ) ) { return null; } - return addFallbackToVar( code ); + return { code: addFallbackToVar( code ), map: null }; }, } ); From b2083a8588706aa86ea88af18162fab1dadc95b1 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 17 Feb 2026 07:35:52 +0900 Subject: [PATCH 4/7] Fix lint errors in .d.mts type declaration files --- .../src/esbuild-plugins/esbuild-ds-token-fallbacks.d.mts | 4 +--- .../src/postcss-plugins/postcss-ds-token-fallbacks.d.mts | 4 +--- packages/theme/src/vite-plugins/vite-ds-token-fallbacks.d.mts | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.d.mts b/packages/theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.d.mts index 40ead2ca892881..72d3b6ad626419 100644 --- a/packages/theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.d.mts +++ b/packages/theme/src/esbuild-plugins/esbuild-ds-token-fallbacks.d.mts @@ -1,4 +1,2 @@ -import type { Plugin } from 'esbuild'; - -declare const plugin: Plugin; +declare const plugin: import('esbuild').Plugin; export default plugin; diff --git a/packages/theme/src/postcss-plugins/postcss-ds-token-fallbacks.d.mts b/packages/theme/src/postcss-plugins/postcss-ds-token-fallbacks.d.mts index f44a48ef090258..50de671cf1e538 100644 --- a/packages/theme/src/postcss-plugins/postcss-ds-token-fallbacks.d.mts +++ b/packages/theme/src/postcss-plugins/postcss-ds-token-fallbacks.d.mts @@ -1,4 +1,2 @@ -import type { PluginCreator } from 'postcss'; - -declare const plugin: PluginCreator; +declare const plugin: import('postcss').PluginCreator< never >; export default plugin; diff --git a/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.d.mts b/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.d.mts index f4ae4b5cff849e..478baf3a283c18 100644 --- a/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.d.mts +++ b/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.d.mts @@ -1,4 +1,2 @@ -import type { Plugin } from 'vite'; - -declare const plugin: () => Plugin; +declare const plugin: () => import('vite').Plugin; export default plugin; From 520600492e34c560c0e4aee3192035d91b06acca Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 24 Feb 2026 23:09:56 +0900 Subject: [PATCH 5/7] Add code comments addressing review feedback --- packages/theme/src/vite-plugins/vite-ds-token-fallbacks.mjs | 2 ++ packages/wp-build/lib/build.mjs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.mjs b/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.mjs index de4d836e19493c..e38a082f2e28bd 100644 --- a/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.mjs +++ b/packages/theme/src/vite-plugins/vite-ds-token-fallbacks.mjs @@ -19,6 +19,8 @@ const plugin = () => ( { if ( ! code.includes( '--wpds-' ) ) { return null; } + // Sourcemap omitted: replacements are small, inline substitutions + // that preserve line structure, so the debugging impact is negligible. return { code: addFallbackToVar( code ), map: null }; }, } ); diff --git a/packages/wp-build/lib/build.mjs b/packages/wp-build/lib/build.mjs index 92bb1361784560..6050c4d9162ad0 100755 --- a/packages/wp-build/lib/build.mjs +++ b/packages/wp-build/lib/build.mjs @@ -1272,6 +1272,10 @@ async function transpilePackage( packageName ) { }, }; const plugins = [ + // Note: dsTokenFallbacksJs and emotionPlugin both use esbuild's onLoad + // hook, which is non-composable — the first to return contents wins. If a + // file contains --wpds-* tokens, the Emotion transform will be skipped. + // Avoid using design tokens in Emotion styles until Emotion is removed. dsTokenFallbacksJs, needsEmotionPlugin && emotionPlugin, wasmInlinePlugin, From cb79fc901535f932193f876ceddf4f7fc6ec38d5 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 25 Feb 2026 03:11:15 +0900 Subject: [PATCH 6/7] Add token fallback plugin to compileStyles --- packages/wp-build/lib/build.mjs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/wp-build/lib/build.mjs b/packages/wp-build/lib/build.mjs index 6050c4d9162ad0..a338fa9636ca29 100755 --- a/packages/wp-build/lib/build.mjs +++ b/packages/wp-build/lib/build.mjs @@ -1434,12 +1434,13 @@ async function compileStyles( packageName ) { embedded: true, ...getSassOptions( packageDir ), async transform( source ) { - // Process with autoprefixer for LTR version - const ltrResult = await postcss( [ - autoprefixer( { grid: true } ), - ] ).process( source, { from: undefined } ); + const ltrResult = await postcss( + [ + dsTokenFallbacks, + autoprefixer( { grid: true } ), + ].filter( Boolean ) + ).process( source, { from: undefined } ); - // Process with rtlcss for RTL version const rtlResult = await postcss( [ rtlcss(), ] ).process( ltrResult.css, { from: undefined } ); From 0e64b790a8cc7a14c780cd12fe326d532328310e Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 25 Feb 2026 03:12:41 +0900 Subject: [PATCH 7/7] Use static token map in Stack for build-time fallback injection --- packages/ui/src/stack/stack.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/stack/stack.tsx b/packages/ui/src/stack/stack.tsx index b339d5fa7ae28b..dd44ef7a980633 100644 --- a/packages/ui/src/stack/stack.tsx +++ b/packages/ui/src/stack/stack.tsx @@ -1,8 +1,21 @@ import { useRender, mergeProps } from '@base-ui/react'; import { forwardRef } from '@wordpress/element'; +import type { GapSize } from '@wordpress/theme'; import { type StackProps } from './types'; import styles from './style.module.css'; +// Static map so that the build-time token fallback plugin can inject fallback +// values into each `var()` call. +const gapTokens: Record< GapSize, string > = { + xs: 'var(--wpds-dimension-gap-xs)', + sm: 'var(--wpds-dimension-gap-sm)', + md: 'var(--wpds-dimension-gap-md)', + lg: 'var(--wpds-dimension-gap-lg)', + xl: 'var(--wpds-dimension-gap-xl)', + '2xl': 'var(--wpds-dimension-gap-2xl)', + '3xl': 'var(--wpds-dimension-gap-3xl)', +}; + /** * A flexible layout component using CSS Flexbox for consistent spacing and alignment. * Built on design tokens for predictable spacing values. @@ -12,7 +25,7 @@ export const Stack = forwardRef< HTMLDivElement, StackProps >( function Stack( ref ) { const style: React.CSSProperties = { - gap: gap && `var(--wpds-dimension-gap-${ gap })`, + gap: gap && gapTokens[ gap ], alignItems: align, justifyContent: justify, flexDirection: direction,