@crustjs/crust-linux-arm64-musl 0.3.5 → 0.4.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/bin/crust-bun-linux-arm64-musl +2 -2
- package/bin/dist/index.d.ts +74 -0
- package/bin/dist/index.js +3163 -0
- package/package.json +1 -1
|
@@ -0,0 +1,3163 @@
|
|
|
1
|
+
import { accessSync, constants, copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
8
|
+
import { once } from "node:events";
|
|
9
|
+
import { text } from "node:stream/consumers";
|
|
10
|
+
//#region ../utils/dist/artifacts.js
|
|
11
|
+
/**
|
|
12
|
+
* Set by `crust build` while it prepares the Command Snapshot: the absolute
|
|
13
|
+
* entry-isolated directory Extension build hooks write into. Core reads it
|
|
14
|
+
* to run the hooks; `resolveArtifactDir` reads it so sections evaluated during
|
|
15
|
+
* that run see the artifacts being built instead of the wiped `.crust/root`.
|
|
16
|
+
*/
|
|
17
|
+
const BUILD_OUT_DIR_ENV = "CRUST_INTERNAL_BUILD_OUT_DIR";
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region ../core/dist/invocation-ka-BNJKZ.js
|
|
20
|
+
/**
|
|
21
|
+
* A typed error for runtime recipe, Extension, Context, documentation, argv, and validation failures.
|
|
22
|
+
*
|
|
23
|
+
* Every `CrustError` carries a {@link CrustErrorCode} that identifies the specific
|
|
24
|
+
* failure, enabling programmatic error handling without fragile message parsing.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { CrustError } from "@crustjs/core";
|
|
29
|
+
*
|
|
30
|
+
* const outcome = await app.run(["deploy"], { args: { target: "prod" } });
|
|
31
|
+
* if (outcome.status === "failed") {
|
|
32
|
+
* const err = outcome.error;
|
|
33
|
+
* if (err instanceof CrustError) {
|
|
34
|
+
* console.error(`[${err.code}] ${err.message}`);
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
var CrustError = class extends Error {
|
|
40
|
+
/** Machine-readable error code for programmatic handling */
|
|
41
|
+
code;
|
|
42
|
+
/** Structured payload for programmatic handling */
|
|
43
|
+
details;
|
|
44
|
+
/** Optional wrapped original error/value */
|
|
45
|
+
cause;
|
|
46
|
+
constructor(code, message, ...details) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "CrustError";
|
|
49
|
+
this.code = code;
|
|
50
|
+
this.details = details[0];
|
|
51
|
+
}
|
|
52
|
+
is(code) {
|
|
53
|
+
return Object.is(this.code, code);
|
|
54
|
+
}
|
|
55
|
+
withCause(cause) {
|
|
56
|
+
this.cause = cause;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
toJSON() {
|
|
60
|
+
return {
|
|
61
|
+
code: this.code,
|
|
62
|
+
message: this.message,
|
|
63
|
+
details: this.details
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
globalThis.AsyncDisposableStack;
|
|
68
|
+
Object.freeze({ [Symbol("crust.finished")]: true });
|
|
69
|
+
function invalidSections({ subject, name }) {
|
|
70
|
+
return new CrustError("DEFINITION", `${subject === "command" ? "Command" : "Extension"} "${name}" contains invalid documentation sections`, {
|
|
71
|
+
subject,
|
|
72
|
+
name,
|
|
73
|
+
reason: "invalid-sections"
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function normalizeSection(section, owner) {
|
|
77
|
+
const { title, body, only, except } = section;
|
|
78
|
+
if (!title.trim() || /[\r\n]/.test(title) || !body.trim() || only?.length === 0 || except?.length === 0 || only !== void 0 && except !== void 0) throw invalidSections(owner);
|
|
79
|
+
const audience = (ids) => {
|
|
80
|
+
return Object.freeze(ids.map((consumer) => typeof consumer === "string" ? consumer : consumer.id));
|
|
81
|
+
};
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
title,
|
|
84
|
+
body,
|
|
85
|
+
...only ? { only: audience(only) } : except ? { except: audience(except) } : {}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function validateCommandSections(name, sections) {
|
|
89
|
+
return sections.map((section) => normalizeSection(section, {
|
|
90
|
+
subject: "command",
|
|
91
|
+
name
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Snapshot subprocess protocol used by first-party build tooling.
|
|
96
|
+
*
|
|
97
|
+
* When set to a non-empty file path, `.execute()` prepares the command tree,
|
|
98
|
+
* validates its documentation sections, optionally runs Extension build hooks when the
|
|
99
|
+
* build output directory is set, writes its final JSON snapshot and Build Report, and exits
|
|
100
|
+
* without dispatching a Command Action. In-process callers use `Crust.snapshot()`.
|
|
101
|
+
*
|
|
102
|
+
* Only source entries run by `crust build` honor it: finished Bun/Node bundles carry
|
|
103
|
+
* `process.env.CRUST_INTERNAL_BUILD === "1"` as a literal and compile the protocol out.
|
|
104
|
+
*/
|
|
105
|
+
const SNAPSHOT_PATH_ENV = "CRUST_INTERNAL_SNAPSHOT_PATH";
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region ../core/dist/index.js
|
|
108
|
+
/** Mint an Extension identity from any non-blank, trimmed string. */
|
|
109
|
+
function defineExtensionId(id) {
|
|
110
|
+
if (!id.trim() || id !== id.trim()) throw new CrustError("DEFINITION", "Extension id must be a non-empty, trimmed string", {
|
|
111
|
+
subject: "extension",
|
|
112
|
+
reason: "empty-id"
|
|
113
|
+
});
|
|
114
|
+
return id;
|
|
115
|
+
}
|
|
116
|
+
const commandDefinitionInternal = Symbol.for("crust.commandDefinition");
|
|
117
|
+
function resolveCommandName(name, aliases = []) {
|
|
118
|
+
if (name.trim() === "") throw new CrustError("DEFINITION", "Command name must be a non-empty string", {
|
|
119
|
+
subject: "command",
|
|
120
|
+
name,
|
|
121
|
+
reason: "empty-name"
|
|
122
|
+
});
|
|
123
|
+
if (name === "__proto__") throw new CrustError("DEFINITION", "Command name \"__proto__\" is reserved", {
|
|
124
|
+
subject: "command",
|
|
125
|
+
name,
|
|
126
|
+
reason: "reserved-name"
|
|
127
|
+
});
|
|
128
|
+
if (aliases.includes(name)) throw new CrustError("DEFINITION", `Command "${name}" must not list its own canonical name as an alias`, {
|
|
129
|
+
subject: "command",
|
|
130
|
+
name,
|
|
131
|
+
reason: "alias-collision"
|
|
132
|
+
});
|
|
133
|
+
return name;
|
|
134
|
+
}
|
|
135
|
+
function isCommandRecipe(value) {
|
|
136
|
+
return typeof value === "function";
|
|
137
|
+
}
|
|
138
|
+
function defineCommand(nameInput, configOrRecipe, maybeRecipe) {
|
|
139
|
+
const hasConfig = !isCommandRecipe(configOrRecipe);
|
|
140
|
+
const config = hasConfig ? configOrRecipe : {};
|
|
141
|
+
const recipe = hasConfig ? maybeRecipe : configOrRecipe;
|
|
142
|
+
const name = resolveCommandName(nameInput, config.aliases);
|
|
143
|
+
for (const alias of config.aliases ?? []) if (alias === "" || /[ \t\n\r\v\f]/.test(alias) || alias.startsWith("-")) throw new CrustError("DEFINITION", `Command "${name}" has an invalid alias "${alias}"`);
|
|
144
|
+
const { sections, version: _rootVersion, ...metaRest } = config;
|
|
145
|
+
const meta = {
|
|
146
|
+
...metaRest,
|
|
147
|
+
...config.aliases ? { aliases: [...config.aliases] } : {},
|
|
148
|
+
...sections ? { sections: validateCommandSections(name, sections) } : {}
|
|
149
|
+
};
|
|
150
|
+
const internal = {
|
|
151
|
+
name,
|
|
152
|
+
recipe,
|
|
153
|
+
meta
|
|
154
|
+
};
|
|
155
|
+
const named = (defName) => {
|
|
156
|
+
return Object.freeze({
|
|
157
|
+
name: defName,
|
|
158
|
+
as: (newName) => named(resolveCommandName(newName, meta.aliases)),
|
|
159
|
+
[commandDefinitionInternal]: Object.freeze({
|
|
160
|
+
...internal,
|
|
161
|
+
name: defName
|
|
162
|
+
})
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
return named(name);
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region ../style/dist/index.js
|
|
169
|
+
var __defProp = Object.defineProperty;
|
|
170
|
+
var __exportAll = (all, no_symbols) => {
|
|
171
|
+
let target = {};
|
|
172
|
+
for (var name in all) __defProp(target, name, {
|
|
173
|
+
get: all[name],
|
|
174
|
+
enumerable: true
|
|
175
|
+
});
|
|
176
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
177
|
+
return target;
|
|
178
|
+
};
|
|
179
|
+
new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
180
|
+
new RegExp([
|
|
181
|
+
String.raw`(?:\x1b\[|\x9b)[^\x1b\x18\x1a\x9c\x40-\x7e]*[\x40-\x7e\x18\x1a\x9c]?`,
|
|
182
|
+
String.raw`(?:\x1b\]|\x9d)[^\x07\x1b\x18\x1a\x9c]*(?:\x07|\x1b\\|[\x18\x1a\x9c])?`,
|
|
183
|
+
String.raw`(?:\x1b[PX^_]|[\x90\x98\x9e\x9f])[^\x1b\x18\x1a\x9c]*(?:\x1b\\|[\x18\x1a\x9c])?`,
|
|
184
|
+
String.raw`\x1b[\x20-\x2f][^\x1b]?`,
|
|
185
|
+
String.raw`\x1b[\x18\x1a\x9c\x30-\x7e]?`
|
|
186
|
+
].join("|"), "g");
|
|
187
|
+
var ansiCodes_exports = /* @__PURE__ */ __exportAll({
|
|
188
|
+
bgBlack: () => bgBlack$1,
|
|
189
|
+
bgBlue: () => bgBlue$1,
|
|
190
|
+
bgBrightBlack: () => bgBrightBlack$1,
|
|
191
|
+
bgBrightBlue: () => bgBrightBlue$1,
|
|
192
|
+
bgBrightCyan: () => bgBrightCyan$1,
|
|
193
|
+
bgBrightGreen: () => bgBrightGreen$1,
|
|
194
|
+
bgBrightMagenta: () => bgBrightMagenta$1,
|
|
195
|
+
bgBrightRed: () => bgBrightRed$1,
|
|
196
|
+
bgBrightWhite: () => bgBrightWhite$1,
|
|
197
|
+
bgBrightYellow: () => bgBrightYellow$1,
|
|
198
|
+
bgCyan: () => bgCyan$1,
|
|
199
|
+
bgGreen: () => bgGreen$1,
|
|
200
|
+
bgMagenta: () => bgMagenta$1,
|
|
201
|
+
bgRed: () => bgRed$1,
|
|
202
|
+
bgWhite: () => bgWhite$1,
|
|
203
|
+
bgYellow: () => bgYellow$1,
|
|
204
|
+
black: () => black$1,
|
|
205
|
+
blue: () => blue$1,
|
|
206
|
+
bold: () => bold$1,
|
|
207
|
+
brightBlue: () => brightBlue$1,
|
|
208
|
+
brightCyan: () => brightCyan$1,
|
|
209
|
+
brightGreen: () => brightGreen$1,
|
|
210
|
+
brightMagenta: () => brightMagenta$1,
|
|
211
|
+
brightRed: () => brightRed$1,
|
|
212
|
+
brightWhite: () => brightWhite$1,
|
|
213
|
+
brightYellow: () => brightYellow$1,
|
|
214
|
+
cyan: () => cyan$1,
|
|
215
|
+
dim: () => dim$1,
|
|
216
|
+
gray: () => gray$1,
|
|
217
|
+
green: () => green$1,
|
|
218
|
+
hidden: () => hidden$1,
|
|
219
|
+
inverse: () => inverse$1,
|
|
220
|
+
isModifierName: () => isModifierName,
|
|
221
|
+
italic: () => italic$1,
|
|
222
|
+
magenta: () => magenta$1,
|
|
223
|
+
red: () => red$1,
|
|
224
|
+
strikethrough: () => strikethrough$1,
|
|
225
|
+
styleMethodNames: () => styleMethodNames,
|
|
226
|
+
underline: () => underline$1,
|
|
227
|
+
white: () => white$1,
|
|
228
|
+
yellow: () => yellow$1
|
|
229
|
+
});
|
|
230
|
+
function pair(open, close) {
|
|
231
|
+
return {
|
|
232
|
+
open: `\x1b[${open}m`,
|
|
233
|
+
close: `\x1b[${close}m`
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/** Bold / increased intensity. */
|
|
237
|
+
const bold$1 = pair(1, 22);
|
|
238
|
+
/** Dim / decreased intensity. */
|
|
239
|
+
const dim$1 = pair(2, 22);
|
|
240
|
+
/** Italic. */
|
|
241
|
+
const italic$1 = pair(3, 23);
|
|
242
|
+
/** Underline. */
|
|
243
|
+
const underline$1 = pair(4, 24);
|
|
244
|
+
/** Inverse / reverse video. */
|
|
245
|
+
const inverse$1 = pair(7, 27);
|
|
246
|
+
/** Hidden / conceal. */
|
|
247
|
+
const hidden$1 = pair(8, 28);
|
|
248
|
+
/** Strikethrough / crossed out. */
|
|
249
|
+
const strikethrough$1 = pair(9, 29);
|
|
250
|
+
const black$1 = pair(30, 39);
|
|
251
|
+
const red$1 = pair(31, 39);
|
|
252
|
+
const green$1 = pair(32, 39);
|
|
253
|
+
const yellow$1 = pair(33, 39);
|
|
254
|
+
const blue$1 = pair(34, 39);
|
|
255
|
+
const magenta$1 = pair(35, 39);
|
|
256
|
+
const cyan$1 = pair(36, 39);
|
|
257
|
+
const white$1 = pair(37, 39);
|
|
258
|
+
/** Bright black (gray). */
|
|
259
|
+
const gray$1 = pair(90, 39);
|
|
260
|
+
const brightRed$1 = pair(91, 39);
|
|
261
|
+
const brightGreen$1 = pair(92, 39);
|
|
262
|
+
const brightYellow$1 = pair(93, 39);
|
|
263
|
+
const brightBlue$1 = pair(94, 39);
|
|
264
|
+
const brightMagenta$1 = pair(95, 39);
|
|
265
|
+
const brightCyan$1 = pair(96, 39);
|
|
266
|
+
const brightWhite$1 = pair(97, 39);
|
|
267
|
+
const bgBlack$1 = pair(40, 49);
|
|
268
|
+
const bgRed$1 = pair(41, 49);
|
|
269
|
+
const bgGreen$1 = pair(42, 49);
|
|
270
|
+
const bgYellow$1 = pair(43, 49);
|
|
271
|
+
const bgBlue$1 = pair(44, 49);
|
|
272
|
+
const bgMagenta$1 = pair(45, 49);
|
|
273
|
+
const bgCyan$1 = pair(46, 49);
|
|
274
|
+
const bgWhite$1 = pair(47, 49);
|
|
275
|
+
const bgBrightBlack$1 = pair(100, 49);
|
|
276
|
+
const bgBrightRed$1 = pair(101, 49);
|
|
277
|
+
const bgBrightGreen$1 = pair(102, 49);
|
|
278
|
+
const bgBrightYellow$1 = pair(103, 49);
|
|
279
|
+
const bgBrightBlue$1 = pair(104, 49);
|
|
280
|
+
const bgBrightMagenta$1 = pair(105, 49);
|
|
281
|
+
const bgBrightCyan$1 = pair(106, 49);
|
|
282
|
+
const bgBrightWhite$1 = pair(107, 49);
|
|
283
|
+
const styleMethodNames = Object.freeze([
|
|
284
|
+
"bold",
|
|
285
|
+
"dim",
|
|
286
|
+
"italic",
|
|
287
|
+
"underline",
|
|
288
|
+
"inverse",
|
|
289
|
+
"hidden",
|
|
290
|
+
"strikethrough",
|
|
291
|
+
"black",
|
|
292
|
+
"red",
|
|
293
|
+
"green",
|
|
294
|
+
"yellow",
|
|
295
|
+
"blue",
|
|
296
|
+
"magenta",
|
|
297
|
+
"cyan",
|
|
298
|
+
"white",
|
|
299
|
+
"gray",
|
|
300
|
+
"brightRed",
|
|
301
|
+
"brightGreen",
|
|
302
|
+
"brightYellow",
|
|
303
|
+
"brightBlue",
|
|
304
|
+
"brightMagenta",
|
|
305
|
+
"brightCyan",
|
|
306
|
+
"brightWhite",
|
|
307
|
+
"bgBlack",
|
|
308
|
+
"bgRed",
|
|
309
|
+
"bgGreen",
|
|
310
|
+
"bgYellow",
|
|
311
|
+
"bgBlue",
|
|
312
|
+
"bgMagenta",
|
|
313
|
+
"bgCyan",
|
|
314
|
+
"bgWhite",
|
|
315
|
+
"bgBrightBlack",
|
|
316
|
+
"bgBrightRed",
|
|
317
|
+
"bgBrightGreen",
|
|
318
|
+
"bgBrightYellow",
|
|
319
|
+
"bgBrightBlue",
|
|
320
|
+
"bgBrightMagenta",
|
|
321
|
+
"bgBrightCyan",
|
|
322
|
+
"bgBrightWhite"
|
|
323
|
+
]);
|
|
324
|
+
const modifierNameSet = /* @__PURE__ */ new Set([
|
|
325
|
+
"bold",
|
|
326
|
+
"dim",
|
|
327
|
+
"italic",
|
|
328
|
+
"underline",
|
|
329
|
+
"inverse",
|
|
330
|
+
"hidden",
|
|
331
|
+
"strikethrough"
|
|
332
|
+
]);
|
|
333
|
+
function isModifierName(name) {
|
|
334
|
+
return modifierNameSet.has(name);
|
|
335
|
+
}
|
|
336
|
+
function read(overrides, key, environment) {
|
|
337
|
+
return overrides !== void 0 && key in overrides ? overrides[key] : environment;
|
|
338
|
+
}
|
|
339
|
+
function readTTY(overrides) {
|
|
340
|
+
return read(overrides, "isTTY", process.stdout?.isTTY) ?? false;
|
|
341
|
+
}
|
|
342
|
+
function readForceColor(overrides) {
|
|
343
|
+
return read(overrides, "forceColor", process.env.FORCE_COLOR);
|
|
344
|
+
}
|
|
345
|
+
/** `FORCE_COLOR=0` / `FORCE_COLOR=false` mean "force off"; any other value forces on. */
|
|
346
|
+
function forceColorDisables(forceColor) {
|
|
347
|
+
return forceColor === "0" || forceColor === "false";
|
|
348
|
+
}
|
|
349
|
+
function isTrueColorTerm(term) {
|
|
350
|
+
const lower = term.toLowerCase();
|
|
351
|
+
return lower.includes("24bit") || lower.includes("truecolor") || lower.endsWith("-direct");
|
|
352
|
+
}
|
|
353
|
+
function detectsTruecolor(colorTerm, term) {
|
|
354
|
+
if (colorTerm !== void 0) {
|
|
355
|
+
const lower = colorTerm.toLowerCase();
|
|
356
|
+
if (lower === "truecolor" || lower === "24bit") return true;
|
|
357
|
+
}
|
|
358
|
+
return term !== void 0 && isTrueColorTerm(term);
|
|
359
|
+
}
|
|
360
|
+
function detectDepth(colorTerm, term) {
|
|
361
|
+
if (detectsTruecolor(colorTerm, term)) return "truecolor";
|
|
362
|
+
if (term !== void 0 && term.toLowerCase().includes("256color")) return "256";
|
|
363
|
+
return "16";
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Resolve the color depth a terminal can emit.
|
|
367
|
+
*
|
|
368
|
+
* Resolution rules:
|
|
369
|
+
* - `"never"` → `"none"`.
|
|
370
|
+
* - `"always"` → `"truecolor"`.
|
|
371
|
+
* - `"auto"`:
|
|
372
|
+
* 1. `FORCE_COLOR` set → decides unconditionally (overrides TTY and
|
|
373
|
+
* `NO_COLOR`, chalk convention): `0` / `false` → `"none"`; `1` →
|
|
374
|
+
* `"16"`; `2` → `"256"`; `3` → `"truecolor"`; any other value
|
|
375
|
+
* (including empty) → forced on at the `COLORTERM` / `TERM`
|
|
376
|
+
* detected depth.
|
|
377
|
+
* 2. Not a TTY OR `NO_COLOR` set non-empty → `"none"`.
|
|
378
|
+
* 3. `COLORTERM` is `"truecolor"` or `"24bit"` (case-insensitive) →
|
|
379
|
+
* `"truecolor"`.
|
|
380
|
+
* 4. `TERM` ends with `-direct` OR contains `truecolor` / `24bit` →
|
|
381
|
+
* `"truecolor"`.
|
|
382
|
+
* 5. `TERM === "dumb"` → `"none"`.
|
|
383
|
+
* 6. `TERM` contains `256color` → `"256"`.
|
|
384
|
+
* 7. Any other TTY value → `"16"`.
|
|
385
|
+
*
|
|
386
|
+
* Detection follows the ecosystem `FORCE_COLOR` / `NO_COLOR` / `COLORTERM`
|
|
387
|
+
* / `TERM` conventions; no bespoke environment variables are introduced.
|
|
388
|
+
*
|
|
389
|
+
* @param mode - The color emission mode.
|
|
390
|
+
* @param overrides - Optional overrides for deterministic testing.
|
|
391
|
+
* @returns The resolved {@link ColorDepth} tier.
|
|
392
|
+
*
|
|
393
|
+
* @example
|
|
394
|
+
* ```ts
|
|
395
|
+
* resolveColorDepth("always"); // "truecolor"
|
|
396
|
+
* resolveColorDepth("never"); // "none"
|
|
397
|
+
* resolveColorDepth("auto", {
|
|
398
|
+
* isTTY: true,
|
|
399
|
+
* noColor: undefined,
|
|
400
|
+
* colorTerm: "truecolor",
|
|
401
|
+
* }); // "truecolor"
|
|
402
|
+
* resolveColorDepth("auto", {
|
|
403
|
+
* isTTY: true,
|
|
404
|
+
* noColor: undefined,
|
|
405
|
+
* colorTerm: undefined,
|
|
406
|
+
* term: "xterm-256color",
|
|
407
|
+
* }); // "256"
|
|
408
|
+
* ```
|
|
409
|
+
*/
|
|
410
|
+
function resolveColorDepth(mode, overrides) {
|
|
411
|
+
if (mode === "never") return "none";
|
|
412
|
+
if (mode === "always") return "truecolor";
|
|
413
|
+
const colorTerm = read(overrides, "colorTerm", process.env.COLORTERM);
|
|
414
|
+
const term = read(overrides, "term", process.env.TERM);
|
|
415
|
+
const forceColor = readForceColor(overrides);
|
|
416
|
+
if (forceColor !== void 0) {
|
|
417
|
+
if (forceColorDisables(forceColor)) return "none";
|
|
418
|
+
if (forceColor === "1") return "16";
|
|
419
|
+
if (forceColor === "2") return "256";
|
|
420
|
+
if (forceColor === "3") return "truecolor";
|
|
421
|
+
return detectDepth(colorTerm, term);
|
|
422
|
+
}
|
|
423
|
+
if (!readTTY(overrides)) return "none";
|
|
424
|
+
const noColor = read(overrides, "noColor", process.env.NO_COLOR);
|
|
425
|
+
if (noColor !== void 0 && noColor !== "") return "none";
|
|
426
|
+
if (term !== void 0 && term.toLowerCase() === "dumb" && !detectsTruecolor(colorTerm, term)) return "none";
|
|
427
|
+
return detectDepth(colorTerm, term);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Resolve whether non-color ANSI modifiers should be emitted.
|
|
431
|
+
*
|
|
432
|
+
* In `"auto"` mode, modifiers are enabled when stdout is a TTY, but are
|
|
433
|
+
* **not** affected by `NO_COLOR` (which only controls color output).
|
|
434
|
+
* `FORCE_COLOR`, when set, decides unconditionally — it is the all-ANSI
|
|
435
|
+
* switch, while `NO_COLOR` is the colors-only switch.
|
|
436
|
+
*
|
|
437
|
+
* @internal Exported only for use by {@link createStyle}; not part of the
|
|
438
|
+
* public surface of `@crustjs/style`.
|
|
439
|
+
*/
|
|
440
|
+
function resolveModifierCapability(mode, overrides) {
|
|
441
|
+
if (mode === "always") return true;
|
|
442
|
+
if (mode === "never") return false;
|
|
443
|
+
const forceColor = readForceColor(overrides);
|
|
444
|
+
if (forceColor !== void 0) return !forceColorDisables(forceColor);
|
|
445
|
+
return readTTY(overrides);
|
|
446
|
+
}
|
|
447
|
+
const namedColorValues = {
|
|
448
|
+
aliceblue: [
|
|
449
|
+
240,
|
|
450
|
+
248,
|
|
451
|
+
255
|
|
452
|
+
],
|
|
453
|
+
antiquewhite: [
|
|
454
|
+
250,
|
|
455
|
+
235,
|
|
456
|
+
215
|
|
457
|
+
],
|
|
458
|
+
aqua: [
|
|
459
|
+
0,
|
|
460
|
+
255,
|
|
461
|
+
255
|
|
462
|
+
],
|
|
463
|
+
aquamarine: [
|
|
464
|
+
127,
|
|
465
|
+
255,
|
|
466
|
+
212
|
|
467
|
+
],
|
|
468
|
+
azure: [
|
|
469
|
+
240,
|
|
470
|
+
255,
|
|
471
|
+
255
|
|
472
|
+
],
|
|
473
|
+
beige: [
|
|
474
|
+
245,
|
|
475
|
+
245,
|
|
476
|
+
220
|
|
477
|
+
],
|
|
478
|
+
bisque: [
|
|
479
|
+
255,
|
|
480
|
+
228,
|
|
481
|
+
196
|
|
482
|
+
],
|
|
483
|
+
black: [
|
|
484
|
+
0,
|
|
485
|
+
0,
|
|
486
|
+
0
|
|
487
|
+
],
|
|
488
|
+
blanchedalmond: [
|
|
489
|
+
255,
|
|
490
|
+
235,
|
|
491
|
+
205
|
|
492
|
+
],
|
|
493
|
+
blue: [
|
|
494
|
+
0,
|
|
495
|
+
0,
|
|
496
|
+
255
|
|
497
|
+
],
|
|
498
|
+
blueviolet: [
|
|
499
|
+
138,
|
|
500
|
+
43,
|
|
501
|
+
226
|
|
502
|
+
],
|
|
503
|
+
brown: [
|
|
504
|
+
165,
|
|
505
|
+
42,
|
|
506
|
+
42
|
|
507
|
+
],
|
|
508
|
+
burlywood: [
|
|
509
|
+
222,
|
|
510
|
+
184,
|
|
511
|
+
135
|
|
512
|
+
],
|
|
513
|
+
cadetblue: [
|
|
514
|
+
95,
|
|
515
|
+
158,
|
|
516
|
+
160
|
|
517
|
+
],
|
|
518
|
+
chartreuse: [
|
|
519
|
+
127,
|
|
520
|
+
255,
|
|
521
|
+
0
|
|
522
|
+
],
|
|
523
|
+
chocolate: [
|
|
524
|
+
210,
|
|
525
|
+
105,
|
|
526
|
+
30
|
|
527
|
+
],
|
|
528
|
+
coral: [
|
|
529
|
+
255,
|
|
530
|
+
127,
|
|
531
|
+
80
|
|
532
|
+
],
|
|
533
|
+
cornflowerblue: [
|
|
534
|
+
100,
|
|
535
|
+
149,
|
|
536
|
+
237
|
|
537
|
+
],
|
|
538
|
+
cornsilk: [
|
|
539
|
+
255,
|
|
540
|
+
248,
|
|
541
|
+
220
|
|
542
|
+
],
|
|
543
|
+
crimson: [
|
|
544
|
+
220,
|
|
545
|
+
20,
|
|
546
|
+
60
|
|
547
|
+
],
|
|
548
|
+
cyan: [
|
|
549
|
+
0,
|
|
550
|
+
255,
|
|
551
|
+
255
|
|
552
|
+
],
|
|
553
|
+
darkblue: [
|
|
554
|
+
0,
|
|
555
|
+
0,
|
|
556
|
+
139
|
|
557
|
+
],
|
|
558
|
+
darkcyan: [
|
|
559
|
+
0,
|
|
560
|
+
139,
|
|
561
|
+
139
|
|
562
|
+
],
|
|
563
|
+
darkgoldenrod: [
|
|
564
|
+
184,
|
|
565
|
+
134,
|
|
566
|
+
11
|
|
567
|
+
],
|
|
568
|
+
darkgray: [
|
|
569
|
+
169,
|
|
570
|
+
169,
|
|
571
|
+
169
|
|
572
|
+
],
|
|
573
|
+
darkgreen: [
|
|
574
|
+
0,
|
|
575
|
+
100,
|
|
576
|
+
0
|
|
577
|
+
],
|
|
578
|
+
darkgrey: [
|
|
579
|
+
169,
|
|
580
|
+
169,
|
|
581
|
+
169
|
|
582
|
+
],
|
|
583
|
+
darkkhaki: [
|
|
584
|
+
189,
|
|
585
|
+
183,
|
|
586
|
+
107
|
|
587
|
+
],
|
|
588
|
+
darkmagenta: [
|
|
589
|
+
139,
|
|
590
|
+
0,
|
|
591
|
+
139
|
|
592
|
+
],
|
|
593
|
+
darkolivegreen: [
|
|
594
|
+
85,
|
|
595
|
+
107,
|
|
596
|
+
47
|
|
597
|
+
],
|
|
598
|
+
darkorange: [
|
|
599
|
+
255,
|
|
600
|
+
140,
|
|
601
|
+
0
|
|
602
|
+
],
|
|
603
|
+
darkorchid: [
|
|
604
|
+
153,
|
|
605
|
+
50,
|
|
606
|
+
204
|
|
607
|
+
],
|
|
608
|
+
darkred: [
|
|
609
|
+
139,
|
|
610
|
+
0,
|
|
611
|
+
0
|
|
612
|
+
],
|
|
613
|
+
darksalmon: [
|
|
614
|
+
233,
|
|
615
|
+
150,
|
|
616
|
+
122
|
|
617
|
+
],
|
|
618
|
+
darkseagreen: [
|
|
619
|
+
143,
|
|
620
|
+
188,
|
|
621
|
+
143
|
|
622
|
+
],
|
|
623
|
+
darkslateblue: [
|
|
624
|
+
72,
|
|
625
|
+
61,
|
|
626
|
+
139
|
|
627
|
+
],
|
|
628
|
+
darkslategray: [
|
|
629
|
+
47,
|
|
630
|
+
79,
|
|
631
|
+
79
|
|
632
|
+
],
|
|
633
|
+
darkslategrey: [
|
|
634
|
+
47,
|
|
635
|
+
79,
|
|
636
|
+
79
|
|
637
|
+
],
|
|
638
|
+
darkturquoise: [
|
|
639
|
+
0,
|
|
640
|
+
206,
|
|
641
|
+
209
|
|
642
|
+
],
|
|
643
|
+
darkviolet: [
|
|
644
|
+
148,
|
|
645
|
+
0,
|
|
646
|
+
211
|
|
647
|
+
],
|
|
648
|
+
deeppink: [
|
|
649
|
+
255,
|
|
650
|
+
20,
|
|
651
|
+
147
|
|
652
|
+
],
|
|
653
|
+
deepskyblue: [
|
|
654
|
+
0,
|
|
655
|
+
191,
|
|
656
|
+
255
|
|
657
|
+
],
|
|
658
|
+
dimgray: [
|
|
659
|
+
105,
|
|
660
|
+
105,
|
|
661
|
+
105
|
|
662
|
+
],
|
|
663
|
+
dimgrey: [
|
|
664
|
+
105,
|
|
665
|
+
105,
|
|
666
|
+
105
|
|
667
|
+
],
|
|
668
|
+
dodgerblue: [
|
|
669
|
+
30,
|
|
670
|
+
144,
|
|
671
|
+
255
|
|
672
|
+
],
|
|
673
|
+
firebrick: [
|
|
674
|
+
178,
|
|
675
|
+
34,
|
|
676
|
+
34
|
|
677
|
+
],
|
|
678
|
+
floralwhite: [
|
|
679
|
+
255,
|
|
680
|
+
250,
|
|
681
|
+
240
|
|
682
|
+
],
|
|
683
|
+
forestgreen: [
|
|
684
|
+
34,
|
|
685
|
+
139,
|
|
686
|
+
34
|
|
687
|
+
],
|
|
688
|
+
fuchsia: [
|
|
689
|
+
255,
|
|
690
|
+
0,
|
|
691
|
+
255
|
|
692
|
+
],
|
|
693
|
+
gainsboro: [
|
|
694
|
+
220,
|
|
695
|
+
220,
|
|
696
|
+
220
|
|
697
|
+
],
|
|
698
|
+
ghostwhite: [
|
|
699
|
+
248,
|
|
700
|
+
248,
|
|
701
|
+
255
|
|
702
|
+
],
|
|
703
|
+
gold: [
|
|
704
|
+
255,
|
|
705
|
+
215,
|
|
706
|
+
0
|
|
707
|
+
],
|
|
708
|
+
goldenrod: [
|
|
709
|
+
218,
|
|
710
|
+
165,
|
|
711
|
+
32
|
|
712
|
+
],
|
|
713
|
+
gray: [
|
|
714
|
+
128,
|
|
715
|
+
128,
|
|
716
|
+
128
|
|
717
|
+
],
|
|
718
|
+
green: [
|
|
719
|
+
0,
|
|
720
|
+
128,
|
|
721
|
+
0
|
|
722
|
+
],
|
|
723
|
+
greenyellow: [
|
|
724
|
+
173,
|
|
725
|
+
255,
|
|
726
|
+
47
|
|
727
|
+
],
|
|
728
|
+
grey: [
|
|
729
|
+
128,
|
|
730
|
+
128,
|
|
731
|
+
128
|
|
732
|
+
],
|
|
733
|
+
honeydew: [
|
|
734
|
+
240,
|
|
735
|
+
255,
|
|
736
|
+
240
|
|
737
|
+
],
|
|
738
|
+
hotpink: [
|
|
739
|
+
255,
|
|
740
|
+
105,
|
|
741
|
+
180
|
|
742
|
+
],
|
|
743
|
+
indianred: [
|
|
744
|
+
205,
|
|
745
|
+
92,
|
|
746
|
+
92
|
|
747
|
+
],
|
|
748
|
+
indigo: [
|
|
749
|
+
75,
|
|
750
|
+
0,
|
|
751
|
+
130
|
|
752
|
+
],
|
|
753
|
+
ivory: [
|
|
754
|
+
255,
|
|
755
|
+
255,
|
|
756
|
+
240
|
|
757
|
+
],
|
|
758
|
+
khaki: [
|
|
759
|
+
240,
|
|
760
|
+
230,
|
|
761
|
+
140
|
|
762
|
+
],
|
|
763
|
+
lavender: [
|
|
764
|
+
230,
|
|
765
|
+
230,
|
|
766
|
+
250
|
|
767
|
+
],
|
|
768
|
+
lavenderblush: [
|
|
769
|
+
255,
|
|
770
|
+
240,
|
|
771
|
+
245
|
|
772
|
+
],
|
|
773
|
+
lawngreen: [
|
|
774
|
+
124,
|
|
775
|
+
252,
|
|
776
|
+
0
|
|
777
|
+
],
|
|
778
|
+
lemonchiffon: [
|
|
779
|
+
255,
|
|
780
|
+
250,
|
|
781
|
+
205
|
|
782
|
+
],
|
|
783
|
+
lightblue: [
|
|
784
|
+
173,
|
|
785
|
+
216,
|
|
786
|
+
230
|
|
787
|
+
],
|
|
788
|
+
lightcoral: [
|
|
789
|
+
240,
|
|
790
|
+
128,
|
|
791
|
+
128
|
|
792
|
+
],
|
|
793
|
+
lightcyan: [
|
|
794
|
+
224,
|
|
795
|
+
255,
|
|
796
|
+
255
|
|
797
|
+
],
|
|
798
|
+
lightgoldenrodyellow: [
|
|
799
|
+
250,
|
|
800
|
+
250,
|
|
801
|
+
210
|
|
802
|
+
],
|
|
803
|
+
lightgray: [
|
|
804
|
+
211,
|
|
805
|
+
211,
|
|
806
|
+
211
|
|
807
|
+
],
|
|
808
|
+
lightgreen: [
|
|
809
|
+
144,
|
|
810
|
+
238,
|
|
811
|
+
144
|
|
812
|
+
],
|
|
813
|
+
lightgrey: [
|
|
814
|
+
211,
|
|
815
|
+
211,
|
|
816
|
+
211
|
|
817
|
+
],
|
|
818
|
+
lightpink: [
|
|
819
|
+
255,
|
|
820
|
+
182,
|
|
821
|
+
193
|
|
822
|
+
],
|
|
823
|
+
lightsalmon: [
|
|
824
|
+
255,
|
|
825
|
+
160,
|
|
826
|
+
122
|
|
827
|
+
],
|
|
828
|
+
lightseagreen: [
|
|
829
|
+
32,
|
|
830
|
+
178,
|
|
831
|
+
170
|
|
832
|
+
],
|
|
833
|
+
lightskyblue: [
|
|
834
|
+
135,
|
|
835
|
+
206,
|
|
836
|
+
250
|
|
837
|
+
],
|
|
838
|
+
lightslategray: [
|
|
839
|
+
119,
|
|
840
|
+
136,
|
|
841
|
+
153
|
|
842
|
+
],
|
|
843
|
+
lightslategrey: [
|
|
844
|
+
119,
|
|
845
|
+
136,
|
|
846
|
+
153
|
|
847
|
+
],
|
|
848
|
+
lightsteelblue: [
|
|
849
|
+
176,
|
|
850
|
+
196,
|
|
851
|
+
222
|
|
852
|
+
],
|
|
853
|
+
lightyellow: [
|
|
854
|
+
255,
|
|
855
|
+
255,
|
|
856
|
+
224
|
|
857
|
+
],
|
|
858
|
+
lime: [
|
|
859
|
+
0,
|
|
860
|
+
255,
|
|
861
|
+
0
|
|
862
|
+
],
|
|
863
|
+
limegreen: [
|
|
864
|
+
50,
|
|
865
|
+
205,
|
|
866
|
+
50
|
|
867
|
+
],
|
|
868
|
+
linen: [
|
|
869
|
+
250,
|
|
870
|
+
240,
|
|
871
|
+
230
|
|
872
|
+
],
|
|
873
|
+
magenta: [
|
|
874
|
+
255,
|
|
875
|
+
0,
|
|
876
|
+
255
|
|
877
|
+
],
|
|
878
|
+
maroon: [
|
|
879
|
+
128,
|
|
880
|
+
0,
|
|
881
|
+
0
|
|
882
|
+
],
|
|
883
|
+
mediumaquamarine: [
|
|
884
|
+
102,
|
|
885
|
+
205,
|
|
886
|
+
170
|
|
887
|
+
],
|
|
888
|
+
mediumblue: [
|
|
889
|
+
0,
|
|
890
|
+
0,
|
|
891
|
+
205
|
|
892
|
+
],
|
|
893
|
+
mediumorchid: [
|
|
894
|
+
186,
|
|
895
|
+
85,
|
|
896
|
+
211
|
|
897
|
+
],
|
|
898
|
+
mediumpurple: [
|
|
899
|
+
147,
|
|
900
|
+
112,
|
|
901
|
+
219
|
|
902
|
+
],
|
|
903
|
+
mediumseagreen: [
|
|
904
|
+
60,
|
|
905
|
+
179,
|
|
906
|
+
113
|
|
907
|
+
],
|
|
908
|
+
mediumslateblue: [
|
|
909
|
+
123,
|
|
910
|
+
104,
|
|
911
|
+
238
|
|
912
|
+
],
|
|
913
|
+
mediumspringgreen: [
|
|
914
|
+
0,
|
|
915
|
+
250,
|
|
916
|
+
154
|
|
917
|
+
],
|
|
918
|
+
mediumturquoise: [
|
|
919
|
+
72,
|
|
920
|
+
209,
|
|
921
|
+
204
|
|
922
|
+
],
|
|
923
|
+
mediumvioletred: [
|
|
924
|
+
199,
|
|
925
|
+
21,
|
|
926
|
+
133
|
|
927
|
+
],
|
|
928
|
+
midnightblue: [
|
|
929
|
+
25,
|
|
930
|
+
25,
|
|
931
|
+
112
|
|
932
|
+
],
|
|
933
|
+
mintcream: [
|
|
934
|
+
245,
|
|
935
|
+
255,
|
|
936
|
+
250
|
|
937
|
+
],
|
|
938
|
+
mistyrose: [
|
|
939
|
+
255,
|
|
940
|
+
228,
|
|
941
|
+
225
|
|
942
|
+
],
|
|
943
|
+
moccasin: [
|
|
944
|
+
255,
|
|
945
|
+
228,
|
|
946
|
+
181
|
|
947
|
+
],
|
|
948
|
+
navajowhite: [
|
|
949
|
+
255,
|
|
950
|
+
222,
|
|
951
|
+
173
|
|
952
|
+
],
|
|
953
|
+
navy: [
|
|
954
|
+
0,
|
|
955
|
+
0,
|
|
956
|
+
128
|
|
957
|
+
],
|
|
958
|
+
oldlace: [
|
|
959
|
+
253,
|
|
960
|
+
245,
|
|
961
|
+
230
|
|
962
|
+
],
|
|
963
|
+
olive: [
|
|
964
|
+
128,
|
|
965
|
+
128,
|
|
966
|
+
0
|
|
967
|
+
],
|
|
968
|
+
olivedrab: [
|
|
969
|
+
107,
|
|
970
|
+
142,
|
|
971
|
+
35
|
|
972
|
+
],
|
|
973
|
+
orange: [
|
|
974
|
+
255,
|
|
975
|
+
165,
|
|
976
|
+
0
|
|
977
|
+
],
|
|
978
|
+
orangered: [
|
|
979
|
+
255,
|
|
980
|
+
69,
|
|
981
|
+
0
|
|
982
|
+
],
|
|
983
|
+
orchid: [
|
|
984
|
+
218,
|
|
985
|
+
112,
|
|
986
|
+
214
|
|
987
|
+
],
|
|
988
|
+
palegoldenrod: [
|
|
989
|
+
238,
|
|
990
|
+
232,
|
|
991
|
+
170
|
|
992
|
+
],
|
|
993
|
+
palegreen: [
|
|
994
|
+
152,
|
|
995
|
+
251,
|
|
996
|
+
152
|
|
997
|
+
],
|
|
998
|
+
paleturquoise: [
|
|
999
|
+
175,
|
|
1000
|
+
238,
|
|
1001
|
+
238
|
|
1002
|
+
],
|
|
1003
|
+
palevioletred: [
|
|
1004
|
+
219,
|
|
1005
|
+
112,
|
|
1006
|
+
147
|
|
1007
|
+
],
|
|
1008
|
+
papayawhip: [
|
|
1009
|
+
255,
|
|
1010
|
+
239,
|
|
1011
|
+
213
|
|
1012
|
+
],
|
|
1013
|
+
peachpuff: [
|
|
1014
|
+
255,
|
|
1015
|
+
218,
|
|
1016
|
+
185
|
|
1017
|
+
],
|
|
1018
|
+
peru: [
|
|
1019
|
+
205,
|
|
1020
|
+
133,
|
|
1021
|
+
63
|
|
1022
|
+
],
|
|
1023
|
+
pink: [
|
|
1024
|
+
255,
|
|
1025
|
+
192,
|
|
1026
|
+
203
|
|
1027
|
+
],
|
|
1028
|
+
plum: [
|
|
1029
|
+
221,
|
|
1030
|
+
160,
|
|
1031
|
+
221
|
|
1032
|
+
],
|
|
1033
|
+
powderblue: [
|
|
1034
|
+
176,
|
|
1035
|
+
224,
|
|
1036
|
+
230
|
|
1037
|
+
],
|
|
1038
|
+
purple: [
|
|
1039
|
+
128,
|
|
1040
|
+
0,
|
|
1041
|
+
128
|
|
1042
|
+
],
|
|
1043
|
+
rebeccapurple: [
|
|
1044
|
+
102,
|
|
1045
|
+
51,
|
|
1046
|
+
153
|
|
1047
|
+
],
|
|
1048
|
+
red: [
|
|
1049
|
+
255,
|
|
1050
|
+
0,
|
|
1051
|
+
0
|
|
1052
|
+
],
|
|
1053
|
+
rosybrown: [
|
|
1054
|
+
188,
|
|
1055
|
+
143,
|
|
1056
|
+
143
|
|
1057
|
+
],
|
|
1058
|
+
royalblue: [
|
|
1059
|
+
65,
|
|
1060
|
+
105,
|
|
1061
|
+
225
|
|
1062
|
+
],
|
|
1063
|
+
saddlebrown: [
|
|
1064
|
+
139,
|
|
1065
|
+
69,
|
|
1066
|
+
19
|
|
1067
|
+
],
|
|
1068
|
+
salmon: [
|
|
1069
|
+
250,
|
|
1070
|
+
128,
|
|
1071
|
+
114
|
|
1072
|
+
],
|
|
1073
|
+
sandybrown: [
|
|
1074
|
+
244,
|
|
1075
|
+
164,
|
|
1076
|
+
96
|
|
1077
|
+
],
|
|
1078
|
+
seagreen: [
|
|
1079
|
+
46,
|
|
1080
|
+
139,
|
|
1081
|
+
87
|
|
1082
|
+
],
|
|
1083
|
+
seashell: [
|
|
1084
|
+
255,
|
|
1085
|
+
245,
|
|
1086
|
+
238
|
|
1087
|
+
],
|
|
1088
|
+
sienna: [
|
|
1089
|
+
160,
|
|
1090
|
+
82,
|
|
1091
|
+
45
|
|
1092
|
+
],
|
|
1093
|
+
silver: [
|
|
1094
|
+
192,
|
|
1095
|
+
192,
|
|
1096
|
+
192
|
|
1097
|
+
],
|
|
1098
|
+
skyblue: [
|
|
1099
|
+
135,
|
|
1100
|
+
206,
|
|
1101
|
+
235
|
|
1102
|
+
],
|
|
1103
|
+
slateblue: [
|
|
1104
|
+
106,
|
|
1105
|
+
90,
|
|
1106
|
+
205
|
|
1107
|
+
],
|
|
1108
|
+
slategray: [
|
|
1109
|
+
112,
|
|
1110
|
+
128,
|
|
1111
|
+
144
|
|
1112
|
+
],
|
|
1113
|
+
slategrey: [
|
|
1114
|
+
112,
|
|
1115
|
+
128,
|
|
1116
|
+
144
|
|
1117
|
+
],
|
|
1118
|
+
snow: [
|
|
1119
|
+
255,
|
|
1120
|
+
250,
|
|
1121
|
+
250
|
|
1122
|
+
],
|
|
1123
|
+
springgreen: [
|
|
1124
|
+
0,
|
|
1125
|
+
255,
|
|
1126
|
+
127
|
|
1127
|
+
],
|
|
1128
|
+
steelblue: [
|
|
1129
|
+
70,
|
|
1130
|
+
130,
|
|
1131
|
+
180
|
|
1132
|
+
],
|
|
1133
|
+
tan: [
|
|
1134
|
+
210,
|
|
1135
|
+
180,
|
|
1136
|
+
140
|
|
1137
|
+
],
|
|
1138
|
+
teal: [
|
|
1139
|
+
0,
|
|
1140
|
+
128,
|
|
1141
|
+
128
|
|
1142
|
+
],
|
|
1143
|
+
thistle: [
|
|
1144
|
+
216,
|
|
1145
|
+
191,
|
|
1146
|
+
216
|
|
1147
|
+
],
|
|
1148
|
+
tomato: [
|
|
1149
|
+
255,
|
|
1150
|
+
99,
|
|
1151
|
+
71
|
|
1152
|
+
],
|
|
1153
|
+
turquoise: [
|
|
1154
|
+
64,
|
|
1155
|
+
224,
|
|
1156
|
+
208
|
|
1157
|
+
],
|
|
1158
|
+
violet: [
|
|
1159
|
+
238,
|
|
1160
|
+
130,
|
|
1161
|
+
238
|
|
1162
|
+
],
|
|
1163
|
+
wheat: [
|
|
1164
|
+
245,
|
|
1165
|
+
222,
|
|
1166
|
+
179
|
|
1167
|
+
],
|
|
1168
|
+
white: [
|
|
1169
|
+
255,
|
|
1170
|
+
255,
|
|
1171
|
+
255
|
|
1172
|
+
],
|
|
1173
|
+
whitesmoke: [
|
|
1174
|
+
245,
|
|
1175
|
+
245,
|
|
1176
|
+
245
|
|
1177
|
+
],
|
|
1178
|
+
yellow: [
|
|
1179
|
+
255,
|
|
1180
|
+
255,
|
|
1181
|
+
0
|
|
1182
|
+
],
|
|
1183
|
+
yellowgreen: [
|
|
1184
|
+
154,
|
|
1185
|
+
205,
|
|
1186
|
+
50
|
|
1187
|
+
]
|
|
1188
|
+
};
|
|
1189
|
+
/**
|
|
1190
|
+
* Apply an ANSI style pair to a string with nesting-safe composition.
|
|
1191
|
+
*
|
|
1192
|
+
* When the input already contains the close sequence for the applied style
|
|
1193
|
+
* (e.g. from a nested style call that shares the same close code), the engine
|
|
1194
|
+
* reopens the outer style after each inner close to prevent style bleed.
|
|
1195
|
+
*
|
|
1196
|
+
* @param text - The string to style.
|
|
1197
|
+
* @param style - The ANSI pair to apply.
|
|
1198
|
+
* @returns The styled string, or the original string if empty.
|
|
1199
|
+
*
|
|
1200
|
+
* @example
|
|
1201
|
+
* ```ts
|
|
1202
|
+
* import { applyStyle } from "./styleEngine.ts";
|
|
1203
|
+
* import { bold, red } from "./ansiCodes.ts";
|
|
1204
|
+
*
|
|
1205
|
+
* // Simple usage
|
|
1206
|
+
* applyStyle("hello", bold); // "\x1b[1mhello\x1b[22m"
|
|
1207
|
+
*
|
|
1208
|
+
* // Nesting: bold wraps a red segment — bold reopens after red's close
|
|
1209
|
+
* const inner = applyStyle("world", red);
|
|
1210
|
+
* applyStyle(`hello ${inner}!`, bold);
|
|
1211
|
+
* ```
|
|
1212
|
+
*/
|
|
1213
|
+
function applyStyle(text, style) {
|
|
1214
|
+
if (text == null) return "";
|
|
1215
|
+
let result = String(text);
|
|
1216
|
+
if (result === "") return "";
|
|
1217
|
+
const { open, close } = style;
|
|
1218
|
+
if (result.includes(close)) result = result.replaceAll(close, close + open);
|
|
1219
|
+
return open + result + close;
|
|
1220
|
+
}
|
|
1221
|
+
/** Foreground close: matches the close of every static fg color (`\x1b[39m`). */
|
|
1222
|
+
const FG_CLOSE = "\x1B[39m";
|
|
1223
|
+
/** Background close: matches the close of every static bg color (`\x1b[49m`). */
|
|
1224
|
+
const BG_CLOSE = "\x1B[49m";
|
|
1225
|
+
/** Foreground/background extended-color SGR introducers. */
|
|
1226
|
+
const FG_INTRODUCER = "\x1B[38;";
|
|
1227
|
+
const BG_INTRODUCER = "\x1B[48;";
|
|
1228
|
+
/** Quote supported inputs, falling back to `String()` for hostile runtime values. */
|
|
1229
|
+
function describeInput(input) {
|
|
1230
|
+
try {
|
|
1231
|
+
return JSON.stringify(input) ?? String(input);
|
|
1232
|
+
} catch {
|
|
1233
|
+
return String(input);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function isNamedColor(value) {
|
|
1237
|
+
return Object.hasOwn(namedColorValues, value);
|
|
1238
|
+
}
|
|
1239
|
+
/** Parse a supported color into an RGB triple. */
|
|
1240
|
+
function parseRgb(input) {
|
|
1241
|
+
if (Array.isArray(input)) {
|
|
1242
|
+
if (input.length === 3 && input.every((channel) => Number.isInteger(channel) && channel >= 0 && channel <= 255)) return [
|
|
1243
|
+
input[0],
|
|
1244
|
+
input[1],
|
|
1245
|
+
input[2]
|
|
1246
|
+
];
|
|
1247
|
+
} else if (typeof input === "string") {
|
|
1248
|
+
const value = input.toLowerCase();
|
|
1249
|
+
if (isNamedColor(value)) return namedColorValues[value];
|
|
1250
|
+
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value)?.[1];
|
|
1251
|
+
if (hex) {
|
|
1252
|
+
const expanded = hex.length === 3 ? hex.split("").map((digit) => digit + digit).join("") : hex;
|
|
1253
|
+
return [
|
|
1254
|
+
Number.parseInt(expanded.slice(0, 2), 16),
|
|
1255
|
+
Number.parseInt(expanded.slice(2, 4), 16),
|
|
1256
|
+
Number.parseInt(expanded.slice(4, 6), 16)
|
|
1257
|
+
];
|
|
1258
|
+
}
|
|
1259
|
+
const rgb = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/.exec(value) ?? /^rgb\(\s*(\d{1,3})\s+(\d{1,3})\s+(\d{1,3})\s*\)$/.exec(value);
|
|
1260
|
+
if (rgb) {
|
|
1261
|
+
const channels = [
|
|
1262
|
+
Number(rgb[1]),
|
|
1263
|
+
Number(rgb[2]),
|
|
1264
|
+
Number(rgb[3])
|
|
1265
|
+
];
|
|
1266
|
+
if (channels.every((channel) => channel <= 255)) return channels;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
throw new TypeError(`Invalid color input: ${describeInput(input)}`);
|
|
1270
|
+
}
|
|
1271
|
+
function rgbToAnsi256(r, g, b) {
|
|
1272
|
+
const levels = [
|
|
1273
|
+
0,
|
|
1274
|
+
95,
|
|
1275
|
+
135,
|
|
1276
|
+
175,
|
|
1277
|
+
215,
|
|
1278
|
+
255
|
|
1279
|
+
];
|
|
1280
|
+
const nearest = (channel) => levels.reduce((best, value, index) => Math.abs(value - channel) < Math.abs(levels[best] - channel) ? index : best, 0);
|
|
1281
|
+
const [ir, ig, ib] = [
|
|
1282
|
+
nearest(r),
|
|
1283
|
+
nearest(g),
|
|
1284
|
+
nearest(b)
|
|
1285
|
+
];
|
|
1286
|
+
const grayIndex = Math.min(23, Math.max(0, Math.round(((r + g + b) / 3 - 8) / 10)));
|
|
1287
|
+
const gray = 8 + grayIndex * 10;
|
|
1288
|
+
const distance = (cr, cg, cb) => (r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2;
|
|
1289
|
+
return distance(gray, gray, gray) < distance(levels[ir], levels[ig], levels[ib]) ? 232 + grayIndex : 16 + 36 * ir + 6 * ig + ib;
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Quantize `[r, g, b]` to a foreground SGR parameter (`30`–`37`, `90`–`97`).
|
|
1293
|
+
* Same algorithm as `ansi-styles` / `chalk`: bucket each channel at 50%,
|
|
1294
|
+
* pack into a 3-bit base color, then add 60 for bright when the max
|
|
1295
|
+
* channel rounds up. Call sites add `+10` for backgrounds.
|
|
1296
|
+
*
|
|
1297
|
+
* @internal
|
|
1298
|
+
*/
|
|
1299
|
+
function rgbToAnsi16Param(r, g, b) {
|
|
1300
|
+
const brightnessBucket = Math.round(Math.max(r, g, b) / 127.5);
|
|
1301
|
+
if (brightnessBucket === 0) return 30;
|
|
1302
|
+
let ansi = 30 + ((Math.round(b / 255) << 2 | Math.round(g / 255) << 1 | Math.round(r / 255)) & 7);
|
|
1303
|
+
if (brightnessBucket === 2) ansi += 60;
|
|
1304
|
+
return ansi;
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Foreground SGR open sequence at `depth`.
|
|
1308
|
+
*
|
|
1309
|
+
* @internal
|
|
1310
|
+
*/
|
|
1311
|
+
function fgOpen(input, depth) {
|
|
1312
|
+
const [r, g, b] = parseRgb(input);
|
|
1313
|
+
if (depth === "16") return `\x1b[${rgbToAnsi16Param(r, g, b)}m`;
|
|
1314
|
+
if (depth === "256") return `\x1b[38;5;${rgbToAnsi256(r, g, b)}m`;
|
|
1315
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Background SGR open sequence at `depth`. For `truecolor` / `256`,
|
|
1319
|
+
* derived from {@link fgOpen} by swapping the `\x1b[38;` introducer for
|
|
1320
|
+
* `\x1b[48;` (both the truecolor and 256-color forms use it). For `16`,
|
|
1321
|
+
* quantized directly to a real background SGR.
|
|
1322
|
+
*
|
|
1323
|
+
* @internal
|
|
1324
|
+
*/
|
|
1325
|
+
function bgOpen(input, depth) {
|
|
1326
|
+
if (depth === "16") {
|
|
1327
|
+
const [r, g, b] = parseRgb(input);
|
|
1328
|
+
return `\x1b[${rgbToAnsi16Param(r, g, b) + 10}m`;
|
|
1329
|
+
}
|
|
1330
|
+
return fgOpen(input, depth).replace(FG_INTRODUCER, BG_INTRODUCER);
|
|
1331
|
+
}
|
|
1332
|
+
function colorPair(kind, input, depth) {
|
|
1333
|
+
if (depth === "none") {
|
|
1334
|
+
parseRgb(input);
|
|
1335
|
+
return {
|
|
1336
|
+
open: "",
|
|
1337
|
+
close: ""
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
return {
|
|
1341
|
+
open: kind === "fg" ? fgOpen(input, depth) : bgOpen(input, depth),
|
|
1342
|
+
close: kind === "fg" ? FG_CLOSE : BG_CLOSE
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Depth-aware foreground `AnsiPair` for chain composition. `depth: "none"`
|
|
1347
|
+
* returns an empty pair (still validates input). Used by `createStyle()`
|
|
1348
|
+
* to back `chainable.fg(input)`.
|
|
1349
|
+
*
|
|
1350
|
+
* @throws {TypeError} If `input` is not a recognized color.
|
|
1351
|
+
* @internal
|
|
1352
|
+
*/
|
|
1353
|
+
function fgPairAtDepth(input, depth) {
|
|
1354
|
+
return colorPair("fg", input, depth);
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Depth-aware background `AnsiPair` for chain composition. Mirrors
|
|
1358
|
+
* {@link fgPairAtDepth}.
|
|
1359
|
+
*
|
|
1360
|
+
* @throws {TypeError} If `input` is not a recognized color.
|
|
1361
|
+
* @internal
|
|
1362
|
+
*/
|
|
1363
|
+
function bgPairAtDepth(input, depth) {
|
|
1364
|
+
return colorPair("bg", input, depth);
|
|
1365
|
+
}
|
|
1366
|
+
function paint(kind, text, input, depth) {
|
|
1367
|
+
const pair = colorPair(kind, input, depth);
|
|
1368
|
+
return depth === "none" ? text : applyStyle(text, pair);
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Apply a foreground color to `text` from any {@link ColorInput}. `depth`
|
|
1372
|
+
* selects the output format (`"truecolor"` default, `"256"`, `"16"`, or
|
|
1373
|
+
* `"none"`). `"none"` returns `text` unchanged but still validates
|
|
1374
|
+
* `input`. Empty `text` short-circuits to `""`.
|
|
1375
|
+
*
|
|
1376
|
+
* @throws {TypeError} If `input` is not a recognized color.
|
|
1377
|
+
*
|
|
1378
|
+
* @example
|
|
1379
|
+
* ```ts
|
|
1380
|
+
* fg("error", "#ff0000");
|
|
1381
|
+
* fg("ocean", "rgb(0, 128, 255)");
|
|
1382
|
+
* fg("custom", [255, 127, 80]);
|
|
1383
|
+
* fg("256-only", "#ff0000", "256"); // \x1b[38;5;196m...
|
|
1384
|
+
* ```
|
|
1385
|
+
*/
|
|
1386
|
+
function fg$1(text, input, depth = "truecolor") {
|
|
1387
|
+
return paint("fg", text, input, depth);
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Apply a background color to `text`. Mirrors {@link fg}.
|
|
1391
|
+
*
|
|
1392
|
+
* @throws {TypeError} If `input` is not a recognized color.
|
|
1393
|
+
*
|
|
1394
|
+
* @example
|
|
1395
|
+
* ```ts
|
|
1396
|
+
* bg("warning", "#ff8800");
|
|
1397
|
+
* bg("info", "rgb(0, 128, 255)");
|
|
1398
|
+
* ```
|
|
1399
|
+
*/
|
|
1400
|
+
function bg$1(text, input, depth = "truecolor") {
|
|
1401
|
+
return paint("bg", text, input, depth);
|
|
1402
|
+
}
|
|
1403
|
+
const OSC = "\x1B]";
|
|
1404
|
+
const ST = "\x1B\\";
|
|
1405
|
+
const HYPERLINK_CLOSE = `${OSC}8;;${ST}`;
|
|
1406
|
+
function assertMatches(value, label, pattern, suffix = "") {
|
|
1407
|
+
if (!pattern.test(value)) throw new TypeError(`Invalid ${label}: ${JSON.stringify(value)} must contain only printable ASCII characters${suffix}.`);
|
|
1408
|
+
}
|
|
1409
|
+
function serializeParams(options) {
|
|
1410
|
+
const id = options?.id;
|
|
1411
|
+
if (id === void 0 || id === "") return "";
|
|
1412
|
+
assertMatches(id, "hyperlink id", /^[\x20-\x7e]*$/);
|
|
1413
|
+
if (id.includes(":") || id.includes(";")) throw new TypeError("Invalid hyperlink id: \":\" and \";\" are reserved by the OSC 8 format.");
|
|
1414
|
+
return `id=${id}`;
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Create an OSC 8 hyperlink escape pair for `url`. Backs {@link link} and
|
|
1418
|
+
* the `link` method on style instances; also used to validate URLs when
|
|
1419
|
+
* hyperlink emission is disabled.
|
|
1420
|
+
*
|
|
1421
|
+
* @param url - Target URL. Must contain only printable ASCII characters
|
|
1422
|
+
* and no spaces (per the OSC 8 spec). URL-encode characters outside
|
|
1423
|
+
* that range before passing.
|
|
1424
|
+
* @param options - Optional {@link HyperlinkOptions} — currently just
|
|
1425
|
+
* `id` for grouping multi-segment links.
|
|
1426
|
+
* @returns An {@link AnsiPair} whose `open` carries the URL and `close`
|
|
1427
|
+
* terminates the hyperlink.
|
|
1428
|
+
* @throws {TypeError} If `url` contains spaces or non-printable ASCII,
|
|
1429
|
+
* or if `options.id` contains `":"` / `";"` (reserved by OSC 8) or
|
|
1430
|
+
* non-printable characters.
|
|
1431
|
+
*
|
|
1432
|
+
* @internal
|
|
1433
|
+
*/
|
|
1434
|
+
function linkCode(url, options) {
|
|
1435
|
+
assertMatches(url, "hyperlink URL", /^[\x21-\x7e]*$/, " without spaces");
|
|
1436
|
+
const params = serializeParams(options);
|
|
1437
|
+
return {
|
|
1438
|
+
open: `${OSC}8;${params};${url}${ST}`,
|
|
1439
|
+
close: HYPERLINK_CLOSE
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Wrap `text` in OSC 8 hyperlink escape sequences.
|
|
1444
|
+
*
|
|
1445
|
+
* @param text - The visible label.
|
|
1446
|
+
* @param url - Target URL. Must contain only printable ASCII characters
|
|
1447
|
+
* and no spaces (per the OSC 8 spec); URL-encode anything outside that
|
|
1448
|
+
* range before passing.
|
|
1449
|
+
* @param options - Optional {@link HyperlinkOptions}.
|
|
1450
|
+
* @returns The styled string.
|
|
1451
|
+
* @throws {TypeError} If `url` contains spaces or non-printable ASCII,
|
|
1452
|
+
* or if `options.id` contains `":"` / `";"` (reserved by OSC 8) or
|
|
1453
|
+
* non-printable characters.
|
|
1454
|
+
*
|
|
1455
|
+
* @example
|
|
1456
|
+
* ```ts
|
|
1457
|
+
* import { link } from "@crustjs/style";
|
|
1458
|
+
*
|
|
1459
|
+
* console.log(link("docs", "https://crustjs.dev"));
|
|
1460
|
+
* console.log(link("page 1", "https://example.com/p1", { id: "intro" }));
|
|
1461
|
+
* ```
|
|
1462
|
+
*/
|
|
1463
|
+
function link$1(text, url, options) {
|
|
1464
|
+
return applyStyle(text, linkCode(url, options));
|
|
1465
|
+
}
|
|
1466
|
+
const dynamicColorKinds = [[
|
|
1467
|
+
"fg",
|
|
1468
|
+
fgPairAtDepth,
|
|
1469
|
+
fg$1
|
|
1470
|
+
], [
|
|
1471
|
+
"bg",
|
|
1472
|
+
bgPairAtDepth,
|
|
1473
|
+
bg$1
|
|
1474
|
+
]];
|
|
1475
|
+
const styleMethodPairs = ansiCodes_exports;
|
|
1476
|
+
function stepIsModifier(step) {
|
|
1477
|
+
return step.kind === "named" && isModifierName(step.name);
|
|
1478
|
+
}
|
|
1479
|
+
function stepPair(step, colorDepth) {
|
|
1480
|
+
if (step.kind === "named") return styleMethodPairs[step.name];
|
|
1481
|
+
return step.kind === "fg" ? fgPairAtDepth(step.input, colorDepth) : bgPairAtDepth(step.input, colorDepth);
|
|
1482
|
+
}
|
|
1483
|
+
function applyChain(text, steps, resolveCapabilities) {
|
|
1484
|
+
if (text == null) return "";
|
|
1485
|
+
let result = String(text);
|
|
1486
|
+
if (result === "") return result;
|
|
1487
|
+
const capabilities = resolveCapabilities();
|
|
1488
|
+
const { modifiersEnabled, colorsEnabled } = capabilities;
|
|
1489
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
1490
|
+
const step = steps[i];
|
|
1491
|
+
if (step === void 0) continue;
|
|
1492
|
+
if (stepIsModifier(step) ? !modifiersEnabled : !colorsEnabled) continue;
|
|
1493
|
+
result = applyStyle(result, stepPair(step, capabilities.colorDepth));
|
|
1494
|
+
}
|
|
1495
|
+
return result;
|
|
1496
|
+
}
|
|
1497
|
+
function isTemplateStringsArray(value) {
|
|
1498
|
+
return Array.isArray(value) && "raw" in value && Array.isArray(value.raw);
|
|
1499
|
+
}
|
|
1500
|
+
function buildChainableStyleFactory(resolveCapabilities, runtime) {
|
|
1501
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1502
|
+
const pairDepth = runtime ? "truecolor" : resolveCapabilities().colorDepth;
|
|
1503
|
+
function createChainableStyle(steps) {
|
|
1504
|
+
const key = steps.map((step) => step.kind === "named" ? step.name : `~${stepPair(step, "truecolor").open}`).join("|");
|
|
1505
|
+
const cached = cache.get(key);
|
|
1506
|
+
if (cached) return cached;
|
|
1507
|
+
const styleFn = ((first, ...rest) => {
|
|
1508
|
+
if (isTemplateStringsArray(first)) {
|
|
1509
|
+
let text = "";
|
|
1510
|
+
for (let i = 0; i < first.length; i++) {
|
|
1511
|
+
text += first[i] ?? "";
|
|
1512
|
+
if (i < rest.length) text += String(rest[i]);
|
|
1513
|
+
}
|
|
1514
|
+
return applyChain(text, steps, resolveCapabilities);
|
|
1515
|
+
}
|
|
1516
|
+
return applyChain(first, steps, resolveCapabilities);
|
|
1517
|
+
});
|
|
1518
|
+
if (cache.size >= 1024) cache.delete(cache.keys().next().value);
|
|
1519
|
+
cache.set(key, styleFn);
|
|
1520
|
+
for (const name of styleMethodNames) Object.defineProperty(styleFn, name, {
|
|
1521
|
+
configurable: false,
|
|
1522
|
+
enumerable: true,
|
|
1523
|
+
get() {
|
|
1524
|
+
return createChainableStyle([...steps, {
|
|
1525
|
+
kind: "named",
|
|
1526
|
+
name
|
|
1527
|
+
}]);
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
for (const [kind, pairAtDepth] of dynamicColorKinds) Object.defineProperty(styleFn, kind, {
|
|
1531
|
+
configurable: false,
|
|
1532
|
+
enumerable: true,
|
|
1533
|
+
value: (input) => {
|
|
1534
|
+
pairAtDepth(input, "truecolor");
|
|
1535
|
+
return createChainableStyle([...steps, {
|
|
1536
|
+
kind,
|
|
1537
|
+
input
|
|
1538
|
+
}]);
|
|
1539
|
+
},
|
|
1540
|
+
writable: false
|
|
1541
|
+
});
|
|
1542
|
+
let open = "";
|
|
1543
|
+
let close = "";
|
|
1544
|
+
for (const step of steps) {
|
|
1545
|
+
const pair = stepPair(step, pairDepth);
|
|
1546
|
+
open += pair.open;
|
|
1547
|
+
close = pair.close + close;
|
|
1548
|
+
}
|
|
1549
|
+
Object.defineProperty(styleFn, "open", {
|
|
1550
|
+
value: open,
|
|
1551
|
+
writable: false,
|
|
1552
|
+
configurable: false,
|
|
1553
|
+
enumerable: true
|
|
1554
|
+
});
|
|
1555
|
+
Object.defineProperty(styleFn, "close", {
|
|
1556
|
+
value: close,
|
|
1557
|
+
writable: false,
|
|
1558
|
+
configurable: false,
|
|
1559
|
+
enumerable: true
|
|
1560
|
+
});
|
|
1561
|
+
return Object.freeze(styleFn);
|
|
1562
|
+
}
|
|
1563
|
+
return createChainableStyle;
|
|
1564
|
+
}
|
|
1565
|
+
function buildStyleMethods(createChainableStyle) {
|
|
1566
|
+
const methods = {};
|
|
1567
|
+
for (const methodName of styleMethodNames) methods[methodName] = createChainableStyle([{
|
|
1568
|
+
kind: "named",
|
|
1569
|
+
name: methodName
|
|
1570
|
+
}]);
|
|
1571
|
+
return methods;
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Create a configured style instance with mode-aware styling functions.
|
|
1575
|
+
*
|
|
1576
|
+
* The returned instance provides the full set of modifier, foreground color,
|
|
1577
|
+
* and background color functions. In `"never"` mode, all functions return
|
|
1578
|
+
* plain text without ANSI codes. In `"always"` mode, ANSI codes are always
|
|
1579
|
+
* emitted. In `"auto"` mode, color methods respect `stdout.isTTY` and
|
|
1580
|
+
* `NO_COLOR`, non-color modifiers (bold, italic, etc.) follow TTY only,
|
|
1581
|
+
* and `FORCE_COLOR`, when set, decides unconditionally for both.
|
|
1582
|
+
*
|
|
1583
|
+
* @param options - Configuration options. Defaults to `{ mode: "auto" }`.
|
|
1584
|
+
* @returns A frozen {@link StyleInstance} with all styling functions.
|
|
1585
|
+
*
|
|
1586
|
+
* @example
|
|
1587
|
+
* ```ts
|
|
1588
|
+
* // Auto-detect terminal capabilities
|
|
1589
|
+
* const s = createStyle();
|
|
1590
|
+
* console.log(s.bold("hello"));
|
|
1591
|
+
*
|
|
1592
|
+
* // Force color output
|
|
1593
|
+
* const color = createStyle({ mode: "always" });
|
|
1594
|
+
* console.log(color.red("error"));
|
|
1595
|
+
* console.log(color.bold.red("critical"));
|
|
1596
|
+
*
|
|
1597
|
+
* // Disable all styling
|
|
1598
|
+
* const plain = createStyle({ mode: "never" });
|
|
1599
|
+
* console.log(plain.red("error")); // "error"
|
|
1600
|
+
*
|
|
1601
|
+
* // Deterministic testing
|
|
1602
|
+
* const test = createStyle({
|
|
1603
|
+
* mode: "auto",
|
|
1604
|
+
* overrides: { isTTY: true, noColor: undefined },
|
|
1605
|
+
* });
|
|
1606
|
+
* ```
|
|
1607
|
+
*/
|
|
1608
|
+
function resolveStyleCapabilities(options) {
|
|
1609
|
+
const mode = options?.mode ?? "auto";
|
|
1610
|
+
const modifiersEnabled = resolveModifierCapability(mode, options?.overrides);
|
|
1611
|
+
const colorDepth = resolveColorDepth(mode, options?.overrides);
|
|
1612
|
+
return {
|
|
1613
|
+
modifiersEnabled,
|
|
1614
|
+
colorDepth,
|
|
1615
|
+
colorsEnabled: colorDepth !== "none",
|
|
1616
|
+
trueColorEnabled: colorDepth === "truecolor"
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
function createStyleInstance(options, runtime) {
|
|
1620
|
+
const resolveCapabilities = runtime ? resolveStyleCapabilities : (() => {
|
|
1621
|
+
const capabilities = resolveStyleCapabilities(options);
|
|
1622
|
+
return () => capabilities;
|
|
1623
|
+
})();
|
|
1624
|
+
const createChainableStyle = buildChainableStyleFactory(resolveCapabilities, runtime);
|
|
1625
|
+
const methods = buildStyleMethods(createChainableStyle);
|
|
1626
|
+
const instance = {
|
|
1627
|
+
get enabled() {
|
|
1628
|
+
const { modifiersEnabled, colorsEnabled } = resolveCapabilities();
|
|
1629
|
+
return modifiersEnabled || colorsEnabled;
|
|
1630
|
+
},
|
|
1631
|
+
get colorsEnabled() {
|
|
1632
|
+
return resolveCapabilities().colorsEnabled;
|
|
1633
|
+
},
|
|
1634
|
+
get trueColorEnabled() {
|
|
1635
|
+
return resolveCapabilities().trueColorEnabled;
|
|
1636
|
+
},
|
|
1637
|
+
get colorDepth() {
|
|
1638
|
+
return resolveCapabilities().colorDepth;
|
|
1639
|
+
},
|
|
1640
|
+
link(text, url, hyperlinkOptions) {
|
|
1641
|
+
if (resolveCapabilities().modifiersEnabled) return link$1(text, url, hyperlinkOptions);
|
|
1642
|
+
linkCode(url, hyperlinkOptions);
|
|
1643
|
+
return text;
|
|
1644
|
+
},
|
|
1645
|
+
...Object.fromEntries(dynamicColorKinds.map(([kind, pairAtDepth, paint]) => [kind, (...args) => {
|
|
1646
|
+
const resolved = resolveCapabilities();
|
|
1647
|
+
if (args.length === 1) {
|
|
1648
|
+
pairAtDepth(args[0], "truecolor");
|
|
1649
|
+
return createChainableStyle([{
|
|
1650
|
+
kind,
|
|
1651
|
+
input: args[0]
|
|
1652
|
+
}]);
|
|
1653
|
+
}
|
|
1654
|
+
return paint(args[0], args[1], resolved.colorDepth);
|
|
1655
|
+
}])),
|
|
1656
|
+
...methods
|
|
1657
|
+
};
|
|
1658
|
+
return Object.freeze(instance);
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Default style instance using `"auto"` mode.
|
|
1662
|
+
*
|
|
1663
|
+
* Emits color ANSI codes when stdout is a TTY and `NO_COLOR` is not set.
|
|
1664
|
+
* Non-color modifiers (bold, italic, etc.) follow TTY only; `FORCE_COLOR`,
|
|
1665
|
+
* when set, decides unconditionally for both.
|
|
1666
|
+
* Import this for convenient access without explicit configuration.
|
|
1667
|
+
*
|
|
1668
|
+
* @example
|
|
1669
|
+
* ```ts
|
|
1670
|
+
* import { style } from "@crustjs/style";
|
|
1671
|
+
*
|
|
1672
|
+
* console.log(style.bold("hello"));
|
|
1673
|
+
* console.log(style.red("error"));
|
|
1674
|
+
* ```
|
|
1675
|
+
*/
|
|
1676
|
+
const style = createStyleInstance(void 0, true);
|
|
1677
|
+
style.black;
|
|
1678
|
+
style.red;
|
|
1679
|
+
const green = style.green;
|
|
1680
|
+
const yellow = style.yellow;
|
|
1681
|
+
style.blue;
|
|
1682
|
+
style.magenta;
|
|
1683
|
+
const cyan = style.cyan;
|
|
1684
|
+
style.white;
|
|
1685
|
+
style.gray;
|
|
1686
|
+
style.brightRed;
|
|
1687
|
+
style.brightGreen;
|
|
1688
|
+
style.brightYellow;
|
|
1689
|
+
style.brightBlue;
|
|
1690
|
+
style.brightMagenta;
|
|
1691
|
+
style.brightCyan;
|
|
1692
|
+
style.brightWhite;
|
|
1693
|
+
style.bgBlack;
|
|
1694
|
+
style.bgRed;
|
|
1695
|
+
style.bgGreen;
|
|
1696
|
+
style.bgYellow;
|
|
1697
|
+
style.bgBlue;
|
|
1698
|
+
style.bgMagenta;
|
|
1699
|
+
style.bgCyan;
|
|
1700
|
+
style.bgWhite;
|
|
1701
|
+
style.bgBrightBlack;
|
|
1702
|
+
style.bgBrightRed;
|
|
1703
|
+
style.bgBrightGreen;
|
|
1704
|
+
style.bgBrightYellow;
|
|
1705
|
+
style.bgBrightBlue;
|
|
1706
|
+
style.bgBrightMagenta;
|
|
1707
|
+
style.bgBrightCyan;
|
|
1708
|
+
style.bgBrightWhite;
|
|
1709
|
+
const bold = style.bold;
|
|
1710
|
+
const dim = style.dim;
|
|
1711
|
+
style.italic;
|
|
1712
|
+
style.underline;
|
|
1713
|
+
style.inverse;
|
|
1714
|
+
style.hidden;
|
|
1715
|
+
style.strikethrough;
|
|
1716
|
+
style.link;
|
|
1717
|
+
style.fg;
|
|
1718
|
+
style.bg;
|
|
1719
|
+
//#endregion
|
|
1720
|
+
//#region ../utils/dist/json.js
|
|
1721
|
+
/** Narrow a JSON document root to an object (not array/scalar/null). */
|
|
1722
|
+
function isJsonObject(value) {
|
|
1723
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1724
|
+
}
|
|
1725
|
+
//#endregion
|
|
1726
|
+
//#region ../utils/dist/path.js
|
|
1727
|
+
/** Returns whether child is parent itself or lies below it. */
|
|
1728
|
+
function isWithin(parent, child) {
|
|
1729
|
+
const path = relative(parent, child);
|
|
1730
|
+
return path === "" || !isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`);
|
|
1731
|
+
}
|
|
1732
|
+
//#endregion
|
|
1733
|
+
//#region ../utils/dist/error.js
|
|
1734
|
+
/** Narrows a caught Node.js system error to its stable errno contract. */
|
|
1735
|
+
function isErrnoException(error) {
|
|
1736
|
+
return error instanceof Error && "code" in error && typeof error.code === "string";
|
|
1737
|
+
}
|
|
1738
|
+
//#endregion
|
|
1739
|
+
//#region ../utils/dist/process.js
|
|
1740
|
+
const WINDOWS_SHELL_UNSAFE = /[\0\r\n"%!^`<>&|]/;
|
|
1741
|
+
const WINDOWS_SHELL_META = /([()\][%!^"`<>&|;, *?])/g;
|
|
1742
|
+
function escapeWindowsShellArgument(value) {
|
|
1743
|
+
let escaped = value.replace(/(?=(\\+?)?)\1"/g, "$1$1\\\"");
|
|
1744
|
+
escaped = escaped.replace(/(?=(\\+?)?)\1$/g, "$1$1");
|
|
1745
|
+
escaped = `"${escaped}"`.replace(WINDOWS_SHELL_META, "^$1");
|
|
1746
|
+
return escaped.replace(WINDOWS_SHELL_META, "^$1");
|
|
1747
|
+
}
|
|
1748
|
+
/** @internal Build Node's escaped Windows command-shim workaround. */
|
|
1749
|
+
function getWindowsShimCommand(command, args, shell, platform = process.platform) {
|
|
1750
|
+
if (shell || platform !== "win32" || !/\.(cmd|bat)$/i.test(command)) return null;
|
|
1751
|
+
const shimCommand = win32.normalize(command);
|
|
1752
|
+
for (const [index, value] of [shimCommand, ...args].entries()) if (WINDOWS_SHELL_UNSAFE.test(value)) {
|
|
1753
|
+
const label = index === 0 ? "command" : `argument ${index}`;
|
|
1754
|
+
throw new Error(`Windows command shim ${label} ${JSON.stringify(value)} contains unsafe shell characters`);
|
|
1755
|
+
}
|
|
1756
|
+
const commandLine = [shimCommand.replace(WINDOWS_SHELL_META, "^$1"), ...args.map(escapeWindowsShellArgument)].join(" ");
|
|
1757
|
+
return {
|
|
1758
|
+
command: process.env.ComSpec ?? "cmd.exe",
|
|
1759
|
+
args: [
|
|
1760
|
+
"/d",
|
|
1761
|
+
"/s",
|
|
1762
|
+
"/c",
|
|
1763
|
+
`"${commandLine}"`
|
|
1764
|
+
],
|
|
1765
|
+
windowsVerbatimArguments: true
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Spawn a process and wait for it to exit without imposing an error policy.
|
|
1770
|
+
*
|
|
1771
|
+
* On Windows, the automatic `.cmd`/`.bat` workaround escapes arguments for
|
|
1772
|
+
* `cmd.exe`; unsafe shell-expansion characters throw before spawning. Explicit
|
|
1773
|
+
* `shell: true` calls are passed through unchanged and own their shell escaping.
|
|
1774
|
+
*/
|
|
1775
|
+
async function runProcess(command, args = [], options = {}) {
|
|
1776
|
+
const windowsShimCommand = getWindowsShimCommand(command, args, options.shell);
|
|
1777
|
+
const collect = (options.stdio ?? "collect") === "collect";
|
|
1778
|
+
const collectStdout = collect && options.stdout !== "ignore";
|
|
1779
|
+
const proc = spawn(windowsShimCommand?.command ?? command, windowsShimCommand?.args ?? args, {
|
|
1780
|
+
cwd: options.cwd,
|
|
1781
|
+
env: options.env,
|
|
1782
|
+
shell: options.shell,
|
|
1783
|
+
stdio: collect ? [
|
|
1784
|
+
"ignore",
|
|
1785
|
+
collectStdout ? "pipe" : "ignore",
|
|
1786
|
+
"pipe"
|
|
1787
|
+
] : "inherit",
|
|
1788
|
+
windowsVerbatimArguments: windowsShimCommand?.windowsVerbatimArguments
|
|
1789
|
+
});
|
|
1790
|
+
const [stdout, stderr, [exitCode]] = await Promise.all([
|
|
1791
|
+
collectStdout ? text(proc.stdout) : "",
|
|
1792
|
+
collect ? text(proc.stderr) : "",
|
|
1793
|
+
once(proc, "close")
|
|
1794
|
+
]);
|
|
1795
|
+
return {
|
|
1796
|
+
exitCode,
|
|
1797
|
+
stdout,
|
|
1798
|
+
stderr
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
/** Resolve a bare executable name from PATH (and PATHEXT on Windows). */
|
|
1802
|
+
function which(command) {
|
|
1803
|
+
if (command.includes(sep) || command.includes("/")) {
|
|
1804
|
+
try {
|
|
1805
|
+
accessSync(command, constants.X_OK);
|
|
1806
|
+
if (statSync(command).isFile()) return command;
|
|
1807
|
+
} catch {}
|
|
1808
|
+
return null;
|
|
1809
|
+
}
|
|
1810
|
+
const extensions = process.platform === "win32" && !extname(command) ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
|
|
1811
|
+
for (const directory of process.env.PATH?.split(delimiter) ?? []) for (const extension of extensions) {
|
|
1812
|
+
const candidate = resolve(join(directory, command + extension));
|
|
1813
|
+
try {
|
|
1814
|
+
accessSync(candidate, constants.X_OK);
|
|
1815
|
+
if (statSync(candidate).isFile()) return candidate;
|
|
1816
|
+
} catch {}
|
|
1817
|
+
}
|
|
1818
|
+
return null;
|
|
1819
|
+
}
|
|
1820
|
+
//#endregion
|
|
1821
|
+
//#region src/utils/build-helpers.ts
|
|
1822
|
+
const BUILD_RUNTIMES = [
|
|
1823
|
+
"bun",
|
|
1824
|
+
"deno",
|
|
1825
|
+
"node"
|
|
1826
|
+
];
|
|
1827
|
+
const BUN_TARGETS = {
|
|
1828
|
+
runtime: "Bun",
|
|
1829
|
+
targets: [
|
|
1830
|
+
"bun-linux-x64",
|
|
1831
|
+
"bun-linux-arm64",
|
|
1832
|
+
"bun-linux-x64-musl",
|
|
1833
|
+
"bun-linux-arm64-musl",
|
|
1834
|
+
"bun-darwin-x64",
|
|
1835
|
+
"bun-darwin-arm64",
|
|
1836
|
+
"bun-windows-x64",
|
|
1837
|
+
"bun-windows-arm64"
|
|
1838
|
+
],
|
|
1839
|
+
info: {
|
|
1840
|
+
"bun-linux-x64": {
|
|
1841
|
+
alias: "linux-x64",
|
|
1842
|
+
platformKey: "linux-x64",
|
|
1843
|
+
os: "linux",
|
|
1844
|
+
cpu: "x64",
|
|
1845
|
+
libc: "glibc"
|
|
1846
|
+
},
|
|
1847
|
+
"bun-linux-arm64": {
|
|
1848
|
+
alias: "linux-arm64",
|
|
1849
|
+
platformKey: "linux-arm64",
|
|
1850
|
+
os: "linux",
|
|
1851
|
+
cpu: "arm64",
|
|
1852
|
+
libc: "glibc"
|
|
1853
|
+
},
|
|
1854
|
+
"bun-linux-x64-musl": {
|
|
1855
|
+
alias: "linux-x64-musl",
|
|
1856
|
+
platformKey: "linux-x64-musl",
|
|
1857
|
+
os: "linux",
|
|
1858
|
+
cpu: "x64",
|
|
1859
|
+
libc: "musl"
|
|
1860
|
+
},
|
|
1861
|
+
"bun-linux-arm64-musl": {
|
|
1862
|
+
alias: "linux-arm64-musl",
|
|
1863
|
+
platformKey: "linux-arm64-musl",
|
|
1864
|
+
os: "linux",
|
|
1865
|
+
cpu: "arm64",
|
|
1866
|
+
libc: "musl"
|
|
1867
|
+
},
|
|
1868
|
+
"bun-darwin-x64": {
|
|
1869
|
+
alias: "darwin-x64",
|
|
1870
|
+
platformKey: "darwin-x64",
|
|
1871
|
+
os: "darwin",
|
|
1872
|
+
cpu: "x64"
|
|
1873
|
+
},
|
|
1874
|
+
"bun-darwin-arm64": {
|
|
1875
|
+
alias: "darwin-arm64",
|
|
1876
|
+
platformKey: "darwin-arm64",
|
|
1877
|
+
os: "darwin",
|
|
1878
|
+
cpu: "arm64"
|
|
1879
|
+
},
|
|
1880
|
+
"bun-windows-x64": {
|
|
1881
|
+
alias: "windows-x64",
|
|
1882
|
+
platformKey: "win32-x64",
|
|
1883
|
+
os: "win32",
|
|
1884
|
+
cpu: "x64"
|
|
1885
|
+
},
|
|
1886
|
+
"bun-windows-arm64": {
|
|
1887
|
+
alias: "windows-arm64",
|
|
1888
|
+
platformKey: "win32-arm64",
|
|
1889
|
+
os: "win32",
|
|
1890
|
+
cpu: "arm64"
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
const DENO_TARGETS = {
|
|
1895
|
+
runtime: "Deno",
|
|
1896
|
+
targets: [
|
|
1897
|
+
"x86_64-unknown-linux-gnu",
|
|
1898
|
+
"aarch64-unknown-linux-gnu",
|
|
1899
|
+
"x86_64-apple-darwin",
|
|
1900
|
+
"aarch64-apple-darwin",
|
|
1901
|
+
"x86_64-pc-windows-msvc",
|
|
1902
|
+
"aarch64-pc-windows-msvc"
|
|
1903
|
+
],
|
|
1904
|
+
info: {
|
|
1905
|
+
"x86_64-unknown-linux-gnu": {
|
|
1906
|
+
alias: "linux-x64",
|
|
1907
|
+
platformKey: "linux-x64",
|
|
1908
|
+
os: "linux",
|
|
1909
|
+
cpu: "x64",
|
|
1910
|
+
libc: "glibc"
|
|
1911
|
+
},
|
|
1912
|
+
"aarch64-unknown-linux-gnu": {
|
|
1913
|
+
alias: "linux-arm64",
|
|
1914
|
+
platformKey: "linux-arm64",
|
|
1915
|
+
os: "linux",
|
|
1916
|
+
cpu: "arm64",
|
|
1917
|
+
libc: "glibc"
|
|
1918
|
+
},
|
|
1919
|
+
"x86_64-apple-darwin": {
|
|
1920
|
+
alias: "darwin-x64",
|
|
1921
|
+
platformKey: "darwin-x64",
|
|
1922
|
+
os: "darwin",
|
|
1923
|
+
cpu: "x64"
|
|
1924
|
+
},
|
|
1925
|
+
"aarch64-apple-darwin": {
|
|
1926
|
+
alias: "darwin-arm64",
|
|
1927
|
+
platformKey: "darwin-arm64",
|
|
1928
|
+
os: "darwin",
|
|
1929
|
+
cpu: "arm64"
|
|
1930
|
+
},
|
|
1931
|
+
"x86_64-pc-windows-msvc": {
|
|
1932
|
+
alias: "windows-x64",
|
|
1933
|
+
platformKey: "win32-x64",
|
|
1934
|
+
os: "win32",
|
|
1935
|
+
cpu: "x64"
|
|
1936
|
+
},
|
|
1937
|
+
"aarch64-pc-windows-msvc": {
|
|
1938
|
+
alias: "windows-arm64",
|
|
1939
|
+
platformKey: "win32-arm64",
|
|
1940
|
+
os: "win32",
|
|
1941
|
+
cpu: "arm64"
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
/** `--target` value that stands for this machine's canonical target. */
|
|
1946
|
+
const HOST_TARGET = "host";
|
|
1947
|
+
/**
|
|
1948
|
+
* Canonical targets for `--target` inputs, deduplicated in input order. No
|
|
1949
|
+
* inputs means every target of the table; `host` means this machine's target.
|
|
1950
|
+
*/
|
|
1951
|
+
function resolveTargets(table, targetFlags) {
|
|
1952
|
+
if (!targetFlags?.length) return [...table.targets];
|
|
1953
|
+
const targets = targetFlags.map((input) => {
|
|
1954
|
+
if (input === "host") {
|
|
1955
|
+
const host = hostTarget(table);
|
|
1956
|
+
if (host === null) throw new Error(`No ${table.runtime} target matches this machine (${hostPlatformKey()}).\n Valid targets: ${table.targets.join(", ")}`);
|
|
1957
|
+
return host;
|
|
1958
|
+
}
|
|
1959
|
+
const exact = table.targets.find((target) => target === input);
|
|
1960
|
+
if (exact) return exact;
|
|
1961
|
+
const canonical = table.targets.find((target) => table.info[target].alias === input);
|
|
1962
|
+
const hint = canonical ? ` Did you mean "${canonical}"?` : "";
|
|
1963
|
+
const runtime = table.runtime === "Bun" ? "" : `${table.runtime} `;
|
|
1964
|
+
throw new Error(`Unknown ${runtime}target "${input}". Targets must use canonical ${table.runtime} names.${hint}\n Valid targets: ${table.targets.join(", ")}`);
|
|
1965
|
+
});
|
|
1966
|
+
return [...new Set(targets)];
|
|
1967
|
+
}
|
|
1968
|
+
/** True on musl-based Linux (Alpine, Void, …). Mirrors the check in Bun's own npm installer. */
|
|
1969
|
+
function isMuslHost() {
|
|
1970
|
+
if (process.platform !== "linux") return false;
|
|
1971
|
+
try {
|
|
1972
|
+
const report = process.report?.getReport();
|
|
1973
|
+
if (report?.header) return report.header.glibcVersionRuntime === void 0;
|
|
1974
|
+
} catch {}
|
|
1975
|
+
return existsSync("/etc/alpine-release");
|
|
1976
|
+
}
|
|
1977
|
+
function hostPlatformKey() {
|
|
1978
|
+
return `${process.platform}-${process.arch}${isMuslHost() ? "-musl" : ""}`;
|
|
1979
|
+
}
|
|
1980
|
+
function hostTarget(table) {
|
|
1981
|
+
const platformKey = hostPlatformKey();
|
|
1982
|
+
return table.targets.find((target) => table.info[target].platformKey === platformKey) ?? null;
|
|
1983
|
+
}
|
|
1984
|
+
function readUserPackageJson(cwd) {
|
|
1985
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
1986
|
+
if (!existsSync(packageJsonPath)) return void 0;
|
|
1987
|
+
try {
|
|
1988
|
+
return JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
1989
|
+
} catch (error) {
|
|
1990
|
+
throw new Error(`Failed to parse package.json in ${cwd}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
function toBunEnvFileArgs(envFiles) {
|
|
1994
|
+
return envFiles.flatMap((envFile) => ["--env-file", envFile]);
|
|
1995
|
+
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Resolve the safest executable to run `bun build`.
|
|
1998
|
+
*
|
|
1999
|
+
* Prefer the real Bun binary when it is available on PATH, because invoking
|
|
2000
|
+
* standalone compilation from inside a compiled Crust executable can trigger
|
|
2001
|
+
* Bun runtime bugs on some host/target combinations.
|
|
2002
|
+
*
|
|
2003
|
+
* Fall back to the current executable with `BUN_BE_BUN=1` so packaged Crust
|
|
2004
|
+
* binaries still work in environments without a separate Bun install. Only a
|
|
2005
|
+
* Bun process can stand in for bun: the library `build()` may run under Node,
|
|
2006
|
+
* where the fallback would spawn node with Bun flags, so that case is an error.
|
|
2007
|
+
*/
|
|
2008
|
+
function resolveBunBuildRunner(runningUnderBun = process.versions.bun !== void 0) {
|
|
2009
|
+
const bunPath = which("bun");
|
|
2010
|
+
if (bunPath) return {
|
|
2011
|
+
command: bunPath,
|
|
2012
|
+
env: { ...process.env }
|
|
2013
|
+
};
|
|
2014
|
+
if (!runningUnderBun) throw new Error("bun was not found on PATH.\n crust build compiles with the Bun bundler and prepares Command Snapshots with bun; install Bun (https://bun.sh) or run the build under bun.");
|
|
2015
|
+
return {
|
|
2016
|
+
command: process.execPath,
|
|
2017
|
+
env: {
|
|
2018
|
+
...process.env,
|
|
2019
|
+
BUN_BE_BUN: "1"
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Bun's `-baseline` spelling of an x64 target, or null for arm64.
|
|
2025
|
+
*
|
|
2026
|
+
* Bun 1.4 ships one x64 build, so the alias yields the same executable (Bun
|
|
2027
|
+
* 1.4.2 downloads a `bun-<target>-baseline-v1.4.2` base that is byte-identical
|
|
2028
|
+
* to the plain one; checked for linux-x64-musl and darwin-x64), but `baseline:
|
|
2029
|
+
* true` is never Bun's compiled-in default, so it is never the self-copy target.
|
|
2030
|
+
* arm64 spellings with the suffix resolve to the plain aarch64 base instead.
|
|
2031
|
+
*/
|
|
2032
|
+
function bunBaselineAlias(target) {
|
|
2033
|
+
return BUN_TARGETS.info[target].cpu === "x64" ? `${target}-baseline` : null;
|
|
2034
|
+
}
|
|
2035
|
+
/**
|
|
2036
|
+
* Target string passed to Bun: the `-baseline` alias when the `BUN_BE_BUN`
|
|
2037
|
+
* fallback runner would otherwise compile `target` by copying itself.
|
|
2038
|
+
*/
|
|
2039
|
+
function bunCompileTarget(target, runner, host = hostTarget(BUN_TARGETS)) {
|
|
2040
|
+
if (target !== host || runner.env.BUN_BE_BUN !== "1") return target;
|
|
2041
|
+
return bunBaselineAlias(target) ?? target;
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Refuse the host target when only the `BUN_BE_BUN` fallback runner is
|
|
2045
|
+
* available and no `-baseline` alias can stand in for it (arm64 hosts).
|
|
2046
|
+
*/
|
|
2047
|
+
function assertTargetsBuildableWithoutBun(targets, host = hostTarget(BUN_TARGETS)) {
|
|
2048
|
+
if (host === null || !targets.includes(host) || bunBaselineAlias(host) !== null || which("bun") !== null) return;
|
|
2049
|
+
const others = targets.filter((target) => target !== host);
|
|
2050
|
+
const alternative = others.length > 0 ? `pass --target with the other targets (e.g. ${others.map((target) => `--target ${target}`).join(" ")})` : "build a different target";
|
|
2051
|
+
throw new Error(`Cannot build ${host} without a separate bun executable on PATH.\n Bun reuses the running crust executable as the base for its own platform, which yields a binary that crashes on start.
|
|
2052
|
+
Install Bun (https://bun.sh), or ${alternative}.`);
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Module source the generated driver imports for a `crust.bunPlugins` specifier.
|
|
2056
|
+
*
|
|
2057
|
+
* Paths become file URLs resolved against the project; bare specifiers are
|
|
2058
|
+
* imported as written so they resolve from the project's own node_modules.
|
|
2059
|
+
*/
|
|
2060
|
+
function resolveBunPluginSource(specifier, cwd) {
|
|
2061
|
+
return specifier.startsWith(".") || isAbsolute(specifier) ? pathToFileURL(resolve(cwd, specifier)).href : specifier;
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Marks every Bun/Node bundle crust produces so `resolveArtifactDir` (core)
|
|
2065
|
+
* can tell a staged bundle from source at runtime. Not a `PUBLIC_*` variable:
|
|
2066
|
+
* it is internal to crust, never user-set. Deno compile has no define, but
|
|
2067
|
+
* standalone Deno binaries are detected directly.
|
|
2068
|
+
*/
|
|
2069
|
+
const CRUST_BUILD_DEFINE = { "process.env.CRUST_INTERNAL_BUILD": "\"1\"" };
|
|
2070
|
+
const CRUST_BUILD_DEFINE_ARG = "process.env.CRUST_INTERNAL_BUILD=\"1\"";
|
|
2071
|
+
/**
|
|
2072
|
+
* Script that runs `Bun.build` with the project's bundler plugins.
|
|
2073
|
+
*
|
|
2074
|
+
* `bun build` has no plugin flag, so plugins are loaded by a generated script
|
|
2075
|
+
* placed in the project root (project module resolution) and run with the
|
|
2076
|
+
* same runner as the CLI path. Every value is embedded via JSON.stringify.
|
|
2077
|
+
*/
|
|
2078
|
+
function createBunPluginDriverScript(options) {
|
|
2079
|
+
return `// Generated by crust build for crust.bunPlugins; deleted when the build finishes.
|
|
2080
|
+
const options = ${JSON.stringify(options)};
|
|
2081
|
+
const plugins = [];
|
|
2082
|
+
for (const { specifier, source } of options.plugins) {
|
|
2083
|
+
let plugin;
|
|
2084
|
+
try {
|
|
2085
|
+
plugin = (await import(source)).default;
|
|
2086
|
+
} catch (error) {
|
|
2087
|
+
// Bun names this (soon deleted) driver as the importer; the project root is the useful location.
|
|
2088
|
+
const message = (error instanceof Error ? error.message : String(error)).replace(\` imported from \${import.meta.path}\`, "");
|
|
2089
|
+
console.error(\`crust.bunPlugins entry \${specifier} could not be imported from \${process.cwd()}: \${message}\`);
|
|
2090
|
+
process.exit(1);
|
|
2091
|
+
}
|
|
2092
|
+
if (typeof plugin !== "object" || plugin === null || typeof plugin.name !== "string" || typeof plugin.setup !== "function") {
|
|
2093
|
+
console.error(\`crust.bunPlugins entry \${specifier} must default-export a Bun bundler plugin ({ name, setup }). Wrap a plugin factory in a module that default-exports the created plugin.\`);
|
|
2094
|
+
process.exit(1);
|
|
2095
|
+
}
|
|
2096
|
+
plugins.push(plugin);
|
|
2097
|
+
}
|
|
2098
|
+
const result = await Bun.build({ ...options.build, define: ${JSON.stringify(CRUST_BUILD_DEFINE)}, plugins, throw: false });
|
|
2099
|
+
if (!result.success) {
|
|
2100
|
+
for (const log of result.logs) console.error(log);
|
|
2101
|
+
process.exit(1);
|
|
2102
|
+
}
|
|
2103
|
+
if (options.build.target === "node") {
|
|
2104
|
+
if (result.outputs.length !== 1) {
|
|
2105
|
+
// Same refusal as \`bun build --outfile\`: a file-type asset import yields entry + asset.
|
|
2106
|
+
console.error("error: cannot write multiple output files without an output directory");
|
|
2107
|
+
process.exit(1);
|
|
2108
|
+
}
|
|
2109
|
+
await Bun.write(options.outfile, result.outputs[0]);
|
|
2110
|
+
}
|
|
2111
|
+
`;
|
|
2112
|
+
}
|
|
2113
|
+
async function runBunPluginDriver(build, outfilePath, bunPlugins, envFiles, cwd) {
|
|
2114
|
+
const driverPath = join(cwd, `.crust-build-${randomBytes(6).toString("hex")}.ts`);
|
|
2115
|
+
await writeFile(driverPath, createBunPluginDriverScript({
|
|
2116
|
+
plugins: bunPlugins.map((specifier) => ({
|
|
2117
|
+
specifier,
|
|
2118
|
+
source: resolveBunPluginSource(specifier, cwd)
|
|
2119
|
+
})),
|
|
2120
|
+
build,
|
|
2121
|
+
outfile: outfilePath
|
|
2122
|
+
}));
|
|
2123
|
+
try {
|
|
2124
|
+
await runBuildProcess(resolveBunBuildRunner(), [...toBunEnvFileArgs(envFiles), driverPath], outfilePath, cwd);
|
|
2125
|
+
} finally {
|
|
2126
|
+
await rm(driverPath, { force: true });
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Compile a single entry file to a standalone executable.
|
|
2131
|
+
*
|
|
2132
|
+
* Uses `bun build --compile` as a subprocess so the standalone compiler runs
|
|
2133
|
+
* in Bun's CLI process rather than inside the current Crust runtime.
|
|
2134
|
+
* This avoids in-process compiler issues seen on some host/target
|
|
2135
|
+
* combinations while still supporting env-file loading natively.
|
|
2136
|
+
*
|
|
2137
|
+
* @param entryPath - Absolute path to the entry file
|
|
2138
|
+
* @param outfilePath - Absolute path to the output binary
|
|
2139
|
+
* @param minify - Whether to enable minification
|
|
2140
|
+
* @param target - Bun compile target
|
|
2141
|
+
* @param envFiles - Optional env files to load during build
|
|
2142
|
+
* @param bunPlugins - Bun bundler plugin specifiers; when present the build
|
|
2143
|
+
* runs through the generated `Bun.build` driver instead of `bun build`
|
|
2144
|
+
* @throws {Error} If the build fails
|
|
2145
|
+
*/
|
|
2146
|
+
async function execBuild(entryPath, outfilePath, minify, target, envFiles, cwd, bunPlugins = []) {
|
|
2147
|
+
const runner = resolveBunBuildRunner();
|
|
2148
|
+
const compileTarget = bunCompileTarget(target, runner);
|
|
2149
|
+
if (bunPlugins.length > 0) {
|
|
2150
|
+
await runBunPluginDriver({
|
|
2151
|
+
entrypoints: [entryPath],
|
|
2152
|
+
minify,
|
|
2153
|
+
env: "PUBLIC_*",
|
|
2154
|
+
target: "bun",
|
|
2155
|
+
compile: {
|
|
2156
|
+
target: compileTarget,
|
|
2157
|
+
outfile: outfilePath,
|
|
2158
|
+
autoloadBunfig: false
|
|
2159
|
+
}
|
|
2160
|
+
}, outfilePath, bunPlugins, envFiles, cwd);
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
await runBuildProcess(runner, createBunCompileArgs(entryPath, outfilePath, minify, compileTarget, envFiles), outfilePath, cwd);
|
|
2164
|
+
}
|
|
2165
|
+
function createBunCompileArgs(entryPath, outfilePath, minify, target, envFiles = []) {
|
|
2166
|
+
return [
|
|
2167
|
+
"build",
|
|
2168
|
+
"--compile",
|
|
2169
|
+
"--no-compile-autoload-bunfig",
|
|
2170
|
+
...toBunEnvFileArgs(envFiles),
|
|
2171
|
+
"--env=PUBLIC_*",
|
|
2172
|
+
"--define",
|
|
2173
|
+
CRUST_BUILD_DEFINE_ARG,
|
|
2174
|
+
"--outfile",
|
|
2175
|
+
outfilePath,
|
|
2176
|
+
...minify ? ["--minify"] : [],
|
|
2177
|
+
"--target",
|
|
2178
|
+
target,
|
|
2179
|
+
entryPath
|
|
2180
|
+
];
|
|
2181
|
+
}
|
|
2182
|
+
function createDenoCompileArgs(entryPath, outfilePath, target) {
|
|
2183
|
+
return [
|
|
2184
|
+
"compile",
|
|
2185
|
+
"-A",
|
|
2186
|
+
"--output",
|
|
2187
|
+
outfilePath,
|
|
2188
|
+
"--target",
|
|
2189
|
+
target,
|
|
2190
|
+
entryPath
|
|
2191
|
+
];
|
|
2192
|
+
}
|
|
2193
|
+
async function runBuildProcess(runner, args, outfilePath, cwd) {
|
|
2194
|
+
const { exitCode, stdout, stderr } = await runProcess(runner.command, args, {
|
|
2195
|
+
env: runner.env,
|
|
2196
|
+
cwd,
|
|
2197
|
+
stdio: "collect"
|
|
2198
|
+
});
|
|
2199
|
+
if (exitCode !== 0) {
|
|
2200
|
+
const output = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
2201
|
+
throw new Error(`Build failed for ${outfilePath}${output ? `:\n${output}` : ""}`);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
async function execNodeBuild(entryPath, outfilePath, minify, envFiles, cwd, bunPlugins = []) {
|
|
2205
|
+
if (bunPlugins.length > 0) await runBunPluginDriver({
|
|
2206
|
+
entrypoints: [entryPath],
|
|
2207
|
+
minify,
|
|
2208
|
+
env: "PUBLIC_*",
|
|
2209
|
+
target: "node",
|
|
2210
|
+
format: "esm"
|
|
2211
|
+
}, outfilePath, bunPlugins, envFiles, cwd);
|
|
2212
|
+
else await runBuildProcess(resolveBunBuildRunner(), [
|
|
2213
|
+
"build",
|
|
2214
|
+
...toBunEnvFileArgs(envFiles),
|
|
2215
|
+
"--env=PUBLIC_*",
|
|
2216
|
+
"--define",
|
|
2217
|
+
CRUST_BUILD_DEFINE_ARG,
|
|
2218
|
+
"--target",
|
|
2219
|
+
"node",
|
|
2220
|
+
"--format",
|
|
2221
|
+
"esm",
|
|
2222
|
+
"--outfile",
|
|
2223
|
+
outfilePath,
|
|
2224
|
+
...minify ? ["--minify"] : [],
|
|
2225
|
+
entryPath
|
|
2226
|
+
], outfilePath, cwd);
|
|
2227
|
+
const output = await readFile(outfilePath, "utf8");
|
|
2228
|
+
await writeFile(outfilePath, "#!/usr/bin/env node\n" + output.replace(/^#![^\n]*(?:\n|$)/, ""));
|
|
2229
|
+
if (process.platform !== "win32") await chmod(outfilePath, 493);
|
|
2230
|
+
}
|
|
2231
|
+
async function execDenoBuild(entryPath, outfilePath, target, cwd) {
|
|
2232
|
+
const denoPath = which("deno");
|
|
2233
|
+
if (!denoPath) throw new Error("Deno is required for the deno runtime but was not found on PATH.\n Install Deno from https://deno.com/ and try again.");
|
|
2234
|
+
await runBuildProcess({
|
|
2235
|
+
command: denoPath,
|
|
2236
|
+
env: { ...process.env }
|
|
2237
|
+
}, createDenoCompileArgs(entryPath, outfilePath, target), outfilePath, cwd);
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* Prepare a CLI entry's Command Snapshot in the user's project context.
|
|
2241
|
+
*
|
|
2242
|
+
* The entry runs as a subprocess with `CRUST_INTERNAL_SNAPSHOT_PATH` pointing
|
|
2243
|
+
* to a temporary file. `.execute()` validates and writes the command graph and
|
|
2244
|
+
* adjacent Build Report, then exits before any following entrypoint code can run.
|
|
2245
|
+
*
|
|
2246
|
+
* Runs with the same bun as compilation (`resolveBunBuildRunner`): bun on
|
|
2247
|
+
* PATH, or a compiled standalone crust executable as `BUN_BE_BUN=1`, so
|
|
2248
|
+
* arbitrary `.ts` entries run without a separate `bun` install.
|
|
2249
|
+
*/
|
|
2250
|
+
const SNAPSHOT_TIMEOUT_MS = 3e4;
|
|
2251
|
+
function isBuildReport(value) {
|
|
2252
|
+
return isJsonObject(value) && Array.isArray(value.extensions) && value.extensions.every((extension) => isJsonObject(extension) && typeof extension.id === "string" && Array.isArray(extension.files) && extension.files.every((file) => typeof file === "string"));
|
|
2253
|
+
}
|
|
2254
|
+
async function buildEntrypoint(entryPath, outDir, envFiles, io, cwd) {
|
|
2255
|
+
const absoluteEntry = resolve(entryPath);
|
|
2256
|
+
const snapshotDir = await mkdtemp(join(tmpdir(), "crust-snapshot-"));
|
|
2257
|
+
const snapshotPath = join(snapshotDir, "command.json");
|
|
2258
|
+
const buildReportPath = join(snapshotDir, "build-report.json");
|
|
2259
|
+
try {
|
|
2260
|
+
const runner = resolveBunBuildRunner();
|
|
2261
|
+
const spawnedAt = Date.now();
|
|
2262
|
+
const proc = spawn(runner.command, [...toBunEnvFileArgs(envFiles), absoluteEntry], {
|
|
2263
|
+
env: {
|
|
2264
|
+
...runner.env,
|
|
2265
|
+
[SNAPSHOT_PATH_ENV]: snapshotPath,
|
|
2266
|
+
[BUILD_OUT_DIR_ENV]: resolve(outDir)
|
|
2267
|
+
},
|
|
2268
|
+
cwd,
|
|
2269
|
+
stdio: [
|
|
2270
|
+
"ignore",
|
|
2271
|
+
"ignore",
|
|
2272
|
+
"pipe"
|
|
2273
|
+
],
|
|
2274
|
+
timeout: SNAPSHOT_TIMEOUT_MS
|
|
2275
|
+
});
|
|
2276
|
+
const stderrPromise = text(proc.stderr);
|
|
2277
|
+
const [exitCode] = await once(proc, "close");
|
|
2278
|
+
const stderr = (await stderrPromise).trim();
|
|
2279
|
+
if (proc.signalCode !== null) {
|
|
2280
|
+
if (Date.now() - spawnedAt >= SNAPSHOT_TIMEOUT_MS) throw new Error(`Command Snapshot preparation timed out after ${SNAPSHOT_TIMEOUT_MS / 1e3}s.\n An Extension build hook may be hanging. Use --no-validate to skip entry preparation and build hooks.`);
|
|
2281
|
+
throw new Error(`Command Snapshot preparation was killed by ${proc.signalCode}.${stderr ? `\n${stderr}` : ""}`);
|
|
2282
|
+
}
|
|
2283
|
+
if (exitCode !== 0) throw new Error(stderr || "Command Snapshot preparation failed");
|
|
2284
|
+
if (stderr) {
|
|
2285
|
+
const styled = stderr.split("\n").map((line) => line.startsWith("Warning:") ? `${yellow("Warning:")}${line.slice(8)}` : line).join("\n");
|
|
2286
|
+
io.stderr(styled);
|
|
2287
|
+
}
|
|
2288
|
+
let serialized;
|
|
2289
|
+
try {
|
|
2290
|
+
serialized = await readFile(snapshotPath, "utf8");
|
|
2291
|
+
} catch (error) {
|
|
2292
|
+
if (isErrnoException(error) && error.code === "ENOENT") throw new Error(`Entry exited without producing a Command Snapshot.\n Ensure ${absoluteEntry} calls await app.execute() and uses a compatible @crustjs/core version.`, { cause: error });
|
|
2293
|
+
throw error;
|
|
2294
|
+
}
|
|
2295
|
+
let snapshot;
|
|
2296
|
+
try {
|
|
2297
|
+
snapshot = JSON.parse(serialized);
|
|
2298
|
+
} catch (error) {
|
|
2299
|
+
throw new Error(`Entry produced an invalid Command Snapshot.\n Ensure ${absoluteEntry} uses a compatible @crustjs/core version.`, { cause: error });
|
|
2300
|
+
}
|
|
2301
|
+
let serializedBuild;
|
|
2302
|
+
try {
|
|
2303
|
+
serializedBuild = await readFile(buildReportPath, "utf8");
|
|
2304
|
+
} catch (error) {
|
|
2305
|
+
if (isErrnoException(error) && error.code === "ENOENT") throw new Error(`Entry produced a Command Snapshot without a Build Report.\n Ensure ${absoluteEntry} uses a compatible @crustjs/core version.`, { cause: error });
|
|
2306
|
+
throw error;
|
|
2307
|
+
}
|
|
2308
|
+
try {
|
|
2309
|
+
const build = JSON.parse(serializedBuild);
|
|
2310
|
+
if (!isBuildReport(build)) throw new Error("Expected extensions with string ids and files arrays.");
|
|
2311
|
+
for (const extension of build.extensions) {
|
|
2312
|
+
defineExtensionId(extension.id);
|
|
2313
|
+
for (const file of extension.files) {
|
|
2314
|
+
const path = resolve(outDir, file);
|
|
2315
|
+
if (file === "." || file.includes("\\") || posix.normalize(file) !== file || win32.isAbsolute(file) || /^[A-Za-z]:/.test(file) || !isWithin(resolve(outDir), path) || !isWithin(realpathSync(outDir), realpathSync(path)) || !lstatSync(path).isFile()) throw new Error(`Reported artifact must be a normalized, contained regular file: ${file}`);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
return {
|
|
2319
|
+
snapshot,
|
|
2320
|
+
build
|
|
2321
|
+
};
|
|
2322
|
+
} catch (error) {
|
|
2323
|
+
throw new Error(`Entry produced an invalid Build Report.\n Ensure ${absoluteEntry} uses a compatible @crustjs/core version; upgrade Core, @crustjs/crust, and build-hook Extensions together to the pure-return build API.`, { cause: error });
|
|
2324
|
+
}
|
|
2325
|
+
} finally {
|
|
2326
|
+
await rm(snapshotDir, {
|
|
2327
|
+
recursive: true,
|
|
2328
|
+
force: true,
|
|
2329
|
+
maxRetries: 3,
|
|
2330
|
+
retryDelay: 100
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
//#endregion
|
|
2335
|
+
//#region src/utils/distribute.ts
|
|
2336
|
+
/** Project-relative directory that `crust build` owns: wiped per build, read by `crust publish`. */
|
|
2337
|
+
const CRUST_DIR = ".crust";
|
|
2338
|
+
const MAX_PACKAGE_NAME_LENGTH = 214;
|
|
2339
|
+
const METADATA_KEYS = [
|
|
2340
|
+
"description",
|
|
2341
|
+
"license",
|
|
2342
|
+
"author",
|
|
2343
|
+
"homepage",
|
|
2344
|
+
"bugs",
|
|
2345
|
+
"repository",
|
|
2346
|
+
"keywords",
|
|
2347
|
+
"publishConfig",
|
|
2348
|
+
"funding",
|
|
2349
|
+
"engines"
|
|
2350
|
+
];
|
|
2351
|
+
function readPackageJson(cwd, packageJson) {
|
|
2352
|
+
if (packageJson === void 0) throw new Error(`package.json not found in ${cwd}\n crust build requires a package.json with name and version fields.`);
|
|
2353
|
+
if (!isJsonObject(packageJson)) throw new Error(`package.json in ${cwd} must contain a JSON object.`);
|
|
2354
|
+
validatePackageIdentity(packageJson, "package.json");
|
|
2355
|
+
return packageJson;
|
|
2356
|
+
}
|
|
2357
|
+
/** Only identity is interpreted here; this is not a complete npm schema validator. */
|
|
2358
|
+
function validatePackageIdentity(value, source) {
|
|
2359
|
+
if (value === void 0 || !isJsonObject(value)) throw new Error(`${source} must contain a JSON object.`);
|
|
2360
|
+
if (typeof value.name !== "string" || value.name.trim() === "") throw new Error(`${source} name field must be a non-empty string.`);
|
|
2361
|
+
if (typeof value.version !== "string" || value.version.trim() === "") throw new Error(`${source} version field must be a non-empty string.`);
|
|
2362
|
+
}
|
|
2363
|
+
function isString$1(value) {
|
|
2364
|
+
return typeof value === "string";
|
|
2365
|
+
}
|
|
2366
|
+
function derivePlatformPackageName(rootPackageName, targetAlias) {
|
|
2367
|
+
const [scope, name] = rootPackageName.startsWith("@") ? rootPackageName.split("/") : [void 0, rootPackageName];
|
|
2368
|
+
const suffixedName = `${name}-${targetAlias}`;
|
|
2369
|
+
return scope ? `${scope}/${suffixedName}` : suffixedName;
|
|
2370
|
+
}
|
|
2371
|
+
function getPackagePathSegment(packageName) {
|
|
2372
|
+
return packageName.startsWith("@") ? packageName.split("/")[1] ?? packageName : packageName;
|
|
2373
|
+
}
|
|
2374
|
+
/** Platform binary filename: `<command>-<target>`, `.exe` on Windows. */
|
|
2375
|
+
function binaryFilename(command, target) {
|
|
2376
|
+
return `${command}-${target.target}${target.os === "win32" ? ".exe" : ""}`;
|
|
2377
|
+
}
|
|
2378
|
+
function platformBinMap(commands, target) {
|
|
2379
|
+
return Object.fromEntries(commands.map((command) => [command, `bin/${binaryFilename(command, target)}`]));
|
|
2380
|
+
}
|
|
2381
|
+
function buildDistributionRootPackageJson(metadata, commands, targets, { artifactDirs, manPages }) {
|
|
2382
|
+
return {
|
|
2383
|
+
...metadata.rootPackageJson,
|
|
2384
|
+
name: metadata.rootPackageName,
|
|
2385
|
+
version: metadata.version,
|
|
2386
|
+
type: "module",
|
|
2387
|
+
files: ["bin", ...artifactDirs],
|
|
2388
|
+
bin: Object.fromEntries(commands.map((command) => [command, `bin/${command}.js`])),
|
|
2389
|
+
...targets.length > 0 ? { optionalDependencies: Object.fromEntries(targets.map((target) => [target.packageName, metadata.version])) } : {},
|
|
2390
|
+
...manPages.length > 0 ? { man: manPages.map((page) => `./man/${page}`) } : {},
|
|
2391
|
+
exports: metadata.exports,
|
|
2392
|
+
peerDependencies: metadata.peerDependencies,
|
|
2393
|
+
peerDependenciesMeta: metadata.peerDependenciesMeta
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
/**
|
|
2397
|
+
* Node rejects export targets whose segments are `.`, `..`, `node_modules`, or
|
|
2398
|
+
* empty (`ERR_INVALID_PACKAGE_TARGET`), also percent-encoded, even when the path
|
|
2399
|
+
* normalizes to a staged file, so filesystem checks alone would pass a target
|
|
2400
|
+
* consumers cannot import.
|
|
2401
|
+
*/
|
|
2402
|
+
function hasNodeInvalidSegment(target) {
|
|
2403
|
+
return target.slice(2).split(/[\\/]/).some((segment) => {
|
|
2404
|
+
let decoded = segment;
|
|
2405
|
+
try {
|
|
2406
|
+
decoded = decodeURIComponent(segment);
|
|
2407
|
+
} catch {}
|
|
2408
|
+
return /^(\.\.?|node_modules|)$/i.test(decoded);
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
/**
|
|
2412
|
+
* Checks a package.json `exports` map against the staged root package: every
|
|
2413
|
+
* target must be a `./`-relative path to a file that staging copied there (a
|
|
2414
|
+
* `crust.include` directory or Extension artifact), so the published root
|
|
2415
|
+
* package resolves exactly what the project's own `exports` promises. `null`
|
|
2416
|
+
* targets (blocked subpaths) and nested condition objects are allowed;
|
|
2417
|
+
* fallback arrays and `*` patterns are rejected rather than half-checked.
|
|
2418
|
+
*/
|
|
2419
|
+
function validateStagedExports(exports, rootDir, sourceType) {
|
|
2420
|
+
const fail = (detail) => {
|
|
2421
|
+
throw new Error(`package.json exports ${detail}\n crust build stages only bin/, Extension artifacts, and crust.include directories into the root package; point exports at a crust.include directory or remove the field.`);
|
|
2422
|
+
};
|
|
2423
|
+
const checkTarget = (target, at) => {
|
|
2424
|
+
if (target === null) return;
|
|
2425
|
+
if (isString$1(target)) {
|
|
2426
|
+
const staged = resolve(rootDir, target);
|
|
2427
|
+
if (!target.startsWith("./") || !isWithin(rootDir, staged)) fail(`target ${JSON.stringify(target)} (${at}) must be a ./-relative path inside the package.`);
|
|
2428
|
+
if (target.includes("*")) fail(`target ${JSON.stringify(target)} (${at}) uses a pattern, which crust build does not support.`);
|
|
2429
|
+
if (hasNodeInvalidSegment(target)) fail(`target ${JSON.stringify(target)} (${at}) contains a path segment Node rejects (".", "..", "node_modules", or empty).`);
|
|
2430
|
+
if (!existsSync(staged) || !statSync(staged).isFile()) fail(`target ${JSON.stringify(target)} (${at}) is not a staged file: ${staged}`);
|
|
2431
|
+
if (sourceType !== "module" && (target.endsWith(".js") || target.endsWith(".d.ts"))) {
|
|
2432
|
+
let scope = dirname(staged);
|
|
2433
|
+
while (scope !== rootDir && !existsSync(join(scope, "package.json"))) scope = dirname(scope);
|
|
2434
|
+
if (scope === rootDir) fail(`target ${JSON.stringify(target)} (${at}) would change module format under the staged root's type: "module". Use .cjs/.d.cts for CommonJS, or include a nested package.json declaring the library's type.`);
|
|
2435
|
+
}
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
if (!isJsonObject(target)) fail(`${at} must be a path, null, or a conditions object, not ${JSON.stringify(target)}.`);
|
|
2439
|
+
for (const [condition, value] of Object.entries(target)) {
|
|
2440
|
+
if (condition.startsWith(".")) fail(`${at} mixes subpath ${JSON.stringify(condition)} into a conditions object.`);
|
|
2441
|
+
checkTarget(value, `${at} -> ${condition}`);
|
|
2442
|
+
}
|
|
2443
|
+
};
|
|
2444
|
+
if (isJsonObject(exports) && Object.keys(exports).some((key) => key.startsWith("."))) {
|
|
2445
|
+
for (const [subpath, target] of Object.entries(exports)) {
|
|
2446
|
+
if (!subpath.startsWith(".")) fail(`mixes condition ${JSON.stringify(subpath)} with subpath keys.`);
|
|
2447
|
+
checkTarget(target, subpath);
|
|
2448
|
+
}
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
checkTarget(exports, "\".\"");
|
|
2452
|
+
}
|
|
2453
|
+
/**
|
|
2454
|
+
* Carries `peerDependencies` (and `peerDependenciesMeta`) into the root package
|
|
2455
|
+
* so a library `exports` entry can declare what its published types import.
|
|
2456
|
+
* Ranges must be publishable as written: the staged manifests go to npm
|
|
2457
|
+
* directly, so `workspace:` and `catalog:` ranges would leak into the registry.
|
|
2458
|
+
*/
|
|
2459
|
+
function validatePeerDependencies(peerDependencies, peerDependenciesMeta) {
|
|
2460
|
+
if (!isJsonObject(peerDependencies)) throw new Error("package.json peerDependencies must be an object of package names to ranges.");
|
|
2461
|
+
const ranges = {};
|
|
2462
|
+
for (const [name, range] of Object.entries(peerDependencies)) {
|
|
2463
|
+
if (!isString$1(range) || /^(workspace|catalog):/.test(range)) throw new Error(`package.json peerDependencies[${JSON.stringify(name)}] must be a publishable range, not ${JSON.stringify(range)}.\n crust build publishes the staged root package as written; workspace: and catalog: ranges are never rewritten.`);
|
|
2464
|
+
ranges[name] = range;
|
|
2465
|
+
}
|
|
2466
|
+
if (peerDependenciesMeta === void 0) return { peerDependencies: ranges };
|
|
2467
|
+
if (!isJsonObject(peerDependenciesMeta)) throw new Error("package.json peerDependenciesMeta must be an object keyed by peer name.");
|
|
2468
|
+
for (const name of Object.keys(peerDependenciesMeta)) if (!Object.hasOwn(ranges, name)) throw new Error(`package.json peerDependenciesMeta[${JSON.stringify(name)}] has no matching peerDependencies entry.`);
|
|
2469
|
+
return {
|
|
2470
|
+
peerDependencies: ranges,
|
|
2471
|
+
peerDependenciesMeta
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
function buildDistributionPlatformPackageJson(metadata, commands, target) {
|
|
2475
|
+
return {
|
|
2476
|
+
...metadata.rootPackageJson,
|
|
2477
|
+
name: target.packageName,
|
|
2478
|
+
version: metadata.version,
|
|
2479
|
+
files: ["bin"],
|
|
2480
|
+
bin: platformBinMap(commands, target),
|
|
2481
|
+
os: [target.os],
|
|
2482
|
+
cpu: [target.cpu],
|
|
2483
|
+
...target.libc ? { libc: [target.libc] } : {}
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
function pickRootMetadata(pkgJson) {
|
|
2487
|
+
const metadata = {
|
|
2488
|
+
name: pkgJson.name,
|
|
2489
|
+
version: pkgJson.version
|
|
2490
|
+
};
|
|
2491
|
+
for (const key of METADATA_KEYS) {
|
|
2492
|
+
const value = pkgJson[key];
|
|
2493
|
+
if (value !== void 0) Object.assign(metadata, { [key]: value });
|
|
2494
|
+
}
|
|
2495
|
+
return metadata;
|
|
2496
|
+
}
|
|
2497
|
+
function validatePackageNameLength(packageName) {
|
|
2498
|
+
if (packageName.length > MAX_PACKAGE_NAME_LENGTH) throw new Error(`Generated package name is too long for npm: ${packageName}\n Keep package names at or below ${MAX_PACKAGE_NAME_LENGTH} characters after the platform suffix is added.`);
|
|
2499
|
+
}
|
|
2500
|
+
function resolveDistributionMetadata(cwd, userPackageJson) {
|
|
2501
|
+
const pkgJson = readPackageJson(cwd, userPackageJson);
|
|
2502
|
+
validatePackageNameLength(pkgJson.name);
|
|
2503
|
+
return {
|
|
2504
|
+
rootPackageName: pkgJson.name,
|
|
2505
|
+
version: pkgJson.version,
|
|
2506
|
+
rootPackageJson: pickRootMetadata(pkgJson),
|
|
2507
|
+
sourceType: pkgJson.type,
|
|
2508
|
+
...pkgJson.exports !== void 0 ? { exports: pkgJson.exports } : {},
|
|
2509
|
+
...pkgJson.peerDependencies !== void 0 ? validatePeerDependencies(pkgJson.peerDependencies, pkgJson.peerDependenciesMeta) : {}
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
function resolveDistributionTarget(table, stageDir, rootPackageName, target) {
|
|
2513
|
+
const info = table.info[target];
|
|
2514
|
+
const packageName = derivePlatformPackageName(rootPackageName, info.alias);
|
|
2515
|
+
validatePackageNameLength(packageName);
|
|
2516
|
+
return {
|
|
2517
|
+
target,
|
|
2518
|
+
platformKey: info.platformKey,
|
|
2519
|
+
targetAlias: info.alias,
|
|
2520
|
+
packageName,
|
|
2521
|
+
packagePathSegment: getPackagePathSegment(packageName),
|
|
2522
|
+
packageDir: resolve(stageDir, info.alias),
|
|
2523
|
+
os: info.os,
|
|
2524
|
+
cpu: info.cpu,
|
|
2525
|
+
...info.libc ? { libc: info.libc } : {}
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2528
|
+
function generateDistributionJsResolver(command, targets) {
|
|
2529
|
+
const targetMap = Object.fromEntries(targets.map((target) => [target.platformKey, {
|
|
2530
|
+
packagePathSegment: target.packagePathSegment,
|
|
2531
|
+
packageName: target.packageName,
|
|
2532
|
+
targetAlias: target.targetAlias,
|
|
2533
|
+
binaryFilename: binaryFilename(command, target)
|
|
2534
|
+
}]));
|
|
2535
|
+
const supportedPlatforms = targets.map((target) => target.targetAlias).join(", ");
|
|
2536
|
+
return `#!/usr/bin/env node
|
|
2537
|
+
// Auto-generated by crust build -- do not edit
|
|
2538
|
+
import { spawn } from "node:child_process";
|
|
2539
|
+
import { chmodSync, existsSync } from "node:fs";
|
|
2540
|
+
import { dirname, resolve } from "node:path";
|
|
2541
|
+
import process from "node:process";
|
|
2542
|
+
import { fileURLToPath } from "node:url";
|
|
2543
|
+
|
|
2544
|
+
const NAME = ${JSON.stringify(command)};
|
|
2545
|
+
const PLATFORMS = ${JSON.stringify(targetMap, null, " ")};
|
|
2546
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
2547
|
+
|
|
2548
|
+
// Same check as Bun's own npm installer: glibc reports its version, musl does not.
|
|
2549
|
+
function isMusl() {
|
|
2550
|
+
try {
|
|
2551
|
+
const report = process.report?.getReport();
|
|
2552
|
+
if (report?.header) return report.header.glibcVersionRuntime === undefined;
|
|
2553
|
+
} catch {}
|
|
2554
|
+
return existsSync("/etc/alpine-release");
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
const platformKey =
|
|
2558
|
+
\`\${process.platform}-\${process.arch}\` + (process.platform === "linux" && isMusl() ? "-musl" : "");
|
|
2559
|
+
const target = PLATFORMS[platformKey];
|
|
2560
|
+
|
|
2561
|
+
if (!target) {
|
|
2562
|
+
\tconsole.error("[" + NAME + "] Unsupported platform: " + platformKey);
|
|
2563
|
+
\tconsole.error("[" + NAME + "] Supported platforms: ${supportedPlatforms}");
|
|
2564
|
+
\tprocess.exit(1);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
const candidates = [
|
|
2568
|
+
\t// Hoisted install: the platform package beside this one in node_modules.
|
|
2569
|
+
\tresolve(dir, "..", "..", target.packagePathSegment, "bin", target.binaryFilename),
|
|
2570
|
+
\t// Nested install: the platform package under this package's node_modules.
|
|
2571
|
+
\tresolve(dir, "..", "node_modules", target.packageName, "bin", target.binaryFilename),
|
|
2572
|
+
\t// In place: the .crust/ tree crust build staged, before any install.
|
|
2573
|
+
\tresolve(dir, "..", "..", target.targetAlias, "bin", target.binaryFilename),
|
|
2574
|
+
];
|
|
2575
|
+
const binPath = candidates.find((candidate) => existsSync(candidate));
|
|
2576
|
+
|
|
2577
|
+
if (!binPath) {
|
|
2578
|
+
\tconsole.error("[" + NAME + "] Missing platform package for " + platformKey);
|
|
2579
|
+
\tconsole.error("[" + NAME + "] Tried:");
|
|
2580
|
+
\tfor (const candidate of candidates) console.error(" " + candidate);
|
|
2581
|
+
\tconsole.error(
|
|
2582
|
+
\t\t"[" + NAME + "] Reinstall dependencies on this platform and ensure optional dependencies are enabled.",
|
|
2583
|
+
\t);
|
|
2584
|
+
\tprocess.exit(1);
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
if (process.platform !== "win32") {
|
|
2588
|
+
\ttry {
|
|
2589
|
+
\t\tchmodSync(binPath, 0o755);
|
|
2590
|
+
\t} catch {
|
|
2591
|
+
\t\t// Ignore permission adjustment failures and let spawn surface real errors.
|
|
2592
|
+
\t}
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
const child = spawn(binPath, process.argv.slice(2), {
|
|
2596
|
+
\tstdio: "inherit",
|
|
2597
|
+
});
|
|
2598
|
+
|
|
2599
|
+
child.on("error", (error) => {
|
|
2600
|
+
\tconsole.error("[" + NAME + "] Failed to launch binary: " + error.message);
|
|
2601
|
+
\tprocess.exit(1);
|
|
2602
|
+
});
|
|
2603
|
+
|
|
2604
|
+
child.on("exit", (code, signal) => {
|
|
2605
|
+
\tif (signal) {
|
|
2606
|
+
\t\ttry {
|
|
2607
|
+
\t\t\tprocess.kill(process.pid, signal);
|
|
2608
|
+
\t\t} catch {
|
|
2609
|
+
\t\t\tprocess.exit(1);
|
|
2610
|
+
\t\t}
|
|
2611
|
+
\t\treturn;
|
|
2612
|
+
\t}
|
|
2613
|
+
|
|
2614
|
+
\tprocess.exit(code ?? 0);
|
|
2615
|
+
});
|
|
2616
|
+
`;
|
|
2617
|
+
}
|
|
2618
|
+
function writeJson(path, value) {
|
|
2619
|
+
writeFileSync(path, `${JSON.stringify(value, null, " ")}\n`);
|
|
2620
|
+
}
|
|
2621
|
+
function copyRootReadme(cwd, rootDir) {
|
|
2622
|
+
const readmePath = join(cwd, "README.md");
|
|
2623
|
+
if (existsSync(readmePath)) copyFileSync(readmePath, join(rootDir, "README.md"));
|
|
2624
|
+
}
|
|
2625
|
+
function copyLicense(cwd, packageDirs) {
|
|
2626
|
+
const licenseName = [
|
|
2627
|
+
"LICENSE",
|
|
2628
|
+
"LICENSE.md",
|
|
2629
|
+
"LICENCE",
|
|
2630
|
+
"LICENCE.md"
|
|
2631
|
+
].find((name) => existsSync(join(cwd, name)));
|
|
2632
|
+
if (!licenseName) return;
|
|
2633
|
+
const licensePath = join(cwd, licenseName);
|
|
2634
|
+
for (const packageDir of packageDirs) copyFileSync(licensePath, join(packageDir, licenseName));
|
|
2635
|
+
}
|
|
2636
|
+
function writeDistributionManifest(stageDir, metadata, commands, targets, build) {
|
|
2637
|
+
const manifest = {
|
|
2638
|
+
version: metadata.version,
|
|
2639
|
+
root: {
|
|
2640
|
+
name: metadata.rootPackageName,
|
|
2641
|
+
dir: "root",
|
|
2642
|
+
bins: [...commands]
|
|
2643
|
+
},
|
|
2644
|
+
packages: targets.map((target) => ({
|
|
2645
|
+
target: target.targetAlias,
|
|
2646
|
+
name: target.packageName,
|
|
2647
|
+
dir: target.targetAlias,
|
|
2648
|
+
os: target.os,
|
|
2649
|
+
cpu: target.cpu,
|
|
2650
|
+
...target.libc ? { libc: target.libc } : {},
|
|
2651
|
+
bins: platformBinMap(commands, target)
|
|
2652
|
+
})),
|
|
2653
|
+
publishOrder: [...targets.map((target) => target.targetAlias), "root"],
|
|
2654
|
+
...build ? { build } : {}
|
|
2655
|
+
};
|
|
2656
|
+
writeJson(join(stageDir, "manifest.json"), manifest);
|
|
2657
|
+
return manifest;
|
|
2658
|
+
}
|
|
2659
|
+
function stageDistributionPackages(cwd, stageDir, metadata, commands, targets, options) {
|
|
2660
|
+
const rootDir = join(stageDir, "root");
|
|
2661
|
+
const rootBinDir = join(rootDir, "bin");
|
|
2662
|
+
mkdirSync(rootBinDir, { recursive: true });
|
|
2663
|
+
writeJson(join(rootDir, "package.json"), buildDistributionRootPackageJson(metadata, commands, targets, options));
|
|
2664
|
+
copyRootReadme(cwd, rootDir);
|
|
2665
|
+
for (const target of targets) {
|
|
2666
|
+
mkdirSync(join(target.packageDir, "bin"), { recursive: true });
|
|
2667
|
+
writeJson(join(target.packageDir, "package.json"), buildDistributionPlatformPackageJson(metadata, commands, target));
|
|
2668
|
+
}
|
|
2669
|
+
copyLicense(cwd, [rootDir, ...targets.map((target) => target.packageDir)]);
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Stages the npm tree in `plan.stageDir`. `build` is each command's Extension
|
|
2673
|
+
* build hook report, recorded in `manifest.json`; omit it when the hooks did not run.
|
|
2674
|
+
* Returns every generated package.json, launcher, and compiled command in staging order.
|
|
2675
|
+
*/
|
|
2676
|
+
async function runDistributeBuild(plan, distribution, io, build) {
|
|
2677
|
+
const metadata = resolveDistributionMetadata(plan.cwd, plan.userPackageJson);
|
|
2678
|
+
const commands = plan.entries.map((entry) => entry.command);
|
|
2679
|
+
const table = distribution.table;
|
|
2680
|
+
const distributionTargets = table ? distribution.targets.map((target) => resolveDistributionTarget(table, plan.stageDir, metadata.rootPackageName, target)) : [];
|
|
2681
|
+
io.stdout(table ? `Staging ${bold(`${distributionTargets.length}`)} distribution target(s) in ${dim(plan.stageDir)}...` : `Staging a root-only npm package in ${dim(plan.stageDir)}...`);
|
|
2682
|
+
const artifactOutDir = plan.validate ? plan.outDir : void 0;
|
|
2683
|
+
const artifacts = collectArtifacts(artifactOutDir);
|
|
2684
|
+
const includeDirs = collectIncludeDirs(plan.cwd, plan.stageDir, plan.include, artifacts.names);
|
|
2685
|
+
stageDistributionPackages(plan.cwd, plan.stageDir, metadata, commands, distributionTargets, {
|
|
2686
|
+
artifactDirs: [...artifacts.names, ...includeDirs],
|
|
2687
|
+
manPages: artifacts.manPages
|
|
2688
|
+
});
|
|
2689
|
+
const rootDir = join(plan.stageDir, "root");
|
|
2690
|
+
const produced = [{
|
|
2691
|
+
kind: "package-json",
|
|
2692
|
+
path: join(rootDir, "package.json")
|
|
2693
|
+
}, ...distributionTargets.map((targetPackage) => ({
|
|
2694
|
+
kind: "package-json",
|
|
2695
|
+
path: join(targetPackage.packageDir, "package.json"),
|
|
2696
|
+
target: targetPackage.target
|
|
2697
|
+
}))];
|
|
2698
|
+
const copies = [...artifactOutDir ? artifacts.names.map((name) => ({
|
|
2699
|
+
name,
|
|
2700
|
+
sourceDir: join(artifactOutDir, name),
|
|
2701
|
+
dereference: false
|
|
2702
|
+
})) : [], ...includeDirs.map((name) => ({
|
|
2703
|
+
name,
|
|
2704
|
+
sourceDir: join(plan.cwd, name),
|
|
2705
|
+
dereference: true
|
|
2706
|
+
}))];
|
|
2707
|
+
for (const { name, sourceDir, dereference } of copies) {
|
|
2708
|
+
const options = {
|
|
2709
|
+
recursive: true,
|
|
2710
|
+
dereference,
|
|
2711
|
+
verbatimSymlinks: !dereference
|
|
2712
|
+
};
|
|
2713
|
+
cpSync(sourceDir, join(rootDir, name), options);
|
|
2714
|
+
for (const targetPackage of distributionTargets) cpSync(sourceDir, join(targetPackage.packageDir, "bin", name), options);
|
|
2715
|
+
}
|
|
2716
|
+
if (metadata.exports !== void 0) validateStagedExports(metadata.exports, rootDir, metadata.sourceType);
|
|
2717
|
+
const rootBinDir = join(rootDir, "bin");
|
|
2718
|
+
if (table) {
|
|
2719
|
+
for (const { command } of plan.entries) {
|
|
2720
|
+
const launcherPath = join(rootBinDir, `${command}.js`);
|
|
2721
|
+
writeFileSync(launcherPath, generateDistributionJsResolver(command, distributionTargets), { mode: 493 });
|
|
2722
|
+
produced.push({
|
|
2723
|
+
kind: "launcher",
|
|
2724
|
+
path: launcherPath,
|
|
2725
|
+
command
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
for (const targetPackage of distributionTargets) for (const { command, entryPath } of plan.entries) {
|
|
2729
|
+
const outfilePath = join(targetPackage.packageDir, "bin", binaryFilename(command, targetPackage));
|
|
2730
|
+
io.stdout(` ${cyan("→")} ${bold(targetPackage.targetAlias)}: ${dim(outfilePath)}`);
|
|
2731
|
+
await distribution.execute(entryPath, outfilePath, targetPackage.target);
|
|
2732
|
+
produced.push({
|
|
2733
|
+
kind: "executable",
|
|
2734
|
+
path: outfilePath,
|
|
2735
|
+
command,
|
|
2736
|
+
target: targetPackage.target
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
} else for (const { command, entryPath } of plan.entries) {
|
|
2740
|
+
const rootBinPath = join(rootBinDir, `${command}.js`);
|
|
2741
|
+
io.stdout(` ${cyan("→")} ${bold("root")}: ${dim(rootBinPath)}`);
|
|
2742
|
+
await distribution.execute(entryPath, rootBinPath);
|
|
2743
|
+
produced.push({
|
|
2744
|
+
kind: "bundle",
|
|
2745
|
+
path: rootBinPath,
|
|
2746
|
+
command
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
writeDistributionManifest(plan.stageDir, metadata, commands, distributionTargets, build);
|
|
2750
|
+
const manifestPath = join(plan.stageDir, "manifest.json");
|
|
2751
|
+
io.stdout(`\n${green("✓")} Staged ${bold(`${distributionTargets.length + 1}`)} npm package(s) successfully:`);
|
|
2752
|
+
io.stdout(` ${rootDir}`);
|
|
2753
|
+
for (const targetPackage of distributionTargets) io.stdout(` ${targetPackage.packageDir}`);
|
|
2754
|
+
io.stdout(`\n${dim("Manifest:")} ${manifestPath}`);
|
|
2755
|
+
return produced;
|
|
2756
|
+
}
|
|
2757
|
+
/**
|
|
2758
|
+
* `crust.include` directories normalized to cwd-relative POSIX names. They are
|
|
2759
|
+
* staged exactly like Extension artifacts.
|
|
2760
|
+
*/
|
|
2761
|
+
function collectIncludeDirs(cwd, stageDir, include, artifactNames) {
|
|
2762
|
+
const names = [...artifactNames];
|
|
2763
|
+
const includeDirs = [];
|
|
2764
|
+
for (const entry of include) {
|
|
2765
|
+
const dir = resolve(cwd, entry);
|
|
2766
|
+
const name = relative(cwd, dir);
|
|
2767
|
+
if (isAbsolute(entry) || name === "" || !isWithin(cwd, dir)) throw new Error(`package.json crust.include entry ${JSON.stringify(entry)} must be a directory inside the project root ${cwd}.`);
|
|
2768
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) throw new Error(`package.json crust.include entry ${JSON.stringify(entry)} is not a directory: ${dir}`);
|
|
2769
|
+
assertResolvesInsideProject(cwd, entry, dir);
|
|
2770
|
+
if (isWithin(stageDir, dir) || isWithin(dir, stageDir)) throw new Error(`package.json crust.include entry ${JSON.stringify(entry)} overlaps the build output directory ${stageDir}, which crust build replaces.`);
|
|
2771
|
+
if (name.split(sep)[0] === "bin") throw new Error(`package.json crust.include entry ${JSON.stringify(entry)} conflicts with the generated npm bin directory.\n Include a directory with a different top-level name.`);
|
|
2772
|
+
const posixName = name.replaceAll(sep, "/");
|
|
2773
|
+
const overlap = names.find((staged) => staged === posixName || staged.startsWith(`${posixName}/`) || posixName.startsWith(`${staged}/`));
|
|
2774
|
+
if (overlap !== void 0) throw new Error(`package.json crust.include entry ${JSON.stringify(entry)} overlaps "${overlap}", which is already staged (duplicate include or Extension artifact directory).`);
|
|
2775
|
+
names.push(posixName);
|
|
2776
|
+
includeDirs.push(posixName);
|
|
2777
|
+
}
|
|
2778
|
+
return includeDirs;
|
|
2779
|
+
}
|
|
2780
|
+
/**
|
|
2781
|
+
* Walks `dir` the way the dereferencing copy will (through symlinked
|
|
2782
|
+
* directories) and rejects any path whose real location leaves the project.
|
|
2783
|
+
*/
|
|
2784
|
+
function assertResolvesInsideProject(cwd, entry, dir) {
|
|
2785
|
+
const realCwd = realpathSync(cwd);
|
|
2786
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2787
|
+
const walk = (path) => {
|
|
2788
|
+
const real = realpathSync(path);
|
|
2789
|
+
if (!isWithin(realCwd, real)) throw new Error(`package.json crust.include entry ${JSON.stringify(entry)} resolves outside the project root: ${relative(cwd, path)} -> ${real}`);
|
|
2790
|
+
if (seen.has(real) || !statSync(path).isDirectory()) return;
|
|
2791
|
+
seen.add(real);
|
|
2792
|
+
for (const child of readdirSync(path)) walk(join(path, child));
|
|
2793
|
+
};
|
|
2794
|
+
walk(dir);
|
|
2795
|
+
}
|
|
2796
|
+
/**
|
|
2797
|
+
* Copies one entry's Extension build hook output into the shared artifact
|
|
2798
|
+
* directory. Identically spelled directories merge; case-only directory aliases
|
|
2799
|
+
* and a file or file/directory mismatch at a path
|
|
2800
|
+
* another entry already produced is an error, so no entry's hooks can replace
|
|
2801
|
+
* another's output. `owners` tracks case-folded POSIX-relative paths across
|
|
2802
|
+
* entries, including directories so file/ancestor conflicts are portable.
|
|
2803
|
+
*/
|
|
2804
|
+
function mergeEntryArtifacts(entryOutDir, artifactDir, command, owners) {
|
|
2805
|
+
const merge = (relativeDir) => {
|
|
2806
|
+
for (const dirent of readdirSync(join(entryOutDir, relativeDir), { withFileTypes: true })) {
|
|
2807
|
+
const relativePath = relativeDir ? `${relativeDir}/${dirent.name}` : dirent.name;
|
|
2808
|
+
if (!dirent.isDirectory() && !dirent.isFile()) throw new Error(`Build artifact "${relativePath}" from bin ${JSON.stringify(command)} must use regular files and directories, not symlinks or other file types.`);
|
|
2809
|
+
const source = join(entryOutDir, relativePath);
|
|
2810
|
+
const destination = join(artifactDir, relativePath);
|
|
2811
|
+
const key = relativePath.toLowerCase();
|
|
2812
|
+
const owner = owners.get(key);
|
|
2813
|
+
const existing = lstatSync(destination, { throwIfNoEntry: false });
|
|
2814
|
+
if (owner && !(dirent.isDirectory() && owner.directory && owner.path === relativePath) || existing && !(dirent.isDirectory() && existing.isDirectory())) throw new Error(`Build artifact "${relativePath}" is written by both bin ${JSON.stringify(owner?.command ?? "an earlier bin")} and ${JSON.stringify(command)}.\n Extension build hooks of different commands must write distinct paths under ${artifactDir}.`);
|
|
2815
|
+
if (!owner) owners.set(key, {
|
|
2816
|
+
command,
|
|
2817
|
+
directory: dirent.isDirectory(),
|
|
2818
|
+
path: relativePath
|
|
2819
|
+
});
|
|
2820
|
+
if (dirent.isDirectory()) {
|
|
2821
|
+
mkdirSync(destination, { recursive: true });
|
|
2822
|
+
merge(relativePath);
|
|
2823
|
+
} else {
|
|
2824
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
2825
|
+
copyFileSync(source, destination);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
};
|
|
2829
|
+
const root = lstatSync(entryOutDir, { throwIfNoEntry: false });
|
|
2830
|
+
if (root === void 0) return;
|
|
2831
|
+
if (!root.isDirectory()) throw new Error(`Build artifact directory for bin ${JSON.stringify(command)} must be a directory, not a symlink or other file type: ${entryOutDir}`);
|
|
2832
|
+
merge("");
|
|
2833
|
+
}
|
|
2834
|
+
function collectArtifacts(artifactOutDir) {
|
|
2835
|
+
if (!artifactOutDir || !existsSync(artifactOutDir)) return {
|
|
2836
|
+
names: [],
|
|
2837
|
+
manPages: []
|
|
2838
|
+
};
|
|
2839
|
+
const names = readdirSync(artifactOutDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
2840
|
+
if (names.includes("bin")) throw new Error(`Artifact directory "bin" in ${artifactOutDir} conflicts with the generated npm bin directory.\n Emit build artifacts under a different top-level name.`);
|
|
2841
|
+
return {
|
|
2842
|
+
names,
|
|
2843
|
+
manPages: names.includes("man") ? readdirSync(join(artifactOutDir, "man"), { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => entry.name).sort() : []
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
//#endregion
|
|
2847
|
+
//#region src/commands/build.ts
|
|
2848
|
+
/** Also mirrored by `schema/package.json`; build.test.ts guards against drift. */
|
|
2849
|
+
const CRUST_CONFIG_KEYS = [
|
|
2850
|
+
"runtime",
|
|
2851
|
+
"targets",
|
|
2852
|
+
"bunPlugins",
|
|
2853
|
+
"include"
|
|
2854
|
+
];
|
|
2855
|
+
function isBuildRuntime(value) {
|
|
2856
|
+
return typeof value === "string" && BUILD_RUNTIMES.some((runtime) => runtime === value);
|
|
2857
|
+
}
|
|
2858
|
+
function isString(value) {
|
|
2859
|
+
return typeof value === "string";
|
|
2860
|
+
}
|
|
2861
|
+
function isStringArray(value) {
|
|
2862
|
+
return Array.isArray(value) && value.every(isString);
|
|
2863
|
+
}
|
|
2864
|
+
function readCrustConfig(pkg) {
|
|
2865
|
+
if (pkg === void 0 || !isJsonObject(pkg) || pkg.crust === void 0) return {};
|
|
2866
|
+
const crust = pkg.crust;
|
|
2867
|
+
const allowed = `Allowed keys: ${CRUST_CONFIG_KEYS.join(", ")}`;
|
|
2868
|
+
if (!isJsonObject(crust)) throw new Error(`package.json crust must be an object. ${allowed}`);
|
|
2869
|
+
const unknown = Object.keys(crust).find((key) => !CRUST_CONFIG_KEYS.some((allowedKey) => allowedKey === key));
|
|
2870
|
+
if (unknown !== void 0) throw new Error(`Unknown package.json crust key ${JSON.stringify(unknown)}. ${allowed}`);
|
|
2871
|
+
const config = {};
|
|
2872
|
+
if (crust.runtime !== void 0) {
|
|
2873
|
+
if (!isBuildRuntime(crust.runtime)) throw new Error(`Invalid package.json crust.runtime ${JSON.stringify(crust.runtime)}. Valid runtimes: ${BUILD_RUNTIMES.join(", ")}`);
|
|
2874
|
+
config.runtime = crust.runtime;
|
|
2875
|
+
}
|
|
2876
|
+
if (crust.targets !== void 0) {
|
|
2877
|
+
if (!isStringArray(crust.targets) || crust.targets.length === 0) throw new Error("package.json crust.targets must be a non-empty array of canonical compiler targets, e.g. [\"bun-linux-x64\", \"bun-darwin-arm64\"].");
|
|
2878
|
+
config.targets = crust.targets;
|
|
2879
|
+
}
|
|
2880
|
+
if (crust.bunPlugins !== void 0) {
|
|
2881
|
+
if (!isStringArray(crust.bunPlugins)) throw new Error("package.json crust.bunPlugins must be an array of Bun plugin module specifiers, e.g. [\"@opentui/solid/bun-plugin\", \"./build/plugin.ts\"].");
|
|
2882
|
+
config.bunPlugins = crust.bunPlugins;
|
|
2883
|
+
}
|
|
2884
|
+
if (crust.include !== void 0) {
|
|
2885
|
+
if (!isStringArray(crust.include)) throw new Error("package.json crust.include must be an array of directory names relative to the project root, e.g. [\"templates\"].");
|
|
2886
|
+
config.include = crust.include;
|
|
2887
|
+
}
|
|
2888
|
+
return config;
|
|
2889
|
+
}
|
|
2890
|
+
function hasDependency(pkg, name) {
|
|
2891
|
+
if (!isJsonObject(pkg)) return false;
|
|
2892
|
+
return [pkg.dependencies, pkg.devDependencies].some((deps) => deps !== void 0 && isJsonObject(deps) && name in deps);
|
|
2893
|
+
}
|
|
2894
|
+
const DENO_CONFIG_FILES = ["deno.json", "deno.jsonc"];
|
|
2895
|
+
/**
|
|
2896
|
+
* package.json `crust.runtime` > inference > Bun. Inference uses only
|
|
2897
|
+
* unambiguous signals: a Deno config file, or `@types/node` without
|
|
2898
|
+
* `@types/bun`. Lockfiles say which package manager installed dependencies,
|
|
2899
|
+
* not which runtime runs the CLI, so they are not consulted.
|
|
2900
|
+
*/
|
|
2901
|
+
function resolveBuildRuntime(pkg, config, cwd) {
|
|
2902
|
+
if (config.runtime !== void 0) return {
|
|
2903
|
+
runtime: config.runtime,
|
|
2904
|
+
source: "from package.json"
|
|
2905
|
+
};
|
|
2906
|
+
const denoConfig = DENO_CONFIG_FILES.find((file) => existsSync(join(cwd, file)));
|
|
2907
|
+
if (denoConfig) return {
|
|
2908
|
+
runtime: "deno",
|
|
2909
|
+
source: `inferred from ${denoConfig}`
|
|
2910
|
+
};
|
|
2911
|
+
if (pkg !== void 0 && hasDependency(pkg, "@types/node") && !hasDependency(pkg, "@types/bun")) return {
|
|
2912
|
+
runtime: "node",
|
|
2913
|
+
source: "inferred from @types/node"
|
|
2914
|
+
};
|
|
2915
|
+
return {
|
|
2916
|
+
runtime: "bun",
|
|
2917
|
+
source: "default"
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
function printBuildReport(command, report, stdout) {
|
|
2921
|
+
if (report.extensions.length === 0) return;
|
|
2922
|
+
stdout(`Preparing Command Snapshot for ${command}...`);
|
|
2923
|
+
const fileCount = (files) => `${files.length} ${files.length === 1 ? "file" : "files"}`;
|
|
2924
|
+
const idWidth = Math.max(...report.extensions.map(({ id }) => id.length));
|
|
2925
|
+
const countWidth = Math.max(...report.extensions.map(({ files }) => fileCount(files).length));
|
|
2926
|
+
for (const { id, files } of report.extensions) {
|
|
2927
|
+
const paths = [...files.slice(0, 3), ...files.length > 3 ? [`+${files.length - 3} more`] : []].join(", ");
|
|
2928
|
+
stdout(` ${id.padEnd(idWidth)} ${fileCount(files).padEnd(countWidth)}${paths ? ` ${paths}` : ""}`);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
function resolveEnvFilePaths(cwd, envFiles) {
|
|
2932
|
+
if (!envFiles || envFiles.length === 0) return [];
|
|
2933
|
+
return envFiles.map((envFile) => {
|
|
2934
|
+
const envPath = resolve(cwd, envFile);
|
|
2935
|
+
if (!existsSync(envPath)) throw new Error(`Env file not found: ${envPath}\n Specify a valid env file with --env-file <path>`);
|
|
2936
|
+
return envPath;
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
/** Entry built when package.json has no `bin`. */
|
|
2940
|
+
const DEFAULT_ENTRY = "src/cli.ts";
|
|
2941
|
+
/**
|
|
2942
|
+
* Command names become `bin/<command>.js`, `<command>-<target>` binary
|
|
2943
|
+
* filenames, and launcher text, so they are restricted to a filename-safe
|
|
2944
|
+
* subset of what npm accepts: no separators, dots-only names, or leading `.`/`-`.
|
|
2945
|
+
*/
|
|
2946
|
+
const COMMAND_NAME_PATTERN = /^[A-Za-z0-9_~][A-Za-z0-9._~-]*$/;
|
|
2947
|
+
const BIN_EXAMPLE = `{ "my-cli": ${JSON.stringify(DEFAULT_ENTRY)} }`;
|
|
2948
|
+
/**
|
|
2949
|
+
* A `bin` value as an absolute path. The path must be relative and lexically
|
|
2950
|
+
* inside the project (a symlink may point elsewhere, as for `crust.include`),
|
|
2951
|
+
* and must name an existing file, not a directory.
|
|
2952
|
+
*/
|
|
2953
|
+
function resolveEntryPath(cwd, command, source) {
|
|
2954
|
+
const entryPath = resolve(cwd, source);
|
|
2955
|
+
if (isAbsolute(source) || entryPath === cwd || !isWithin(cwd, entryPath)) throw new Error(`package.json bin ${JSON.stringify(command)} entry ${JSON.stringify(source)} must be a file inside the project root ${cwd}.`);
|
|
2956
|
+
if (!existsSync(entryPath)) throw new Error(`Entry file not found: ${entryPath}\n Point package.json bin ${JSON.stringify(command)} at your CLI source entry (default ${DEFAULT_ENTRY}).`);
|
|
2957
|
+
if (!statSync(entryPath).isFile()) throw new Error(`package.json bin ${JSON.stringify(command)} entry ${JSON.stringify(source)} is not a file: ${entryPath}`);
|
|
2958
|
+
return entryPath;
|
|
2959
|
+
}
|
|
2960
|
+
/**
|
|
2961
|
+
* package.json `bin` as build entries, in declaration order. Object values are
|
|
2962
|
+
* source files built for the command named by their key; a string `bin` is the
|
|
2963
|
+
* source of a command named after the unscoped package name, and no `bin` builds
|
|
2964
|
+
* `src/cli.ts` under that name.
|
|
2965
|
+
*
|
|
2966
|
+
* Two commands cannot share one entry file: entries are compared by real path,
|
|
2967
|
+
* so `./src/cli.ts`, `src/../src/cli.ts`, and a symlink to `src/cli.ts` all
|
|
2968
|
+
* collide. Command names are compared case-insensitively, because `Tool` and
|
|
2969
|
+
* `tool` would be the same `bin/` file on a case-insensitive filesystem.
|
|
2970
|
+
*/
|
|
2971
|
+
function resolveBinEntries(cwd, pkg) {
|
|
2972
|
+
const packageJson = pkg !== void 0 && isJsonObject(pkg) ? pkg : {};
|
|
2973
|
+
const bin = packageJson.bin;
|
|
2974
|
+
let declared;
|
|
2975
|
+
if (bin === void 0 || isString(bin)) {
|
|
2976
|
+
const name = packageJson.name;
|
|
2977
|
+
if (name === void 0 || !isString(name) || name === "") throw new Error(`package.json is missing a name field.\n Without an object bin, the unscoped package name is the command name, e.g. "bin": ${BIN_EXAMPLE}.`);
|
|
2978
|
+
declared = [[name.replace(/^@[^/]+\//, ""), bin ?? "src/cli.ts"]];
|
|
2979
|
+
} else if (isJsonObject(bin) && Object.keys(bin).length > 0) declared = Object.entries(bin);
|
|
2980
|
+
else throw new Error(`package.json bin must be a source entry path or a non-empty object mapping command names to source entries, e.g. ${BIN_EXAMPLE}.`);
|
|
2981
|
+
const commandByLowerName = /* @__PURE__ */ new Map();
|
|
2982
|
+
const commandByRealPath = /* @__PURE__ */ new Map();
|
|
2983
|
+
return declared.map(([command, source]) => {
|
|
2984
|
+
if (!COMMAND_NAME_PATTERN.test(command)) throw new Error(`package.json bin key ${JSON.stringify(command)} is not a valid command name.\n Use letters, digits, ".", "_", "~", and "-", not starting with "." or "-".`);
|
|
2985
|
+
const sameName = commandByLowerName.get(command.toLowerCase());
|
|
2986
|
+
if (sameName !== void 0) throw new Error(`package.json bin keys ${JSON.stringify(sameName)} and ${JSON.stringify(command)} differ only by case.\n Command names become bin/ file names, which collide on case-insensitive filesystems; rename one of them.`);
|
|
2987
|
+
commandByLowerName.set(command.toLowerCase(), command);
|
|
2988
|
+
if (!isString(source)) throw new Error(`package.json bin ${JSON.stringify(command)} must be a project-relative source entry path, e.g. ${JSON.stringify(DEFAULT_ENTRY)}.`);
|
|
2989
|
+
const entryPath = resolveEntryPath(cwd, command, source);
|
|
2990
|
+
const realPath = realpathSync(entryPath);
|
|
2991
|
+
const other = commandByRealPath.get(realPath);
|
|
2992
|
+
if (other !== void 0) throw new Error(`package.json bin ${JSON.stringify(other)} and ${JSON.stringify(command)} both build ${realPath}.\n Each command needs its own entry file; aliases of one entry are not supported.`);
|
|
2993
|
+
commandByRealPath.set(realPath, command);
|
|
2994
|
+
return {
|
|
2995
|
+
command,
|
|
2996
|
+
entryPath
|
|
2997
|
+
};
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
3000
|
+
function planBuild(options, cwd) {
|
|
3001
|
+
const userPackageJson = readUserPackageJson(cwd);
|
|
3002
|
+
const config = readCrustConfig(userPackageJson);
|
|
3003
|
+
const { runtime, source: runtimeSource } = resolveBuildRuntime(userPackageJson, config, cwd);
|
|
3004
|
+
const entries = resolveBinEntries(cwd, userPackageJson);
|
|
3005
|
+
validatePackageIdentity(userPackageJson, "package.json");
|
|
3006
|
+
const envFiles = resolveEnvFilePaths(cwd, options.envFiles);
|
|
3007
|
+
const bunPlugins = config.bunPlugins ?? [];
|
|
3008
|
+
if (runtime === "node" && options.targets?.length) throw new Error("--target cannot be used with the node runtime.\n Node builds produce one portable JavaScript artifact.");
|
|
3009
|
+
if (runtime === "node" && config.targets !== void 0) throw new Error("package.json crust.targets is not supported with the node runtime.\n Node builds produce one portable JavaScript artifact; remove crust.targets or set crust.runtime to bun or deno.");
|
|
3010
|
+
if (runtime === "deno" && options.minify) throw new Error("--minify is not supported with the deno runtime.\n deno compile has no minification step; drop the flag.");
|
|
3011
|
+
if (runtime === "deno" && envFiles.length > 0) throw new Error("--env-file is not supported with the deno runtime.\n deno compile embeds every variable from the file into the binary — secrets included —\n with no PUBLIC_* filter. Load configuration at runtime instead (e.g. deno run --env-file).");
|
|
3012
|
+
if (runtime === "deno" && bunPlugins.length > 0) throw new Error("package.json crust.bunPlugins is not supported with the deno runtime.\n deno compile has no Bun bundler; remove crust.bunPlugins or set crust.runtime to bun.");
|
|
3013
|
+
const stageDir = resolve(cwd, CRUST_DIR);
|
|
3014
|
+
const common = {
|
|
3015
|
+
cwd,
|
|
3016
|
+
userPackageJson,
|
|
3017
|
+
runtimeSource,
|
|
3018
|
+
entries,
|
|
3019
|
+
envFiles,
|
|
3020
|
+
bunPlugins,
|
|
3021
|
+
include: config.include ?? [],
|
|
3022
|
+
outDir: join(stageDir, "artifacts"),
|
|
3023
|
+
stageDir,
|
|
3024
|
+
validate: options.validate ?? true,
|
|
3025
|
+
minify: runtime === "deno" ? false : options.minify ?? true
|
|
3026
|
+
};
|
|
3027
|
+
if (runtime === "node") return {
|
|
3028
|
+
...common,
|
|
3029
|
+
runtime
|
|
3030
|
+
};
|
|
3031
|
+
const targetInputs = options.targets?.length ? options.targets : config.targets;
|
|
3032
|
+
if (runtime === "bun") {
|
|
3033
|
+
const targets = resolveTargets(BUN_TARGETS, targetInputs);
|
|
3034
|
+
assertTargetsBuildableWithoutBun(targets);
|
|
3035
|
+
return {
|
|
3036
|
+
...common,
|
|
3037
|
+
runtime,
|
|
3038
|
+
targets
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
return {
|
|
3042
|
+
...common,
|
|
3043
|
+
runtime,
|
|
3044
|
+
targets: resolveTargets(DENO_TARGETS, targetInputs)
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
3047
|
+
/** Bun and Deno stage platform packages behind a Node launcher; Node stages a root-only bundle. */
|
|
3048
|
+
async function runStagedBuild(plan, io, reports) {
|
|
3049
|
+
if (plan.runtime === "bun") return runDistributeBuild(plan, {
|
|
3050
|
+
table: BUN_TARGETS,
|
|
3051
|
+
targets: plan.targets,
|
|
3052
|
+
execute: (entry, outfile, target) => execBuild(entry, outfile, plan.minify, target, plan.envFiles, plan.cwd, plan.bunPlugins)
|
|
3053
|
+
}, io, reports);
|
|
3054
|
+
if (plan.runtime === "deno") return runDistributeBuild(plan, {
|
|
3055
|
+
table: DENO_TARGETS,
|
|
3056
|
+
targets: plan.targets,
|
|
3057
|
+
execute: (entry, outfile, target) => execDenoBuild(entry, outfile, target, plan.cwd)
|
|
3058
|
+
}, io, reports);
|
|
3059
|
+
return runDistributeBuild(plan, { execute: (entry, outfile) => execNodeBuild(entry, outfile, plan.minify, plan.envFiles, plan.cwd, plan.bunPlugins) }, io, reports);
|
|
3060
|
+
}
|
|
3061
|
+
/**
|
|
3062
|
+
* Prepares every entry's Command Snapshot and checks that its root command is
|
|
3063
|
+
* named after the bin key, then merges the Extension build hook output into
|
|
3064
|
+
* `plan.outDir`. Each entry's hooks write into their own temporary directory;
|
|
3065
|
+
* the merge fails on any path two entries both write. Returns each command's
|
|
3066
|
+
* Build Report.
|
|
3067
|
+
*/
|
|
3068
|
+
async function prepareEntries(plan, io) {
|
|
3069
|
+
const owners = /* @__PURE__ */ new Map();
|
|
3070
|
+
const reports = {};
|
|
3071
|
+
for (const { command, entryPath } of plan.entries) {
|
|
3072
|
+
const entryOutDir = await mkdtemp(join(tmpdir(), "crust-artifacts-"));
|
|
3073
|
+
try {
|
|
3074
|
+
const { snapshot, build: report } = await buildEntrypoint(entryPath, entryOutDir, plan.envFiles, io, plan.cwd);
|
|
3075
|
+
if (snapshot.meta.name !== command) throw new Error(`package.json bin ${JSON.stringify(command)} builds ${entryPath}, whose root command is named ${JSON.stringify(snapshot.meta.name)}.\n The installed command, help, man pages, and skills use the root command name, so new Crust(name) must match the bin key; rename one of them.`);
|
|
3076
|
+
printBuildReport(command, report, io.stdout);
|
|
3077
|
+
mergeEntryArtifacts(entryOutDir, plan.outDir, command, owners);
|
|
3078
|
+
reports[command] = report;
|
|
3079
|
+
} finally {
|
|
3080
|
+
rmSync(entryOutDir, {
|
|
3081
|
+
recursive: true,
|
|
3082
|
+
force: true
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
return reports;
|
|
3087
|
+
}
|
|
3088
|
+
const activeBuilds = /* @__PURE__ */ new Set();
|
|
3089
|
+
/**
|
|
3090
|
+
* Stages the publishable npm tree in `<cwd>/.crust`: a root package with one
|
|
3091
|
+
* `bin/<command>.js` per package.json `bin` entry, each a Node launcher (Bun,
|
|
3092
|
+
* Deno) or the bundle itself (Node), plus one platform package per target for
|
|
3093
|
+
* Bun and Deno holding one binary per command. Command names and source
|
|
3094
|
+
* entries come from `bin`; the runtime, Bun plugins, and extra directories
|
|
3095
|
+
* from package.json `crust`. Compilation and Command Snapshots run in bun
|
|
3096
|
+
* subprocesses (bun on PATH, or the running Bun executable), so this works
|
|
3097
|
+
* under Node as well when Bun is installed.
|
|
3098
|
+
*
|
|
3099
|
+
* Throws on any failure. Planning failures (bad options or package.json) leave
|
|
3100
|
+
* the previous `.crust/` stage untouched; failures after planning leave a
|
|
3101
|
+
* wiped stage without a completion `manifest.json`. Overlapping calls for the
|
|
3102
|
+
* same real project directory in this process are rejected before staging.
|
|
3103
|
+
*/
|
|
3104
|
+
async function build(options = {}) {
|
|
3105
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
3106
|
+
const project = realpathSync(cwd);
|
|
3107
|
+
if (activeBuilds.has(project)) throw new Error(`crust build is already building ${project}`);
|
|
3108
|
+
activeBuilds.add(project);
|
|
3109
|
+
try {
|
|
3110
|
+
const onLog = options.onLog ?? (() => {});
|
|
3111
|
+
const io = {
|
|
3112
|
+
stdout: (line) => onLog(line, "stdout"),
|
|
3113
|
+
stderr: (line) => onLog(line, "stderr")
|
|
3114
|
+
};
|
|
3115
|
+
const plan = planBuild(options, cwd);
|
|
3116
|
+
io.stdout(`${dim("Runtime:")} ${plan.runtime} ${dim(`(${plan.runtimeSource})`)}`);
|
|
3117
|
+
rmSync(plan.stageDir, {
|
|
3118
|
+
recursive: true,
|
|
3119
|
+
force: true
|
|
3120
|
+
});
|
|
3121
|
+
const reports = plan.validate ? await prepareEntries(plan, io) : void 0;
|
|
3122
|
+
const artifacts = await runStagedBuild(plan, io, reports);
|
|
3123
|
+
return {
|
|
3124
|
+
stageDir: plan.stageDir,
|
|
3125
|
+
artifacts,
|
|
3126
|
+
...reports ? { reports } : {}
|
|
3127
|
+
};
|
|
3128
|
+
} finally {
|
|
3129
|
+
activeBuilds.delete(project);
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
defineCommand("build", { description: "Build your CLI for Bun, Deno, or Node" }, (command) => command.flags({
|
|
3133
|
+
name: "target",
|
|
3134
|
+
type: "string",
|
|
3135
|
+
multiple: true,
|
|
3136
|
+
description: `Canonical compiler target(s), or "${HOST_TARGET}" for this machine; repeatable. Omit to stage package.json crust.targets, or every Bun/Deno target`,
|
|
3137
|
+
short: "t"
|
|
3138
|
+
}, {
|
|
3139
|
+
name: "env-file",
|
|
3140
|
+
type: "string",
|
|
3141
|
+
multiple: true,
|
|
3142
|
+
description: "Explicit env file(s) used for build-time constants; repeatable"
|
|
3143
|
+
}, {
|
|
3144
|
+
name: "validate",
|
|
3145
|
+
type: "boolean",
|
|
3146
|
+
description: "Materialize command definitions before compiling; --no-validate also skips Extension build hooks",
|
|
3147
|
+
default: true
|
|
3148
|
+
}, {
|
|
3149
|
+
name: "minify",
|
|
3150
|
+
type: "boolean",
|
|
3151
|
+
description: "Minify the output (default for bun and node; unsupported for deno)"
|
|
3152
|
+
}).action(async ({ flags, stdout, stderr }) => {
|
|
3153
|
+
await build({
|
|
3154
|
+
cwd: process.cwd(),
|
|
3155
|
+
targets: flags.target,
|
|
3156
|
+
envFiles: flags["env-file"],
|
|
3157
|
+
minify: flags.minify,
|
|
3158
|
+
validate: flags.validate,
|
|
3159
|
+
onLog: (line, stream) => (stream === "stderr" ? stderr : stdout)(line)
|
|
3160
|
+
});
|
|
3161
|
+
}));
|
|
3162
|
+
//#endregion
|
|
3163
|
+
export { build };
|