Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
21650c4
spike(GMT-1715): Panda styling PoC — styled factory + providers + the…
dreamwasp Jul 30, 2026
fc06949
spike(GMT-1715): convert Panda spike to yarn workspace + nx; example …
dreamwasp Jul 31, 2026
726088c
spike(GMT-1715): prototype the two escape hatches (getColorValue + st…
dreamwasp Jul 31, 2026
6f01912
spike(GMT-1715): force-generate recipe variants via staticCss; docume…
dreamwasp Jul 31, 2026
30ceba7
spike(GMT-1715): add CSP-aware themeable-dynamic demo (token.var + in…
dreamwasp Jul 31, 2026
858a81e
spike(GMT-1715): use the REAL Gamut Core theme + fonts
dreamwasp Aug 3, 2026
0da507e
spike(GMT-1715): style control heights via [value] escape hatch, drop…
dreamwasp Aug 3, 2026
68bbe63
spike(GMT-1715): reproduce full Gamut Button atom API; split GamutPro…
dreamwasp Aug 3, 2026
45c770c
spike(GMT-1715): rebuild ToolTip in Panda; wire into IconButton
dreamwasp Aug 3, 2026
7ba36d6
docs(GMT-1715): document known Emotion→Panda breaking changes with co…
dreamwasp Aug 3, 2026
873d3d3
docs(GMT-1715): expand breaking change #2 with internal Gamut authori…
dreamwasp Aug 3, 2026
4b570a6
spike(GMT-1715): Rspack + Module Federation — styling across MF bound…
dreamwasp Aug 3, 2026
c9cc3b0
spike(GMT-1715): button atom parity fixes (focus ring, CTA shadow)
dreamwasp Aug 6, 2026
e98b61a
spike(GMT-1715): Emotion-free styling engine — composed call shape su…
dreamwasp Aug 6, 2026
366cfb3
docs(GMT-1715): fix README section ordering after engine insert
dreamwasp Aug 6, 2026
c70a776
spike(GMT-1715): perf approximation, MF runtime verification, precomp…
dreamwasp Aug 6, 2026
9e8c7c1
spike(GMT-1715): three consumer tiers — memoise module-scope styled
dreamwasp Aug 6, 2026
9624fe0
docs(GMT-1715): disambiguate the two tier-numbering axes in the spike…
dreamwasp Aug 6, 2026
283a916
docs(GMT-1715): runtime-value guidance + the unbounded-class footgun
dreamwasp Aug 6, 2026
d969350
spike(GMT-1715): pin the states() overlap fidelity risk with a test
dreamwasp Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@
},
"workspaces": {
"packages": [
"packages/*"
"packages/*",
"spikes/*"
]
}
}
5 changes: 5 additions & 0 deletions spikes/panda-mf-poc/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
styled-system/
dist/
src/ui/gamut.css
.mf/
86 changes: 86 additions & 0 deletions spikes/panda-mf-poc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Rspack + Module Federation spike (GMT-1715)

Branch: `cass-GMT-1715`. A yarn workspace + nx project. Answers: **does Rspack +
Module Federation (platform's setup) affect the styling-engine decision?**

A minimal MF pair — **host** (`variant="primary"`) + **remote** (exposes a
`Widget` using `variant="danger"`, a variant the host never renders) — both using
a Panda-built Gamut `Button`. The host imports the **one complete** Gamut static
stylesheet; the remote is federated in at runtime.

## Run it

```
yarn install
yarn nx run panda-mf-poc:build # codegen + cssgen + rspack MF build (host + remote)
# to see it live (dist is static): serve each and open the host
npx http-server dist/remote -p 3001 --cors &
npx http-server dist/host -p 3000 &
# open http://localhost:3000 → host renders its own primary button + the federated remote danger button
```

## Answer: does MF affect the styling engine? — Yes, but it's satisfiable, and Panda is _safer_ here than the alternatives

### The one real requirement (and why)

A **zero-runtime** engine emits static CSS and tree-shakes to what each build
uses. Module Federation loads a remote's components into a host **at runtime** —
so the remote's styles must already be in the document. A host's own build won't
include CSS for a variant only the remote uses.

**→ Gamut must ship a COMPLETE static stylesheet** (all recipe variants via
`staticCss`), imported once by the host. Then every federated remote's Gamut
components are covered regardless of what the host itself renders.

**Proven here:** the host build emits `gmt-button--variant_danger` even though the
host only renders `variant="primary"` — because `staticCss: { recipes: { button:
['*'] } }` force-emits every variant. The remote's `danger` widget is styled by
the host's sheet. (This is the same `staticCss` requirement documented in
BREAKING-CHANGES §2/§3.)

### Why Panda is actually SAFER across MF than some alternatives

