@msom/xbuild
Version:
@msom/xbuild
231 lines (214 loc) • 7.48 kB
text/typescript
import { types as babelTypes } from "@babel/core";
import generate from "@babel/generator";
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import { isPromiseLike } from "@msom/common";
import fs from "fs";
import path from "path";
import { RolldownPluginOption, rolldown } from "rolldown";
import { PluginManager } from "../core/plugin";
import { LoadedXbuildConfig, UserConfig, XBuildConfig } from "../core/types";
import { toFileUrl } from "./common";
import { XBuildENV } from "./env";
import { Logger } from "./logger";
function getDefault<T extends object>(d: T): T {
const _default = Reflect.get(d, "default", d);
if (_default) {
return _default as T;
} else {
return d;
}
}
function __decoratorHandle() {
return {
name: "class-decorator-first",
transform(code, id) {
try {
const ast = parse(code, {
sourceType: "module",
plugins: ["decorators-legacy", "typescript"],
});
getDefault(traverse)(ast, {
Program(path: any) {
const programBody = path.node.body;
// 判断是否有装饰器语法,__decorate函数的使用
if (!Array.isArray(programBody)) {
return;
}
const decorators: any[] = [];
let changed = false;
const newBody: any[] = [];
const cleanDecorators = () => {
if (decorators.length) {
newBody.push(...decorators);
decorators.length = 0;
}
};
programBody.forEach((node) => {
if (babelTypes.isExpressionStatement(node)) {
const expression = node.expression;
if (babelTypes.isCallExpression(expression)) {
const callee = expression.callee;
if (
babelTypes.isIdentifier(callee) &&
callee.name === "__decorate"
) {
/* 普通装饰器 __decorate([ _property, __metadata("design:type", Object) ], AAA.prototype, "AAA", void 0); */
decorators.push(node);
changed = true;
return;
}
}
if (babelTypes.isAssignmentExpression(expression)) {
const right = expression.right;
if (babelTypes.isCallExpression(right)) {
const callee = right.callee;
if (
babelTypes.isIdentifier(callee) &&
callee.name === "__decorate"
) {
/* 类装饰器 AAA = __decorate([ _class ], AAA); */
newBody.push(node, ...decorators);
decorators.length = 0;
changed = true;
return;
}
}
}
}
// 当前不是装饰器的声明,表示上一个类的装饰器已经声明完成,检查是否有未添加的装饰器声明
cleanDecorators();
newBody.push(node);
});
// 防止最后一段body是普通装饰器声明
cleanDecorators();
changed && (path.node.body = newBody);
},
});
const output = getDefault(generate)(ast, { retainLines: true }, code);
return {
code: output.code,
map: output.map,
};
} catch (err) {
console.error(`[class-decorator-first] Error processing ${id}:`, err);
return code;
}
},
};
}
const logger = new Logger("Config");
interface LoadConfigOptions {
compile: boolean;
}
export async function loadConfig(
userConfigPath: string | undefined,
options?: LoadConfigOptions
): Promise<LoadedXbuildConfig | undefined> {
const configPath = findConfigFile(userConfigPath);
const finalConfig: LoadedXbuildConfig = {
pluginManager: new PluginManager(),
};
if (!configPath) {
logger.warn("No config file found, using default configuration");
return finalConfig;
}
try {
logger.info(`Loading configuration from ${toFileUrl(configPath)}`);
const tmpConfigPath = ".xbuild.config.mjs";
await (
await rolldown({
input: configPath,
external(id, parentId, isResolved) {
return true;
},
})
).write({
file: tmpConfigPath,
format: "esm",
});
// 动态导入配置文件
const userConfigModule = await import(
toFileUrl(path.resolve(tmpConfigPath))
).finally(() => {
fs.rmSync(path.resolve(tmpConfigPath), { recursive: true });
});
const userConfig: UserConfig = userConfigModule.default || userConfigModule;
// 处理函数式配置
const resolvedConfig: XBuildConfig = await Promise.resolve(
typeof userConfig === "function"
? userConfig({
mode: XBuildENV.env || "production",
})
: userConfig
);
Object.assign(finalConfig, resolvedConfig);
finalConfig.pluginManager.addPlugins(
resolvedConfig.plugins?.filter(({ name }) => {
if (!options?.compile) {
return true;
} else {
return name !== "typescript";
}
})
);
finalConfig.pluginManager.addPlugin(__decoratorHandle());
if (finalConfig.build?.plugins) {
let plugins = finalConfig.build.plugins as Awaited<RolldownPluginOption>;
if (isPromiseLike<Awaited<RolldownPluginOption>>(plugins)) {
plugins = await plugins;
}
const pluginsHandle = (plugins: Awaited<RolldownPluginOption>) => {
if (!plugins) {
return plugins;
} else if (Array.isArray(plugins)) {
return plugins.filter(pluginsHandle);
} else if (typeof plugins === "object" && plugins !== null) {
if (options?.compile) {
return plugins["name"] === "typescript" ? false : plugins;
} else {
return plugins;
}
}
};
finalConfig.build.plugins = pluginsHandle(plugins);
}
return finalConfig;
} catch (error) {
logger.error(`Failed to load config file: ${toFileUrl(configPath)}`, error);
throw error;
}
}
function findConfigFile(userPath?: string): string | null {
const possiblePaths = [
userPath,
"xbuild.config.ts",
"xbuild.config.js",
"xbuild.config.mjs",
"xbuild.config.cjs",
path.join("config", "xbuild.config.ts"),
].filter(Boolean) as string[];
for (const configPath of possiblePaths) {
const absolutePath = path.resolve(process.cwd(), configPath);
if (fs.existsSync(absolutePath)) {
return absolutePath;
}
// 尝试添加扩展名
const withTsExt = absolutePath.endsWith(".ts")
? absolutePath
: `${absolutePath}.ts`;
if (fs.existsSync(withTsExt)) {
return withTsExt;
}
const withJsExt = absolutePath.endsWith(".js")
? absolutePath
: `${absolutePath}.js`;
if (fs.existsSync(withJsExt)) {
return withJsExt;
}
}
return null;
}
export function defineConfig(config: UserConfig): UserConfig {
return config;
}