@dsh-workbench/module-tools 0.1.3 → 0.1.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-workbench/module-tools",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "模块包的构建与门禁工具:build(单文件 ESM 宿主侧 + 套 loader 壳的浏览器侧)、check(契约/产物边界/源码边界/卡片渲染)。模块包不再各带一份构建脚本。",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/build.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
 
2
2
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
3
4
  import { builtinModules } from 'node:module';
4
- import { join } from 'node:path';
5
+ import { isAbsolute, join, relative } from 'node:path';
5
6
  import { build as esbuild } from 'esbuild';
6
7
  import { SEED_SPECIFIERS } from './seed.mjs';
7
8
 
@@ -46,6 +47,11 @@ function aliasResolver({ root, unit }) {
46
47
  `模块自己的文件用 @/modules/${unit ?? '<unit>'}/… 或相对路径`,
47
48
  );
48
49
  }
50
+ // `@/…/X.module.css` 这类:把解析结果直接交给 CSS 落点(namespace 由 cssInjector 接管),
51
+ // 不要按 .ts/.tsx 去找 —— 否则别名插件的候选表把样式文件挡在外面。
52
+ if (base.endsWith('.css') && existsSync(base)) {
53
+ return { path: base, namespace: 'dsh-wb-css' };
54
+ }
49
55
  for (const candidate of [`${base}.ts`, `${base}.tsx`, `${base}/index.ts`, `${base}/index.tsx`]) {
50
56
  if (existsSync(candidate)) return { path: candidate };
51
57
  }
@@ -55,6 +61,64 @@ function aliasResolver({ root, unit }) {
55
61
  };
56
62
  }
57
63
 
64
+ function cssInjector({ root }) {
65
+ return {
66
+ name: 'dsh-wb-module:css',
67
+ setup(context) {
68
+ context.onResolve({ filter: /\.css$/ }, (args) => ({
69
+ path: isAbsolute(args.path) ? args.path : join(args.resolveDir === '' ? root : args.resolveDir, args.path),
70
+ namespace: 'dsh-wb-css',
71
+ }));
72
+ context.onLoad({ filter: /.*/, namespace: 'dsh-wb-css' }, (args) => {
73
+ const raw = readFileSync(args.path, 'utf8');
74
+ const key = relative(root, args.path).split('/').reverse()[0] + ':' + args.path;
75
+ const scoped = /\.module\.css$/.test(args.path);
76
+ // `*.module.css` 按 CSS Modules 处理:类名哈希后写回 CSS,同时把映射交给调用点
77
+ // (`import styles from './X.module.css'` → `styles.foo`)。哈希取"模块内相对路径",
78
+ // 同一个类在同一次构建里稳定,跨模块不同名。非 module 的 .css 原样注入(依赖里的就是这种)。
79
+ let css = raw;
80
+ let map = '{}';
81
+ if (scoped) {
82
+ const hash = createHash('sha1').update(relative(root, args.path)).digest('hex').slice(0, 6);
83
+ // `:global(...)` 里的类名**不许哈希** —— 那是外壳的契约类(比如 `.wb-iconButton`),
84
+ // 模块借用它时只能这样写:`.wb-envRefresh:global(.wb-iconButton)`。先把这些片段挖出来存着,
85
+ // 哈希完再放回去。
86
+ const globals = [];
87
+ const guarded = raw.replace(/:global\([^)]*\)/g, (piece) => {
88
+ globals.push(piece.slice(':global('.length, -1));
89
+ return `@dshwb-global-${globals.length - 1}@`;
90
+ });
91
+ const names = [
92
+ ...new Set([...guarded.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)].map((match) => match[1])),
93
+ ].sort((a, b) => b.length - a.length);
94
+ // 类名**不能以数字开头**:哈希是十六进制,直接接在 `.` 后面会写出 `.3a0f86_x` 这种
95
+ // 非法选择器 —— 浏览器会把整条规则丢掉(实测:20 条规则只解析出 1 条,界面整屏掉样式)。
96
+ // 前面加一个下划线,保持 CSS Modules 的老习惯(`._3a0f86_x`)。
97
+ const pairs = names.map((name) => [name, `_${hash}_${name}`]);
98
+ css = guarded;
99
+ for (const [from, to] of pairs) css = css.split(`.${from}`).join(`.${to}`);
100
+ css = css.replace(/@dshwb-global-(\d+)@/g, (_whole, at) => globals[Number(at)] ?? '');
101
+ map = JSON.stringify(Object.fromEntries(pairs));
102
+ }
103
+ return {
104
+ loader: 'js',
105
+ contents: [
106
+ `const css = ${JSON.stringify(css)};`,
107
+ `const key = ${JSON.stringify(key)};`,
108
+ "if (typeof document !== 'undefined' && document.querySelector('style[data-dsh-wb-css=\"' + key + '\"]') === null) {",
109
+ " const style = document.createElement('style');",
110
+ " style.setAttribute('data-dsh-wb-css', key);",
111
+ " style.textContent = css;",
112
+ " document.head.appendChild(style);",
113
+ "}",
114
+ `export default ${map};`,
115
+ ].join('\n'),
116
+ };
117
+ });
118
+ },
119
+ };
120
+ }
121
+
58
122
  export async function build({ root }) {
59
123
  const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
60
124
  const id = manifest.name;
@@ -96,7 +160,7 @@ export async function build({ root }) {
96
160
  jsxFactory: 'h',
97
161
  jsxFragment: 'Fragment',
98
162
  resolveExtensions: RESOLVE_EXTENSIONS,
99
- plugins,
163
+ plugins: [...plugins, cssInjector({ root })],
100
164
  write: false,
101
165
  logLevel: 'warning',
102
166
  });
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
- import { join, relative } from 'node:path';
2
+ import { dirname, join, relative, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { discoverModulePackages } from '../discover.mjs';
5
5
 
@@ -68,6 +68,42 @@ for (const pkg of modules) {
68
68
  }
69
69
  }
