@dsh-workbench/module-tools 0.1.4 → 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 +1 -1
- package/src/build.mjs +66 -2
- package/src/checks/style.mjs +37 -1
package/package.json
CHANGED
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
|
});
|
package/src/checks/style.mjs
CHANGED
|
@@ -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}`);
|