tmagic-editor/scripts/check-headless-dist.mjs
roymondchen 93dc0d2187 feat(form): 新增无渲染校验入口,支持 Node/CI 环境执行表单校验
将 submitForm/validateForm 改为无 DOM 的 headless 实现,并抽取字段登记与编辑器复合字段配置,便于脚本与 CI 批量校验。
2026-08-28 15:33:54 +08:00

93 lines
3.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 校验各包 headless 子路径的**发布产物**能在原生 Node 里跑起来。
*
* 单测走 vitest别名指向 `src`、依赖由 vite 解析,看不到 Node 自己的 ESM 解析规则
* (例如 `dayjs/plugin/utc` 这种无 exports 映射、又不带扩展名的深路径会直接 404
* 这里在真实的 node_modules 里 `import` / `require` 一次 dist把这类只在发布后
* 才暴露的问题挡在构建阶段。
*/
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { execa } from 'execa';
const dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(dirname, '..');
/** 每个子路径入口应该导出的若干个符号,顺带验证产物不是空壳 */
const targets = [
{ specifier: '@tmagic/design/headless', expects: ['getDesignConfig', 'appendValidateSuggestion'] },
{ specifier: '@tmagic/form/headless', expects: ['validateForm', 'submitForm', 'registerField', 'builtInFields'] },
{ specifier: '@tmagic/editor/headless', expects: ['editorFields'] },
];
/**
* 找工作区里该包自己的目录当 cwd。
*
* 在包根里 `import '@tmagic/xxx/headless'` 走的是 Node 的 self-reference
* 命中的仍是包自己 `exports` 里声明的那份产物路径,同时深层裸依赖按 realpath
* 从 `<pkg>/node_modules` 解析。比挑一个「依赖了该包的目录」更可靠——依赖方的
* 链接可能指向 registry 上的同名旧版本,那样检查的就是别人的产物了。
*/
const findPackageDir = (specifier) => {
const pkgName = specifier.split('/').slice(0, 2).join('/');
const groups = ['packages', 'runtime', 'vue-components', 'react-components'].filter((group) =>
existsSync(path.join(root, group)),
);
for (const group of groups) {
for (const name of readdirSync(path.join(root, group))) {
const manifest = path.join(root, group, name, 'package.json');
if (!existsSync(manifest)) continue;
if (JSON.parse(readFileSync(manifest, 'utf-8')).name === pkgName) {
return path.join(root, group, name);
}
}
}
};
const run = async (cwd, code) => {
const { stdout } = await execa('node', ['--input-type=module', '-e', code], { cwd });
return stdout;
};
let failed = 0;
for (const { specifier, expects } of targets) {
const cwd = findPackageDir(specifier);
if (!cwd) {
console.error(`${specifier}: 工作区里找不到这个包`);
failed += 1;
continue;
}
const assertExports = `
const missing = ${JSON.stringify(expects)}.filter((name) => mod[name] === undefined);
if (missing.length) throw new Error('missing exports: ' + missing.join(', '));
`;
for (const [kind, code] of [
['import', `const mod = await import('${specifier}');${assertExports}`],
[
'require',
`const { createRequire } = await import('node:module');
const mod = createRequire(process.cwd() + '/index.js')('${specifier}');${assertExports}`,
],
]) {
try {
await run(cwd, code);
console.log(`${kind} '${specifier}'`);
} catch (error) {
failed += 1;
console.error(`${kind} '${specifier}'\n${error.stderr || error.message}`);
}
}
}
if (failed) {
console.error(`\n${failed} 个 headless 产物检查失败`);
process.exit(1);
}