/** * 校验各包 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 * 从 `/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); }