70
70
 
71
+ const bindings = [];
72
+ for (const pkg of modules) {
73
+ const defined = new Map();
74
+ for (const file of walk(pkg.dir)) {
75
+ if (!/\.module\.css$/.test(file)) continue;
76
+ const text = readFileSync(file, 'utf8').replace(/:global\([^)]*\)/g, '');
77
+ defined.set(file, new Set(Array.from(text.matchAll(/\.([A-Za-z][\w-]*)/g), (m) => m[1])));
78
+ }
79
+ for (const file of walk(pkg.dir)) {
80
+ if (!/\.tsx?$/.test(file) || isTestSource(file)) continue;
81
+ const text = readFileSync(file, 'utf8');
82
+ for (const hit of text.matchAll(/import\s+([A-Za-z_$][\w$]*)\s+from\s+'([^']*\.module\.css)'/g)) {
83
+ const bind = hit[1];
84
+ const names = defined.get(resolve(dirname(file), hit[2]));
85
+ if (names === undefined) continue;
86
+ for (const use of text.matchAll(new RegExp(`\\b${bind}\\.([A-Za-z_$][\\w$]*)`, 'g'))) {
87
+ if (names.has(use[1])) continue;
88
+ bindings.push(
89
+ `${pkg.manifest.name}: ${relative(pkg.dir, file)} 用了 ${bind}.${use[1]},` +
90
+ `${relative(pkg.dir, resolve(dirname(file), hit[2]))} 里没有这个类`,
91
+ );
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ if (bindings.length > 0) {
98
+ console.error(`✖ 样式绑定指向了 CSS 里不存在的类:${bindings.length} 处`);
99
+ for (const one of bindings) console.error(` · ${one}`);
100
+ console.error(
101
+ ' (CSS Modules 的哈希类名靠"同一份文件、同一个导出"配对:调用点拿到的会是 undefined,' +
102
+ '真机上渲染成 class="undefined" —— 元素整片没样式,而且控制台一声不响)',
103
+ );
104
+ process.exit(1);
105
+ }
106
+
71
107
  if (problems.length > 0) {
72
108
  console.error(`✖ 模块不许定义外壳前缀的 CSS 规则:${problems.length} 处`);
73
109
  for (const problem of problems) console.error(` · ${problem}`);