- **Deterministic, content-based class names** — `gmt-button--variant_danger`,
`.bg_primary` (recipe-name + property_value), identical across independent
builds. Federated host/remote builds produce the **same** class for the same
style, so duplicate CSS is idempotent and there are **no collisions**. Contrast
**CSS Modules**, which hash class names **per build** → federated host/remotes
get mismatched/duplicated names (a known MF hazard). Zero-runtime _atomic_ CSS
is a good fit for MF.
- **Tokens at `:root`** — `--colors-*` are defined once (host's sheet); remote
components reference the same vars. No per-remote theme runtime needed.

### Coexistence / version notes

- Keep **Gamut a shared singleton** across host + remotes (MF `shared`) so class
names + tokens match. **Version skew** (host on Gamut vX, remote on vY) is the
real risk — different generated classes/tokens → visual drift. Same discipline
MF already needs for React.
- Contrast **Emotion (today)**: runtime injection means each remote self-styles
(works out of the box), but MF apps commonly hit **multiple Emotion caches**
across remotes (duplicate `<style>`, insertion-order/`:first-child` warnings) +
runtime cost. Panda trades that for "ship one static sheet + shared singleton."

### Bottom line

MF **does not block** a Panda swap. It adds two requirements, both satisfied here:
**(1)** Gamut ships a complete static stylesheet (`staticCss`), imported once by
the host; **(2)** Gamut is a shared singleton version across host + remotes. With
those, federated components style correctly, and atomic/recipe class determinism
makes it _less_ fragile than per-build-hashed CSS Modules.

## Still simplified (vs platform's real setup)

- One host + one remote (platform has ~13 remotes); no runtime version-skew test.
- Build-validated + CSS-coverage-verified here; live run is the `http-server` step
above (not visually verified in CI).
- Consumer-authored (non-Gamut) styles in a remote still inject per-remote build;
content-based atomic classes dedupe, but that path isn't exercised here.

## Files

- `panda.config.ts` — one `button` recipe, `staticCss` all variants
- `src/ui/gamut.tsx` — the shared Panda `Button`
- `src/remote/*` — exposes `Widget` (`variant="danger"`)
- `src/host/*` — renders `variant="primary"` + lazy-loads `remote/Widget`; imports the complete sheet
- `build.mjs` — rspack `ModuleFederationPlugin` configs for host + remote
108 changes: 108 additions & 0 deletions spikes/panda-mf-poc/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';

import { rspack } from '@rspack/core';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const styledSystem = path.resolve(__dirname, 'styled-system');

/* Aliased so the runtime engine can be declared a federation `shared` singleton —
* `shared` keys are request strings, and a relative path can't be one. In real
* Gamut this is just `@codecademy/gamut-styles`. */
const gamutEngine = path.resolve(
__dirname,
'../panda-styling-poc/src/gamut/engine'
);

const swc = {
test: /\.tsx?$/,
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: { syntax: 'typescript', tsx: true },
transform: { react: { runtime: 'automatic' } },
},
},
};

const common = {
mode: 'development',
devtool: false,
resolve: {
extensions: ['.tsx', '.ts', '.mjs', '.js'],
alias: { 'styled-system': styledSystem, '@gamut-engine': gamutEngine },
},
module: { rules: [swc, { test: /\.css$/, type: 'css' }] },
experiments: { css: true },
};

const { ModuleFederationPlugin } = rspack.container;
const shared = {
react: { singleton: true, requiredVersion: false },
'react-dom': { singleton: true, requiredVersion: false },
/* MANDATORY, not an optimisation. The runtime engine holds React context (the
* theme) and the injected-rule registry in module scope; an unshared copy in a
* remote gets its own of each, so the host's ThemeProvider becomes invisible to
* it. See src/federation/verify.cjs for the failure reproduced. */
'@gamut-engine': { singleton: true, requiredVersion: false },
};

const remoteConfig = {
...common,
name: 'remote',
entry: { remote: './src/remote/index.ts' },
output: {
path: path.resolve(__dirname, 'dist/remote'),
publicPath: 'http://localhost:3001/',
uniqueName: 'remote',
clean: true,
},
plugins: [
new ModuleFederationPlugin({
name: 'remote',
filename: 'remoteEntry.js',
exposes: { './Widget': './src/remote/Widget.tsx' },
shared,
}),
new rspack.HtmlRspackPlugin({ template: './remote.html' }),
],
};

const hostConfig = {
...common,
name: 'host',
entry: { host: './src/host/index.ts' },
output: {
path: path.resolve(__dirname, 'dist/host'),
publicPath: 'http://localhost:3000/',
uniqueName: 'host',
clean: true,
},
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: { remote: 'remote@http://localhost:3001/remoteEntry.js' },
shared,
}),
new rspack.HtmlRspackPlugin({ template: './host.html' }),
],
};

