@kb-labs/devkit 2.118.2 → 2.119.0-canary.0b1ec4151
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
|
-
//
|
|
6
|
-
//
|
|
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.
|
|
4
|
+
"version": "2.119.0-canary.0b1ec4151",
|
|
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.
|
|
139
|
+
"@kb-labs/devkit": "2.119.0-canary.0b1ec4151"
|
|
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/**',
|