@mongez/pkgist 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +4 -0
- package/.prettierrc +8 -0
- package/README.md +320 -0
- package/builder.ts +183 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1125 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +202 -0
- package/dist/index.js +824 -0
- package/dist/index.js.map +1 -0
- package/eslint.config.js +98 -0
- package/package.json +49 -0
- package/src/build/family-builder.ts +76 -0
- package/src/build/index.ts +4 -0
- package/src/build/package-builder.ts +155 -0
- package/src/build/parallel-builder.ts +36 -0
- package/src/cli.ts +51 -0
- package/src/commands/build-all.ts +88 -0
- package/src/commands/build-family.ts +55 -0
- package/src/commands/build.ts +84 -0
- package/src/commands/index.ts +5 -0
- package/src/commands/list.ts +74 -0
- package/src/commands/validate.ts +125 -0
- package/src/compile/index.ts +1 -0
- package/src/compile/tsdown-compiler.ts +344 -0
- package/src/config/define-config.ts +9 -0
- package/src/config/index.ts +3 -0
- package/src/config/load-config.ts +103 -0
- package/src/files/clone-files.ts +45 -0
- package/src/files/file-manager.ts +109 -0
- package/src/files/index.ts +3 -0
- package/src/files/package-json.ts +191 -0
- package/src/git/index.ts +1 -0
- package/src/git/operations.ts +87 -0
- package/src/index.ts +26 -0
- package/src/publish/index.ts +1 -0
- package/src/publish/npm-publisher.ts +36 -0
- package/src/types/config.ts +33 -0
- package/src/types/index.ts +2 -0
- package/src/types/package.ts +81 -0
- package/src/utils/errors.ts +40 -0
- package/src/utils/exec.ts +58 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/logger.ts +44 -0
- package/src/utils/paths.ts +48 -0
- package/src/version/increment.ts +51 -0
- package/src/version/index.ts +1 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +34 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { build } from "tsdown";
|
|
4
|
+
import type { InputOptions } from "rolldown";
|
|
5
|
+
import type { PackageBase } from "../types/index.js";
|
|
6
|
+
import { joinPath, toForwardSlash, resolvePath } from "../utils/paths.js";
|
|
7
|
+
import { ensureDir, moveFile, listFiles, pathExists } from "../files/file-manager.js";
|
|
8
|
+
import { wrapError } from "../utils/errors.js";
|
|
9
|
+
import { logger } from "../utils/logger.js";
|
|
10
|
+
|
|
11
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Collect all dependency names that should be treated as external.
|
|
15
|
+
* Both `dependencies` and `peerDependencies` must never be bundled.
|
|
16
|
+
*/
|
|
17
|
+
function collectExternals(sourceJson: Record<string, unknown>): string[] {
|
|
18
|
+
const deps = Object.keys((sourceJson["dependencies"] as Record<string, string> | undefined) ?? {});
|
|
19
|
+
const peers = Object.keys((sourceJson["peerDependencies"] as Record<string, string> | undefined) ?? {});
|
|
20
|
+
return [...new Set([...deps, ...peers])];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve entry file paths to absolute forward-slash paths.
|
|
25
|
+
*/
|
|
26
|
+
function resolveEntries(pkg: PackageBase, packageRoot: string): string[] {
|
|
27
|
+
const srcDir = pkg.srcDir ?? "src";
|
|
28
|
+
const rawEntries = pkg.entries ?? ["index.ts"];
|
|
29
|
+
const entryList = Array.isArray(rawEntries) ? rawEntries : [rawEntries];
|
|
30
|
+
return entryList.map((e) => toForwardSlash(resolvePath(packageRoot, srcDir, e)));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Return the path to the tsconfig.json inside the package root, if it exists.
|
|
35
|
+
*/
|
|
36
|
+
function findTsconfig(packageRoot: string): string | undefined {
|
|
37
|
+
const local = joinPath(packageRoot, "tsconfig.json");
|
|
38
|
+
return pathExists(local) ? local : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Main compiler ──────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compile a single package using tsdown.
|
|
45
|
+
*
|
|
46
|
+
* When `preserveModules` is on (the default), we run two separate tsdown builds
|
|
47
|
+
* in parallel so that each format's output options are fully independent:
|
|
48
|
+
*
|
|
49
|
+
* ESM build — preserveModules: true, dts: true → _tmp_esm/ → esm/*.mjs + *.d.mts
|
|
50
|
+
* CJS build — preserveModules: false, dts: false → _tmp_cjs/ → cjs/*.cjs
|
|
51
|
+
*
|
|
52
|
+
* CJS skips DTS because rolldown has a bug in CJS preserveModules mode where
|
|
53
|
+
* `export { default as X }` assigns the whole module object to exports.X instead
|
|
54
|
+
* of unwrapping .default. Bundled CJS has no such issue. Types are shared from
|
|
55
|
+
* the ESM declarations (TypeScript resolves them correctly via the exports map).
|
|
56
|
+
*
|
|
57
|
+
* When `preserveModules` is false, a single combined build is used.
|
|
58
|
+
*/
|
|
59
|
+
export async function compilePackage(
|
|
60
|
+
pkg: PackageBase,
|
|
61
|
+
packageRoot: string,
|
|
62
|
+
buildPath: string,
|
|
63
|
+
sourceJson: Record<string, unknown>,
|
|
64
|
+
dryRun: boolean,
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
if (dryRun) {
|
|
67
|
+
logger.info(`[dry-run] compile ${pkg.name} → ${buildPath}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const entries = resolveEntries(pkg, packageRoot);
|
|
72
|
+
const formats = pkg.formats ?? ["esm", "cjs"];
|
|
73
|
+
const externals = collectExternals(sourceJson);
|
|
74
|
+
const tsconfig = findTsconfig(packageRoot);
|
|
75
|
+
const shouldPreserveModules = pkg.preserveModules !== false;
|
|
76
|
+
const srcDir = toForwardSlash(resolvePath(packageRoot, pkg.srcDir ?? "src"));
|
|
77
|
+
|
|
78
|
+
logger.step(`Compiling ${pkg.name} (${formats.join(", ")}) → ${buildPath}`);
|
|
79
|
+
logger.debug(` entries: ${entries.join(", ")}`);
|
|
80
|
+
logger.debug(` externals: ${externals.slice(0, 8).join(", ")}${externals.length > 8 ? "…" : ""}`);
|
|
81
|
+
|
|
82
|
+
// Wipe any previous esm/ and cjs/ output so re-runs of the same version don't
|
|
83
|
+
// leave stale files from a different build mode.
|
|
84
|
+
for (const subDir of ["esm", "cjs"]) {
|
|
85
|
+
const dir = resolvePath(buildPath, subDir);
|
|
86
|
+
if (fs.existsSync(dir)) {
|
|
87
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Build inputOptions for rolldown — used to set the JSX transform for React packages.
|
|
92
|
+
const rolldownInputOptions: InputOptions | undefined =
|
|
93
|
+
pkg.type === "react"
|
|
94
|
+
? {
|
|
95
|
+
transform: {
|
|
96
|
+
jsx: "react-jsx", // automatic runtime — works with React 17+
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
: undefined;
|
|
100
|
+
|
|
101
|
+
// Common options shared across all tsdown invocations.
|
|
102
|
+
const sharedOptions = {
|
|
103
|
+
entry: entries,
|
|
104
|
+
deps: { neverBundle: externals },
|
|
105
|
+
sourcemap: pkg.sourcemap !== false,
|
|
106
|
+
clean: true,
|
|
107
|
+
minify: pkg.minify ?? false,
|
|
108
|
+
tsconfig: tsconfig ?? true,
|
|
109
|
+
...(rolldownInputOptions ? { inputOptions: rolldownInputOptions } : {}),
|
|
110
|
+
} as const;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
if (shouldPreserveModules && formats.includes("esm") && formats.includes("cjs")) {
|
|
114
|
+
// Two parallel builds: ESM with preserveModules + DTS, CJS bundled without DTS.
|
|
115
|
+
const tmpEsm = toForwardSlash(resolvePath(buildPath, "_tmp_esm"));
|
|
116
|
+
const tmpCjs = toForwardSlash(resolvePath(buildPath, "_tmp_cjs"));
|
|
117
|
+
ensureDir(tmpEsm);
|
|
118
|
+
ensureDir(tmpCjs);
|
|
119
|
+
|
|
120
|
+
await Promise.all([
|
|
121
|
+
build({
|
|
122
|
+
...sharedOptions,
|
|
123
|
+
format: ["esm"],
|
|
124
|
+
dts: pkg.dts !== false,
|
|
125
|
+
outDir: tmpEsm,
|
|
126
|
+
outputOptions: {
|
|
127
|
+
preserveModules: true,
|
|
128
|
+
preserveModulesRoot: srcDir,
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
build({
|
|
132
|
+
...sharedOptions,
|
|
133
|
+
format: ["cjs"],
|
|
134
|
+
// CJS skips DTS — types are served from ESM declarations (see package-json.ts).
|
|
135
|
+
dts: false,
|
|
136
|
+
outDir: tmpCjs,
|
|
137
|
+
}),
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
reorganiseOutput(tmpEsm, buildPath, ["esm"], pkg, true);
|
|
141
|
+
// keepNativeExtension=true: CJS files come out as .cjs from tsdown; we keep that
|
|
142
|
+
// extension so the exports map (./cjs/index.cjs) resolves correctly.
|
|
143
|
+
reorganiseOutput(tmpCjs, buildPath, ["cjs"], pkg, false, true);
|
|
144
|
+
rmTmp(tmpEsm);
|
|
145
|
+
rmTmp(tmpCjs);
|
|
146
|
+
} else {
|
|
147
|
+
// Single build: either one format only, or preserveModules disabled.
|
|
148
|
+
const tmpDir = toForwardSlash(resolvePath(buildPath, "_tmp"));
|
|
149
|
+
ensureDir(tmpDir);
|
|
150
|
+
|
|
151
|
+
await build({
|
|
152
|
+
...sharedOptions,
|
|
153
|
+
format: formats as ("esm" | "cjs")[],
|
|
154
|
+
dts: pkg.dts !== false,
|
|
155
|
+
outDir: tmpDir,
|
|
156
|
+
...(shouldPreserveModules && formats.includes("esm")
|
|
157
|
+
? { outputOptions: { preserveModules: true, preserveModulesRoot: srcDir } }
|
|
158
|
+
: {}),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
reorganiseOutput(tmpDir, buildPath, formats as ("esm" | "cjs")[], pkg, shouldPreserveModules);
|
|
162
|
+
rmTmp(tmpDir);
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
throw wrapError("tsdown-build", pkg.name, err);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
logger.success(`Compiled ${pkg.name}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function rmTmp(dir: string): void {
|
|
172
|
+
try {
|
|
173
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
174
|
+
} catch {
|
|
175
|
+
// non-fatal — stale tmp is harmless
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── Output reorganisation ──────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Move the flat tsdown output (in tmpDir) into `esm/` and `cjs/` subdirectories
|
|
183
|
+
* under buildPath. Renames .mjs → .js and .cjs → .js so that relative imports
|
|
184
|
+
* inside each format directory resolve correctly.
|
|
185
|
+
*/
|
|
186
|
+
function reorganiseOutput(
|
|
187
|
+
tmpDir: string,
|
|
188
|
+
buildPath: string,
|
|
189
|
+
formats: ("esm" | "cjs")[],
|
|
190
|
+
pkg: PackageBase,
|
|
191
|
+
preserveModules: boolean,
|
|
192
|
+
keepNativeExtension = false,
|
|
193
|
+
): void {
|
|
194
|
+
const esmDir = joinPath(buildPath, "esm");
|
|
195
|
+
const cjsDir = joinPath(buildPath, "cjs");
|
|
196
|
+
|
|
197
|
+
if (formats.includes("esm")) ensureDir(esmDir);
|
|
198
|
+
if (formats.includes("cjs")) ensureDir(cjsDir);
|
|
199
|
+
|
|
200
|
+
const mainType = pkg.mainType ?? (formats.includes("cjs") ? "cjs" : "esm");
|
|
201
|
+
|
|
202
|
+
processDirectory(tmpDir, { esmDir, cjsDir, formats, mainType, pkg, tmpDir, preserveModules, keepNativeExtension });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface ReorgContext {
|
|
206
|
+
esmDir: string;
|
|
207
|
+
cjsDir: string;
|
|
208
|
+
formats: ("esm" | "cjs")[];
|
|
209
|
+
mainType: "esm" | "cjs";
|
|
210
|
+
pkg: PackageBase;
|
|
211
|
+
/** Absolute path to the flat _tmp/ directory — used to compute relative paths when preserveModules is on. */
|
|
212
|
+
tmpDir: string;
|
|
213
|
+
preserveModules: boolean;
|
|
214
|
+
/**
|
|
215
|
+
* When true, keep the native .mjs/.cjs extensions instead of normalising to .js.
|
|
216
|
+
* Used in the CJS-only leg of the two-build path so the final file stays .cjs.
|
|
217
|
+
*/
|
|
218
|
+
keepNativeExtension: boolean;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function processDirectory(dir: string, ctx: ReorgContext): void {
|
|
222
|
+
if (!fs.existsSync(dir)) return;
|
|
223
|
+
|
|
224
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
const fullPath = joinPath(dir, entry.name);
|
|
227
|
+
if (entry.isDirectory()) {
|
|
228
|
+
// Recursively handle subdirectories (chunk splits, etc.)
|
|
229
|
+
processDirectory(fullPath, ctx);
|
|
230
|
+
} else {
|
|
231
|
+
processFile(fullPath, entry.name, ctx);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function processFile(filePath: string, name: string, ctx: ReorgContext): void {
|
|
237
|
+
const { esmDir, cjsDir, formats, mainType, pkg, tmpDir, preserveModules } = ctx;
|
|
238
|
+
|
|
239
|
+
// Relative subdirectory from the tmp root — preserves folder structure for both
|
|
240
|
+
// preserveModules output and multi-entry single-bundle output (where each entry
|
|
241
|
+
// lands in its own subdirectory, e.g. cli/index.cjs).
|
|
242
|
+
// path.relative may return backslashes on Windows — normalise to forward slashes.
|
|
243
|
+
const relDir = toForwardSlash(path.relative(tmpDir, path.dirname(filePath)));
|
|
244
|
+
|
|
245
|
+
if (preserveModules) {
|
|
246
|
+
if (isEsmFile(name)) {
|
|
247
|
+
if (formats.includes("esm")) {
|
|
248
|
+
moveFile(filePath, joinPath(esmDir, relDir, name), pkg.name);
|
|
249
|
+
}
|
|
250
|
+
} else if (isCjsFile(name)) {
|
|
251
|
+
if (formats.includes("cjs")) {
|
|
252
|
+
moveFile(filePath, joinPath(cjsDir, relDir, name), pkg.name);
|
|
253
|
+
}
|
|
254
|
+
} else if (isDtsFile(name)) {
|
|
255
|
+
const targetDir = formats.includes("esm") ? esmDir : cjsDir;
|
|
256
|
+
moveFile(filePath, joinPath(targetDir, relDir, name), pkg.name);
|
|
257
|
+
}
|
|
258
|
+
// Unknown extensions silently ignored.
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Single-bundle mode ──────────────────────────────────────────────────────
|
|
263
|
+
const { keepNativeExtension } = ctx;
|
|
264
|
+
|
|
265
|
+
if (isEsmFile(name)) {
|
|
266
|
+
if (formats.includes("esm")) {
|
|
267
|
+
const destName = keepNativeExtension ? name : normaliseEsmName(name);
|
|
268
|
+
moveFile(filePath, joinPath(esmDir, relDir, destName), pkg.name);
|
|
269
|
+
}
|
|
270
|
+
} else if (isCjsFile(name)) {
|
|
271
|
+
if (formats.includes("cjs")) {
|
|
272
|
+
const destName = keepNativeExtension ? name : normaliseCjsName(name);
|
|
273
|
+
moveFile(filePath, joinPath(cjsDir, relDir, destName), pkg.name);
|
|
274
|
+
}
|
|
275
|
+
} else if (isDtsFile(name)) {
|
|
276
|
+
const targetDir = formats.includes("esm") ? esmDir : cjsDir;
|
|
277
|
+
moveFile(filePath, joinPath(targetDir, relDir, name), pkg.name);
|
|
278
|
+
} else if (isJsFile(name)) {
|
|
279
|
+
// Plain .js / .js.map — emitted when the format array contains only one format
|
|
280
|
+
// (tsdown uses .js instead of .mjs/.cjs when there's no other format to differentiate from).
|
|
281
|
+
const targetDir = mainType === "esm" ? esmDir : cjsDir;
|
|
282
|
+
if (formats.includes(mainType)) {
|
|
283
|
+
// Files landing in cjsDir should use the .cjs extension for consistency.
|
|
284
|
+
const destName =
|
|
285
|
+
targetDir === cjsDir
|
|
286
|
+
? name.replace(/\.js\.map$/, ".cjs.map").replace(/\.js$/, ".cjs")
|
|
287
|
+
: name;
|
|
288
|
+
moveFile(filePath, joinPath(targetDir, relDir, destName), pkg.name);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Unknown extensions are silently ignored.
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ─── Extension predicates ───────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
function isEsmFile(name: string): boolean {
|
|
297
|
+
return (
|
|
298
|
+
name.endsWith(".mjs") ||
|
|
299
|
+
name.endsWith(".mjs.map") ||
|
|
300
|
+
name.endsWith(".d.mts") ||
|
|
301
|
+
name.endsWith(".d.mts.map")
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function isCjsFile(name: string): boolean {
|
|
306
|
+
return (
|
|
307
|
+
name.endsWith(".cjs") ||
|
|
308
|
+
name.endsWith(".cjs.map") ||
|
|
309
|
+
name.endsWith(".d.cts") ||
|
|
310
|
+
name.endsWith(".d.cts.map")
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function isDtsFile(name: string): boolean {
|
|
315
|
+
return name.endsWith(".d.ts") || name.endsWith(".d.ts.map");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function isJsFile(name: string): boolean {
|
|
319
|
+
return (
|
|
320
|
+
(name.endsWith(".js") || name.endsWith(".js.map")) &&
|
|
321
|
+
!name.endsWith(".d.ts") &&
|
|
322
|
+
!name.endsWith(".d.ts.map")
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ─── Name normalisation ─────────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
/** Rename .mjs / .d.mts → .js / .d.ts for the esm/ directory. */
|
|
329
|
+
function normaliseEsmName(name: string): string {
|
|
330
|
+
return name
|
|
331
|
+
.replace(/\.mjs\.map$/, ".js.map")
|
|
332
|
+
.replace(/\.mjs$/, ".js")
|
|
333
|
+
.replace(/\.d\.mts\.map$/, ".d.ts.map")
|
|
334
|
+
.replace(/\.d\.mts$/, ".d.ts");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Rename .cjs / .d.cts → .js / .d.ts for the cjs/ directory. */
|
|
338
|
+
function normaliseCjsName(name: string): string {
|
|
339
|
+
return name
|
|
340
|
+
.replace(/\.cjs\.map$/, ".js.map")
|
|
341
|
+
.replace(/\.cjs$/, ".js")
|
|
342
|
+
.replace(/\.d\.cts\.map$/, ".d.ts.map")
|
|
343
|
+
.replace(/\.d\.cts$/, ".d.ts");
|
|
344
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BundlerConfig } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type-safe config factory.
|
|
5
|
+
* Simply returns the config object unchanged — used purely for IDE autocomplete and type checking.
|
|
6
|
+
*/
|
|
7
|
+
export function defineConfig(config: BundlerConfig): BundlerConfig {
|
|
8
|
+
return config;
|
|
9
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import type { BundlerConfig } from "../types/index.js";
|
|
6
|
+
import { resolvePath } from "../utils/paths.js";
|
|
7
|
+
import { wrapError } from "../utils/errors.js";
|
|
8
|
+
|
|
9
|
+
export interface LoadedConfig {
|
|
10
|
+
config: BundlerConfig;
|
|
11
|
+
/** Absolute, forward-slash normalised path to the config file */
|
|
12
|
+
configPath: string;
|
|
13
|
+
/** Directory containing the config file — used to resolve relative roots */
|
|
14
|
+
configDir: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load and validate a builder config file.
|
|
19
|
+
*
|
|
20
|
+
* The file must be a TypeScript or JavaScript module with a default export
|
|
21
|
+
* produced by `defineConfig(...)`.
|
|
22
|
+
*
|
|
23
|
+
* When the file is TypeScript (`.ts`), we use `tsx` via Node's `--import` flag
|
|
24
|
+
* (which is already set up when the CLI is invoked via the compiled entry), or
|
|
25
|
+
* we fall back to requiring `tsx/esm` at runtime if available.
|
|
26
|
+
*/
|
|
27
|
+
export async function loadConfig(configFilePath: string): Promise<LoadedConfig> {
|
|
28
|
+
const absolute = resolvePath(configFilePath);
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(absolute)) {
|
|
31
|
+
throw new Error(`Config file not found: ${absolute}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let mod: { default?: BundlerConfig };
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// Convert to a file:// URL so dynamic import works correctly on Windows.
|
|
38
|
+
const fileUrl = pathToFileURL(absolute).href;
|
|
39
|
+
mod = await import(fileUrl);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw wrapError("load-config", absolute, err);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const config = mod.default;
|
|
45
|
+
|
|
46
|
+
if (!config || typeof config !== "object") {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Config file ${absolute} must export a default object from defineConfig(). Got: ${typeof config}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
validateConfig(config, absolute);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
config,
|
|
56
|
+
configPath: absolute,
|
|
57
|
+
configDir: resolvePath(path.dirname(absolute)),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validateConfig(config: BundlerConfig, filePath: string): void {
|
|
62
|
+
if (!config.settings) {
|
|
63
|
+
throw new Error(`Config "${filePath}" must have a "settings" object.`);
|
|
64
|
+
}
|
|
65
|
+
if (!config.settings.buildDir) {
|
|
66
|
+
throw new Error(`Config "${filePath}" settings.buildDir is required.`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const names = new Set<string>();
|
|
70
|
+
|
|
71
|
+
for (const pkg of config.standalone ?? []) {
|
|
72
|
+
if (!pkg.name) throw new Error(`Standalone package is missing "name" in ${filePath}`);
|
|
73
|
+
if (!pkg.root) throw new Error(`Standalone package "${pkg.name}" is missing "root"`);
|
|
74
|
+
if (names.has(pkg.name)) throw new Error(`Duplicate package name: "${pkg.name}"`);
|
|
75
|
+
names.add(pkg.name);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const family of config.families ?? []) {
|
|
79
|
+
if (!family.name) throw new Error(`A family is missing "name" in ${filePath}`);
|
|
80
|
+
for (const pkg of family.packages) {
|
|
81
|
+
if (!pkg.name) throw new Error(`Package inside family "${family.name}" is missing "name"`);
|
|
82
|
+
if (!pkg.root) throw new Error(`Package "${pkg.name}" in family "${family.name}" is missing "root"`);
|
|
83
|
+
if (names.has(pkg.name)) throw new Error(`Duplicate package name: "${pkg.name}"`);
|
|
84
|
+
names.add(pkg.name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Attempt to find the default config file in the working directory.
|
|
91
|
+
* Tries: builder.ts, builder.js, mongez.ts, mongez.js
|
|
92
|
+
*/
|
|
93
|
+
export function findDefaultConfigPath(cwd: string): string {
|
|
94
|
+
const candidates = ["builder.ts", "builder.js", "mongez.ts", "mongez.js"];
|
|
95
|
+
for (const candidate of candidates) {
|
|
96
|
+
const full = path.join(cwd, candidate);
|
|
97
|
+
if (fs.existsSync(full)) return full;
|
|
98
|
+
}
|
|
99
|
+
throw new Error(
|
|
100
|
+
`No config file found in ${cwd}. Tried: ${candidates.join(", ")}. ` +
|
|
101
|
+
`Use --config <path> to specify a custom location.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { copyFile, copyDir, pathExists } from "./file-manager.js";
|
|
3
|
+
import { joinPath } from "../utils/paths.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Clone extra files (README, license, etc.) from the package root into the build directory.
|
|
8
|
+
*
|
|
9
|
+
* Each entry in the `clone` array can be:
|
|
10
|
+
* - a plain string: copied from `<packageRoot>/<str>` to `<buildDir>/<str>`
|
|
11
|
+
* - a tuple [src, dest]: copied from `<packageRoot>/<src>` to `<buildDir>/<dest>`
|
|
12
|
+
*/
|
|
13
|
+
export function cloneFiles(
|
|
14
|
+
packageRoot: string,
|
|
15
|
+
buildDir: string,
|
|
16
|
+
cloneList: (string | [string, string])[],
|
|
17
|
+
packageName: string,
|
|
18
|
+
dryRun: boolean,
|
|
19
|
+
): void {
|
|
20
|
+
for (const entry of cloneList) {
|
|
21
|
+
const [srcRel, destRel] =
|
|
22
|
+
typeof entry === "string" ? [entry, entry] : entry;
|
|
23
|
+
|
|
24
|
+
const src = joinPath(packageRoot, srcRel);
|
|
25
|
+
const dest = joinPath(buildDir, destRel);
|
|
26
|
+
|
|
27
|
+
if (!pathExists(src)) {
|
|
28
|
+
logger.warn(`[clone-files] ${packageName}: source file not found, skipping — ${src}`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (dryRun) {
|
|
33
|
+
logger.info(`[dry-run] clone ${src} → ${dest}`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const stat = fs.statSync(src);
|
|
38
|
+
if (stat.isDirectory()) {
|
|
39
|
+
copyDir(src, dest);
|
|
40
|
+
} else {
|
|
41
|
+
copyFile(src, dest);
|
|
42
|
+
}
|
|
43
|
+
logger.debug(`Cloned ${src} → ${dest}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { resolvePath, joinPath } from "../utils/paths.js";
|
|
4
|
+
import { wrapError } from "../utils/errors.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Ensure a directory exists, creating all intermediate directories as needed.
|
|
8
|
+
*/
|
|
9
|
+
export function ensureDir(dir: string): void {
|
|
10
|
+
try {
|
|
11
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
12
|
+
} catch (err) {
|
|
13
|
+
throw wrapError("ensure-dir", dir, err);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Copy a single file from src to dest.
|
|
19
|
+
* Creates the destination directory if it does not exist.
|
|
20
|
+
*/
|
|
21
|
+
export function copyFile(src: string, dest: string): void {
|
|
22
|
+
ensureDir(path.dirname(dest));
|
|
23
|
+
try {
|
|
24
|
+
fs.copyFileSync(src, dest);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
throw wrapError("copy-file", src, err);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Directories that are never copied in any directory tree operation. */
|
|
31
|
+
const COPY_EXCLUDES = new Set([".git", "node_modules", "dist", ".turbo", ".cache"]);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Copy an entire directory tree recursively, skipping .git and node_modules.
|
|
35
|
+
*/
|
|
36
|
+
export function copyDir(src: string, dest: string): void {
|
|
37
|
+
ensureDir(dest);
|
|
38
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (COPY_EXCLUDES.has(entry.name)) continue;
|
|
41
|
+
const srcPath = joinPath(src, entry.name);
|
|
42
|
+
const destPath = joinPath(dest, entry.name);
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
copyDir(srcPath, destPath);
|
|
45
|
+
} else {
|
|
46
|
+
copyFile(srcPath, destPath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read a file as a UTF-8 string, or throw a BundlerError with context.
|
|
53
|
+
*/
|
|
54
|
+
export function readFile(filePath: string, packageName = filePath): string {
|
|
55
|
+
try {
|
|
56
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
57
|
+
} catch (err) {
|
|
58
|
+
throw wrapError("read-file", packageName, err);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Write a UTF-8 string to a file, creating intermediate directories.
|
|
64
|
+
*/
|
|
65
|
+
export function writeFile(filePath: string, content: string, packageName = filePath): void {
|
|
66
|
+
ensureDir(path.dirname(filePath));
|
|
67
|
+
try {
|
|
68
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
69
|
+
} catch (err) {
|
|
70
|
+
throw wrapError("write-file", packageName, err);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Move all files matching a glob-like condition.
|
|
76
|
+
* Actually does a rename — works only within the same filesystem volume.
|
|
77
|
+
*/
|
|
78
|
+
export function moveFile(src: string, dest: string, packageName = src): void {
|
|
79
|
+
ensureDir(path.dirname(dest));
|
|
80
|
+
try {
|
|
81
|
+
fs.renameSync(src, dest);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
// rename across devices fails — fall back to copy+unlink
|
|
84
|
+
try {
|
|
85
|
+
fs.copyFileSync(src, dest);
|
|
86
|
+
fs.unlinkSync(src);
|
|
87
|
+
} catch (fallbackErr) {
|
|
88
|
+
throw wrapError("move-file", packageName, fallbackErr);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check whether a path exists on the filesystem.
|
|
95
|
+
*/
|
|
96
|
+
export function pathExists(p: string): boolean {
|
|
97
|
+
return fs.existsSync(p);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* List all file paths inside a directory (non-recursive).
|
|
102
|
+
*/
|
|
103
|
+
export function listFiles(dir: string): string[] {
|
|
104
|
+
if (!fs.existsSync(dir)) return [];
|
|
105
|
+
return fs
|
|
106
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
107
|
+
.filter((e) => e.isFile())
|
|
108
|
+
.map((e) => joinPath(dir, e.name));
|
|
109
|
+
}
|