mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-10-28 13:30:46 +00:00
- 新增 packages/editor/src/sdk,通过 sdk.ts 对外导出 - 将 services 下的核心服务迁移至 sdk/services - 工具函数拆分至 sdk/utils,校验逻辑迁移至 sdk/validate,新增 sdk/wiring - services/sdk-instance.ts 负责在编辑器中实例化 SDK - 补充 SDK 相关单元测试,新增 docs/api/editor/sdk.md 文档
158 lines
5.3 KiB
JavaScript
158 lines
5.3 KiB
JavaScript
/**
|
||
* 校验各包 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'] },
|
||
{ specifier: '@tmagic/editor/sdk', expects: ['createDslSdk', 'DslValidationError'] },
|
||
];
|
||
|
||
/**
|
||
* 找工作区里该包自己的目录当 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}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* sdk 链路必须能在无 DOM 的 Node 里运行:沿着 `dist/es/sdk.js` 的相对 import 走一遍,
|
||
* 确认没有引入编辑器界面、画布、UI 库或 `.vue` 组件。
|
||
* vue 本身不在禁用之列:`@tmagic/form/headless` 只用到它的响应式 API。
|
||
*/
|
||
const SDK_FORBIDDEN = [
|
||
/^@tmagic\/stage/,
|
||
/^@tmagic\/design$/,
|
||
/^@tmagic\/form$/,
|
||
/^@tmagic\/table/,
|
||
/^monaco-editor/,
|
||
/\.vue/,
|
||
];
|
||
|
||
const checkSdkImports = () => {
|
||
const dir = findPackageDir('@tmagic/editor/sdk');
|
||
if (!dir) return;
|
||
|
||
const esDir = path.join(dir, 'dist/es');
|
||
const entry = path.join(esDir, 'sdk.js');
|
||
if (!existsSync(entry)) {
|
||
console.error(`✗ sdk 产物不存在:${entry}`);
|
||
failed += 1;
|
||
return;
|
||
}
|
||
|
||
const seen = new Set();
|
||
const queue = [entry];
|
||
const violations = [];
|
||
const importRe = /(?:import|export)\s[^'"]*?from\s*['"]([^'"]+)['"]|import\s*['"]([^'"]+)['"]/g;
|
||
|
||
while (queue.length) {
|
||
const file = queue.pop();
|
||
if (seen.has(file)) continue;
|
||
seen.add(file);
|
||
|
||
const code = readFileSync(file, 'utf-8');
|
||
for (const match of code.matchAll(importRe)) {
|
||
const specifier = match[1] || match[2];
|
||
if (specifier.startsWith('.')) {
|
||
const resolved = path.resolve(path.dirname(file), specifier);
|
||
if (/\.vue(\.js)?$/.test(resolved)) {
|
||
violations.push(`${path.relative(esDir, file)} -> ${specifier}`);
|
||
} else if (existsSync(resolved) && resolved.endsWith('.js')) {
|
||
queue.push(resolved);
|
||
}
|
||
continue;
|
||
}
|
||
if (SDK_FORBIDDEN.some((re) => re.test(specifier))) {
|
||
violations.push(`${path.relative(esDir, file)} -> ${specifier}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (violations.length) {
|
||
failed += 1;
|
||
console.error(`✗ sdk 产物引入了禁用依赖:\n ${violations.join('\n ')}`);
|
||
} else {
|
||
console.log(`✓ sdk 依赖链干净(共 ${seen.size} 个模块)`);
|
||
}
|
||
};
|
||
|
||
checkSdkImports();
|
||
|
||
if (failed) {
|
||
console.error(`\n${failed} 个 headless 产物检查失败`);
|
||
process.exit(1);
|
||
}
|