const run = (cfg) =>
new Promise((res, rej) =>
rspack(cfg, (err, stats) => {
if (err) return rej(err);
if (stats.hasErrors()) {
console.error(
stats.toString({ colors: false, chunks: false, modules: false })
);
return rej(new Error(`${cfg.name} build failed`));
}
console.log(`${cfg.name} built OK`);
res();
})
);

await run(remoteConfig);
await run(hostConfig);
console.log('MF build complete.');
10 changes: 10 additions & 0 deletions spikes/panda-mf-poc/host.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>MF host — Panda Gamut</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
24 changes: 24 additions & 0 deletions spikes/panda-mf-poc/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "panda-mf-poc",
"description": "GMT-1715 spike: Rspack + Module Federation with a Panda-built Gamut (styling across MF boundaries)",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"codegen": "panda codegen",
"cssgen": "panda cssgen --outfile src/ui/gamut.css",
"build": "panda codegen && panda cssgen --outfile src/ui/gamut.css && node build.mjs",
"federation": "esbuild src/federation/copy.tsx --bundle --platform=node --format=cjs --jsx=automatic --external:react --external:react-dom --outfile=.mf/copyA.cjs --log-level=warning && esbuild src/federation/copy.tsx --bundle --platform=node --format=cjs --jsx=automatic --external:react --external:react-dom --outfile=.mf/copyB.cjs --log-level=warning && node src/federation/verify.cjs"
},
"dependencies": {
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@pandacss/dev": "^0.53.0",
"@rspack/core": "^1.1.8",
"@types/react": "18.3.27",
"@types/react-dom": "18.3.1",
"typescript": "5.9.3"
}
}
67 changes: 67 additions & 0 deletions spikes/panda-mf-poc/panda.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { defineConfig, defineRecipe } from '@pandacss/dev';

/* Minimal Panda setup for the Module Federation spike — one `button` recipe with
* all variants force-emitted (staticCss), so the COMPLETE static sheet a host
* imports covers any variant a federated remote uses. */
const mk = (c: string) => ({
bg: c,
color: 'background',
_hover: { bg: `${c}-hover` },
});

const button = defineRecipe({
className: 'gmt-button',
jsx: ['Button'],
base: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderWidth: '2',
borderStyle: 'solid',
borderColor: 'transparent',
borderRadius: 'md',
px: '16',
py: '8',
fontFamily: 'base',
cursor: 'pointer',
},
variants: {
variant: {
primary: mk('primary'),
secondary: mk('secondary'),
danger: mk('danger'),
interface: mk('interface'),
},
},
defaultVariants: { variant: 'primary' },
});

export default defineConfig({
preflight: false,
jsxFramework: 'react',
include: ['./src/**/*.{ts,tsx}'],
outdir: 'styled-system',
staticCss: { recipes: { button: ['*'] } },
theme: {
extend: {
tokens: {
colors: {
primary: { value: '#1557FF' },
'primary-hover': { value: '#1145CC' },
secondary: { value: '#10162F' },
'secondary-hover': { value: '#000000' },
danger: { value: '#C60C0C' },
'danger-hover': { value: '#9E0A0A' },
interface: { value: '#374151' },
'interface-hover': { value: '#111111' },
background: { value: '#ffffff' },
},
radii: { md: { value: '4px' } },
fonts: { base: { value: 'system-ui, sans-serif' } },
spacing: { '8': { value: '8px' }, '16': { value: '16px' } },
borderWidths: { '2': { value: '2px' } },
},
recipes: { button },
},
},
});
38 changes: 38 additions & 0 deletions spikes/panda-mf-poc/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "panda-mf-poc",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "spikes/panda-mf-poc/src",
"projectType": "application",
"tags": [],
"targets": {
"codegen": {
"executor": "nx:run-commands",
"options": {
"cwd": "spikes/panda-mf-poc",
"command": "yarn codegen"
}
},
"cssgen": {
"executor": "nx:run-commands",
"dependsOn": ["codegen"],
"options": {
"cwd": "spikes/panda-mf-poc",
"command": "yarn cssgen"
}
},
"build": {
"executor": "nx:run-commands",
"options": {
"cwd": "spikes/panda-mf-poc",
"command": "yarn build"
}
},
"federation": {
"executor": "nx:run-commands",
"options": {
"cwd": "spikes/panda-mf-poc",
"command": "yarn federation"
}
}
}
}
10 changes: 10 additions & 0 deletions spikes/panda-mf-poc/remote.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>MF remote — Panda Gamut</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Loading
Loading