@kb-labs/devkit 2.119.0-canary.8c5512847 → 2.119.0-canary.b20fd4a5b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,8 @@ import { readFileSync } from 'node:fs'
2
2
  import { fileURLToPath } from 'node:url'
3
3
  import { describe, expect, it } from 'vitest'
4
4
 
5
- // kb-create merges this snippet into a fresh project's CLAUDE.md during
6
- // install (tools/kb-create/internal/claude/claudemd.go), so a stale command
7
- // here reaches every new user's onboarding doc verbatim.
5
+ // The platform onboarding surface consumes this snippet during project setup;
6
+ // keep it independent from the launcher's V2 artifact application boundary.
8
7
  const snippetPath = fileURLToPath(new URL('./CLAUDE.md.snippet', import.meta.url))
9
8
  const snippet = readFileSync(snippetPath, 'utf8')
10
9
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kb-labs/devkit",
3
3
  "description": "Shared developer toolkit for KB Labs projects: TS/ESLint/Prettier/Vitest/Tsup presets and reusable GitHub Actions.",
4
- "version": "2.119.0-canary.8c5512847",
4
+ "version": "2.119.0-canary.b20fd4a5b",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "exports": {
@@ -39,11 +39,13 @@
39
39
  "./tsup/node.js": "./tsup/node.js",
40
40
  "./tsup/react-lib.js": "./tsup/react-lib.js",
41
41
  "./tsup/sdk.js": "./tsup/sdk.js",
42
+ "./tsup/css-modules-plugin.js": "./tsup/css-modules-plugin.js",
42
43
  "./tsup/node": "./tsup/node.js",
43
44
  "./tsup/react-lib": "./tsup/react-lib.js",
44
45
  "./tsup/bin": "./tsup/bin.js",
45
46
  "./tsup/sdk": "./tsup/sdk.js",
46
47
  "./tsup/dual": "./tsup/dual.js",
48
+ "./tsup/css-modules-plugin": "./tsup/css-modules-plugin.js",
47
49
  "./agents/": "./agents/",
48
50
  "./assets/": "./assets/",
49
51
  "./assets/claude/manifest.json": "./assets/claude/manifest.json",
@@ -114,12 +116,15 @@
114
116
  "eslint-plugin-react-hooks": "^5.2.0",
115
117
  "eslint-plugin-unused-imports": "^4.1.4",
116
118
  "glob": "^11.0.0",
119
+ "postcss": "^8.5.23",
120
+ "postcss-modules": "^9.0.1",
117
121
  "yaml": "^2.8.0"
118
122
  },
119
123
  "devDependencies": {
120
124
  "@types/node": "^24.3.3",
121
125
  "@vitest/coverage-istanbul": "^4.1.4",
122
126
  "@vitest/coverage-v8": "^3.2.4",
127
+ "esbuild": "^0.28.1",
123
128
  "eslint": "^9.35.0",
124
129
  "eslint-plugin-filenames-simple": "^0.9.0",
125
130
  "eslint-plugin-sonarjs": "^4.0.3",
@@ -131,7 +136,7 @@
131
136
  "typescript": "^5.9.2",
132
137
  "typescript-eslint": "^8.44.0",
133
138
  "vitest": "^3.2.6",
134
- "@kb-labs/devkit": "2.119.0-canary.8c5512847"
139
+ "@kb-labs/devkit": "2.119.0-canary.b20fd4a5b"
135
140
  },
