@maltty/cli 1.0.0-rc.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/LICENSE +21 -0
- package/README.md +88 -0
- package/bin/maltty.js +3 -0
- package/dist/_format-CeQhQTl5.js +23 -0
- package/dist/_format-CeQhQTl5.js.map +1 -0
- package/dist/attemptAsync-BdXzRSIT.js +42 -0
- package/dist/attemptAsync-BdXzRSIT.js.map +1 -0
- package/dist/chunk-DsIazq42.js +26 -0
- package/dist/commands/add/command.d.ts +7 -0
- package/dist/commands/add/command.d.ts.map +1 -0
- package/dist/commands/add/command.js +123 -0
- package/dist/commands/add/command.js.map +1 -0
- package/dist/commands/add/config.d.ts +7 -0
- package/dist/commands/add/config.d.ts.map +1 -0
- package/dist/commands/add/config.js +57 -0
- package/dist/commands/add/config.js.map +1 -0
- package/dist/commands/add/index.d.ts +7 -0
- package/dist/commands/add/index.d.ts.map +1 -0
- package/dist/commands/add/index.js +7 -0
- package/dist/commands/add/index.js.map +1 -0
- package/dist/commands/add/middleware.d.ts +7 -0
- package/dist/commands/add/middleware.d.ts.map +1 -0
- package/dist/commands/add/middleware.js +88 -0
- package/dist/commands/add/middleware.js.map +1 -0
- package/dist/commands/build.d.ts +15 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +246 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/commands.d.ts +14 -0
- package/dist/commands/commands.d.ts.map +1 -0
- package/dist/commands/commands.js +189 -0
- package/dist/commands/commands.js.map +1 -0
- package/dist/commands/dev.d.ts +13 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +58 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/doctor.d.ts +13 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +658 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +305 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/run.d.ts +19 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +478 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/stories.d.ts +11 -0
- package/dist/commands/stories.d.ts.map +1 -0
- package/dist/commands/stories.js +22 -0
- package/dist/commands/stories.js.map +1 -0
- package/dist/config-helpers-pv_2Spux.js +15 -0
- package/dist/config-helpers-pv_2Spux.js.map +1 -0
- package/dist/defu-BBwssjW3.js +192 -0
- package/dist/defu-BBwssjW3.js.map +1 -0
- package/dist/detect-AOq1ehTE.js +61 -0
- package/dist/detect-AOq1ehTE.js.map +1 -0
- package/dist/dist-DW_gRP7_.js +1119 -0
- package/dist/dist-DW_gRP7_.js.map +1 -0
- package/dist/dist-Xa07jNxS.js +15617 -0
- package/dist/dist-Xa07jNxS.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/jiti-CxWlVmZ_.js +5458 -0
- package/dist/jiti-CxWlVmZ_.js.map +1 -0
- package/dist/json-D7WbuVK1.js +43 -0
- package/dist/json-D7WbuVK1.js.map +1 -0
- package/dist/json5-z-PGDMyy.js +777 -0
- package/dist/json5-z-PGDMyy.js.map +1 -0
- package/dist/jsonc-DqZkfC3S.js +401 -0
- package/dist/jsonc-DqZkfC3S.js.map +1 -0
- package/dist/jsonc-KADMdSjU.js +2 -0
- package/dist/lib/templates/command/command.ts.liquid +12 -0
- package/dist/lib/templates/config/config.ts.liquid +16 -0
- package/dist/lib/templates/middleware/middleware.ts.liquid +9 -0
- package/dist/lib/templates/project/gitignore.liquid +3 -0
- package/dist/lib/templates/project/maltty.config.ts.liquid +6 -0
- package/dist/lib/templates/project/package.json.liquid +30 -0
- package/dist/lib/templates/project/src/commands/hello.ts.liquid +12 -0
- package/dist/lib/templates/project/src/config.ts.liquid +16 -0
- package/dist/lib/templates/project/src/index.ts.liquid +13 -0
- package/dist/lib/templates/project/tsconfig.json.liquid +19 -0
- package/dist/lib/templates/project/vitest.config.ts.liquid +8 -0
- package/dist/main-C_8x2aB3.js +274 -0
- package/dist/main-C_8x2aB3.js.map +1 -0
- package/dist/manifest-BIKVDJML.js +112 -0
- package/dist/manifest-BIKVDJML.js.map +1 -0
- package/dist/multipart-parser-MNkxNO83.js +174 -0
- package/dist/multipart-parser-MNkxNO83.js.map +1 -0
- package/dist/node-C91xGGqn.js +644 -0
- package/dist/node-C91xGGqn.js.map +1 -0
- package/dist/node-Drmc03fL.js +3973 -0
- package/dist/node-Drmc03fL.js.map +1 -0
- package/dist/ohash-BPoV4zBH.js +185 -0
- package/dist/ohash-BPoV4zBH.js.map +1 -0
- package/dist/pnpm-workspace.yaml +26 -0
- package/dist/toml-LuHsHQtV.js +646 -0
- package/dist/toml-LuHsHQtV.js.map +1 -0
- package/dist/utils-DlIJNGLQ.js +1905 -0
- package/dist/utils-DlIJNGLQ.js.map +1 -0
- package/dist/validate-C_SXBoLq.js +18 -0
- package/dist/validate-C_SXBoLq.js.map +1 -0
- package/dist/write-CSwG4Stj.js +152 -0
- package/dist/write-CSwG4Stj.js.map +1 -0
- package/dist/yaml-BS9RDuUr.js +1067 -0
- package/dist/yaml-BS9RDuUr.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,1119 @@
|
|
|
1
|
+
import { i as toError, n as err, r as ok, t as attemptAsync } from "./attemptAsync-BdXzRSIT.js";
|
|
2
|
+
import { a as z, i as M, n as process_exports, r as fs_exports, t as path_exports } from "./node-C91xGGqn.js";
|
|
3
|
+
import { t as readManifest } from "./manifest-BIKVDJML.js";
|
|
4
|
+
import { t as compileTargets } from "./utils-DlIJNGLQ.js";
|
|
5
|
+
import { builtinModules, createRequire } from "node:module";
|
|
6
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
7
|
+
import { readdir } from "node:fs/promises";
|
|
8
|
+
import { build } from "tsdown";
|
|
9
|
+
//#region ../../node_modules/.pnpm/es-toolkit@1.46.1/node_modules/es-toolkit/dist/function/noop.mjs
|
|
10
|
+
function noop() {}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region ../../node_modules/.pnpm/es-toolkit@1.46.1/node_modules/es-toolkit/dist/predicate/isNil.mjs
|
|
13
|
+
function isNil(x) {
|
|
14
|
+
return x == null;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region ../bundler/dist/index.js
|
|
18
|
+
/**
|
|
19
|
+
* Generate JavaScript source code for a static autoloader virtual module.
|
|
20
|
+
*
|
|
21
|
+
* The generated module statically imports every discovered command file and
|
|
22
|
+
* exports an `autoload()` function that returns the pre-built CommandMap.
|
|
23
|
+
* Directory commands that merge a parent handler with subcommands are re-tagged
|
|
24
|
+
* via `withTag` because the TAG symbol is non-enumerable and lost on spread.
|
|
25
|
+
*
|
|
26
|
+
* @param params - The scan result and path to the tag utility module.
|
|
27
|
+
* @returns JavaScript source code for the static autoloader.
|
|
28
|
+
*/
|
|
29
|
+
function generateStaticAutoloader(params) {
|
|
30
|
+
const importLines = buildImportStatements(collectImports(params.scan), params.tagModulePath);
|
|
31
|
+
const commandsObject = buildCommandsObject(params.scan);
|
|
32
|
+
return [
|
|
33
|
+
...importLines,
|
|
34
|
+
"",
|
|
35
|
+
`const commands = ${commandsObject}`,
|
|
36
|
+
"",
|
|
37
|
+
"export async function autoload() {",
|
|
38
|
+
" return commands",
|
|
39
|
+
"}",
|
|
40
|
+
""
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Collect all import entries from a scan result.
|
|
45
|
+
*
|
|
46
|
+
* @private
|
|
47
|
+
* @param scan - The scan result to collect imports from.
|
|
48
|
+
* @returns A flat array of all import entries.
|
|
49
|
+
*/
|
|
50
|
+
function collectImports(scan) {
|
|
51
|
+
return [...scan.files.map((file) => fileToImport(file, [])), ...scan.dirs.flatMap((dir) => collectDirImports(dir, []))];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Recursively collect import entries from a scanned directory.
|
|
55
|
+
*
|
|
56
|
+
* @private
|
|
57
|
+
* @param dir - The scanned directory.
|
|
58
|
+
* @param parentPath - The path segments leading to this directory.
|
|
59
|
+
* @returns A flat array of import entries for the directory and its children.
|
|
60
|
+
*/
|
|
61
|
+
function collectDirImports(dir, parentPath) {
|
|
62
|
+
const currentPath = [...parentPath, dir.name];
|
|
63
|
+
return [
|
|
64
|
+
...buildIndexImport(dir.index, currentPath),
|
|
65
|
+
...dir.files.map((file) => fileToImport(file, currentPath)),
|
|
66
|
+
...dir.dirs.flatMap((sub) => collectDirImports(sub, currentPath))
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create an import entry for a leaf command file.
|
|
71
|
+
*
|
|
72
|
+
* @private
|
|
73
|
+
* @param file - The scanned file.
|
|
74
|
+
* @param parentPath - The path segments leading to the file's parent directory.
|
|
75
|
+
* @returns An import entry with a generated identifier.
|
|
76
|
+
*/
|
|
77
|
+
function fileToImport(file, parentPath) {
|
|
78
|
+
return {
|
|
79
|
+
filePath: file.filePath,
|
|
80
|
+
identifier: toIdentifier([...parentPath, file.name])
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Convert a path segment array to a valid JavaScript identifier.
|
|
85
|
+
*
|
|
86
|
+
* Joins segments with underscores and prefixes with `_`.
|
|
87
|
+
* Example: `['deploy', 'preview']` becomes `_deploy_preview`.
|
|
88
|
+
*
|
|
89
|
+
* @private
|
|
90
|
+
* @param segments - The path segments to convert.
|
|
91
|
+
* @returns A valid JavaScript identifier string.
|
|
92
|
+
*/
|
|
93
|
+
function toIdentifier(segments) {
|
|
94
|
+
return `_${segments.map((s) => s.replaceAll("-", "$")).join("_")}`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Build the array of import statement lines.
|
|
98
|
+
*
|
|
99
|
+
* @private
|
|
100
|
+
* @param imports - All collected import entries.
|
|
101
|
+
* @param tagModulePath - Absolute path to the tag utility module.
|
|
102
|
+
* @returns An array of import statement strings.
|
|
103
|
+
*/
|
|
104
|
+
function buildImportStatements(imports, tagModulePath) {
|
|
105
|
+
const tagLine = buildTagImportLine(imports, tagModulePath);
|
|
106
|
+
const importLines = imports.map((entry) => `import ${entry.identifier} from '${path_exports.toImportUrl(entry.filePath)}'`);
|
|
107
|
+
return [
|
|
108
|
+
...tagLine,
|
|
109
|
+
"",
|
|
110
|
+
...importLines
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Build the JavaScript object literal string for the top-level commands map.
|
|
115
|
+
*
|
|
116
|
+
* @private
|
|
117
|
+
* @param scan - The scan result.
|
|
118
|
+
* @returns A string representation of the commands object literal.
|
|
119
|
+
*/
|
|
120
|
+
function buildCommandsObject(scan) {
|
|
121
|
+
return formatObject([...scan.files.map((file) => buildFileEntry(file, [])), ...scan.dirs.map((dir) => buildDirEntry(dir, []))]);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Build an object entry string for a leaf command file.
|
|
125
|
+
*
|
|
126
|
+
* @private
|
|
127
|
+
* @param file - The scanned file.
|
|
128
|
+
* @param parentPath - Path segments to the file's parent.
|
|
129
|
+
* @returns A string like `'status': _status`.
|
|
130
|
+
*/
|
|
131
|
+
function buildFileEntry(file, parentPath) {
|
|
132
|
+
const identifier = toIdentifier([...parentPath, file.name]);
|
|
133
|
+
return `'${file.name}': ${identifier}`;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Build an object entry string for a directory command (possibly with subcommands).
|
|
137
|
+
*
|
|
138
|
+
* @private
|
|
139
|
+
* @param dir - The scanned directory.
|
|
140
|
+
* @param parentPath - Path segments to the directory's parent.
|
|
141
|
+
* @returns A string representing the directory command with withTag wrapping.
|
|
142
|
+
*/
|
|
143
|
+
function buildDirEntry(dir, parentPath) {
|
|
144
|
+
const currentPath = [...parentPath, dir.name];
|
|
145
|
+
const commandsObj = formatObject([...dir.files.map((file) => buildFileEntry(file, currentPath)), ...dir.dirs.map((sub) => buildDirEntry(sub, currentPath))]);
|
|
146
|
+
if (dir.index) {
|
|
147
|
+
const indexIdentifier = toIdentifier(currentPath);
|
|
148
|
+
return `'${dir.name}': withTag({ ...${indexIdentifier}, commands: ${commandsObj} }, 'Command')`;
|
|
149
|
+
}
|
|
150
|
+
return `'${dir.name}': withTag({ commands: ${commandsObj} }, 'Command')`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Build the import entry for an index file, if present.
|
|
154
|
+
*
|
|
155
|
+
* @private
|
|
156
|
+
* @param index - The absolute path to the index file, or undefined.
|
|
157
|
+
* @param currentPath - The current path segments for identifier generation.
|
|
158
|
+
* @returns An array with zero or one import entries.
|
|
159
|
+
*/
|
|
160
|
+
function buildIndexImport(index, currentPath) {
|
|
161
|
+
if (!index) return [];
|
|
162
|
+
return [{
|
|
163
|
+
filePath: index,
|
|
164
|
+
identifier: toIdentifier(currentPath)
|
|
165
|
+
}];
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Build the tag import line if any imports exist.
|
|
169
|
+
*
|
|
170
|
+
* @private
|
|
171
|
+
* @param imports - The collected import entries.
|
|
172
|
+
* @param tagModulePath - The absolute path to the tag module.
|
|
173
|
+
* @returns An array with zero or one import statement strings.
|
|
174
|
+
*/
|
|
175
|
+
function buildTagImportLine(imports, tagModulePath) {
|
|
176
|
+
if (imports.length === 0) return [];
|
|
177
|
+
return [`import { withTag } from '${path_exports.toImportUrl(tagModulePath)}'`];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Format an array of key-value strings as a JavaScript object literal.
|
|
181
|
+
*
|
|
182
|
+
* @private
|
|
183
|
+
* @param entries - The key-value pair strings.
|
|
184
|
+
* @returns A formatted object literal string.
|
|
185
|
+
*/
|
|
186
|
+
function formatObject(entries) {
|
|
187
|
+
if (entries.length === 0) return "{}";
|
|
188
|
+
return `{\n${entries.map((entry) => ` ${entry},`).join("\n")}\n}`;
|
|
189
|
+
}
|
|
190
|
+
const VALID_EXTENSIONS = new Set([
|
|
191
|
+
".ts",
|
|
192
|
+
".js",
|
|
193
|
+
".mjs",
|
|
194
|
+
".tsx",
|
|
195
|
+
".jsx"
|
|
196
|
+
]);
|
|
197
|
+
const INDEX_NAME = "index";
|
|
198
|
+
/**
|
|
199
|
+
* Scan a commands directory and produce a tree structure for static code generation.
|
|
200
|
+
*
|
|
201
|
+
* Mirrors the runtime autoloader's rules: valid extensions are `.ts`, `.js`, `.mjs`;
|
|
202
|
+
* files and directories starting with `_` or `.` are skipped; `index` files in
|
|
203
|
+
* subdirectories become parent command handlers.
|
|
204
|
+
*
|
|
205
|
+
* @param dir - Absolute path to the commands directory.
|
|
206
|
+
* @returns A tree of scanned files and directories.
|
|
207
|
+
*/
|
|
208
|
+
async function scanCommandsDir(dir) {
|
|
209
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
210
|
+
const files = entries.filter(isCommandFile).map((entry) => toScannedFile(dir, entry));
|
|
211
|
+
return {
|
|
212
|
+
dirs: await Promise.all(entries.filter(isCommandDir).map((entry) => scanSubDir(join(dir, entry.name)))),
|
|
213
|
+
files
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Recursively scan a subdirectory into a ScannedDir.
|
|
218
|
+
*
|
|
219
|
+
* @private
|
|
220
|
+
* @param dir - Absolute path to the subdirectory.
|
|
221
|
+
* @returns A ScannedDir representing the directory and its contents.
|
|
222
|
+
*/
|
|
223
|
+
async function scanSubDir(dir) {
|
|
224
|
+
const name = basename(dir);
|
|
225
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
226
|
+
const indexEntry = findIndexEntry(entries);
|
|
227
|
+
const files = entries.filter(isCommandFile).map((entry) => toScannedFile(dir, entry));
|
|
228
|
+
return {
|
|
229
|
+
dirs: await Promise.all(entries.filter(isCommandDir).map((entry) => scanSubDir(join(dir, entry.name)))),
|
|
230
|
+
files,
|
|
231
|
+
index: resolveIndexPath(dir, indexEntry),
|
|
232
|
+
name
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Convert a directory entry into a ScannedFile.
|
|
237
|
+
*
|
|
238
|
+
* @private
|
|
239
|
+
* @param dir - Parent directory absolute path.
|
|
240
|
+
* @param entry - The directory entry for the file.
|
|
241
|
+
* @returns A ScannedFile with name and absolute file path.
|
|
242
|
+
*/
|
|
243
|
+
function toScannedFile(dir, entry) {
|
|
244
|
+
return {
|
|
245
|
+
filePath: join(dir, entry.name),
|
|
246
|
+
name: basename(entry.name, extname(entry.name))
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Find the index file entry among a list of directory entries.
|
|
251
|
+
*
|
|
252
|
+
* @private
|
|
253
|
+
* @param entries - The directory entries to search.
|
|
254
|
+
* @returns The Dirent for the index file, or undefined.
|
|
255
|
+
*/
|
|
256
|
+
function findIndexEntry(entries) {
|
|
257
|
+
return entries.find((entry) => entry.isFile() && !entry.name.endsWith(".d.ts") && !entry.name.endsWith(".d.tsx") && VALID_EXTENSIONS.has(extname(entry.name)) && basename(entry.name, extname(entry.name)) === INDEX_NAME);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Predicate: entry is a valid command file (not index, not hidden/private).
|
|
261
|
+
*
|
|
262
|
+
* @private
|
|
263
|
+
* @param entry - The directory entry to check.
|
|
264
|
+
* @returns True when the entry is a scannable command file.
|
|
265
|
+
*/
|
|
266
|
+
function isCommandFile(entry) {
|
|
267
|
+
if (!entry.isFile()) return false;
|
|
268
|
+
if (entry.name.startsWith("_") || entry.name.startsWith(".")) return false;
|
|
269
|
+
if (entry.name.endsWith(".d.ts") || entry.name.endsWith(".d.tsx")) return false;
|
|
270
|
+
if (!VALID_EXTENSIONS.has(extname(entry.name))) return false;
|
|
271
|
+
return basename(entry.name, extname(entry.name)) !== INDEX_NAME;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Predicate: entry is a scannable command directory (not hidden/private).
|
|
275
|
+
*
|
|
276
|
+
* @private
|
|
277
|
+
* @param entry - The directory entry to check.
|
|
278
|
+
* @returns True when the entry is a scannable command directory.
|
|
279
|
+
*/
|
|
280
|
+
function isCommandDir(entry) {
|
|
281
|
+
if (!entry.isDirectory()) return false;
|
|
282
|
+
return !entry.name.startsWith("_") && !entry.name.startsWith(".");
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Resolve the absolute path to an index file entry, or undefined.
|
|
286
|
+
*
|
|
287
|
+
* @private
|
|
288
|
+
* @param dir - The parent directory absolute path.
|
|
289
|
+
* @param entry - The index file Dirent, or undefined.
|
|
290
|
+
* @returns The absolute path to the index file, or undefined.
|
|
291
|
+
*/
|
|
292
|
+
function resolveIndexPath(dir, entry) {
|
|
293
|
+
if (!entry) return;
|
|
294
|
+
return join(dir, entry.name);
|
|
295
|
+
}
|
|
296
|
+
const VIRTUAL_MODULE_ID = "virtual:maltty-static-commands";
|
|
297
|
+
const RESOLVED_VIRTUAL_ID = `\0${VIRTUAL_MODULE_ID}`;
|
|
298
|
+
const AUTOLOADER_REGION_START = "//#region src/autoload.ts";
|
|
299
|
+
const AUTOLOADER_REGION_END = "//#endregion";
|
|
300
|
+
/**
|
|
301
|
+
* Create a rolldown plugin that replaces the runtime autoloader with a static version.
|
|
302
|
+
*
|
|
303
|
+
* Uses a three-hook approach to break the circular dependency between maltty's
|
|
304
|
+
* dist and user command files (which `import { command } from 'maltty'`):
|
|
305
|
+
*
|
|
306
|
+
* 1. `transform` — detects maltty's pre-bundled dist and replaces the autoloader
|
|
307
|
+
* region with a dynamic `import()` to a virtual module
|
|
308
|
+
* 2. `resolveId` — resolves the virtual module identifier
|
|
309
|
+
* 3. `load` — scans the commands directory and generates a static autoloader
|
|
310
|
+
* module with all command imports pre-resolved
|
|
311
|
+
*
|
|
312
|
+
* The dynamic import ensures command files execute after maltty's code is fully
|
|
313
|
+
* initialized, avoiding `ReferenceError` from accessing `TAG` before its
|
|
314
|
+
* declaration.
|
|
315
|
+
*
|
|
316
|
+
* @param params - The commands directory and tag module path.
|
|
317
|
+
* @returns A rolldown plugin for static autoloading.
|
|
318
|
+
*/
|
|
319
|
+
function createAutoloadPlugin(params) {
|
|
320
|
+
return {
|
|
321
|
+
async load(id) {
|
|
322
|
+
if (id !== RESOLVED_VIRTUAL_ID) return null;
|
|
323
|
+
return generateStaticAutoloader({
|
|
324
|
+
scan: await scanCommandsDir(params.commandsDir),
|
|
325
|
+
tagModulePath: params.tagModulePath
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
name: "maltty-static-autoloader",
|
|
329
|
+
resolveId(source) {
|
|
330
|
+
if (source === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_ID;
|
|
331
|
+
return null;
|
|
332
|
+
},
|
|
333
|
+
transform(code, _id) {
|
|
334
|
+
const regionStart = code.indexOf(AUTOLOADER_REGION_START);
|
|
335
|
+
if (regionStart === -1) return null;
|
|
336
|
+
const regionEnd = code.indexOf(AUTOLOADER_REGION_END, regionStart);
|
|
337
|
+
if (regionEnd === -1) return null;
|
|
338
|
+
const before = code.slice(0, regionStart);
|
|
339
|
+
const after = code.slice(regionEnd + 12);
|
|
340
|
+
return `${before}${buildStaticRegion()}${after}`;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Build the replacement autoloader region that delegates to the virtual module.
|
|
346
|
+
*
|
|
347
|
+
* @private
|
|
348
|
+
* @returns The replacement region string with dynamic import.
|
|
349
|
+
*/
|
|
350
|
+
function buildStaticRegion() {
|
|
351
|
+
return [
|
|
352
|
+
"//#region src/autoload.ts (static)",
|
|
353
|
+
"async function autoload() {",
|
|
354
|
+
` const mod = await import('${VIRTUAL_MODULE_ID}')`,
|
|
355
|
+
" return mod.autoload()",
|
|
356
|
+
"}",
|
|
357
|
+
"//#endregion"
|
|
358
|
+
].join("\n");
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Shebang line prepended to CLI entry files.
|
|
362
|
+
*/
|
|
363
|
+
const SHEBANG = "#!/usr/bin/env node\n";
|
|
364
|
+
/**
|
|
365
|
+
* Default entry point for the CLI source.
|
|
366
|
+
*/
|
|
367
|
+
const DEFAULT_ENTRY = "./src/index.ts";
|
|
368
|
+
/**
|
|
369
|
+
* Default directory for CLI commands.
|
|
370
|
+
*/
|
|
371
|
+
const DEFAULT_COMMANDS = "./commands";
|
|
372
|
+
/**
|
|
373
|
+
* File extensions produced by maltty builds that are safe to remove during clean.
|
|
374
|
+
*/
|
|
375
|
+
const BUILD_ARTIFACT_EXTENSIONS = [
|
|
376
|
+
".js",
|
|
377
|
+
".mjs",
|
|
378
|
+
".js.map",
|
|
379
|
+
".mjs.map"
|
|
380
|
+
];
|
|
381
|
+
/**
|
|
382
|
+
* Packages that must always be bundled into the output.
|
|
383
|
+
*
|
|
384
|
+
* The `maltty` framework and its internal `@maltty/*` packages must be inlined
|
|
385
|
+
* so the autoload plugin can intercept and replace the runtime autoloader
|
|
386
|
+
* with a static version for compiled binaries.
|
|
387
|
+
*/
|
|
388
|
+
const ALWAYS_BUNDLE = [/^@?maltty/];
|
|
389
|
+
/**
|
|
390
|
+
* Packages that are optional or conditional dependencies of bundled libraries
|
|
391
|
+
* (c12, ink) that must be stubbed during compile-mode builds. These are behind
|
|
392
|
+
* dynamic `import()` calls or runtime guards that never execute in production,
|
|
393
|
+
* but when all deps are inlined, rolldown traces into them statically.
|
|
394
|
+
*/
|
|
395
|
+
const STUB_PACKAGES = [
|
|
396
|
+
"chokidar",
|
|
397
|
+
"magicast",
|
|
398
|
+
"giget",
|
|
399
|
+
"react-devtools-core"
|
|
400
|
+
];
|
|
401
|
+
/**
|
|
402
|
+
* Node.js builtin modules in both bare and `node:` prefixed forms.
|
|
403
|
+
*/
|
|
404
|
+
const NODE_BUILTINS = [...builtinModules, ...builtinModules.map((m) => `node:${m}`)];
|
|
405
|
+
const TAG_MODULE_PATH = createRequire(import.meta.url).resolve("@maltty/utils/tag");
|
|
406
|
+
/**
|
|
407
|
+
* Convert a resolved bundler config to a tsdown InlineConfig for production builds.
|
|
408
|
+
*
|
|
409
|
+
* @param params - The resolved config and optional version for compile-time injection.
|
|
410
|
+
* @returns A tsdown InlineConfig ready for `build()`.
|
|
411
|
+
*/
|
|
412
|
+
function toTsdownBuildConfig(params) {
|
|
413
|
+
return {
|
|
414
|
+
banner: SHEBANG,
|
|
415
|
+
clean: false,
|
|
416
|
+
config: false,
|
|
417
|
+
cwd: params.config.cwd,
|
|
418
|
+
define: buildDefine({
|
|
419
|
+
define: params.config.build.define,
|
|
420
|
+
version: params.config.version
|
|
421
|
+
}),
|
|
422
|
+
deps: buildDeps(params.config.build.external, params.compile ?? false),
|
|
423
|
+
dts: false,
|
|
424
|
+
entry: { index: params.config.entry },
|
|
425
|
+
format: "esm",
|
|
426
|
+
inputOptions: { resolve: { mainFields: ["module", "main"] } },
|
|
427
|
+
logLevel: "silent",
|
|
428
|
+
minify: params.config.build.minify,
|
|
429
|
+
outDir: params.config.buildOutDir,
|
|
430
|
+
outputOptions: { codeSplitting: false },
|
|
431
|
+
platform: "node",
|
|
432
|
+
plugins: [createAutoloadPlugin({
|
|
433
|
+
commandsDir: params.config.commands,
|
|
434
|
+
tagModulePath: TAG_MODULE_PATH
|
|
435
|
+
}), ...buildPlugins(params.compile ?? false)],
|
|
436
|
+
sourcemap: params.config.build.sourcemap,
|
|
437
|
+
target: params.config.build.target,
|
|
438
|
+
treeshake: true
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Convert a resolved bundler config to a tsdown InlineConfig for watch mode.
|
|
443
|
+
*
|
|
444
|
+
* @param params - The resolved config, optional version, and optional success callback.
|
|
445
|
+
* @returns A tsdown InlineConfig with `watch: true`.
|
|
446
|
+
*/
|
|
447
|
+
function toTsdownWatchConfig(params) {
|
|
448
|
+
return {
|
|
449
|
+
...toTsdownBuildConfig({ config: params.config }),
|
|
450
|
+
logLevel: "error",
|
|
451
|
+
onSuccess: params.onSuccess,
|
|
452
|
+
watch: true
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Build the `deps` configuration for tsdown.
|
|
457
|
+
*
|
|
458
|
+
* When compiling to a standalone binary, all dependencies must be inlined so
|
|
459
|
+
* `bun build --compile` never encounters unresolvable bare imports. Only
|
|
460
|
+
* Node.js builtins and explicit user externals are kept external.
|
|
461
|
+
*
|
|
462
|
+
* In normal (non-compile) mode, only `@maltty/*` packages are force-bundled
|
|
463
|
+
* and everything else in `node_modules` is left external by tsdown's default.
|
|
464
|
+
*
|
|
465
|
+
* @private
|
|
466
|
+
* @param userExternals - Additional packages the user explicitly marked external.
|
|
467
|
+
* @param compile - Whether the build targets a compiled binary.
|
|
468
|
+
* @returns A tsdown `deps` configuration object.
|
|
469
|
+
*/
|
|
470
|
+
function buildDeps(userExternals, compile) {
|
|
471
|
+
return {
|
|
472
|
+
alwaysBundle: resolveAlwaysBundle(compile),
|
|
473
|
+
neverBundle: [...NODE_BUILTINS, ...userExternals]
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Resolve the `alwaysBundle` rule list based on whether we're compiling.
|
|
478
|
+
*
|
|
479
|
+
* @private
|
|
480
|
+
* @param compile - Whether the build targets a compiled binary.
|
|
481
|
+
* @returns The matchers tsdown should always bundle.
|
|
482
|
+
*/
|
|
483
|
+
function resolveAlwaysBundle(compile) {
|
|
484
|
+
if (compile) return [/./];
|
|
485
|
+
return [...ALWAYS_BUNDLE];
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Build the `define` map for tsdown/rolldown.
|
|
489
|
+
*
|
|
490
|
+
* Merges three sources (lowest to highest precedence):
|
|
491
|
+
* 1. `MALTTY_PUBLIC_*` env vars — prefixed with `process.env.` for rolldown replacement
|
|
492
|
+
* 2. `__MALTTY_VERSION__` — injected when a version string is available
|
|
493
|
+
* 3. Explicit `define` from `maltty.config.ts` — user overrides win
|
|
494
|
+
*
|
|
495
|
+
* @private
|
|
496
|
+
* @param params - The version and user-defined constants.
|
|
497
|
+
* @returns A define map for tsdown/rolldown.
|
|
498
|
+
*/
|
|
499
|
+
function buildDefine(params) {
|
|
500
|
+
const envVars = resolveBuildVars();
|
|
501
|
+
const envDefines = Object.fromEntries(Object.entries(envVars).map(([key, value]) => [`process.env.${key}`, value]));
|
|
502
|
+
const versionDefine = M(params.version).with(void 0, () => ({})).otherwise((v) => ({ __MALTTY_VERSION__: JSON.stringify(v) }));
|
|
503
|
+
return {
|
|
504
|
+
...envDefines,
|
|
505
|
+
...versionDefine,
|
|
506
|
+
...params.define
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Resolve `MALTTY_PUBLIC_*` environment variables into a define map.
|
|
511
|
+
*
|
|
512
|
+
* Scans `process.env` for keys prefixed with `MALTTY_PUBLIC_` and returns
|
|
513
|
+
* clean key-value pairs with JSON-stringified values.
|
|
514
|
+
*
|
|
515
|
+
* @param env - The environment variables to scan (defaults to `process.env`).
|
|
516
|
+
* @returns A define map with clean keys (no `process.env.` prefix).
|
|
517
|
+
*/
|
|
518
|
+
function resolveBuildVars(env = process.env) {
|
|
519
|
+
return Object.fromEntries(Object.entries(env).filter(([key]) => key.startsWith("MALTTY_PUBLIC_")).map(([key, value]) => [key, JSON.stringify(value ?? "")]));
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Build additional plugins needed when compiling to a standalone binary.
|
|
523
|
+
*
|
|
524
|
+
* When `compile` is true, includes a stub plugin that replaces optional/conditional
|
|
525
|
+
* dependencies with empty modules so rolldown (and subsequently bun compile) never
|
|
526
|
+
* attempts to resolve packages that don't exist at runtime.
|
|
527
|
+
*
|
|
528
|
+
* @private
|
|
529
|
+
* @param compile - Whether the build targets a compiled binary.
|
|
530
|
+
* @returns An array of rolldown plugins (empty when not compiling).
|
|
531
|
+
*/
|
|
532
|
+
function buildPlugins(compile) {
|
|
533
|
+
if (compile) return [createStubPlugin(STUB_PACKAGES)];
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Create a rolldown plugin that replaces specified packages with empty modules.
|
|
538
|
+
*
|
|
539
|
+
* Libraries like c12 and ink have optional/conditional dependencies behind
|
|
540
|
+
* dynamic `import()` calls or runtime guards (e.g. `isDev()`). When all deps
|
|
541
|
+
* are inlined for compile mode, rolldown traces into these statically. Stubbing
|
|
542
|
+
* at the resolve level ensures the real package is never loaded.
|
|
543
|
+
*
|
|
544
|
+
* @private
|
|
545
|
+
* @param packages - Package names to replace with empty modules.
|
|
546
|
+
* @returns A rolldown plugin.
|
|
547
|
+
*/
|
|
548
|
+
function createStubPlugin(packages) {
|
|
549
|
+
const stubbed = new Set(packages);
|
|
550
|
+
const STUB_PREFIX = "\0stub:";
|
|
551
|
+
return {
|
|
552
|
+
name: "maltty-stub-packages",
|
|
553
|
+
resolveId(source) {
|
|
554
|
+
if (stubbed.has(source)) return `${STUB_PREFIX}${source}`;
|
|
555
|
+
return null;
|
|
556
|
+
},
|
|
557
|
+
load(id) {
|
|
558
|
+
if (id.startsWith(STUB_PREFIX)) return "export default undefined;";
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Remove maltty build artifacts from the output directory.
|
|
565
|
+
*
|
|
566
|
+
* Removes files matching known build artifact extensions (`.js`, `.mjs`,
|
|
567
|
+
* `.js.map`, `.mjs.map`). When compile mode is active, also removes the
|
|
568
|
+
* exact binary files that would be produced based on the resolved compile
|
|
569
|
+
* name and targets.
|
|
570
|
+
*
|
|
571
|
+
* @param params - The resolved config and whether compile mode is active.
|
|
572
|
+
* @returns A {@link CleanResult} describing what was removed and what was skipped.
|
|
573
|
+
*/
|
|
574
|
+
async function clean(params) {
|
|
575
|
+
const [listError, entries] = await fs_exports.list(params.resolved.buildOutDir);
|
|
576
|
+
if (listError) return {
|
|
577
|
+
foreign: [],
|
|
578
|
+
removed: []
|
|
579
|
+
};
|
|
580
|
+
const binaryNames = M(params).with({ compile: true }, ({ resolved }) => buildBinaryNames(resolved.compile.name, resolved.compile.targets)).otherwise(() => /* @__PURE__ */ new Set());
|
|
581
|
+
const results = await Promise.all(entries.map(async (name) => {
|
|
582
|
+
if (isBuildArtifact(name) || binaryNames.has(name)) {
|
|
583
|
+
const [removeError] = await fs_exports.remove(join(params.resolved.buildOutDir, name));
|
|
584
|
+
if (removeError) return {
|
|
585
|
+
type: "foreign",
|
|
586
|
+
name
|
|
587
|
+
};
|
|
588
|
+
return {
|
|
589
|
+
type: "removed",
|
|
590
|
+
name
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
type: "foreign",
|
|
595
|
+
name
|
|
596
|
+
};
|
|
597
|
+
}));
|
|
598
|
+
return {
|
|
599
|
+
removed: results.filter((r) => r.type === "removed").map((r) => r.name),
|
|
600
|
+
foreign: results.filter((r) => r.type === "foreign").map((r) => r.name)
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Check whether a filename matches a known build artifact extension.
|
|
605
|
+
*
|
|
606
|
+
* @private
|
|
607
|
+
* @param filename - The filename to check.
|
|
608
|
+
* @returns `true` when the file ends with a known build artifact extension.
|
|
609
|
+
*/
|
|
610
|
+
function isBuildArtifact(filename) {
|
|
611
|
+
return BUILD_ARTIFACT_EXTENSIONS.some((ext) => filename.endsWith(ext));
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Build the set of exact binary filenames that compile would produce.
|
|
615
|
+
*
|
|
616
|
+
* Single-target builds produce `{name}`, multi-target builds produce
|
|
617
|
+
* `{name}-{target}`. Windows targets append `.exe` to match the file bun
|
|
618
|
+
* actually creates on disk.
|
|
619
|
+
*
|
|
620
|
+
* @private
|
|
621
|
+
* @param name - The resolved binary base name.
|
|
622
|
+
* @param targets - The resolved compile targets (may be empty → defaults used).
|
|
623
|
+
* @returns A set of filenames to remove.
|
|
624
|
+
*/
|
|
625
|
+
function buildBinaryNames(name, targets) {
|
|
626
|
+
const resolvedTargets = M(targets).with([], () => compileTargets.filter((t) => t.default).map((t) => t.target)).otherwise(() => targets);
|
|
627
|
+
const names = resolvedTargets.map((target) => {
|
|
628
|
+
const base = M(resolvedTargets.length).with(1, () => name).otherwise(() => `${name}-${target}`);
|
|
629
|
+
if (target.startsWith("windows-")) return `${base}.exe`;
|
|
630
|
+
return base;
|
|
631
|
+
});
|
|
632
|
+
return new Set(names);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Build a descriptive error message for a failed tsdown operation.
|
|
636
|
+
*
|
|
637
|
+
* When verbose is false only a short header is returned. When verbose is true
|
|
638
|
+
* the underlying error message is appended so callers get actionable detail.
|
|
639
|
+
*
|
|
640
|
+
* @param params - The phase, error, and verbose flag.
|
|
641
|
+
* @returns A formatted error message.
|
|
642
|
+
*/
|
|
643
|
+
function formatBuildError(params) {
|
|
644
|
+
const header = `tsdown ${params.phase} failed`;
|
|
645
|
+
const detail = toError(params.error).message.trim();
|
|
646
|
+
if (params.verbose && detail.length > 0) return `${header}\n${detail}`;
|
|
647
|
+
return header;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Known entry file names produced by tsdown for ESM builds, in preference order.
|
|
651
|
+
*/
|
|
652
|
+
const ENTRY_CANDIDATES = ["index.mjs", "index.js"];
|
|
653
|
+
/**
|
|
654
|
+
* Resolve the bundled entry file in a build output directory.
|
|
655
|
+
*
|
|
656
|
+
* tsdown may produce `index.mjs` or `index.js` depending on the project's
|
|
657
|
+
* `package.json` `type` field and tsdown configuration. This function checks
|
|
658
|
+
* for both candidates and returns the first one that exists on disk.
|
|
659
|
+
*
|
|
660
|
+
* @param outDir - Absolute path to the build output directory.
|
|
661
|
+
* @returns The absolute path to the entry file, or `undefined` when none is found.
|
|
662
|
+
*/
|
|
663
|
+
async function resolveBuildEntry(outDir) {
|
|
664
|
+
const candidates = ENTRY_CANDIDATES.map((name) => join(outDir, name));
|
|
665
|
+
return M((await Promise.all(candidates.map(async (path) => ({
|
|
666
|
+
path,
|
|
667
|
+
found: await fs_exports.exists(path)
|
|
668
|
+
})))).find((r) => r.found)).with(void 0, () => void 0).otherwise((entry) => entry.path);
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Run the tsdown build with a resolved config.
|
|
672
|
+
*
|
|
673
|
+
* Cleans artifacts when enabled, maps to a tsdown InlineConfig, and invokes
|
|
674
|
+
* tsdown's build API.
|
|
675
|
+
*
|
|
676
|
+
* @param params - The resolved config and whether compile mode is active.
|
|
677
|
+
* @returns A result tuple with build output on success or an Error on failure.
|
|
678
|
+
*/
|
|
679
|
+
async function build$1(params) {
|
|
680
|
+
if (params.resolved.build.clean) await clean({
|
|
681
|
+
resolved: params.resolved,
|
|
682
|
+
compile: params.compile
|
|
683
|
+
});
|
|
684
|
+
const inlineConfig = toTsdownBuildConfig({
|
|
685
|
+
compile: params.compile,
|
|
686
|
+
config: params.resolved
|
|
687
|
+
});
|
|
688
|
+
const [buildError] = await attemptAsync(() => build(inlineConfig));
|
|
689
|
+
if (buildError) return err(new Error(formatBuildError({
|
|
690
|
+
phase: "build",
|
|
691
|
+
error: buildError,
|
|
692
|
+
verbose: params.verbose ?? false
|
|
693
|
+
}), { cause: buildError }));
|
|
694
|
+
const entryFile = await resolveBuildEntry(params.resolved.buildOutDir);
|
|
695
|
+
if (!entryFile) return err(/* @__PURE__ */ new Error(`build produced no entry file in ${params.resolved.buildOutDir}`));
|
|
696
|
+
return ok({
|
|
697
|
+
define: {
|
|
698
|
+
...resolveBuildVars(),
|
|
699
|
+
...params.resolved.build.define
|
|
700
|
+
},
|
|
701
|
+
entryFile,
|
|
702
|
+
outDir: params.resolved.buildOutDir,
|
|
703
|
+
version: params.resolved.version
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Start a watch-mode build using tsdown.
|
|
708
|
+
*
|
|
709
|
+
* The returned promise resolves only when tsdown's watch terminates (typically on process exit).
|
|
710
|
+
*
|
|
711
|
+
* @param params - The resolved config, optional success callback, and verbose flag.
|
|
712
|
+
* @returns A result tuple with void on success or an Error on failure.
|
|
713
|
+
*/
|
|
714
|
+
async function watch(params) {
|
|
715
|
+
const watchConfig = toTsdownWatchConfig({
|
|
716
|
+
config: params.resolved,
|
|
717
|
+
onSuccess: params.onSuccess
|
|
718
|
+
});
|
|
719
|
+
const [watchError] = await attemptAsync(() => build(watchConfig));
|
|
720
|
+
if (watchError) return err(new Error(formatBuildError({
|
|
721
|
+
phase: "watch",
|
|
722
|
+
error: watchError,
|
|
723
|
+
verbose: params.verbose ?? false
|
|
724
|
+
}), { cause: watchError }));
|
|
725
|
+
return ok();
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Compile a maltty CLI tool into standalone binaries using `bun build --compile`.
|
|
729
|
+
*
|
|
730
|
+
* Expects the bundled entry to already exist in `outDir` (i.e., `build()` must
|
|
731
|
+
* be run first). For each requested target (or defaults when none configured),
|
|
732
|
+
* spawns `bun build --compile` to produce a self-contained binary.
|
|
733
|
+
*
|
|
734
|
+
* @param params - The resolved config, lifecycle hooks, and verbose flag.
|
|
735
|
+
* @returns A result tuple with compile output on success or an Error on failure.
|
|
736
|
+
*/
|
|
737
|
+
async function compile(params) {
|
|
738
|
+
if (!await process_exports.exists("bun")) return err(/* @__PURE__ */ new Error("bun is not installed or not found in PATH. Install it from https://bun.sh to use compile."));
|
|
739
|
+
const bundledEntry = await resolveBuildEntry(params.resolved.buildOutDir);
|
|
740
|
+
if (!bundledEntry) return err(/* @__PURE__ */ new Error(`bundled entry not found in ${params.resolved.buildOutDir} — run build() first`));
|
|
741
|
+
const targets = resolveTargets(params.resolved.compile.targets);
|
|
742
|
+
const isMultiTarget = targets.length > 1;
|
|
743
|
+
const results = await compileTargetsSequentially({
|
|
744
|
+
autoloadDotenv: params.resolved.compile.autoloadDotenv,
|
|
745
|
+
bundledEntry,
|
|
746
|
+
cwd: params.resolved.cwd,
|
|
747
|
+
isMultiTarget,
|
|
748
|
+
lifecycle: params.lifecycle,
|
|
749
|
+
name: params.resolved.compile.name,
|
|
750
|
+
outDir: params.resolved.compileOutDir,
|
|
751
|
+
targets,
|
|
752
|
+
verbose: params.verbose ?? false
|
|
753
|
+
});
|
|
754
|
+
await cleanBunBuildArtifacts(params.resolved.cwd);
|
|
755
|
+
const failedResult = results.find((r) => r[0] !== null);
|
|
756
|
+
if (failedResult) return err(failedResult[0]);
|
|
757
|
+
return ok({ binaries: results.filter((r) => r[1] !== null).map(([, binary]) => binary) });
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Look up the human-readable label for a compile target.
|
|
761
|
+
*
|
|
762
|
+
* @param target - The compile target identifier.
|
|
763
|
+
* @returns A descriptive label (e.g., "macOS Apple Silicon").
|
|
764
|
+
*/
|
|
765
|
+
function resolveTargetLabel(target) {
|
|
766
|
+
const entry = compileTargets.find((t) => t.target === target);
|
|
767
|
+
if (entry) return entry.label;
|
|
768
|
+
return target;
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Compile targets one at a time to avoid overwhelming bun with concurrent processes.
|
|
772
|
+
*
|
|
773
|
+
* @private
|
|
774
|
+
* @param params - The targets and compilation parameters.
|
|
775
|
+
* @returns The accumulated result tuples for each target.
|
|
776
|
+
*/
|
|
777
|
+
async function compileTargetsSequentially(params) {
|
|
778
|
+
return params.targets.reduce(async (accPromise, target) => {
|
|
779
|
+
const acc = await accPromise;
|
|
780
|
+
const meta = {
|
|
781
|
+
target,
|
|
782
|
+
label: resolveTargetLabel(target)
|
|
783
|
+
};
|
|
784
|
+
if (params.lifecycle.onStepStart) await params.lifecycle.onStepStart({
|
|
785
|
+
phase: "compile",
|
|
786
|
+
step: "target",
|
|
787
|
+
meta
|
|
788
|
+
});
|
|
789
|
+
const result = await compileSingleTarget({
|
|
790
|
+
autoloadDotenv: params.autoloadDotenv,
|
|
791
|
+
bundledEntry: params.bundledEntry,
|
|
792
|
+
cwd: params.cwd,
|
|
793
|
+
isMultiTarget: params.isMultiTarget,
|
|
794
|
+
name: params.name,
|
|
795
|
+
outDir: params.outDir,
|
|
796
|
+
target,
|
|
797
|
+
verbose: params.verbose
|
|
798
|
+
});
|
|
799
|
+
if (params.lifecycle.onStepFinish) await params.lifecycle.onStepFinish({
|
|
800
|
+
phase: "compile",
|
|
801
|
+
step: "target",
|
|
802
|
+
meta
|
|
803
|
+
});
|
|
804
|
+
return [...acc, result];
|
|
805
|
+
}, Promise.resolve([]));
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Compile a single target via `bun build --compile`.
|
|
809
|
+
*
|
|
810
|
+
* @private
|
|
811
|
+
* @param params - Target compilation parameters.
|
|
812
|
+
* @returns A result tuple with the compiled binary info or an error.
|
|
813
|
+
*/
|
|
814
|
+
async function compileSingleTarget(params) {
|
|
815
|
+
const binaryName = resolveBinaryName$1(params.name, params.target, params.isMultiTarget);
|
|
816
|
+
const outfile = join(params.outDir, binaryName);
|
|
817
|
+
const [mapError, bunTarget] = mapCompileTarget(params.target);
|
|
818
|
+
if (mapError) return err(mapError);
|
|
819
|
+
const args = [
|
|
820
|
+
"build",
|
|
821
|
+
"--compile",
|
|
822
|
+
...resolveAutoloadFlags({ autoloadDotenv: params.autoloadDotenv }),
|
|
823
|
+
params.bundledEntry,
|
|
824
|
+
"--outfile",
|
|
825
|
+
outfile,
|
|
826
|
+
"--target",
|
|
827
|
+
bunTarget
|
|
828
|
+
];
|
|
829
|
+
const [execError] = await process_exports.exec({
|
|
830
|
+
cmd: "bun",
|
|
831
|
+
args,
|
|
832
|
+
cwd: params.cwd
|
|
833
|
+
});
|
|
834
|
+
if (execError) return err(new Error(formatCompileError(params.target, execError, params.verbose), { cause: execError }));
|
|
835
|
+
return ok({
|
|
836
|
+
label: resolveTargetLabel(params.target),
|
|
837
|
+
path: outfile,
|
|
838
|
+
target: params.target
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Resolve the list of compile targets, falling back to the default set.
|
|
843
|
+
*
|
|
844
|
+
* @private
|
|
845
|
+
* @param explicit - User-specified targets (may be empty).
|
|
846
|
+
* @returns The targets to compile for.
|
|
847
|
+
*/
|
|
848
|
+
function resolveTargets(explicit) {
|
|
849
|
+
if (explicit.length > 0) return explicit;
|
|
850
|
+
return compileTargets.filter((t) => t.default).map((t) => t.target);
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Build the output binary name, appending the target suffix for multi-target
|
|
854
|
+
* builds and the `.exe` extension for Windows targets.
|
|
855
|
+
*
|
|
856
|
+
* Windows targets must end with `.exe` because that is the file bun actually
|
|
857
|
+
* produces (bun auto-appends `.exe` for any `bun-windows-*` target). Recording
|
|
858
|
+
* the path without `.exe` would cause filesystem operations on the recorded
|
|
859
|
+
* path to fail and would diverge from what is on disk.
|
|
860
|
+
*
|
|
861
|
+
* @private
|
|
862
|
+
* @param name - Base binary name.
|
|
863
|
+
* @param target - The compile target.
|
|
864
|
+
* @param isMultiTarget - Whether multiple targets are being compiled.
|
|
865
|
+
* @returns The resolved binary file name.
|
|
866
|
+
*/
|
|
867
|
+
function resolveBinaryName$1(name, target, isMultiTarget) {
|
|
868
|
+
return appendBinaryExtension(buildBaseBinaryName(name, target, isMultiTarget), target);
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Build the base name without the platform extension.
|
|
872
|
+
*
|
|
873
|
+
* @private
|
|
874
|
+
* @param name - Base binary name.
|
|
875
|
+
* @param target - The compile target.
|
|
876
|
+
* @param isMultiTarget - Whether multiple targets are being compiled.
|
|
877
|
+
* @returns The base binary name with optional target suffix.
|
|
878
|
+
*/
|
|
879
|
+
function buildBaseBinaryName(name, target, isMultiTarget) {
|
|
880
|
+
if (isMultiTarget) return `${name}-${target}`;
|
|
881
|
+
return name;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Append the `.exe` extension to Windows binary names.
|
|
885
|
+
*
|
|
886
|
+
* @private
|
|
887
|
+
* @param base - The base binary name.
|
|
888
|
+
* @param target - The compile target.
|
|
889
|
+
* @returns The base name plus `.exe` for Windows targets, or the base unchanged.
|
|
890
|
+
*/
|
|
891
|
+
function appendBinaryExtension(base, target) {
|
|
892
|
+
if (target.startsWith("windows-")) return `${base}.exe`;
|
|
893
|
+
return base;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Map a `CompileTarget` to Bun's `--target` string.
|
|
897
|
+
*
|
|
898
|
+
* Every supported maltty target must have an explicit mapping. An unrecognized
|
|
899
|
+
* target is a fatal error — it means `compileTargets` was extended without
|
|
900
|
+
* updating this function.
|
|
901
|
+
*
|
|
902
|
+
* @private
|
|
903
|
+
* @param target - The maltty compile target.
|
|
904
|
+
* @returns A result with the Bun target string, or an Error for unknown targets.
|
|
905
|
+
*/
|
|
906
|
+
function mapCompileTarget(target) {
|
|
907
|
+
return M(target).with("darwin-arm64", () => ok("bun-darwin-arm64")).with("darwin-x64", () => ok("bun-darwin-x64")).with("linux-arm64", () => ok("bun-linux-arm64")).with("linux-x64", () => ok("bun-linux-x64")).with("linux-x64-musl", () => ok("bun-linux-x64-musl")).with("windows-arm64", () => ok("bun-windows-arm64")).with("windows-x64", () => ok("bun-windows-x64")).otherwise(() => err(/* @__PURE__ */ new Error(`unknown compile target: ${target}`)));
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Build a descriptive error message for a failed compile target.
|
|
911
|
+
*
|
|
912
|
+
* @private
|
|
913
|
+
* @param target - The compile target that failed.
|
|
914
|
+
* @param execError - The error returned by execFile.
|
|
915
|
+
* @param verbose - Whether to include stderr output in the message.
|
|
916
|
+
* @returns A formatted error message.
|
|
917
|
+
*/
|
|
918
|
+
function formatCompileError(target, execError, verbose) {
|
|
919
|
+
const header = `bun build --compile failed for target ${target}`;
|
|
920
|
+
if (!verbose) return header;
|
|
921
|
+
const { stderr } = execError;
|
|
922
|
+
if (stderr && stderr.trim().length > 0) return `${header}\n${stderr.trim()}`;
|
|
923
|
+
return header;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Build the CLI flags that control Bun's compile-time config autoloading.
|
|
927
|
+
*
|
|
928
|
+
* `bunfig.toml` loading is always disabled — maltty CLIs should never load
|
|
929
|
+
* Bun runtime config. `.env` loading is controlled by the `autoloadDotenv`
|
|
930
|
+
* option (disabled by default).
|
|
931
|
+
*
|
|
932
|
+
* @private
|
|
933
|
+
* @param params - The autoload settings.
|
|
934
|
+
* @returns An array of CLI flag strings.
|
|
935
|
+
*/
|
|
936
|
+
function resolveAutoloadFlags(params) {
|
|
937
|
+
return [{
|
|
938
|
+
enabled: false,
|
|
939
|
+
flag: "--no-compile-autoload-bunfig"
|
|
940
|
+
}, {
|
|
941
|
+
enabled: params.autoloadDotenv,
|
|
942
|
+
flag: "--no-compile-autoload-dotenv"
|
|
943
|
+
}].filter((c) => !c.enabled).map((c) => c.flag);
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Remove temporary `.bun-build` files that `bun build --compile` leaves behind.
|
|
947
|
+
*
|
|
948
|
+
* @private
|
|
949
|
+
* @param cwd - The working directory to clean.
|
|
950
|
+
*/
|
|
951
|
+
async function cleanBunBuildArtifacts(cwd) {
|
|
952
|
+
const [listError, entries] = await fs_exports.list(cwd);
|
|
953
|
+
if (listError) return;
|
|
954
|
+
const artifacts = entries.filter((name) => name.endsWith(".bun-build")).map((name) => join(cwd, name));
|
|
955
|
+
await Promise.allSettled(artifacts.map(fs_exports.remove));
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Normalize the `compile` config field from `boolean | CompileOptions | undefined` to `CompileOptions`.
|
|
959
|
+
*
|
|
960
|
+
* - `true` → `{}` (compile with defaults)
|
|
961
|
+
* - `false` / `undefined` → `{}` (no explicit options, caller decides whether to compile)
|
|
962
|
+
* - object → pass through
|
|
963
|
+
*
|
|
964
|
+
* @param value - The raw compile config value.
|
|
965
|
+
* @returns A normalized CompileOptions object.
|
|
966
|
+
*/
|
|
967
|
+
function normalizeCompileOptions(value) {
|
|
968
|
+
if (typeof value === "object") return value;
|
|
969
|
+
return {};
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Fill defaults and resolve relative paths against `cwd`.
|
|
973
|
+
*
|
|
974
|
+
* This is a pure function — the incoming config is already validated by `@maltty/config`.
|
|
975
|
+
* It only fills missing optional fields with defaults and resolves paths to absolute.
|
|
976
|
+
*
|
|
977
|
+
* @param params - The raw config and working directory.
|
|
978
|
+
* @returns A fully resolved bundler configuration.
|
|
979
|
+
*/
|
|
980
|
+
function resolveConfig(params) {
|
|
981
|
+
const { config, cwd } = params;
|
|
982
|
+
const entry = resolve(cwd, config.entry ?? "./src/index.ts");
|
|
983
|
+
const commands = resolve(cwd, config.commands ?? "./commands");
|
|
984
|
+
const buildOpts = config.build ?? {};
|
|
985
|
+
const compileOpts = normalizeCompileOptions(config.compile);
|
|
986
|
+
const buildOutDir = resolve(cwd, buildOpts.out ?? "./dist");
|
|
987
|
+
const compileOutDir = resolve(cwd, compileOpts.out ?? "./dist");
|
|
988
|
+
return {
|
|
989
|
+
build: {
|
|
990
|
+
clean: buildOpts.clean ?? true,
|
|
991
|
+
define: buildOpts.define ?? {},
|
|
992
|
+
external: buildOpts.external ?? [],
|
|
993
|
+
minify: buildOpts.minify ?? true,
|
|
994
|
+
sourcemap: buildOpts.sourcemap ?? true,
|
|
995
|
+
target: buildOpts.target ?? "node18"
|
|
996
|
+
},
|
|
997
|
+
buildOutDir,
|
|
998
|
+
commands,
|
|
999
|
+
compile: {
|
|
1000
|
+
autoloadDotenv: compileOpts.autoloadDotenv ?? false,
|
|
1001
|
+
name: compileOpts.name ?? params.binaryName,
|
|
1002
|
+
targets: compileOpts.targets ?? []
|
|
1003
|
+
},
|
|
1004
|
+
compileOutDir,
|
|
1005
|
+
cwd,
|
|
1006
|
+
entry,
|
|
1007
|
+
include: config.include ?? [],
|
|
1008
|
+
version: params.version
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Create a bundler instance for a maltty CLI project.
|
|
1013
|
+
*
|
|
1014
|
+
* Reads the project manifest once, resolves config, and returns methods
|
|
1015
|
+
* that share the resolved state. Lifecycle hooks fire at phase boundaries.
|
|
1016
|
+
* Per-call overrides replace base hooks for that invocation.
|
|
1017
|
+
*
|
|
1018
|
+
* @param params - The config, working directory, and optional lifecycle hooks.
|
|
1019
|
+
* @returns A bundler with build, watch, and compile methods.
|
|
1020
|
+
*/
|
|
1021
|
+
async function createBundler(params) {
|
|
1022
|
+
const [, manifest] = await readManifest(params.cwd);
|
|
1023
|
+
const { version, name: packageName } = manifest ?? {};
|
|
1024
|
+
const binaryName = M(params.config.compile).with({ name: z.string }, (c) => c.name).otherwise(() => resolveBinaryName(packageName));
|
|
1025
|
+
const resolved = resolveConfig({
|
|
1026
|
+
config: params.config,
|
|
1027
|
+
cwd: params.cwd,
|
|
1028
|
+
version,
|
|
1029
|
+
binaryName
|
|
1030
|
+
});
|
|
1031
|
+
const hasCompile = !isNil(params.config.compile);
|
|
1032
|
+
const baseLifecycle = {
|
|
1033
|
+
onFinish: params.onFinish,
|
|
1034
|
+
onStart: params.onStart,
|
|
1035
|
+
onStepFinish: params.onStepFinish,
|
|
1036
|
+
onStepStart: params.onStepStart
|
|
1037
|
+
};
|
|
1038
|
+
return {
|
|
1039
|
+
build: async (overrides = {}) => {
|
|
1040
|
+
const lifecycle = resolveLifecycle(baseLifecycle, overrides);
|
|
1041
|
+
await lifecycle.onStart({ phase: "build" });
|
|
1042
|
+
const result = await build$1({
|
|
1043
|
+
compile: hasCompile,
|
|
1044
|
+
resolved,
|
|
1045
|
+
verbose: overrides.verbose
|
|
1046
|
+
});
|
|
1047
|
+
await lifecycle.onFinish({ phase: "build" });
|
|
1048
|
+
return result;
|
|
1049
|
+
},
|
|
1050
|
+
watch: async (overrides = {}) => {
|
|
1051
|
+
const lifecycle = resolveLifecycle(baseLifecycle, overrides);
|
|
1052
|
+
await lifecycle.onStart({ phase: "watch" });
|
|
1053
|
+
const result = await watch({
|
|
1054
|
+
onSuccess: overrides.onSuccess,
|
|
1055
|
+
resolved,
|
|
1056
|
+
verbose: overrides.verbose
|
|
1057
|
+
});
|
|
1058
|
+
await lifecycle.onFinish({ phase: "watch" });
|
|
1059
|
+
return result;
|
|
1060
|
+
},
|
|
1061
|
+
compile: async (overrides = {}) => {
|
|
1062
|
+
const lifecycle = resolveLifecycle(baseLifecycle, overrides);
|
|
1063
|
+
await lifecycle.onStart({ phase: "compile" });
|
|
1064
|
+
const result = await compile({
|
|
1065
|
+
lifecycle,
|
|
1066
|
+
resolved,
|
|
1067
|
+
verbose: overrides.verbose
|
|
1068
|
+
});
|
|
1069
|
+
await lifecycle.onFinish({ phase: "compile" });
|
|
1070
|
+
return result;
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Derive the binary name from the package.json name, stripping scope.
|
|
1076
|
+
*
|
|
1077
|
+
* @private
|
|
1078
|
+
* @param packageName - The package name from manifest, or undefined.
|
|
1079
|
+
* @returns The binary name.
|
|
1080
|
+
*/
|
|
1081
|
+
function resolveBinaryName(packageName) {
|
|
1082
|
+
if (!packageName) return "cli";
|
|
1083
|
+
return stripScope(packageName);
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Strip the npm scope prefix from a package name.
|
|
1087
|
+
*
|
|
1088
|
+
* @private
|
|
1089
|
+
* @param name - The package name (e.g. `@scope/my-cli`).
|
|
1090
|
+
* @returns The unscoped name (e.g. `my-cli`).
|
|
1091
|
+
*/
|
|
1092
|
+
function stripScope(name) {
|
|
1093
|
+
const slashIndex = name.indexOf("/");
|
|
1094
|
+
if (name.startsWith("@") && slashIndex > 0) return name.slice(slashIndex + 1);
|
|
1095
|
+
return name;
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Merge base lifecycle hooks with per-call overrides.
|
|
1099
|
+
*
|
|
1100
|
+
* Per-call hooks replace base hooks (no chaining). Missing hooks
|
|
1101
|
+
* are filled with no-ops so callers don't need null checks.
|
|
1102
|
+
*
|
|
1103
|
+
* @private
|
|
1104
|
+
* @param base - The base lifecycle hooks from the factory.
|
|
1105
|
+
* @param overrides - Per-call hook overrides.
|
|
1106
|
+
* @returns A lifecycle with all hooks guaranteed to be defined.
|
|
1107
|
+
*/
|
|
1108
|
+
function resolveLifecycle(base, overrides = {}) {
|
|
1109
|
+
return {
|
|
1110
|
+
onFinish: overrides.onFinish ?? base.onFinish ?? noop,
|
|
1111
|
+
onStart: overrides.onStart ?? base.onStart ?? noop,
|
|
1112
|
+
onStepFinish: overrides.onStepFinish ?? base.onStepFinish ?? noop,
|
|
1113
|
+
onStepStart: overrides.onStepStart ?? base.onStepStart ?? noop
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
//#endregion
|
|
1117
|
+
export { normalizeCompileOptions as i, DEFAULT_ENTRY as n, createBundler as r, DEFAULT_COMMANDS as t };
|
|
1118
|
+
|
|
1119
|
+
//# sourceMappingURL=dist-DW_gRP7_.js.map
|