@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,191 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { readFile, writeFile } from "./file-manager.js";
|
|
3
|
+
import { joinPath } from "../utils/paths.js";
|
|
4
|
+
import { wrapError } from "../utils/errors.js";
|
|
5
|
+
import type { PackageBase } from "../types/index.js";
|
|
6
|
+
|
|
7
|
+
// Fields kept verbatim from the source package.json into the build package.json.
|
|
8
|
+
const KEPT_FIELDS = [
|
|
9
|
+
"name",
|
|
10
|
+
"description",
|
|
11
|
+
"keywords",
|
|
12
|
+
"author",
|
|
13
|
+
"license",
|
|
14
|
+
"repository",
|
|
15
|
+
"homepage",
|
|
16
|
+
"bugs",
|
|
17
|
+
"dependencies",
|
|
18
|
+
"peerDependencies",
|
|
19
|
+
"sideEffects",
|
|
20
|
+
"bin",
|
|
21
|
+
"engines",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export type SourcePackageJson = Record<string, unknown> & {
|
|
25
|
+
version: string;
|
|
26
|
+
dependencies?: Record<string, string>;
|
|
27
|
+
peerDependencies?: Record<string, string>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read and parse the package.json at <packageRoot>/package.json.
|
|
32
|
+
* Throws a BundlerError if the file is missing or malformed.
|
|
33
|
+
*/
|
|
34
|
+
export function readSourcePackageJson(
|
|
35
|
+
packageRoot: string,
|
|
36
|
+
packageName: string,
|
|
37
|
+
): SourcePackageJson {
|
|
38
|
+
const pkgPath = joinPath(packageRoot, "package.json");
|
|
39
|
+
let raw: string;
|
|
40
|
+
try {
|
|
41
|
+
raw = readFile(pkgPath, packageName);
|
|
42
|
+
} catch {
|
|
43
|
+
throw wrapError(
|
|
44
|
+
"read-package-json",
|
|
45
|
+
packageName,
|
|
46
|
+
new Error(`package.json not found at ${pkgPath}`),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(raw) as SourcePackageJson;
|
|
52
|
+
if (!parsed.version) {
|
|
53
|
+
throw new Error(`package.json at ${pkgPath} is missing "version" field`);
|
|
54
|
+
}
|
|
55
|
+
return parsed;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
throw wrapError("parse-package-json", packageName, err);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Write the new version back into the source package.json.
|
|
63
|
+
*/
|
|
64
|
+
export function writeSourceVersion(
|
|
65
|
+
packageRoot: string,
|
|
66
|
+
packageName: string,
|
|
67
|
+
newVersion: string,
|
|
68
|
+
): void {
|
|
69
|
+
const pkgPath = joinPath(packageRoot, "package.json");
|
|
70
|
+
const raw = readFile(pkgPath, packageName);
|
|
71
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
72
|
+
parsed.version = newVersion;
|
|
73
|
+
writeFile(pkgPath, JSON.stringify(parsed, null, 2) + "\n", packageName);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build and write the clean package.json for the build output directory.
|
|
78
|
+
*/
|
|
79
|
+
export function writeBuildPackageJson(
|
|
80
|
+
pkg: PackageBase,
|
|
81
|
+
sourceJson: SourcePackageJson,
|
|
82
|
+
buildPath: string,
|
|
83
|
+
newVersion: string,
|
|
84
|
+
): void {
|
|
85
|
+
const formats = pkg.formats ?? ["esm", "cjs"];
|
|
86
|
+
const hasEsm = formats.includes("esm");
|
|
87
|
+
const hasCjs = formats.includes("cjs");
|
|
88
|
+
const esmOnly = hasEsm && !hasCjs;
|
|
89
|
+
|
|
90
|
+
// When preserveModules is on (the default), tsdown keeps one file per source
|
|
91
|
+
// module and uses native extensions (.mjs/.cjs) so internal cross-file imports
|
|
92
|
+
// resolve correctly. When bundling, everything is renamed to .js for simplicity.
|
|
93
|
+
const preserve = pkg.preserveModules !== false;
|
|
94
|
+
const esmExt = preserve ? ".mjs" : ".js";
|
|
95
|
+
const cjsExt = preserve ? ".cjs" : ".js";
|
|
96
|
+
const esmDts = preserve ? ".d.mts" : ".d.ts";
|
|
97
|
+
const cjsDts = preserve ? ".d.cts" : ".d.ts";
|
|
98
|
+
|
|
99
|
+
// Determine entry base names from entries config
|
|
100
|
+
const rawEntries = pkg.entries ?? ["index.ts"];
|
|
101
|
+
const entryList = Array.isArray(rawEntries) ? rawEntries : [rawEntries];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Convert an entry path (relative to srcDir) into:
|
|
105
|
+
* basePath — path without extension, used to build file paths (e.g. "cli/index")
|
|
106
|
+
* exportPath — the key in the exports map (e.g. "./cli")
|
|
107
|
+
*
|
|
108
|
+
* When the basename is "index" the directory becomes the export path so that
|
|
109
|
+
* `cli/index.ts` maps to `"./cli"` and `index.ts` maps to `"."`.
|
|
110
|
+
*/
|
|
111
|
+
function entryPaths(entry: string): { basePath: string; exportPath: string } {
|
|
112
|
+
const basePath = entry.replace(/\.tsx?$/, "");
|
|
113
|
+
const parts = basePath.split("/");
|
|
114
|
+
const last = parts[parts.length - 1];
|
|
115
|
+
let exportPath: string;
|
|
116
|
+
if (last === "index") {
|
|
117
|
+
const dir = parts.slice(0, -1).join("/");
|
|
118
|
+
exportPath = dir ? `./${dir}` : ".";
|
|
119
|
+
} else {
|
|
120
|
+
exportPath = `./${basePath}`;
|
|
121
|
+
}
|
|
122
|
+
return { basePath, exportPath };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Primary entry drives `main` / `module` / `types` fields.
|
|
126
|
+
const { basePath: primaryBase } = entryPaths(entryList[0]!);
|
|
127
|
+
|
|
128
|
+
// Build the full exports map — one condition block per entry.
|
|
129
|
+
const exportsMap: Record<string, unknown> = {};
|
|
130
|
+
|
|
131
|
+
for (const entry of entryList) {
|
|
132
|
+
const { basePath, exportPath } = entryPaths(entry);
|
|
133
|
+
const conditions: Record<string, unknown> = {};
|
|
134
|
+
|
|
135
|
+
if (hasEsm) {
|
|
136
|
+
conditions["import"] = {
|
|
137
|
+
types: `./esm/${basePath}${esmDts}`,
|
|
138
|
+
default: `./esm/${basePath}${esmExt}`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (hasCjs) {
|
|
142
|
+
// When preserveModules is on, CJS is built without DTS to avoid a rolldown bug.
|
|
143
|
+
// TypeScript resolves types correctly from the ESM declarations via the exports map.
|
|
144
|
+
const cjsTypes = preserve && hasEsm
|
|
145
|
+
? `./esm/${basePath}${esmDts}`
|
|
146
|
+
: `./cjs/${basePath}${cjsDts}`;
|
|
147
|
+
conditions["require"] = {
|
|
148
|
+
types: cjsTypes,
|
|
149
|
+
default: `./cjs/${basePath}${cjsExt}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
exportsMap[exportPath] = conditions;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const output: Record<string, unknown> = {};
|
|
157
|
+
|
|
158
|
+
// Copy allowed fields from source
|
|
159
|
+
for (const field of KEPT_FIELDS) {
|
|
160
|
+
if (field in sourceJson && sourceJson[field] !== undefined) {
|
|
161
|
+
output[field] = sourceJson[field];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Override name / version
|
|
166
|
+
output["name"] = pkg.name;
|
|
167
|
+
output["version"] = newVersion;
|
|
168
|
+
|
|
169
|
+
if (esmOnly) {
|
|
170
|
+
output["type"] = "module";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Entry points
|
|
174
|
+
const mainType = pkg.mainType ?? "cjs";
|
|
175
|
+
if (mainType === "esm" || esmOnly) {
|
|
176
|
+
output["main"] = `./esm/${primaryBase}${esmExt}`;
|
|
177
|
+
} else {
|
|
178
|
+
output["main"] = `./cjs/${primaryBase}${cjsExt}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (hasEsm) {
|
|
182
|
+
output["module"] = `./esm/${primaryBase}${esmExt}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
output["types"] = `./esm/${primaryBase}${esmDts}`;
|
|
186
|
+
|
|
187
|
+
output["exports"] = exportsMap;
|
|
188
|
+
|
|
189
|
+
const pkgPath = joinPath(buildPath, "package.json");
|
|
190
|
+
writeFile(pkgPath, JSON.stringify(output, null, 2) + "\n", pkg.name);
|
|
191
|
+
}
|
package/src/git/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { gitCommitTagPush, isGitRepo, currentBranch } from "./operations.js";
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import simpleGit, { SimpleGit } from "simple-git";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { joinPath } from "../utils/paths.js";
|
|
5
|
+
import { wrapError } from "../utils/errors.js";
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns true if the given directory is inside a git repository.
|
|
10
|
+
*/
|
|
11
|
+
export async function isGitRepo(dir: string): Promise<boolean> {
|
|
12
|
+
try {
|
|
13
|
+
const git: SimpleGit = simpleGit(dir);
|
|
14
|
+
const result = await git.checkIsRepo();
|
|
15
|
+
return result;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Perform git operations for a package after a successful build:
|
|
23
|
+
* 1. git add -A (in the package root)
|
|
24
|
+
* 2. git commit -m <message>
|
|
25
|
+
* 3. git push origin <branch>
|
|
26
|
+
* 4. git tag v<version>
|
|
27
|
+
* 5. git push origin <tag>
|
|
28
|
+
*/
|
|
29
|
+
export async function gitCommitTagPush(
|
|
30
|
+
packageRoot: string,
|
|
31
|
+
packageName: string,
|
|
32
|
+
version: string,
|
|
33
|
+
commitMessage: string,
|
|
34
|
+
branch: string,
|
|
35
|
+
dryRun: boolean,
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
const isRepo = await isGitRepo(packageRoot);
|
|
38
|
+
if (!isRepo) {
|
|
39
|
+
logger.warn(`[git] ${packageName}: directory is not a git repo — skipping git operations`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const tag = `v${version}`;
|
|
44
|
+
logger.step(`[git] ${packageName}: commit "${commitMessage}", tag ${tag}, push to ${branch}`);
|
|
45
|
+
|
|
46
|
+
if (dryRun) {
|
|
47
|
+
logger.info(`[dry-run] git add + commit "${commitMessage}" + tag ${tag} + push`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const git: SimpleGit = simpleGit(packageRoot);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// Stage all changes
|
|
55
|
+
await git.add("-A");
|
|
56
|
+
|
|
57
|
+
// Commit
|
|
58
|
+
await git.commit(commitMessage);
|
|
59
|
+
|
|
60
|
+
// Push to remote
|
|
61
|
+
await git.push("origin", branch);
|
|
62
|
+
|
|
63
|
+
// Tag
|
|
64
|
+
await git.addTag(tag);
|
|
65
|
+
|
|
66
|
+
// Push tags
|
|
67
|
+
await git.pushTags("origin");
|
|
68
|
+
|
|
69
|
+
logger.success(`[git] ${packageName}: pushed ${branch}, tagged ${tag}`);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
throw wrapError("git-operations", packageName, err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the current branch name in the given directory.
|
|
77
|
+
* Falls back to "main" if the directory is not a git repo.
|
|
78
|
+
*/
|
|
79
|
+
export async function currentBranch(dir: string): Promise<string> {
|
|
80
|
+
try {
|
|
81
|
+
const git: SimpleGit = simpleGit(dir);
|
|
82
|
+
const branch = await git.revparse(["--abbrev-ref", "HEAD"]);
|
|
83
|
+
return branch.trim() || "main";
|
|
84
|
+
} catch {
|
|
85
|
+
return "main";
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Public API — used by builder config files via: import { defineConfig } from "@mongez/bundler"
|
|
2
|
+
export { defineConfig } from "./config/define-config.js";
|
|
3
|
+
|
|
4
|
+
// Type exports
|
|
5
|
+
export type {
|
|
6
|
+
PackageBase,
|
|
7
|
+
StandalonePackage,
|
|
8
|
+
FamilyPackage,
|
|
9
|
+
Family,
|
|
10
|
+
BundlerConfig,
|
|
11
|
+
BuilderSettings,
|
|
12
|
+
BuildOptions,
|
|
13
|
+
} from "./types/index.js";
|
|
14
|
+
|
|
15
|
+
// Re-export loadConfig for programmatic usage
|
|
16
|
+
export { loadConfig, findDefaultConfigPath } from "./config/load-config.js";
|
|
17
|
+
export type { LoadedConfig } from "./config/load-config.js";
|
|
18
|
+
|
|
19
|
+
// Programmatic build API
|
|
20
|
+
export { buildPackage } from "./build/package-builder.js";
|
|
21
|
+
export type { BuildResult } from "./build/package-builder.js";
|
|
22
|
+
export { buildFamily } from "./build/family-builder.js";
|
|
23
|
+
export { runParallel } from "./build/parallel-builder.js";
|
|
24
|
+
|
|
25
|
+
// Version utilities
|
|
26
|
+
export { resolveVersion } from "./version/increment.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { publishPackage } from "./npm-publisher.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { exec } from "../utils/exec.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
import type { PackageBase } from "../types/index.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Run `npm publish` from the build output directory.
|
|
7
|
+
*/
|
|
8
|
+
export async function publishPackage(
|
|
9
|
+
pkg: PackageBase,
|
|
10
|
+
buildPath: string,
|
|
11
|
+
dryRun: boolean,
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
if (pkg.publish === false) {
|
|
14
|
+
logger.info(`[publish] ${pkg.name}: publish=false, skipping`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const access = pkg.access ?? "public";
|
|
19
|
+
|
|
20
|
+
if (dryRun) {
|
|
21
|
+
logger.info(`[dry-run] npm publish --access ${access} from ${buildPath}`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
logger.step(`Publishing ${pkg.name}@... to npm (access: ${access})`);
|
|
26
|
+
|
|
27
|
+
await exec(
|
|
28
|
+
"npm-publish",
|
|
29
|
+
pkg.name,
|
|
30
|
+
"npm",
|
|
31
|
+
["publish", "--access", access],
|
|
32
|
+
buildPath,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
logger.success(`Published ${pkg.name}`);
|
|
36
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { StandalonePackage, Family } from "./package.js";
|
|
2
|
+
|
|
3
|
+
export interface BuilderSettings {
|
|
4
|
+
/** Maximum number of packages compiled in parallel. default: 4 */
|
|
5
|
+
concurrency?: number;
|
|
6
|
+
/**
|
|
7
|
+
* Directory where versioned build artifacts are stored.
|
|
8
|
+
* Relative path is resolved from the config file location.
|
|
9
|
+
* e.g. "../builds" → <configDir>/../builds
|
|
10
|
+
*/
|
|
11
|
+
buildDir: string;
|
|
12
|
+
/**
|
|
13
|
+
* Directory where source snapshots are copied before compilation.
|
|
14
|
+
* Relative path is resolved from the config file location.
|
|
15
|
+
* Optional — if absent, no source backup is performed.
|
|
16
|
+
*/
|
|
17
|
+
sourcesDir?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BundlerConfig {
|
|
21
|
+
settings: BuilderSettings;
|
|
22
|
+
standalone?: StandalonePackage[];
|
|
23
|
+
families?: Family[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Runtime options injected by the CLI into every build operation */
|
|
27
|
+
export interface BuildOptions {
|
|
28
|
+
dryRun: boolean;
|
|
29
|
+
noPublish: boolean;
|
|
30
|
+
noGit: boolean;
|
|
31
|
+
concurrency?: number;
|
|
32
|
+
configPath: string;
|
|
33
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export interface PackageBase {
|
|
2
|
+
/** npm package name, e.g. "@mongez/reinforcements" */
|
|
3
|
+
name: string;
|
|
4
|
+
/** relative path to package root, resolved from the builder config file location */
|
|
5
|
+
root: string;
|
|
6
|
+
/** package type — affects jsx transform for react packages */
|
|
7
|
+
type?: "typescript" | "react";
|
|
8
|
+
/** primary output format — determines "main" field in built package.json */
|
|
9
|
+
mainType?: "cjs" | "esm";
|
|
10
|
+
/** output formats to produce. default: ["esm", "cjs"] */
|
|
11
|
+
formats?: ("esm" | "cjs")[];
|
|
12
|
+
/** entry files inside srcDir. default: ["index.ts"] */
|
|
13
|
+
entries?: string | string[];
|
|
14
|
+
/** source directory relative to root. default: "src" */
|
|
15
|
+
srcDir?: string;
|
|
16
|
+
/** minify output. default: false */
|
|
17
|
+
minify?: boolean;
|
|
18
|
+
/** emit sourcemaps. default: true */
|
|
19
|
+
sourcemap?: boolean;
|
|
20
|
+
/** emit d.ts declarations. default: true */
|
|
21
|
+
dts?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Preserve the original module structure in the output instead of bundling
|
|
24
|
+
* everything into a single index.js.
|
|
25
|
+
*
|
|
26
|
+
* When true, each source file becomes its own output file (e.g.
|
|
27
|
+
* `src/array/chunk.ts` → `esm/array/chunk.mjs`). Produces much more useful
|
|
28
|
+
* stack traces because file names are preserved.
|
|
29
|
+
*
|
|
30
|
+
* When false, all sources are bundled into a single `esm/index.js` +
|
|
31
|
+
* `cjs/index.js`. Useful for tiny single-file packages.
|
|
32
|
+
*
|
|
33
|
+
* @default true
|
|
34
|
+
*/
|
|
35
|
+
preserveModules?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Files to copy into the build directory.
|
|
38
|
+
* A plain string copies from the package root to the build root with the same name.
|
|
39
|
+
* A tuple [src, dest] copies src (relative to package root) to dest (relative to build dir).
|
|
40
|
+
*/
|
|
41
|
+
clone?: (string | [string, string])[];
|
|
42
|
+
/** run npm publish after build. default: true */
|
|
43
|
+
publish?: boolean;
|
|
44
|
+
/** npm access scope. default: "public" */
|
|
45
|
+
access?: "public" | "restricted";
|
|
46
|
+
/** git commit message. if absent, git operations are skipped entirely */
|
|
47
|
+
commit?: string;
|
|
48
|
+
/** git branch to push to. default: current branch */
|
|
49
|
+
branch?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface StandalonePackage extends PackageBase {
|
|
53
|
+
/**
|
|
54
|
+
* Version strategy:
|
|
55
|
+
* - "auto" / "patch" → patch-bump the current version from source package.json
|
|
56
|
+
* - "minor" → minor-bump (x.Y.0)
|
|
57
|
+
* - "major" → major-bump (X.0.0)
|
|
58
|
+
* - semver string → use exactly this version string
|
|
59
|
+
* - omitted → defaults to "auto"
|
|
60
|
+
*/
|
|
61
|
+
version?: "auto" | "patch" | "minor" | "major" | string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface FamilyPackage extends PackageBase {
|
|
65
|
+
// No per-package version — version comes from the family definition
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface Family {
|
|
69
|
+
/** Identifier used with `build:family <name>` */
|
|
70
|
+
name: string;
|
|
71
|
+
/**
|
|
72
|
+
* Version strategy for all packages in this family.
|
|
73
|
+
* - "auto" → patch-bump from the first package's current version
|
|
74
|
+
* - string → use exactly this version string
|
|
75
|
+
* default: "auto"
|
|
76
|
+
*/
|
|
77
|
+
version?: "auto" | "patch" | "minor" | "major" | string;
|
|
78
|
+
/** git commit message that overrides per-package commit */
|
|
79
|
+
commit?: string;
|
|
80
|
+
packages: FamilyPackage[];
|
|
81
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Base error class for all bundler errors. Carries a step name for structured logging. */
|
|
2
|
+
export class BundlerError extends Error {
|
|
3
|
+
constructor(
|
|
4
|
+
public readonly step: string,
|
|
5
|
+
public readonly packageName: string,
|
|
6
|
+
message: string,
|
|
7
|
+
public readonly cause?: unknown,
|
|
8
|
+
) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "BundlerError";
|
|
11
|
+
if (cause instanceof Error && cause.stack) {
|
|
12
|
+
this.stack = `${this.stack}\nCaused by: ${cause.stack}`;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Wrap any thrown value into a BundlerError with context. */
|
|
18
|
+
export function wrapError(
|
|
19
|
+
step: string,
|
|
20
|
+
packageName: string,
|
|
21
|
+
error: unknown,
|
|
22
|
+
): BundlerError {
|
|
23
|
+
if (error instanceof BundlerError) return error;
|
|
24
|
+
const msg =
|
|
25
|
+
error instanceof Error
|
|
26
|
+
? error.message
|
|
27
|
+
: typeof error === "string"
|
|
28
|
+
? error
|
|
29
|
+
: JSON.stringify(error);
|
|
30
|
+
return new BundlerError(step, packageName, `[${step}] ${packageName}: ${msg}`, error);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Throw a wrapped error immediately. */
|
|
34
|
+
export function failWith(
|
|
35
|
+
step: string,
|
|
36
|
+
packageName: string,
|
|
37
|
+
error: unknown,
|
|
38
|
+
): never {
|
|
39
|
+
throw wrapError(step, packageName, error);
|
|
40
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import { wrapError } from "./errors.js";
|
|
3
|
+
|
|
4
|
+
export interface ExecResult {
|
|
5
|
+
stdout: string;
|
|
6
|
+
stderr: string;
|
|
7
|
+
exitCode: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Run a shell command and return stdout/stderr.
|
|
12
|
+
* Throws a BundlerError on non-zero exit.
|
|
13
|
+
*/
|
|
14
|
+
export async function exec(
|
|
15
|
+
step: string,
|
|
16
|
+
packageName: string,
|
|
17
|
+
command: string,
|
|
18
|
+
args: string[],
|
|
19
|
+
cwd?: string,
|
|
20
|
+
): Promise<ExecResult> {
|
|
21
|
+
try {
|
|
22
|
+
const result = await execa(command, args, {
|
|
23
|
+
cwd,
|
|
24
|
+
all: true,
|
|
25
|
+
reject: false,
|
|
26
|
+
});
|
|
27
|
+
if (result.exitCode !== 0) {
|
|
28
|
+
const combined = result.all ?? result.stderr ?? "";
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Command "${command} ${args.join(" ")}" exited with code ${result.exitCode}.\n${combined}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
stdout: result.stdout ?? "",
|
|
35
|
+
stderr: result.stderr ?? "",
|
|
36
|
+
exitCode: result.exitCode,
|
|
37
|
+
};
|
|
38
|
+
} catch (err) {
|
|
39
|
+
throw wrapError(step, packageName, err);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Run a command and ignore its output. Does NOT throw on non-zero exit.
|
|
45
|
+
* Useful for optional operations (e.g. `git tag -d`).
|
|
46
|
+
*/
|
|
47
|
+
export async function execSilent(
|
|
48
|
+
command: string,
|
|
49
|
+
args: string[],
|
|
50
|
+
cwd?: string,
|
|
51
|
+
): Promise<number> {
|
|
52
|
+
try {
|
|
53
|
+
const result = await execa(command, args, { cwd, reject: false });
|
|
54
|
+
return result.exitCode ?? 0;
|
|
55
|
+
} catch {
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
export type LogLevel = "info" | "success" | "warn" | "error" | "debug" | "step";
|
|
4
|
+
|
|
5
|
+
let _verbose = false;
|
|
6
|
+
|
|
7
|
+
export function setVerbose(v: boolean): void {
|
|
8
|
+
_verbose = v;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function log(level: LogLevel, message: string): void {
|
|
12
|
+
const ts = new Date().toISOString().replace("T", " ").replace("Z", "");
|
|
13
|
+
switch (level) {
|
|
14
|
+
case "info":
|
|
15
|
+
console.log(chalk.cyan(`[${ts}] INFO ${message}`));
|
|
16
|
+
break;
|
|
17
|
+
case "success":
|
|
18
|
+
console.log(chalk.green(`[${ts}] OK ${message}`));
|
|
19
|
+
break;
|
|
20
|
+
case "warn":
|
|
21
|
+
console.warn(chalk.yellow(`[${ts}] WARN ${message}`));
|
|
22
|
+
break;
|
|
23
|
+
case "error":
|
|
24
|
+
console.error(chalk.red(`[${ts}] ERROR ${message}`));
|
|
25
|
+
break;
|
|
26
|
+
case "step":
|
|
27
|
+
console.log(chalk.blueBright(`[${ts}] STEP ${message}`));
|
|
28
|
+
break;
|
|
29
|
+
case "debug":
|
|
30
|
+
if (_verbose) {
|
|
31
|
+
console.log(chalk.gray(`[${ts}] DEBUG ${message}`));
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const logger = {
|
|
38
|
+
info: (msg: string) => log("info", msg),
|
|
39
|
+
success: (msg: string) => log("success", msg),
|
|
40
|
+
warn: (msg: string) => log("warn", msg),
|
|
41
|
+
error: (msg: string) => log("error", msg),
|
|
42
|
+
step: (msg: string) => log("step", msg),
|
|
43
|
+
debug: (msg: string) => log("debug", msg),
|
|
44
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
/** Normalise any path to forward-slash form (Windows-safe). */
|
|
4
|
+
export function toForwardSlash(p: string): string {
|
|
5
|
+
return p.replace(/\\/g, "/");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Join path segments and normalise the result to forward slashes. */
|
|
9
|
+
export function joinPath(...segments: string[]): string {
|
|
10
|
+
return toForwardSlash(path.join(...segments));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Resolve an absolute path from segments and normalise to forward slashes. */
|
|
14
|
+
export function resolvePath(...segments: string[]): string {
|
|
15
|
+
return toForwardSlash(path.resolve(...segments));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Given a package name like "@mongez/reinforcements",
|
|
20
|
+
* return just the scoped part without the scope prefix: "reinforcements".
|
|
21
|
+
*/
|
|
22
|
+
export function scopelessName(packageName: string): string {
|
|
23
|
+
const slash = packageName.indexOf("/");
|
|
24
|
+
return slash === -1 ? packageName : packageName.slice(slash + 1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build the versioned build output path:
|
|
29
|
+
* <buildDir>/<scopeless-name>/<version>/
|
|
30
|
+
*/
|
|
31
|
+
export function buildOutputPath(
|
|
32
|
+
buildDir: string,
|
|
33
|
+
packageName: string,
|
|
34
|
+
version: string,
|
|
35
|
+
): string {
|
|
36
|
+
return joinPath(buildDir, scopelessName(packageName), version);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build the sources snapshot path:
|
|
41
|
+
* <sourcesDir>/<scopeless-name>/
|
|
42
|
+
*/
|
|
43
|
+
export function sourceSnapshotPath(
|
|
44
|
+
sourcesDir: string,
|
|
45
|
+
packageName: string,
|
|
46
|
+
): string {
|
|
47
|
+
return joinPath(sourcesDir, scopelessName(packageName));
|
|
48
|
+
}
|