import tcolor from "@suseejs/color";
import type { SuseePlugin, SuseePluginFunction } from "@suseejs/type";
import ts from "typescript";
export type OutputFormat = ("commonjs" | "esm")[];
export interface EntryPoint {
/**
* Entry of file path of package
*
* required
*/
entry: string;
/**
* Info for output
*
* required
*/
/**
* path for package
*
* required
*/
exportPath: "." | `./${string}`;
/**
* Output module type of package
*
* default - [esm]
*/
format?: OutputFormat;
/**
* Custom tsconfig.json path for package typescript compiler options
*
* Priority -
* 1. this custom tsconfig.json
* 2. tsconfig.json at root directory
* 3. default compiler options of susee
*
* default - undefined
*/
tsconfigFilePath?: string | undefined;
/**
* When bundling , if there are duplicate declared names , susee will auto rename , if renameDuplicates = false exist with code 1.
*
* default - true
*/
renameDuplicates?: boolean;
/**
* Array of susee plugins
*
* default - []
*/
plugins?: (SuseePlugin | SuseePluginFunction)[];
/**
* When generating a dependency graph, Susee checks whether referenced npm modules are installed.
* If a module is not installed in your project, Susee emits a warning message.
* If this option is `true`, Susee treats those warnings as fatal and exits with code 1.
*
* default - false
*/
warning?: boolean;
}
/**
* Configuration for Susee Bundler
*/
export interface SuSeeConfig {
/**
* Array of entry points object
*
* required
*/
entryPoints: EntryPoint[];
/**
* Out directory
*
* default - dist
*/
outDir?: string;
/**
* Allow bundler to update your package.json.
*
* default - false
*/
allowUpdatePackageJson?: boolean;
}
/**
* Finds the path of the susee.config file if it exists.
* It checks for the existence of "susee.config.ts", "susee.config.js", and "susee.config.mjs" in the current working directory.
* The first file found is returned.
* @returns {string | undefined} - path to the susee.config file or undefined if it does not exist.
*/
const getConfigPath = (): string | undefined => {
const fileNames = ["susee.config.ts", "susee.config.js", "susee.config.mjs"];
let configFile: string | undefined;
for (const file of fileNames) {
const _file = ts.sys.resolvePath(file);
if (ts.sys.fileExists(_file)) {
configFile = _file;
break;
}
}
return configFile;
};
/**
* Checks if the given entries have at least one entry and if there are any duplicate export paths.
* If there are no entries, it will exit with code 1 and print an error message.
* If there are any duplicate export paths, it will exit with code 1 and print an error message.
* It will also check if each entry file exists, if not, it will exit with code 1 and print an error message.
* @param {EntryPoint[]} entries - array of entry points
*/
function checkEntries(entries: EntryPoint[]) {
if (entries.length < 1) {
console.error(
tcolor.magenta(
`No entry found in susee.config file or build options, at least one entry required`,
),
);
ts.sys.exit(1);
}
const objectStore: Record<string, boolean> = {};
const duplicateExportPaths: string[] = [];
for (const obj of entries) {
const value = obj.exportPath;
if (objectStore[value]) {
duplicateExportPaths.push(`"${value}"`);
} else {
objectStore[value] = true;
}
}
if (duplicateExportPaths.length > 0) {
console.error(
tcolor.magenta(
`Duplicate export paths/path (${duplicateExportPaths.join(",")}) found in your susee.config file or build options , that will error for bundled output`,
),
);
ts.sys.exit(1);
}
for (const obj of entries) {
if (!ts.sys.fileExists(ts.sys.resolvePath(obj.entry))) {
console.error(tcolor.magenta(`Entry file ${obj.entry} dose not exists.`));
ts.sys.exit(1);
}
}
}
export type BuildEntryPoint = {
entry: string;
exportPath: "." | `./${string}`;
format: OutputFormat;
tsconfigFilePath: string | undefined;
rename: boolean;
plugins: (SuseePlugin | SuseePluginFunction)[];
outputDirectoryPath: string;
warning: boolean;
};
export type BuildOptions = {
buildEntryPoints: BuildEntryPoint[];
updatePackage: boolean;
outDir: string;
};
/**
* Generates normalized build options from the user config.
* It validates entry points, applies default values, removes duplicate formats,
* and resolves the output directory for each export path.
* @param {SuSeeConfig} config - raw susee configuration object.
* @returns {BuildOptions} normalized build options for the compiler.
*/
function generateBuildOptions(config: SuSeeConfig) {
const outDir = config.outDir ?? "dist";
const points: BuildEntryPoint[] = [];
checkEntries(config.entryPoints);
for (const ent of config.entryPoints) {
const entry = ent.entry;
const exportPath = ent.exportPath;
const tsconfigFilePath = ent.tsconfigFilePath ?? undefined;
const format: OutputFormat = ent.format
? [...new Set(ent.format)]
: ["esm"];
const warning = ent.warning ?? false;
const rename = ent.renameDuplicates ?? true;
const plugins = ent.plugins ?? [];
const outputDirectoryPath =
ent.exportPath === "." ? outDir : `${outDir}${ent.exportPath.slice(1)}`;
points.push({
entry,
exportPath,
format,
tsconfigFilePath,
rename,
plugins,
warning,
outputDirectoryPath,
});
}
return {
buildEntryPoints: points,
updatePackage: config.allowUpdatePackageJson ?? false,
outDir,
} as BuildOptions;
}
/**
* Loads the susee config file from the current working directory and converts it into build options.
* If no supported config file is found, it returns `undefined`.
* @returns {Promise<BuildOptions | undefined>} normalized build options or undefined when no config file exists.
*/
async function finalSuseeConfig(): Promise<BuildOptions | undefined> {
const configPath = getConfigPath();
if (configPath) {
const _default: { default: SuSeeConfig } = await import(
configPath as string
);
const config = _default.default;
return generateBuildOptions(config);
}
}
export { finalSuseeConfig, generateBuildOptions };