136
141
  "engines": {
137
142
  "node": ">=22.0.0",
@@ -0,0 +1,86 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import postcss from 'postcss';
4
+ import postcssModules from 'postcss-modules';
5
+
6
+ /**
7
+ * esbuild plugin: real CSS Modules for tsup-built libraries.
8
+ *
9
+ * esbuild has no built-in CSS Modules support — a bare `import styles from
10
+ * './x.module.css'` under tsup/esbuild always resolves to `{}` (see
11
+ * infra/devkit/tsup/react-lib.js). This plugin processes `.module.css`
12
+ * files through postcss-modules so `styles.foo` resolves to a real,
13
+ * collision-safe class name, and the scoped CSS still gets bundled into
14
+ * the package's output stylesheet.
15
+ *
16
+ * Naming matches the Studio host's native Rspack CSS Modules convention
17
+ * (`type: 'css/auto'`, `exportsConvention: 'as-is'` in studio/app/rspack.config.mjs)
18
+ * as closely as a different bundler allows: keys stay as-authored (no
19
+ * camelCasing), values are `<local>_<hash>` so class names stay readable
20
+ * in devtools while still being scoped per file.
21
+ *
22
+ * Virtual resolved paths deliberately do NOT end in `.css` (they carry a
23
+ * `?kb-css-modules-*` suffix instead). tsup registers its own postcss
24
+ * onLoad handler for `/\.css$/` with no namespace restriction, and
25
+ * esbuild's namespace-less onLoad filters match every namespace, not just
26
+ * `file` — so a path that still looked like `*.module.css` after being
27
+ * moved into our namespace was silently intercepted by tsup's handler
28
+ * before this plugin ever got a chance to run.
29
+ */
30
+ export function cssModulesPlugin() {
31
+ /** @type {Map<string, { css: string, tokens: Record<string, string> }>} */
32
+ const cache = new Map();
33
+
34
+ async function process(filePath) {
35
+ const cached = cache.get(filePath);
36
+ if (cached) {
37
+ return cached;
38
+ }
39
+ const source = await readFile(filePath, 'utf8');
40
+ let tokens = {};
41
+ const result = await postcss([
42
+ postcssModules({
43
+ getJSON(_from, json) {
44
+ tokens = json;
45
+ },
46
+ generateScopedName: '[local]_[hash:base64:5]',
47
+ }),
48
+ ]).process(source, { from: filePath, map: false });
49
+ const entry = { css: result.css, tokens };
50
+ cache.set(filePath, entry);
51
+ return entry;
52
+ }
53
+
54
+ return {
55
+ name: 'kb-css-modules',
56
+ setup(build) {
57
+ build.onResolve({ filter: /\.module\.css$/ }, (args) => ({
58
+ path: `${path.isAbsolute(args.path) ? args.path : path.join(args.resolveDir, args.path)}?kb-css-modules-tokens`,
59
+ namespace: 'kb-css-modules-tokens',
60
+ }));
61
+
62
+ build.onLoad({ filter: /\?kb-css-modules-tokens$/, namespace: 'kb-css-modules-tokens' }, async (args) => {
63
+ const realPath = args.path.replace(/\?kb-css-modules-tokens$/, '');
64
+ const { tokens } = await process(realPath);
65
+ return {
66
+ // Side-effect import pulls in the scoped CSS via the paired
67
+ // virtual-css namespace below; this module only exports tokens.
68
+ contents: `import ${JSON.stringify(`${realPath}?kb-css-modules-css`)};\nexport default ${JSON.stringify(tokens)};`,
69
+ loader: 'js',
70
+ resolveDir: path.dirname(realPath),
71
+ };
72
+ });
73
+
74
+ build.onResolve({ filter: /\?kb-css-modules-css$/ }, (args) => ({
75
+ path: args.path,
76
+ namespace: 'kb-css-modules-css',
77
+ }));
78
+
79
+ build.onLoad({ filter: /\?kb-css-modules-css$/, namespace: 'kb-css-modules-css' }, async (args) => {
80
+ const realPath = args.path.replace(/\?kb-css-modules-css$/, '');
81
+ const { css } = await process(realPath);
82
+ return { contents: css, loader: 'css', resolveDir: path.dirname(realPath) };
83
+ });
84
+ },
85
+ };
86
+ }
@@ -0,0 +1,158 @@
1
+ import { describe, expect, it, afterEach } from 'vitest';
2
+ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import * as esbuild from 'esbuild';
6
+ import { cssModulesPlugin } from './css-modules-plugin.js';
7
+
8
+ /**
9
+ * Regression coverage for infra/devkit/tsup/css-modules-plugin.js.
10
+ *
11
+ * Every scenario here is a real failure this plugin went through before it
12
+ * worked (see docs/adr/0040-real-css-modules-for-tsup-built-libraries.md):
13
+ * esbuild not touching `.module.css` at all (empty token map), and — the
14
+ * subtle one — tsup's own bundled postcss onLoad handler racing this
15
+ * plugin's onLoad and winning because esbuild's namespace-less filters
16
+ * match every namespace, not just `file`.
17
+ */
18
+
19
+ const dirs: string[] = [];
20
+
21
+ function makeFixtureDir(): string {
22
+ const dir = mkdtempSync(path.join(tmpdir(), 'kb-css-modules-test-'));
23
+ dirs.push(dir);
24
+ return dir;
25
+ }
26
+
27
+ afterEach(() => {
28
+ while (dirs.length) {
29
+ rmSync(dirs.pop()!, { recursive: true, force: true });
30
+ }
31
+ });
32
+
33
+ /** Mimics tsup's bundled postcss plugin: an onLoad for /\.css$/ with no namespace filter. */
34
+ const competingNamespaceLessCssLoader: esbuild.Plugin = {
35
+ name: 'competing-css-loader',
36
+ setup(build) {
37
+ build.onLoad({ filter: /\.css$/ }, async (args) => {
38
+ const { readFile } = await import('node:fs/promises');
39
+ return { contents: await readFile(args.path, 'utf8'), loader: 'css' };
40
+ });
41
+ },
42
+ };
43
+
44
+ async function bundle(dir: string, extraPlugins: esbuild.Plugin[] = []) {
45
+ const result = await esbuild.build({
46
+ entryPoints: [path.join(dir, 'entry.js')],
47
+ bundle: true,
48
+ write: false,
49
+ outdir: path.join(dir, 'out'),
50
+ format: 'esm',
51
+ plugins: [...extraPlugins, cssModulesPlugin()],
52
+ });
53
+ const js = result.outputFiles.find((f) => f.path.endsWith('.js'))!.text;
54
+ const css = result.outputFiles.find((f) => f.path.endsWith('.css'))?.text ?? '';
55
+ return { js, css };
56
+ }
57
+
58
+ describe('cssModulesPlugin', () => {
59
+ it('resolves styles.<class> to a real, non-empty scoped class name', async () => {
60
+ const dir = makeFixtureDir();
61
+ writeFileSync(path.join(dir, 'entry.js'), `
62
+ import styles from './x.module.css';
63
+ export const cls = styles.foo;
64
+ `);
65
+ writeFileSync(path.join(dir, 'x.module.css'), `.foo { color: red; }`);
66
+
67
+ const { js, css } = await bundle(dir);
68
+
69
+ // The pre-fix behavior: esbuild has no CSS Modules support, so a bare
70
+ // `.module.css` default import resolves to `{}` and every className is
71
+ // silently undefined at runtime.
72
+ expect(js).not.toContain('= {};');
73
+ expect(js).toMatch(/var \w+_default = \{\s*"foo":\s*"foo_[\w-]+"\s*\};/);
74
+
75
+ const match = js.match(/"foo":\s*"(foo_[\w-]+)"/);
76
+ expect(match).not.toBeNull();
77
+ const scopedName = match![1];
78
+
79
+ expect(css).toContain(`.${scopedName} {`);
80
+ expect(css).not.toContain('.foo {'); // original literal selector must not survive unscoped
81
+ });
82
+
83
+ it('keeps :global(...) selectors as real, unscoped Ant Design class names', async () => {
84
+ const dir = makeFixtureDir();
85
+ writeFileSync(path.join(dir, 'entry.js'), `
86
+ import styles from './btn.module.css';
87
+ export const cls = styles.uiButton;
88
+ `);
89
+ writeFileSync(
90
+ path.join(dir, 'btn.module.css'),
91
+ `.uiButton:global(.ant-btn-primary) { color: blue; }`,
92
+ );
93
+
94
+ const { css } = await bundle(dir);
95
+
96
+ // The real (unscoped) Ant Design class must appear verbatim so it still
97
+ // matches Ant Design's actual rendered DOM class.
98
+ expect(css).toContain('.ant-btn-primary');
99
+ // ...while the local part of the same compound selector is still scoped.
100
+ expect(css).not.toMatch(/\.uiButton(?!_\w)/);
101
+ });
102
+
103
+ it('rewrites @keyframes and their animation-name reference to the same scoped name', async () => {
104
+ const dir = makeFixtureDir();
105
+ writeFileSync(path.join(dir, 'entry.js'), `import './anim.module.css';`);
106
+ writeFileSync(
107
+ path.join(dir, 'anim.module.css'),
108
+ `
109
+ .spin { animation: spin 1s linear infinite; }
110
+ @keyframes spin { from { transform: rotate(0); } to { transform: rotate(360deg); } }
111
+ `,
112
+ );
113
+
114
+ const { css } = await bundle(dir);
115
+
116
+ const keyframesMatch = css.match(/@keyframes (spin_[\w-]+)/);
117
+ expect(keyframesMatch).not.toBeNull();
118
+ const scopedKeyframesName = keyframesMatch![1];
119
+
120
+ expect(css).toContain(`animation: ${scopedKeyframesName} 1s linear infinite;`);
121
+ });
122
+
123
+ it('still resolves real tokens when a namespace-less onLoad(/\\.css$/) plugin runs first', async () => {
124
+ // Regression test for the actual bug hit while building this plugin:
125
+ // tsup registers a `.css` onLoad with no namespace restriction, ahead of
126
+ // any user esbuildPlugins. esbuild's namespace-less onLoad filters match
127
+ // every namespace — not just `file` — so an earlier version of this
128
+ // plugin that resolved `.module.css` into a custom namespace, but kept a
129
+ // path that still *looked* like `*.module.css`, was silently intercepted
130
+ // by that competing handler before ever reaching postcss-modules.
131
+ const dir = makeFixtureDir();
132
+ writeFileSync(path.join(dir, 'entry.js'), `
133
+ import styles from './race.module.css';
134
+ export const cls = styles.foo;
135
+ `);
136
+ writeFileSync(path.join(dir, 'race.module.css'), `.foo { color: green; }`);
137
+
138
+ const { js } = await bundle(dir, [competingNamespaceLessCssLoader]);
139
+
140
+ expect(js).not.toContain('= {};');
141
+ expect(js).toMatch(/"foo":\s*"foo_[\w-]+"/);
142
+ });
143
+
144
+ it('produces stable, deterministic scoped names across repeated builds of the same file', async () => {
145
+ const dir = makeFixtureDir();
146
+ writeFileSync(path.join(dir, 'entry.js'), `
147
+ import styles from './stable.module.css';
148
+ export const cls = styles.foo;
149
+ `);
150
+ writeFileSync(path.join(dir, 'stable.module.css'), `.foo { color: red; }`);
151
+
152
+ const first = await bundle(dir);
153
+ const second = await bundle(dir);
154
+
155
+ const extract = (js: string) => js.match(/"foo":\s*"(foo_[\w-]+)"/)?.[1];
156
+ expect(extract(first.js)).toBe(extract(second.js));
157
+ });
158
+ });
package/tsup/react-lib.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { defineConfig } from 'tsup'
2
+ import { cssModulesPlugin } from './css-modules-plugin.js'
2
3
 
3
4
  export default defineConfig({
4
5
  entry: ['src/index.ts'],
@@ -14,6 +15,10 @@ export default defineConfig({
14
15
  splitting: false,
15
16
  skipNodeModulesBundle: true,
16
17
  shims: false,
18
+ // esbuild has no built-in CSS Modules support (`import styles from './x.module.css'`
19
+ // otherwise resolves to `{}`) — see css-modules-plugin.js for why and how this
20
+ // matches the Studio host's Rspack CSS Modules convention.
21
+ esbuildPlugins: [cssModulesPlugin()],
17
22
  ignoreWatch: [
18
23
  '**/node_modules/**',
19
24
  '**/dist/**',