1
0
mirror of synced 2026-10-27 21:46:13 +08:00
Files
tmagic-editor/scripts/build.mjs
roymondchen 94fae10ae0 feat(editor): 新增 SDK 层并将核心服务迁移至 sdk 目录
- 新增 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 文档
2026-10-08 19:58:03 +08:00

231 lines
8.2 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.
import fs from 'node:fs';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { build as buildVite } from 'vite';
import vue from '@vitejs/plugin-vue';
import minimist from 'minimist';
import { rimrafSync } from 'rimraf';
import * as sass from 'sass-embedded';
const args = minimist(process.argv.slice(2));
/** 除 index 外的子路径入口(`src/<name>.ts`),存在才构建 */
const subEntries = ['headless', 'sdk'];
const toPascalCase = (str) => str.replace(/(^\w|-\w)/g, (text) => text.replace(/-/, '').toUpperCase());
const dirname = path.dirname(fileURLToPath(import.meta.url));
const packagesDir = path.resolve(dirname, '../packages');
const runtimeDir = path.resolve(dirname, '../runtime');
/**
* 一个包的全部产物。
*
* 同一个包内必须串行:umd 与 headless-umd 都写 `dist/`,并发写会互相覆盖
* (`emptyOutDir: false` 只是不清目录,不解决同时写同名 chunk 的问题)。
* 包之间仍然并发。
*/
async function buildPackage({ packageName, packagesDir, requirePath }) {
rimrafSync(path.resolve(packagesDir, `./${packageName}/dist`));
const pkg = createRequire(import.meta.url)(`${requirePath}/${packageName}/package.json`);
await build({ packageName, format: 'es', pkg, packagesDir });
await build({ packageName, format: 'umd', pkg, packagesDir });
for (const subEntry of subEntries) {
await buildSubEntryUmd({ packageName, pkg, packagesDir, subEntry });
}
buildThemes({ packageName, packagesDir });
}
async function main() {
if (args.package) {
const pkgRoot = path.resolve(packagesDir, args.package);
if (!fs.statSync(pkgRoot).isDirectory()) return;
await buildPackage({ packageName: args.package, packagesDir, requirePath: '../packages' });
return;
}
await Promise.all([
...getPackageNames(packagesDir).map((packageName) =>
buildPackage({ packageName, packagesDir, requirePath: '../packages' }),
),
...getPackageNames(runtimeDir).map((packageName) =>
buildPackage({ packageName, packagesDir: runtimeDir, requirePath: '../runtime' }),
),
]);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
// rolldown 在 UMD 输出顶部会注入
// Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
// 当内联的依赖(如 lodash-es 的 _Symbol.js)声明 `var Symbol = root.Symbol;`
// 时,由于 var hoisting,该局部 `Symbol` 会把上面一行引用到的全局 `Symbol`
// 遮蔽掉(此时局部变量还未赋值),运行时抛出
// TypeError: Cannot read properties of undefined (reading 'toStringTag')
// 这里通过后处理把该引用改为 `globalThis.Symbol.toStringTag`,绕开被 hoist
// 的局部绑定。rolldown 修好前先用此 workaround。
function fixUmdSymbolShadow() {
return {
name: 'tmagic:fix-umd-symbol-shadow',
generateBundle(outputOptions, bundle) {
if (outputOptions.format !== 'umd') return;
for (const file of Object.values(bundle)) {
if (file.type !== 'chunk' || typeof file.code !== 'string') continue;
file.code = file.code.replace(
/Object\.defineProperty\(exports,\s*Symbol\.toStringTag,/g,
'Object.defineProperty(exports, globalThis.Symbol.toStringTag,',
);
}
},
};
}
function buildSubEntryUmd({ packageName, pkg, packagesDir, subEntry }) {
const pkgRoot = path.resolve(packagesDir, `./${packageName}`);
if (!fs.existsSync(path.join(pkgRoot, `src/${subEntry}.ts`))) return;
return build({
packageName,
format: 'umd',
pkg,
packagesDir,
entry: `src/${subEntry}.ts`,
name: `TMagic${toPascalCase(packageName)}${toPascalCase(subEntry)}`,
fileName: `tmagic-${packageName}-${subEntry}`,
cssFileName: `style-${subEntry}`,
});
}
async function build({ packageName, format, pkg, packagesDir, entry, name, fileName, cssFileName = 'style' }) {
const pkgRoot = path.resolve(packagesDir, `./${packageName}`);
const esEntries =
!entry && format === 'es'
? Object.fromEntries(
subEntries
.filter((name) => fs.existsSync(path.join(pkgRoot, `src/${name}.ts`)))
.map((name) => [name, `src/${name}.ts`]),
)
: {};
await buildVite({
root: pkgRoot,
clearScreen: false,
configFile: false,
plugins: [vue()],
build: {
outDir: format === 'es' ? 'dist/es' : 'dist',
emptyOutDir: false,
cssCodeSplit: false,
sourcemap: false,
minify: false,
target: 'esnext',
lib: {
entry: entry ?? (Object.keys(esEntries).length ? { index: 'src/index.ts', ...esEntries } : 'src/index.ts'),
name: name ?? `TMagic${toPascalCase(packageName)}`,
fileName: fileName ?? `tmagic-${packageName}`,
formats: [format],
cssFileName,
},
rolldownOptions: {
plugins: [fixUmdSymbolShadow()],
// 确保外部化处理那些你不想打包进库的依赖
external(id) {
if (format === 'umd' && id === 'lodash-es') {
return false;
}
return Object.keys({
...pkg.dependencies,
...pkg.peerDependencies,
}).some((k) => new RegExp(`^${k}`).test(id));
},
output: {
// 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
globals: {
vue: 'Vue',
'element-plus': 'ElementPlus',
},
// ES 格式保留模块结构,让消费者的 bundler 按模块粒度 tree-shake
...(format === 'es'
? {
preserveModules: true,
preserveModulesRoot: 'src',
entryFileNames: '[name].js',
}
: {}),
},
},
},
resolve: {
alias: [
{ find: /^@data-source/, replacement: path.join(packagesDir, '/data-source/src') },
{ find: /^@editor/, replacement: path.join(packagesDir, './editor/src') },
{ find: /^@form/, replacement: path.join(packagesDir, './form/src') },
],
},
});
}
/**
* 扫描 `<package>/src/theme/themes/<theme>/index.scss`,
* 为每个主题单独编译输出 `<package>/dist/themes/<theme>.css`。
*
* 主题样式独立于主样式包(dist/style.css),消费者通过
* `import '@tmagic/<pkg>/dist/themes/<theme>.css'` 按需引入,
* 并在 `<MEditor :theme="..." />` / `<MForm :theme="..." />`
* 上挂载对应的 `m-editor--<theme>` / `m-form--<theme>` 修饰类后生效。
*/
function buildThemes({ packageName, packagesDir }) {
const themesDir = path.resolve(packagesDir, `./${packageName}/src/theme/themes`);
if (!fs.existsSync(themesDir)) return;
const themes = fs.readdirSync(themesDir).filter((name) => {
const themePath = path.resolve(themesDir, name);
return fs.statSync(themePath).isDirectory() && fs.existsSync(path.resolve(themePath, 'index.scss'));
});
if (themes.length === 0) return;
const outputDir = path.resolve(packagesDir, `./${packageName}/dist/themes`);
fs.mkdirSync(outputDir, { recursive: true });
// 把 `<pkg>/node_modules` 也加到 loadPaths,使主题 SCSS 内
// `@use "@tmagic/<pkg>/src/.../index.scss"` 能通过 pnpm 在 node_modules
// 下的 symlink 解析到对应的源码包。
const nodeModulesDir = path.resolve(packagesDir, `./${packageName}/node_modules`);
const loadPaths = [path.resolve(packagesDir, `./${packageName}/src`)];
if (fs.existsSync(nodeModulesDir)) loadPaths.push(nodeModulesDir);
for (const theme of themes) {
const input = path.resolve(themesDir, theme, 'index.scss');
const result = sass.compile(input, {
style: 'expanded',
sourceMap: false,
loadPaths: [path.resolve(themesDir, theme), ...loadPaths],
});
fs.writeFileSync(path.resolve(outputDir, `${theme}.css`), result.css);
console.log(`[themes] ${packageName}: ${theme}.css`);
}
}
function getPackageNames(packagesDir) {
return fs.readdirSync(packagesDir).filter((p) => {
const pkgRoot = path.resolve(packagesDir, p);
if (fs.statSync(pkgRoot).isDirectory()) {
const pkg = JSON.parse(fs.readFileSync(path.resolve(pkgRoot, 'package.json'), 'utf-8'));
return !pkg.private && !pkg.bin;
}
return false;
});
}