@crustjs/crust-linux-x64-musl 0.5.3 → 0.5.5
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-x64-musl +1 -1
- package/bin/dist/index.d.ts +9 -9
- package/bin/dist/index.js +1000 -1131
- package/bin/schema/package.json +8 -0
- package/package.json +2 -2
package/bin/dist/index.js
CHANGED
|
@@ -2,14 +2,14 @@ import { accessSync, constants, copyFileSync, cpSync, existsSync, lstatSync, mkd
|
|
|
2
2
|
import { chmod, copyFile, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
7
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7
8
|
import { once } from "node:events";
|
|
8
9
|
import { text } from "node:stream/consumers";
|
|
9
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
10
|
//#region \0rolldown/runtime.js
|
|
11
11
|
var __create = Object.create;
|
|
12
|
-
var __defProp
|
|
12
|
+
var __defProp = Object.defineProperty;
|
|
13
13
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
14
14
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
15
15
|
var __getProtoOf = Object.getPrototypeOf;
|
|
@@ -18,29 +18,112 @@ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).expor
|
|
|
18
18
|
var __copyProps = (to, from, except, desc) => {
|
|
19
19
|
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
20
20
|
key = keys[i];
|
|
21
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
22
22
|
get: ((k) => from[k]).bind(null, key),
|
|
23
23
|
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
return to;
|
|
27
27
|
};
|
|
28
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp
|
|
28
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
29
29
|
value: mod,
|
|
30
30
|
enumerable: true
|
|
31
31
|
}) : target, mod));
|
|
32
32
|
//#endregion
|
|
33
|
-
//#region ../
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
//#region ../utils/dist/artifacts.js
|
|
34
|
+
/**
|
|
35
|
+
* Set by `crust build` while it prepares the Command Snapshot: the absolute
|
|
36
|
+
* entry-isolated directory Extension build hooks write into. Core reads it
|
|
37
|
+
* to run the hooks; `resolveArtifactDir` reads it so sections evaluated during
|
|
38
|
+
* that run see the artifacts being built instead of the wiped `.crust/root`.
|
|
39
|
+
*/
|
|
40
|
+
const BUILD_OUT_DIR_ENV = "CRUST_INTERNAL_BUILD_OUT_DIR";
|
|
41
|
+
/**
|
|
42
|
+
* `Symbol.for` registry key of the packaged-build marker, for bundlers without a
|
|
43
|
+
* `define` option (native `deno bundle`). `crust build` prepends
|
|
44
|
+
* `globalThis[Symbol.for(PACKAGED_BUILD_KEY)] = true` to the command bundle,
|
|
45
|
+
* before the inlined application graph, so every bundled copy sees it.
|
|
46
|
+
* The registry key is shared across module copies; the value is never read from
|
|
47
|
+
* the environment, so detection needs no Deno env permission.
|
|
48
|
+
*/
|
|
49
|
+
const PACKAGED_BUILD_KEY = "crustjs.packagedBuild";
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region ../core/dist/invocation-BstcKEMM.js
|
|
52
|
+
/**
|
|
53
|
+
* A typed error for runtime recipe, Extension, Context, documentation, argv, and validation failures.
|
|
54
|
+
*
|
|
55
|
+
* Every `CrustError` carries a {@link CrustErrorCode} that identifies the specific
|
|
56
|
+
* failure, enabling programmatic error handling without fragile message parsing.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { CrustError } from "@crustjs/core";
|
|
61
|
+
*
|
|
62
|
+
* const outcome = await app.run(["deploy"], { args: { target: "prod" } });
|
|
63
|
+
* if (outcome.status === "failed") {
|
|
64
|
+
* const err = outcome.error;
|
|
65
|
+
* if (err instanceof CrustError) {
|
|
66
|
+
* console.error(`[${err.code}] ${err.message}`);
|
|
67
|
+
* }
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
var CrustError = class extends Error {
|
|
72
|
+
/** Machine-readable error code for programmatic handling */
|
|
73
|
+
code;
|
|
74
|
+
/** Structured payload for programmatic handling */
|
|
75
|
+
details;
|
|
76
|
+
/** Optional wrapped original error/value */
|
|
77
|
+
cause;
|
|
78
|
+
constructor(code, message, ...details) {
|
|
79
|
+
super(message);
|
|
80
|
+
this.name = "CrustError";
|
|
81
|
+
this.code = code;
|
|
82
|
+
this.details = details[0];
|
|
83
|
+
}
|
|
84
|
+
is(code) {
|
|
85
|
+
return Object.is(this.code, code);
|
|
86
|
+
}
|
|
87
|
+
withCause(cause) {
|
|
88
|
+
this.cause = cause;
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
toJSON() {
|
|
92
|
+
return {
|
|
93
|
+
code: this.code,
|
|
94
|
+
message: this.message,
|
|
95
|
+
details: this.details
|
|
96
|
+
};
|
|
97
|
+
}
|
|
43
98
|
};
|
|
99
|
+
Object.freeze({ [Symbol("crust.handled")]: true });
|
|
100
|
+
/**
|
|
101
|
+
* Snapshot subprocess protocol used by first-party build tooling.
|
|
102
|
+
*
|
|
103
|
+
* When set to a non-empty file path, `.execute()` prepares the command tree,
|
|
104
|
+
* validates its documentation sections, optionally runs Extension build hooks when the
|
|
105
|
+
* build output directory is set, writes its final JSON snapshot and Build Report, and exits
|
|
106
|
+
* without dispatching a Command Action. In-process callers use `Crust.snapshot()`.
|
|
107
|
+
*
|
|
108
|
+
* Only source entries run by `crust build` honor it: finished Bun/Node bundles carry
|
|
109
|
+
* `process.env.CRUST_INTERNAL_BUILD === "1"` as a literal and compile the protocol out;
|
|
110
|
+
* packaged Deno bundles set the `isPackagedBuild()` marker and skip it without
|
|
111
|
+
* reading the environment.
|
|
112
|
+
*/
|
|
113
|
+
const SNAPSHOT_PATH_ENV = "CRUST_INTERNAL_SNAPSHOT_PATH";
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region ../core/dist/tooling.js
|
|
116
|
+
const INSTALLED_COMMAND_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
117
|
+
/** {@link isInstalledCommandName}'s rule as prose, for error messages: "Use <rule>." */
|
|
118
|
+
const INSTALLED_COMMAND_NAME_RULE = "letters, digits, \".\", \"_\", and \"-\", starting with a letter or digit";
|
|
119
|
+
/**
|
|
120
|
+
* Whether `name` is valid as an installed command name: the `bin` keys
|
|
121
|
+
* `crust build` accepts, the project names `create-crust` scaffolds, and the
|
|
122
|
+
* root names shell completion targets.
|
|
123
|
+
*/
|
|
124
|
+
function isInstalledCommandName(name) {
|
|
125
|
+
return INSTALLED_COMMAND_NAME_PATTERN.test(name);
|
|
126
|
+
}
|
|
44
127
|
new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
45
128
|
new RegExp([
|
|
46
129
|
String.raw`(?:\x1b\[|\x9b)[^\x1b\x18\x1a\x9c\x40-\x7e]*[\x40-\x7e\x18\x1a\x9c]?`,
|
|
@@ -49,154 +132,69 @@ new RegExp([
|
|
|
49
132
|
String.raw`\x1b[\x20-\x2f][^\x1b]?`,
|
|
50
133
|
String.raw`\x1b[\x18\x1a\x9c\x30-\x7e]?`
|
|
51
134
|
].join("|"), "g");
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
135
|
+
const modifierCodes = {
|
|
136
|
+
/** Bold / increased intensity. */
|
|
137
|
+
bold: [1, 22],
|
|
138
|
+
/** Dim / decreased intensity. */
|
|
139
|
+
dim: [2, 22],
|
|
140
|
+
italic: [3, 23],
|
|
141
|
+
underline: [4, 24],
|
|
142
|
+
/** Inverse / reverse video. */
|
|
143
|
+
inverse: [7, 27],
|
|
144
|
+
/** Hidden / conceal. */
|
|
145
|
+
hidden: [8, 28],
|
|
146
|
+
/** Strikethrough / crossed out. */
|
|
147
|
+
strikethrough: [9, 29]
|
|
148
|
+
};
|
|
149
|
+
const colorCodes = {
|
|
150
|
+
black: [30, 39],
|
|
151
|
+
red: [31, 39],
|
|
152
|
+
green: [32, 39],
|
|
153
|
+
yellow: [33, 39],
|
|
154
|
+
blue: [34, 39],
|
|
155
|
+
magenta: [35, 39],
|
|
156
|
+
cyan: [36, 39],
|
|
157
|
+
white: [37, 39],
|
|
158
|
+
/** Bright black (gray). */
|
|
159
|
+
gray: [90, 39],
|
|
160
|
+
brightRed: [91, 39],
|
|
161
|
+
brightGreen: [92, 39],
|
|
162
|
+
brightYellow: [93, 39],
|
|
163
|
+
brightBlue: [94, 39],
|
|
164
|
+
brightMagenta: [95, 39],
|
|
165
|
+
brightCyan: [96, 39],
|
|
166
|
+
brightWhite: [97, 39],
|
|
167
|
+
bgBlack: [40, 49],
|
|
168
|
+
bgRed: [41, 49],
|
|
169
|
+
bgGreen: [42, 49],
|
|
170
|
+
bgYellow: [43, 49],
|
|
171
|
+
bgBlue: [44, 49],
|
|
172
|
+
bgMagenta: [45, 49],
|
|
173
|
+
bgCyan: [46, 49],
|
|
174
|
+
bgWhite: [47, 49],
|
|
175
|
+
bgBrightBlack: [100, 49],
|
|
176
|
+
bgBrightRed: [101, 49],
|
|
177
|
+
bgBrightGreen: [102, 49],
|
|
178
|
+
bgBrightYellow: [103, 49],
|
|
179
|
+
bgBrightBlue: [104, 49],
|
|
180
|
+
bgBrightMagenta: [105, 49],
|
|
181
|
+
bgBrightCyan: [106, 49],
|
|
182
|
+
bgBrightWhite: [107, 49]
|
|
183
|
+
};
|
|
184
|
+
const styleMethodCodes = {
|
|
185
|
+
...modifierCodes,
|
|
186
|
+
...colorCodes
|
|
187
|
+
};
|
|
188
|
+
const styleMethodNames = Object.freeze(Object.keys(styleMethodCodes));
|
|
189
|
+
const styleMethodPairs = Object.freeze(Object.fromEntries(styleMethodNames.map((name) => {
|
|
190
|
+
const [open, close] = styleMethodCodes[name];
|
|
191
|
+
return [name, {
|
|
97
192
|
open: `\x1b[${open}m`,
|
|
98
193
|
close: `\x1b[${close}m`
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
/** Bold / increased intensity. */
|
|
102
|
-
const bold$1 = pair(1, 22);
|
|
103
|
-
/** Dim / decreased intensity. */
|
|
104
|
-
const dim$1 = pair(2, 22);
|
|
105
|
-
/** Italic. */
|
|
106
|
-
const italic$1 = pair(3, 23);
|
|
107
|
-
/** Underline. */
|
|
108
|
-
const underline$1 = pair(4, 24);
|
|
109
|
-
/** Inverse / reverse video. */
|
|
110
|
-
const inverse$1 = pair(7, 27);
|
|
111
|
-
/** Hidden / conceal. */
|
|
112
|
-
const hidden$1 = pair(8, 28);
|
|
113
|
-
/** Strikethrough / crossed out. */
|
|
114
|
-
const strikethrough$1 = pair(9, 29);
|
|
115
|
-
const black$1 = pair(30, 39);
|
|
116
|
-
const red$1 = pair(31, 39);
|
|
117
|
-
const green$1 = pair(32, 39);
|
|
118
|
-
const yellow$1 = pair(33, 39);
|
|
119
|
-
const blue$1 = pair(34, 39);
|
|
120
|
-
const magenta$1 = pair(35, 39);
|
|
121
|
-
const cyan$1 = pair(36, 39);
|
|
122
|
-
const white$1 = pair(37, 39);
|
|
123
|
-
/** Bright black (gray). */
|
|
124
|
-
const gray$1 = pair(90, 39);
|
|
125
|
-
const brightRed$1 = pair(91, 39);
|
|
126
|
-
const brightGreen$1 = pair(92, 39);
|
|
127
|
-
const brightYellow$1 = pair(93, 39);
|
|
128
|
-
const brightBlue$1 = pair(94, 39);
|
|
129
|
-
const brightMagenta$1 = pair(95, 39);
|
|
130
|
-
const brightCyan$1 = pair(96, 39);
|
|
131
|
-
const brightWhite$1 = pair(97, 39);
|
|
132
|
-
const bgBlack$1 = pair(40, 49);
|
|
133
|
-
const bgRed$1 = pair(41, 49);
|
|
134
|
-
const bgGreen$1 = pair(42, 49);
|
|
135
|
-
const bgYellow$1 = pair(43, 49);
|
|
136
|
-
const bgBlue$1 = pair(44, 49);
|
|
137
|
-
const bgMagenta$1 = pair(45, 49);
|
|
138
|
-
const bgCyan$1 = pair(46, 49);
|
|
139
|
-
const bgWhite$1 = pair(47, 49);
|
|
140
|
-
const bgBrightBlack$1 = pair(100, 49);
|
|
141
|
-
const bgBrightRed$1 = pair(101, 49);
|
|
142
|
-
const bgBrightGreen$1 = pair(102, 49);
|
|
143
|
-
const bgBrightYellow$1 = pair(103, 49);
|
|
144
|
-
const bgBrightBlue$1 = pair(104, 49);
|
|
145
|
-
const bgBrightMagenta$1 = pair(105, 49);
|
|
146
|
-
const bgBrightCyan$1 = pair(106, 49);
|
|
147
|
-
const bgBrightWhite$1 = pair(107, 49);
|
|
148
|
-
const styleMethodNames = Object.freeze([
|
|
149
|
-
"bold",
|
|
150
|
-
"dim",
|
|
151
|
-
"italic",
|
|
152
|
-
"underline",
|
|
153
|
-
"inverse",
|
|
154
|
-
"hidden",
|
|
155
|
-
"strikethrough",
|
|
156
|
-
"black",
|
|
157
|
-
"red",
|
|
158
|
-
"green",
|
|
159
|
-
"yellow",
|
|
160
|
-
"blue",
|
|
161
|
-
"magenta",
|
|
162
|
-
"cyan",
|
|
163
|
-
"white",
|
|
164
|
-
"gray",
|
|
165
|
-
"brightRed",
|
|
166
|
-
"brightGreen",
|
|
167
|
-
"brightYellow",
|
|
168
|
-
"brightBlue",
|
|
169
|
-
"brightMagenta",
|
|
170
|
-
"brightCyan",
|
|
171
|
-
"brightWhite",
|
|
172
|
-
"bgBlack",
|
|
173
|
-
"bgRed",
|
|
174
|
-
"bgGreen",
|
|
175
|
-
"bgYellow",
|
|
176
|
-
"bgBlue",
|
|
177
|
-
"bgMagenta",
|
|
178
|
-
"bgCyan",
|
|
179
|
-
"bgWhite",
|
|
180
|
-
"bgBrightBlack",
|
|
181
|
-
"bgBrightRed",
|
|
182
|
-
"bgBrightGreen",
|
|
183
|
-
"bgBrightYellow",
|
|
184
|
-
"bgBrightBlue",
|
|
185
|
-
"bgBrightMagenta",
|
|
186
|
-
"bgBrightCyan",
|
|
187
|
-
"bgBrightWhite"
|
|
188
|
-
]);
|
|
189
|
-
const modifierNameSet = /* @__PURE__ */ new Set([
|
|
190
|
-
"bold",
|
|
191
|
-
"dim",
|
|
192
|
-
"italic",
|
|
193
|
-
"underline",
|
|
194
|
-
"inverse",
|
|
195
|
-
"hidden",
|
|
196
|
-
"strikethrough"
|
|
197
|
-
]);
|
|
194
|
+
}];
|
|
195
|
+
})));
|
|
198
196
|
function isModifierName(name) {
|
|
199
|
-
return
|
|
197
|
+
return Object.hasOwn(modifierCodes, name);
|
|
200
198
|
}
|
|
201
199
|
function read(overrides, key, environment) {
|
|
202
200
|
return overrides !== void 0 && key in overrides ? overrides[key] : environment;
|
|
@@ -1064,15 +1062,15 @@ const namedColorValues = {
|
|
|
1064
1062
|
*
|
|
1065
1063
|
* @example
|
|
1066
1064
|
* ```ts
|
|
1067
|
-
* import { applyStyle } from "./
|
|
1068
|
-
* import {
|
|
1065
|
+
* import { applyStyle } from "./style-engine.ts";
|
|
1066
|
+
* import { styleMethodPairs } from "./ansi-codes.ts";
|
|
1069
1067
|
*
|
|
1070
1068
|
* // Simple usage
|
|
1071
|
-
* applyStyle("hello", bold); // "\x1b[1mhello\x1b[22m"
|
|
1069
|
+
* applyStyle("hello", styleMethodPairs.bold); // "\x1b[1mhello\x1b[22m"
|
|
1072
1070
|
*
|
|
1073
1071
|
* // Nesting: bold wraps a red segment — bold reopens after red's close
|
|
1074
|
-
* const inner = applyStyle("world", red);
|
|
1075
|
-
* applyStyle(`hello ${inner}!`, bold);
|
|
1072
|
+
* const inner = applyStyle("world", styleMethodPairs.red);
|
|
1073
|
+
* applyStyle(`hello ${inner}!`, styleMethodPairs.bold);
|
|
1076
1074
|
* ```
|
|
1077
1075
|
*/
|
|
1078
1076
|
function applyStyle(text, style) {
|
|
@@ -1158,8 +1156,6 @@ function rgbToAnsi256(r, g, b) {
|
|
|
1158
1156
|
* Same algorithm as `ansi-styles` / `chalk`: bucket each channel at 50%,
|
|
1159
1157
|
* pack into a 3-bit base color, then add 60 for bright when the max
|
|
1160
1158
|
* channel rounds up. Call sites add `+10` for backgrounds.
|
|
1161
|
-
*
|
|
1162
|
-
* @internal
|
|
1163
1159
|
*/
|
|
1164
1160
|
function rgbToAnsi16Param(r, g, b) {
|
|
1165
1161
|
const brightnessBucket = Math.round(Math.max(r, g, b) / 127.5);
|
|
@@ -1170,8 +1166,6 @@ function rgbToAnsi16Param(r, g, b) {
|
|
|
1170
1166
|
}
|
|
1171
1167
|
/**
|
|
1172
1168
|
* Foreground SGR open sequence at `depth`.
|
|
1173
|
-
*
|
|
1174
|
-
* @internal
|
|
1175
1169
|
*/
|
|
1176
1170
|
function fgOpen(input, depth) {
|
|
1177
1171
|
const [r, g, b] = parseRgb(input);
|
|
@@ -1184,8 +1178,6 @@ function fgOpen(input, depth) {
|
|
|
1184
1178
|
* derived from {@link fgOpen} by swapping the `\x1b[38;` introducer for
|
|
1185
1179
|
* `\x1b[48;` (both the truecolor and 256-color forms use it). For `16`,
|
|
1186
1180
|
* quantized directly to a real background SGR.
|
|
1187
|
-
*
|
|
1188
|
-
* @internal
|
|
1189
1181
|
*/
|
|
1190
1182
|
function bgOpen(input, depth) {
|
|
1191
1183
|
if (depth === "16") {
|
|
@@ -1207,64 +1199,10 @@ function colorPair(kind, input, depth) {
|
|
|
1207
1199
|
close: kind === "fg" ? FG_CLOSE : BG_CLOSE
|
|
1208
1200
|
};
|
|
1209
1201
|
}
|
|
1210
|
-
/**
|
|
1211
|
-
* Depth-aware foreground `AnsiPair` for chain composition. `depth: "none"`
|
|
1212
|
-
* returns an empty pair (still validates input). Used by `createStyle()`
|
|
1213
|
-
* to back `chainable.fg(input)`.
|
|
1214
|
-
*
|
|
1215
|
-
* @throws {TypeError} If `input` is not a recognized color.
|
|
1216
|
-
* @internal
|
|
1217
|
-
*/
|
|
1218
|
-
function fgPairAtDepth(input, depth) {
|
|
1219
|
-
return colorPair("fg", input, depth);
|
|
1220
|
-
}
|
|
1221
|
-
/**
|
|
1222
|
-
* Depth-aware background `AnsiPair` for chain composition. Mirrors
|
|
1223
|
-
* {@link fgPairAtDepth}.
|
|
1224
|
-
*
|
|
1225
|
-
* @throws {TypeError} If `input` is not a recognized color.
|
|
1226
|
-
* @internal
|
|
1227
|
-
*/
|
|
1228
|
-
function bgPairAtDepth(input, depth) {
|
|
1229
|
-
return colorPair("bg", input, depth);
|
|
1230
|
-
}
|
|
1231
1202
|
function paint(kind, text, input, depth) {
|
|
1232
1203
|
const pair = colorPair(kind, input, depth);
|
|
1233
1204
|
return depth === "none" ? text : applyStyle(text, pair);
|
|
1234
1205
|
}
|
|
1235
|
-
/**
|
|
1236
|
-
* Apply a foreground color to `text` from any {@link ColorInput}. `depth`
|
|
1237
|
-
* selects the output format (`"truecolor"` default, `"256"`, `"16"`, or
|
|
1238
|
-
* `"none"`). `"none"` returns `text` unchanged but still validates
|
|
1239
|
-
* `input`. Empty `text` short-circuits to `""`.
|
|
1240
|
-
*
|
|
1241
|
-
* @throws {TypeError} If `input` is not a recognized color.
|
|
1242
|
-
*
|
|
1243
|
-
* @example
|
|
1244
|
-
* ```ts
|
|
1245
|
-
* fg("error", "#ff0000");
|
|
1246
|
-
* fg("ocean", "rgb(0, 128, 255)");
|
|
1247
|
-
* fg("custom", [255, 127, 80]);
|
|
1248
|
-
* fg("256-only", "#ff0000", "256"); // \x1b[38;5;196m...
|
|
1249
|
-
* ```
|
|
1250
|
-
*/
|
|
1251
|
-
function fg$1(text, input, depth = "truecolor") {
|
|
1252
|
-
return paint("fg", text, input, depth);
|
|
1253
|
-
}
|
|
1254
|
-
/**
|
|
1255
|
-
* Apply a background color to `text`. Mirrors {@link fg}.
|
|
1256
|
-
*
|
|
1257
|
-
* @throws {TypeError} If `input` is not a recognized color.
|
|
1258
|
-
*
|
|
1259
|
-
* @example
|
|
1260
|
-
* ```ts
|
|
1261
|
-
* bg("warning", "#ff8800");
|
|
1262
|
-
* bg("info", "rgb(0, 128, 255)");
|
|
1263
|
-
* ```
|
|
1264
|
-
*/
|
|
1265
|
-
function bg$1(text, input, depth = "truecolor") {
|
|
1266
|
-
return paint("bg", text, input, depth);
|
|
1267
|
-
}
|
|
1268
1206
|
const OSC = "\x1B]";
|
|
1269
1207
|
const ST = "\x1B\\";
|
|
1270
1208
|
const HYPERLINK_CLOSE = `${OSC}8;;${ST}`;
|
|
@@ -1321,21 +1259,19 @@ function linkCode(url, options) {
|
|
|
1321
1259
|
* ```ts
|
|
1322
1260
|
* import { link } from "@crustjs/style";
|
|
1323
1261
|
*
|
|
1324
|
-
* console.log(link("docs", "https://crustjs.
|
|
1262
|
+
* console.log(link("docs", "https://crustjs.com"));
|
|
1325
1263
|
* console.log(link("page 1", "https://example.com/p1", { id: "intro" }));
|
|
1326
1264
|
* ```
|
|
1327
1265
|
*/
|
|
1328
1266
|
function link$1(text, url, options) {
|
|
1329
1267
|
return applyStyle(text, linkCode(url, options));
|
|
1330
1268
|
}
|
|
1331
|
-
const dynamicColorKinds = [
|
|
1332
|
-
const styleMethodPairs = ansiCodes_exports;
|
|
1269
|
+
const dynamicColorKinds = ["fg", "bg"];
|
|
1333
1270
|
function stepIsModifier(step) {
|
|
1334
1271
|
return step.kind === "named" && isModifierName(step.name);
|
|
1335
1272
|
}
|
|
1336
1273
|
function stepPair(step, colorDepth) {
|
|
1337
|
-
|
|
1338
|
-
return step.kind === "fg" ? fgPairAtDepth(step.input, colorDepth) : bgPairAtDepth(step.input, colorDepth);
|
|
1274
|
+
return step.kind === "named" ? styleMethodPairs[step.name] : colorPair(step.kind, step.input, colorDepth);
|
|
1339
1275
|
}
|
|
1340
1276
|
function colorStep(kind, input) {
|
|
1341
1277
|
const step = {
|
|
@@ -1399,7 +1335,7 @@ function buildChainableStyleFactory(resolveCapabilities, runtime) {
|
|
|
1399
1335
|
}]);
|
|
1400
1336
|
}
|
|
1401
1337
|
});
|
|
1402
|
-
for (const
|
|
1338
|
+
for (const kind of dynamicColorKinds) Object.defineProperty(styleFn, kind, {
|
|
1403
1339
|
configurable: false,
|
|
1404
1340
|
enumerable: true,
|
|
1405
1341
|
value: (input) => createChainableStyle([...steps, colorStep(kind, input)]),
|
|
@@ -1436,41 +1372,6 @@ function buildStyleMethods(createChainableStyle) {
|
|
|
1436
1372
|
}]);
|
|
1437
1373
|
return methods;
|
|
1438
1374
|
}
|
|
1439
|
-
/**
|
|
1440
|
-
* Create a configured style instance with mode-aware styling functions.
|
|
1441
|
-
*
|
|
1442
|
-
* The returned instance provides the full set of modifier, foreground color,
|
|
1443
|
-
* and background color functions. In `"never"` mode, all functions return
|
|
1444
|
-
* plain text without ANSI codes. In `"always"` mode, ANSI codes are always
|
|
1445
|
-
* emitted. In `"auto"` mode, color methods respect `stdout.isTTY` and
|
|
1446
|
-
* `NO_COLOR`, non-color modifiers (bold, italic, etc.) follow TTY only,
|
|
1447
|
-
* and `FORCE_COLOR`, when set, decides unconditionally for both.
|
|
1448
|
-
*
|
|
1449
|
-
* @param options - Configuration options. Defaults to `{ mode: "auto" }`.
|
|
1450
|
-
* @returns A frozen {@link StyleInstance} with all styling functions.
|
|
1451
|
-
*
|
|
1452
|
-
* @example
|
|
1453
|
-
* ```ts
|
|
1454
|
-
* // Auto-detect terminal capabilities
|
|
1455
|
-
* const s = createStyle();
|
|
1456
|
-
* console.log(s.bold("hello"));
|
|
1457
|
-
*
|
|
1458
|
-
* // Force color output
|
|
1459
|
-
* const color = createStyle({ mode: "always" });
|
|
1460
|
-
* console.log(color.red("error"));
|
|
1461
|
-
* console.log(color.bold.red("critical"));
|
|
1462
|
-
*
|
|
1463
|
-
* // Disable all styling
|
|
1464
|
-
* const plain = createStyle({ mode: "never" });
|
|
1465
|
-
* console.log(plain.red("error")); // "error"
|
|
1466
|
-
*
|
|
1467
|
-
* // Deterministic testing
|
|
1468
|
-
* const test = createStyle({
|
|
1469
|
-
* mode: "auto",
|
|
1470
|
-
* overrides: { isTTY: true, noColor: undefined },
|
|
1471
|
-
* });
|
|
1472
|
-
* ```
|
|
1473
|
-
*/
|
|
1474
1375
|
function resolveStyleCapabilities(options) {
|
|
1475
1376
|
const mode = options?.mode ?? "auto";
|
|
1476
1377
|
const modifiersEnabled = resolveModifierCapability(mode, options?.overrides);
|
|
@@ -1508,10 +1409,9 @@ function createStyleInstance(options, runtime) {
|
|
|
1508
1409
|
linkCode(url, hyperlinkOptions);
|
|
1509
1410
|
return text;
|
|
1510
1411
|
},
|
|
1511
|
-
...Object.fromEntries(dynamicColorKinds.map((
|
|
1512
|
-
const resolved = resolveCapabilities();
|
|
1412
|
+
...Object.fromEntries(dynamicColorKinds.map((kind) => [kind, (...args) => {
|
|
1513
1413
|
if (args.length === 1) return createChainableStyle([colorStep(kind, args[0])]);
|
|
1514
|
-
return paint(args[0], args[1],
|
|
1414
|
+
return paint(kind, args[0], args[1], resolveCapabilities().colorDepth);
|
|
1515
1415
|
}])),
|
|
1516
1416
|
...methods
|
|
1517
1417
|
};
|
|
@@ -1596,96 +1496,100 @@ function isWithin(parent, child) {
|
|
|
1596
1496
|
return path === "" || !isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`);
|
|
1597
1497
|
}
|
|
1598
1498
|
//#endregion
|
|
1599
|
-
//#region
|
|
1499
|
+
//#region src/artifacts.ts
|
|
1600
1500
|
/**
|
|
1601
|
-
*
|
|
1602
|
-
*
|
|
1603
|
-
* to run the hooks; `resolveArtifactDir` reads it so sections evaluated during
|
|
1604
|
-
* that run see the artifacts being built instead of the wiped `.crust/root`.
|
|
1501
|
+
* `crust.include` directories normalized to cwd-relative POSIX names. They are
|
|
1502
|
+
* staged exactly like Extension artifacts.
|
|
1605
1503
|
*/
|
|
1606
|
-
|
|
1504
|
+
function collectIncludeDirs(cwd, stageDir, include, artifactNames) {
|
|
1505
|
+
const names = [...artifactNames];
|
|
1506
|
+
const includeDirs = [];
|
|
1507
|
+
for (const entry of include) {
|
|
1508
|
+
const dir = resolve(cwd, entry);
|
|
1509
|
+
const name = relative(cwd, dir);
|
|
1510
|
+
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}.`);
|
|
1511
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) throw new Error(`package.json crust.include entry ${JSON.stringify(entry)} is not a directory: ${dir}`);
|
|
1512
|
+
assertResolvesInsideProject(cwd, entry, dir);
|
|
1513
|
+
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.`);
|
|
1514
|
+
if (name.toLowerCase().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.`);
|
|
1515
|
+
const posixName = name.replaceAll(sep, "/");
|
|
1516
|
+
const key = posixName.toLowerCase();
|
|
1517
|
+
const overlap = names.find((staged) => {
|
|
1518
|
+
const stagedKey = staged.toLowerCase();
|
|
1519
|
+
return stagedKey === key || stagedKey.startsWith(`${key}/`) || key.startsWith(`${stagedKey}/`);
|
|
1520
|
+
});
|
|
1521
|
+
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).`);
|
|
1522
|
+
names.push(posixName);
|
|
1523
|
+
includeDirs.push(posixName);
|
|
1524
|
+
}
|
|
1525
|
+
return includeDirs;
|
|
1526
|
+
}
|
|
1607
1527
|
/**
|
|
1608
|
-
* `
|
|
1609
|
-
*
|
|
1610
|
-
* `globalThis[Symbol.for(PACKAGED_BUILD_KEY)] = true` to the command bundle,
|
|
1611
|
-
* before the inlined application graph, so every bundled copy sees it.
|
|
1612
|
-
* The registry key is shared across module copies; the value is never read from
|
|
1613
|
-
* the environment, so detection needs no Deno env permission.
|
|
1528
|
+
* Walks `dir` the way the dereferencing copy will (through symlinked
|
|
1529
|
+
* directories) and rejects any path whose real location leaves the project.
|
|
1614
1530
|
*/
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1531
|
+
function assertResolvesInsideProject(cwd, entry, dir) {
|
|
1532
|
+
const realCwd = realpathSync(cwd);
|
|
1533
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1534
|
+
const walk = (path) => {
|
|
1535
|
+
const real = realpathSync(path);
|
|
1536
|
+
if (!isWithin(realCwd, real)) throw new Error(`package.json crust.include entry ${JSON.stringify(entry)} resolves outside the project root: ${relative(cwd, path)} -> ${real}`);
|
|
1537
|
+
if (seen.has(real) || !statSync(path).isDirectory()) return;
|
|
1538
|
+
seen.add(real);
|
|
1539
|
+
for (const child of readdirSync(path)) walk(join(path, child));
|
|
1540
|
+
};
|
|
1541
|
+
walk(dir);
|
|
1542
|
+
}
|
|
1618
1543
|
/**
|
|
1619
|
-
*
|
|
1620
|
-
*
|
|
1621
|
-
*
|
|
1622
|
-
*
|
|
1623
|
-
*
|
|
1624
|
-
*
|
|
1625
|
-
* ```ts
|
|
1626
|
-
* import { CrustError } from "@crustjs/core";
|
|
1627
|
-
*
|
|
1628
|
-
* const outcome = await app.run(["deploy"], { args: { target: "prod" } });
|
|
1629
|
-
* if (outcome.status === "failed") {
|
|
1630
|
-
* const err = outcome.error;
|
|
1631
|
-
* if (err instanceof CrustError) {
|
|
1632
|
-
* console.error(`[${err.code}] ${err.message}`);
|
|
1633
|
-
* }
|
|
1634
|
-
* }
|
|
1635
|
-
* ```
|
|
1544
|
+
* Copies one entry's Extension build hook output into the shared artifact
|
|
1545
|
+
* directory. Identically spelled directories merge; case-only directory aliases
|
|
1546
|
+
* and a file or file/directory mismatch at a path
|
|
1547
|
+
* another entry already produced is an error, so no entry's hooks can replace
|
|
1548
|
+
* another's output. `owners` tracks case-folded POSIX-relative paths across
|
|
1549
|
+
* entries, including directories so file/ancestor conflicts are portable.
|
|
1636
1550
|
*/
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
};
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
const SNAPSHOT_PATH_ENV = "CRUST_INTERNAL_SNAPSHOT_PATH";
|
|
1680
|
-
//#endregion
|
|
1681
|
-
//#region ../core/dist/index.js
|
|
1682
|
-
/** Mint an Extension identity from any non-blank, trimmed string. */
|
|
1683
|
-
function defineExtensionId(id) {
|
|
1684
|
-
if (!id.trim() || id !== id.trim()) throw new CrustError("DEFINITION", "Extension id must be a non-empty, trimmed string", {
|
|
1685
|
-
subject: "extension",
|
|
1686
|
-
reason: "empty-id"
|
|
1687
|
-
});
|
|
1688
|
-
return id;
|
|
1551
|
+
function mergeEntryArtifacts(entryOutDir, artifactDir, command, owners) {
|
|
1552
|
+
const merge = (relativeDir) => {
|
|
1553
|
+
for (const dirent of readdirSync(join(entryOutDir, relativeDir), { withFileTypes: true })) {
|
|
1554
|
+
const relativePath = relativeDir ? `${relativeDir}/${dirent.name}` : dirent.name;
|
|
1555
|
+
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.`);
|
|
1556
|
+
const source = join(entryOutDir, relativePath);
|
|
1557
|
+
const destination = join(artifactDir, relativePath);
|
|
1558
|
+
const key = relativePath.toLowerCase();
|
|
1559
|
+
const owner = owners.get(key);
|
|
1560
|
+
const existing = lstatSync(destination, { throwIfNoEntry: false });
|
|
1561
|
+
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}.`);
|
|
1562
|
+
if (!owner) owners.set(key, {
|
|
1563
|
+
command,
|
|
1564
|
+
directory: dirent.isDirectory(),
|
|
1565
|
+
path: relativePath
|
|
1566
|
+
});
|
|
1567
|
+
if (dirent.isDirectory()) {
|
|
1568
|
+
mkdirSync(destination, { recursive: true });
|
|
1569
|
+
merge(relativePath);
|
|
1570
|
+
} else {
|
|
1571
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
1572
|
+
copyFileSync(source, destination);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
const root = lstatSync(entryOutDir, { throwIfNoEntry: false });
|
|
1577
|
+
if (root === void 0) return;
|
|
1578
|
+
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}`);
|
|
1579
|
+
merge("");
|
|
1580
|
+
}
|
|
1581
|
+
function collectArtifacts(artifactOutDir) {
|
|
1582
|
+
if (!artifactOutDir || !existsSync(artifactOutDir)) return {
|
|
1583
|
+
names: [],
|
|
1584
|
+
manPages: []
|
|
1585
|
+
};
|
|
1586
|
+
const names = readdirSync(artifactOutDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
1587
|
+
const binName = names.find((name) => name.toLowerCase() === "bin");
|
|
1588
|
+
if (binName !== void 0) throw new Error(`Artifact directory "${binName}" in ${artifactOutDir} conflicts with the generated npm bin directory.\n Emit build artifacts under a different top-level name.`);
|
|
1589
|
+
return {
|
|
1590
|
+
names,
|
|
1591
|
+
manPages: names.includes("man") ? readdirSync(join(artifactOutDir, "man"), { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => entry.name).sort() : []
|
|
1592
|
+
};
|
|
1689
1593
|
}
|
|
1690
1594
|
//#endregion
|
|
1691
1595
|
//#region ../utils/dist/process.js
|
|
@@ -1742,7 +1646,7 @@ function killProcessTree(child) {
|
|
|
1742
1646
|
try {
|
|
1743
1647
|
process.kill(-child.pid, "SIGKILL");
|
|
1744
1648
|
} catch (error) {
|
|
1745
|
-
if (error.code === "ESRCH") return;
|
|
1649
|
+
if (isErrnoException(error) && error.code === "ESRCH") return;
|
|
1746
1650
|
child.kill("SIGKILL");
|
|
1747
1651
|
throw error;
|
|
1748
1652
|
}
|
|
@@ -1802,22 +1706,21 @@ async function runProcess(command, args = [], options = {}) {
|
|
|
1802
1706
|
clearTimeout(timer);
|
|
1803
1707
|
}
|
|
1804
1708
|
}
|
|
1709
|
+
function isExecutableFile(path) {
|
|
1710
|
+
try {
|
|
1711
|
+
accessSync(path, constants.X_OK);
|
|
1712
|
+
return statSync(path).isFile();
|
|
1713
|
+
} catch {
|
|
1714
|
+
return false;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1805
1717
|
/** Resolve a bare executable name from PATH (and PATHEXT on Windows). */
|
|
1806
1718
|
function which(command) {
|
|
1807
|
-
if (command.includes(sep) || command.includes("/"))
|
|
1808
|
-
try {
|
|
1809
|
-
accessSync(command, constants.X_OK);
|
|
1810
|
-
if (statSync(command).isFile()) return command;
|
|
1811
|
-
} catch {}
|
|
1812
|
-
return null;
|
|
1813
|
-
}
|
|
1719
|
+
if (command.includes(sep) || command.includes("/")) return isExecutableFile(command) ? command : null;
|
|
1814
1720
|
const extensions = process.platform === "win32" && !extname(command) ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
|
|
1815
1721
|
for (const directory of process.env.PATH?.split(delimiter) ?? []) for (const extension of extensions) {
|
|
1816
1722
|
const candidate = resolve(join(directory, command + extension));
|
|
1817
|
-
|
|
1818
|
-
accessSync(candidate, constants.X_OK);
|
|
1819
|
-
if (statSync(candidate).isFile()) return candidate;
|
|
1820
|
-
} catch {}
|
|
1723
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
1821
1724
|
}
|
|
1822
1725
|
return null;
|
|
1823
1726
|
}
|
|
@@ -2668,16 +2571,10 @@ var require_valid = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
2668
2571
|
module.exports = validRange;
|
|
2669
2572
|
}));
|
|
2670
2573
|
//#endregion
|
|
2671
|
-
//#region
|
|
2574
|
+
//#region src/targets.ts
|
|
2672
2575
|
var import_satisfies = /* @__PURE__ */ __toESM(require_satisfies(), 1);
|
|
2673
2576
|
var import_valid = /* @__PURE__ */ __toESM(require_valid$1(), 1);
|
|
2674
2577
|
var import_valid$1 = /* @__PURE__ */ __toESM(require_valid(), 1);
|
|
2675
|
-
var optionalDependencies = {
|
|
2676
|
-
"@tsdown/exe": "0.23.0",
|
|
2677
|
-
"tsdown": "0.23.0"
|
|
2678
|
-
};
|
|
2679
|
-
//#endregion
|
|
2680
|
-
//#region src/utils/build-helpers.ts
|
|
2681
2578
|
const BUILD_RUNTIMES = [
|
|
2682
2579
|
"bun",
|
|
2683
2580
|
"deno",
|
|
@@ -2889,17 +2786,11 @@ function hostTarget(table) {
|
|
|
2889
2786
|
const platformKey = hostPlatformKey();
|
|
2890
2787
|
return table.targets.find((target) => table.info[target].platformKey === platformKey) ?? null;
|
|
2891
2788
|
}
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
} catch (error) {
|
|
2898
|
-
throw new Error(`Failed to parse package.json in ${cwd}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
2899
|
-
}
|
|
2900
|
-
}
|
|
2901
|
-
function toBunEnvFileArgs(envFiles) {
|
|
2902
|
-
return envFiles.flatMap((envFile) => ["--env-file", envFile]);
|
|
2789
|
+
//#endregion
|
|
2790
|
+
//#region src/compilers.ts
|
|
2791
|
+
/** A finished compiler process's stderr, then stdout, for error messages. */
|
|
2792
|
+
function processOutput({ stdout, stderr }) {
|
|
2793
|
+
return [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
2903
2794
|
}
|
|
2904
2795
|
/**
|
|
2905
2796
|
* Resolve the safest executable to run `bun build`.
|
|
@@ -2949,13 +2840,11 @@ function bunCompileTarget(target, runner, host = hostTarget(BUN_TARGETS)) {
|
|
|
2949
2840
|
return bunBaselineAlias(target) ?? target;
|
|
2950
2841
|
}
|
|
2951
2842
|
/**
|
|
2952
|
-
* Refuse the host target when
|
|
2953
|
-
*
|
|
2954
|
-
* Pass the selected `runner` to judge that compiler instead of looking up
|
|
2955
|
-
* `bun` on PATH again.
|
|
2843
|
+
* Refuse the host target when the selected `runner` is the `BUN_BE_BUN`
|
|
2844
|
+
* fallback and no `-baseline` alias can stand in for it (arm64 hosts).
|
|
2956
2845
|
*/
|
|
2957
|
-
function assertTargetsBuildableWithoutBun(targets, host = hostTarget(BUN_TARGETS)
|
|
2958
|
-
const externalBun = runner
|
|
2846
|
+
function assertTargetsBuildableWithoutBun(targets, runner, host = hostTarget(BUN_TARGETS)) {
|
|
2847
|
+
const externalBun = runner.env.BUN_BE_BUN !== "1";
|
|
2959
2848
|
if (host === null || !targets.includes(host) || bunBaselineAlias(host) !== null || externalBun) return;
|
|
2960
2849
|
const others = targets.filter((target) => target !== host);
|
|
2961
2850
|
const alternative = others.length > 0 ? `pass --target with the other targets (e.g. ${others.map((target) => `--target ${target}`).join(" ")})` : "build a different target";
|
|
@@ -2996,17 +2885,18 @@ const COMPILER_VERSION_TIMEOUT_MS = 3e4;
|
|
|
2996
2885
|
* fails the build, before `.crust/` is replaced.
|
|
2997
2886
|
*/
|
|
2998
2887
|
async function readCompilerVersion(runtime, runner, cwd) {
|
|
2999
|
-
const
|
|
2888
|
+
const result = await runProcess(runner.command, ["--version"], {
|
|
3000
2889
|
env: runner.env,
|
|
3001
2890
|
cwd,
|
|
3002
2891
|
stdio: "collect",
|
|
3003
2892
|
timeout: COMPILER_VERSION_TIMEOUT_MS
|
|
3004
2893
|
});
|
|
3005
|
-
const
|
|
3006
|
-
const
|
|
2894
|
+
const stdout = result.stdout.trim();
|
|
2895
|
+
const reported = runtime === "deno" ? /^deno (\S+)/.exec(stdout)?.[1] : stdout;
|
|
2896
|
+
const version = result.exitCode === 0 && reported !== void 0 ? (0, import_valid.default)(reported) : null;
|
|
3007
2897
|
if (version === null) {
|
|
3008
|
-
const output =
|
|
3009
|
-
throw new Error(`Could not read the ${RUNTIME_LABELS[runtime]} version from ${runner.command} --version (exit ${exitCode})${output ? `:\n${output}` : "."}`);
|
|
2898
|
+
const output = processOutput(result);
|
|
2899
|
+
throw new Error(`Could not read the ${RUNTIME_LABELS[runtime]} version from ${runner.command} --version (exit ${result.exitCode})${output ? `:\n${output}` : "."}`);
|
|
3010
2900
|
}
|
|
3011
2901
|
return version;
|
|
3012
2902
|
}
|
|
@@ -3019,7 +2909,6 @@ function isVersionRange(value) {
|
|
|
3019
2909
|
* value is a requirement, not a request to install that version.
|
|
3020
2910
|
*/
|
|
3021
2911
|
function assertCompilerSatisfiesEngines(compiler, userPackageJson) {
|
|
3022
|
-
if (userPackageJson === void 0 || !isJsonObject(userPackageJson)) return;
|
|
3023
2912
|
const { engines } = userPackageJson;
|
|
3024
2913
|
if (engines === void 0) return;
|
|
3025
2914
|
if (!isJsonObject(engines)) throw new Error("package.json engines must be an object of runtime version ranges.");
|
|
@@ -3050,135 +2939,33 @@ async function resolveBinaryCompiler(runtime, userPackageJson, cwd) {
|
|
|
3050
2939
|
return compiler;
|
|
3051
2940
|
}
|
|
3052
2941
|
/**
|
|
3053
|
-
*
|
|
3054
|
-
*
|
|
3055
|
-
*
|
|
3056
|
-
*
|
|
3057
|
-
*/
|
|
3058
|
-
const NODE_EXE_BACKEND_PACKAGES = ["tsdown", "@tsdown/exe"];
|
|
3059
|
-
/**
|
|
3060
|
-
* A file of Crust's own installation that the backend is resolved from, never
|
|
3061
|
-
* the project or PATH. A compiled crust serves its modules from Bun's virtual
|
|
3062
|
-
* filesystem (`/$bunfs/`, `B:/~BUN/` on Windows), so there it is the
|
|
3063
|
-
* executable, inside its platform package.
|
|
3064
|
-
*/
|
|
3065
|
-
function crustInstallPath(moduleUrl = import.meta.url, execPath = process.execPath) {
|
|
3066
|
-
return /^file:\/\/\/(?:\$bunfs|[A-Za-z]:\/~BUN)\//.test(moduleUrl) ? execPath : fileURLToPath(moduleUrl);
|
|
3067
|
-
}
|
|
3068
|
-
/**
|
|
3069
|
-
* `<dir>/node_modules/<name>/package.json` for the nearest `dir` at or above
|
|
3070
|
-
* `from`: Node's node_modules lookup without its NODE_PATH and global-folder
|
|
3071
|
-
* fallbacks, so only an installation next to `from` can supply the package.
|
|
3072
|
-
* Package managers hoist crust's backend into the project's node_modules, so
|
|
3073
|
-
* the found package may be the project's own: callers check its version.
|
|
3074
|
-
*/
|
|
3075
|
-
function findInstalledPackageJson(from, name) {
|
|
3076
|
-
for (let dir = from;; dir = dirname(dir)) {
|
|
3077
|
-
const candidate = join(dir, "node_modules", name, "package.json");
|
|
3078
|
-
if (existsSync(candidate)) return candidate;
|
|
3079
|
-
if (dirname(dir) === dir) return null;
|
|
3080
|
-
}
|
|
3081
|
-
}
|
|
3082
|
-
function isVersionedPackageJson(value) {
|
|
3083
|
-
return isJsonObject(value) && typeof value.version === "string";
|
|
3084
|
-
}
|
|
3085
|
-
function isTsdownPackageJson(value) {
|
|
3086
|
-
return isVersionedPackageJson(value) && value.engines !== void 0 && isJsonObject(value.engines) && value.engines.node !== void 0 && isVersionRange(value.engines.node);
|
|
3087
|
-
}
|
|
3088
|
-
function createNodeExeProbeScript(tsdownPackageJson) {
|
|
3089
|
-
return `// Generated by crust build to read tsdown's executable minimum; deleted when it exits.
|
|
3090
|
-
import { createRequire } from "node:module";
|
|
3091
|
-
import { pathToFileURL } from "node:url";
|
|
3092
|
-
|
|
3093
|
-
const internal = createRequire(${JSON.stringify(tsdownPackageJson)}).resolve("tsdown/internal");
|
|
3094
|
-
console.log((await import(pathToFileURL(internal).href)).NODE_SEA_MIN_VERSION);
|
|
3095
|
-
`;
|
|
3096
|
-
}
|
|
3097
|
-
const NODE_TOOLCHAIN_GUIDANCE = " Binaries embed the selected node's version; crust does not install or upgrade it.\n Put a supported node first on PATH (e.g. with your version manager).";
|
|
3098
|
-
/**
|
|
3099
|
-
* Checks the selected Node against the installed backend's two separate
|
|
3100
|
-
* requirements: tsdown's package `engines.node` and its executable builder's
|
|
3101
|
-
* minimum (skipped while unknown). Both come from the installed tsdown, not
|
|
3102
|
-
* from this file.
|
|
3103
|
-
*/
|
|
3104
|
-
function assertNodeExeBackendSupports(compiler, backend) {
|
|
3105
|
-
const selected = `Node ${compiler.version} (${compiler.runner.command})`;
|
|
3106
|
-
if (!(0, import_satisfies.default)(compiler.version, backend.engines)) throw new Error(`${selected} is not supported by tsdown ${backend.version}, which builds node standalone binaries: its package requires Node "${backend.engines}".\n${NODE_TOOLCHAIN_GUIDANCE}`);
|
|
3107
|
-
if (backend.seaMinVersion !== void 0 && !(0, import_satisfies.default)(compiler.version, `>=${backend.seaMinVersion}`)) throw new Error(`${selected} cannot build standalone executables: tsdown ${backend.version}'s executable builder requires Node ${backend.seaMinVersion} or later.\n${NODE_TOOLCHAIN_GUIDANCE}`);
|
|
3108
|
-
}
|
|
3109
|
-
/**
|
|
3110
|
-
* Finds Crust's installed tsdown and @tsdown/exe from `installPath`, at the
|
|
3111
|
-
* versions its package.json `optionalDependencies` pin, and validates the
|
|
3112
|
-
* selected Node against tsdown's requirements, loading the executable
|
|
3113
|
-
* builder's minimum under that Node. `cwd` is the project
|
|
3114
|
-
* directory, as for every other compiler step.
|
|
2942
|
+
* Oldest `deno` accepted to bundle a Deno runtime package. Deno 2.4 resolves a
|
|
2943
|
+
* package.json project's npm dependencies from the registry instead of its
|
|
2944
|
+
* installed node_modules, so it can bundle a different Crust than the project
|
|
2945
|
+
* installed. Tested on Linux x64 with Deno 2.5.0 through 2.9.6.
|
|
3115
2946
|
*/
|
|
3116
|
-
|
|
3117
|
-
const reinstall = ` @crustjs/crust ships ${NODE_EXE_BACKEND_PACKAGES.join(" and ")} as optional dependencies, which package managers skip when optional dependencies are disabled or the installing node does not satisfy tsdown's engines.\n Reinstall @crustjs/crust with optional dependencies enabled under a node that can build Node binaries (e.g. the one on PATH here).`;
|
|
3118
|
-
const packageJsonPath = findInstalledPackageJson(dirname(installPath), "tsdown");
|
|
3119
|
-
if (packageJsonPath === null) throw new Error(`tsdown, which builds node standalone binaries, is not installed with crust (${installPath}).\n${reinstall}`);
|
|
3120
|
-
const exePackageJsonPath = findInstalledPackageJson(dirname(realpathSync(packageJsonPath)), "@tsdown/exe");
|
|
3121
|
-
if (exePackageJsonPath === null) throw new Error(`@tsdown/exe is not installed beside crust's tsdown (${packageJsonPath}).\n${reinstall}`);
|
|
3122
|
-
for (const [name, path] of [["tsdown", packageJsonPath], ["@tsdown/exe", exePackageJsonPath]]) {
|
|
3123
|
-
const installed = JSON.parse(readFileSync(path, "utf8"));
|
|
3124
|
-
const version = isVersionedPackageJson(installed) ? installed.version : "without a version";
|
|
3125
|
-
const pinned = optionalDependencies[name];
|
|
3126
|
-
if (!(0, import_satisfies.default)(version, pinned)) throw new Error(`${name} ${version} (${path}) is not the ${name} ${pinned} that crust builds node standalone binaries with.\n${reinstall}`);
|
|
3127
|
-
}
|
|
3128
|
-
const tsdownPackage = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
3129
|
-
if (!isTsdownPackageJson(tsdownPackage)) throw new Error(`Unexpected tsdown package metadata in ${packageJsonPath}.\n${reinstall}`);
|
|
3130
|
-
const backend = {
|
|
3131
|
-
packageJsonPath,
|
|
3132
|
-
version: tsdownPackage.version,
|
|
3133
|
-
engines: tsdownPackage.engines.node
|
|
3134
|
-
};
|
|
3135
|
-
assertNodeExeBackendSupports(compiler, backend);
|
|
3136
|
-
const workDir = await mkdtemp(join(tmpdir(), "crust-node-exe-probe-"));
|
|
3137
|
-
try {
|
|
3138
|
-
const scriptPath = join(workDir, "probe.mjs");
|
|
3139
|
-
await writeFile(scriptPath, createNodeExeProbeScript(packageJsonPath));
|
|
3140
|
-
const { exitCode, stdout, stderr } = await runProcess(compiler.runner.command, [scriptPath], {
|
|
3141
|
-
env: compiler.runner.env,
|
|
3142
|
-
cwd,
|
|
3143
|
-
stdio: "collect"
|
|
3144
|
-
});
|
|
3145
|
-
const seaMinVersion = exitCode === 0 ? (0, import_valid.default)(stdout.trim()) : null;
|
|
3146
|
-
if (seaMinVersion === null) {
|
|
3147
|
-
const output = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
3148
|
-
throw new Error(`tsdown ${backend.version}'s executable builder could not be loaded with ${compiler.runner.command} (exit ${exitCode}).\n${reinstall}${output ? `\n${output}` : ""}`);
|
|
3149
|
-
}
|
|
3150
|
-
const resolved = {
|
|
3151
|
-
...backend,
|
|
3152
|
-
seaMinVersion
|
|
3153
|
-
};
|
|
3154
|
-
assertNodeExeBackendSupports(compiler, resolved);
|
|
3155
|
-
return resolved;
|
|
3156
|
-
} finally {
|
|
3157
|
-
await rm(workDir, {
|
|
3158
|
-
recursive: true,
|
|
3159
|
-
force: true
|
|
3160
|
-
});
|
|
3161
|
-
}
|
|
3162
|
-
}
|
|
2947
|
+
const DENO_PACKAGE_MIN_VERSION = "2.5.0";
|
|
3163
2948
|
/**
|
|
3164
|
-
* Select the
|
|
3165
|
-
* version
|
|
3166
|
-
*
|
|
3167
|
-
*
|
|
2949
|
+
* Select the external deno that bundles a Deno runtime package, reading its
|
|
2950
|
+
* version in the project directory `cwd`. Call it before staging is replaced.
|
|
2951
|
+
* A runtime package embeds no Deno, so `engines.deno` stays a consumer
|
|
2952
|
+
* requirement and is not checked against this bundler.
|
|
3168
2953
|
*/
|
|
3169
|
-
async function
|
|
3170
|
-
const
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
runner,
|
|
3174
|
-
version: await readCompilerVersion("node", runner, cwd)
|
|
3175
|
-
};
|
|
3176
|
-
assertCompilerSatisfiesEngines(compiler, userPackageJson);
|
|
2954
|
+
async function resolveDenoPackageBundler(cwd, runner = resolveDenoBuildRunner()) {
|
|
2955
|
+
const version = await readCompilerVersion("deno", runner, cwd);
|
|
2956
|
+
if (!(0, import_satisfies.default)(version, `>=2.5.0`, { includePrerelease: true })) throw new Error(`Deno ${version} (${runner.command}) cannot bundle a Deno runtime package; deno ${DENO_PACKAGE_MIN_VERSION} or newer is required.\n Older deno bundle resolves package.json dependencies from the npm registry instead of the installed node_modules.
|
|
2957
|
+
crust does not install or upgrade Deno; put a newer deno first on PATH (e.g. with your version manager).`);
|
|
3177
2958
|
return {
|
|
3178
|
-
|
|
3179
|
-
|
|
2959
|
+
runtime: "deno",
|
|
2960
|
+
runner,
|
|
2961
|
+
version
|
|
3180
2962
|
};
|
|
3181
2963
|
}
|
|
2964
|
+
//#endregion
|
|
2965
|
+
//#region src/bundle.ts
|
|
2966
|
+
function toBunEnvFileArgs(envFiles) {
|
|
2967
|
+
return envFiles.flatMap((envFile) => ["--env-file", envFile]);
|
|
2968
|
+
}
|
|
3182
2969
|
/**
|
|
3183
2970
|
* Module source the generated driver imports for a `crust.bunPlugins` specifier.
|
|
3184
2971
|
*
|
|
@@ -3238,7 +3025,7 @@ if (!options.build.compile) {
|
|
|
3238
3025
|
}
|
|
3239
3026
|
`;
|
|
3240
3027
|
}
|
|
3241
|
-
async function runBunPluginDriver(build, outfilePath,
|
|
3028
|
+
async function runBunPluginDriver(build, outfilePath, { cwd, envFiles, bunPlugins }, runner) {
|
|
3242
3029
|
const driverPath = join(cwd, `.crust-build-${randomBytes(6).toString("hex")}.ts`);
|
|
3243
3030
|
await writeFile(driverPath, createBunPluginDriverScript({
|
|
3244
3031
|
plugins: bunPlugins.map((specifier) => ({
|
|
@@ -3264,20 +3051,16 @@ async function runBunPluginDriver(build, outfilePath, bunPlugins, envFiles, cwd,
|
|
|
3264
3051
|
*
|
|
3265
3052
|
* @param entryPath - Absolute path to the entry file
|
|
3266
3053
|
* @param outfilePath - Absolute path to the output binary
|
|
3267
|
-
* @param minify - Whether to enable minification
|
|
3268
3054
|
* @param target - Bun compile target
|
|
3269
|
-
* @param envFiles - Optional env files to load during build
|
|
3270
|
-
* @param bunPlugins - Bun bundler plugin specifiers; when present the build
|
|
3271
|
-
* runs through the generated `Bun.build` driver instead of `bun build`
|
|
3272
3055
|
* @param runner - The selected Bun compiler (`resolveBinaryCompiler`)
|
|
3273
3056
|
* @throws {Error} If the build fails
|
|
3274
3057
|
*/
|
|
3275
|
-
async function execBuild(entryPath, outfilePath,
|
|
3058
|
+
async function execBuild(entryPath, outfilePath, target, settings, runner = resolveBunBuildRunner()) {
|
|
3276
3059
|
const compileTarget = bunCompileTarget(target, runner);
|
|
3277
|
-
if (bunPlugins.length > 0) {
|
|
3060
|
+
if (settings.bunPlugins.length > 0) {
|
|
3278
3061
|
await runBunPluginDriver({
|
|
3279
3062
|
entrypoints: [entryPath],
|
|
3280
|
-
minify,
|
|
3063
|
+
minify: settings.minify,
|
|
3281
3064
|
env: "PUBLIC_*",
|
|
3282
3065
|
target: "bun",
|
|
3283
3066
|
compile: {
|
|
@@ -3285,10 +3068,10 @@ async function execBuild(entryPath, outfilePath, minify, target, envFiles, cwd,
|
|
|
3285
3068
|
outfile: outfilePath,
|
|
3286
3069
|
autoloadBunfig: false
|
|
3287
3070
|
}
|
|
3288
|
-
}, outfilePath,
|
|
3071
|
+
}, outfilePath, settings, runner);
|
|
3289
3072
|
return;
|
|
3290
3073
|
}
|
|
3291
|
-
await runBuildProcess(runner, createBunCompileArgs(entryPath, outfilePath, minify, compileTarget, envFiles), outfilePath, cwd);
|
|
3074
|
+
await runBuildProcess(runner, createBunCompileArgs(entryPath, outfilePath, settings.minify, compileTarget, settings.envFiles), outfilePath, settings.cwd);
|
|
3292
3075
|
}
|
|
3293
3076
|
function createBunCompileArgs(entryPath, outfilePath, minify, target, envFiles = []) {
|
|
3294
3077
|
return [
|
|
@@ -3318,36 +3101,31 @@ function createDenoCompileArgs(entryPath, outfilePath, target) {
|
|
|
3318
3101
|
entryPath
|
|
3319
3102
|
];
|
|
3320
3103
|
}
|
|
3104
|
+
/** Runs one compiler step; returns its output, which a successful step may still print (warnings). */
|
|
3321
3105
|
async function runBuildProcess(runner, args, outfilePath, cwd) {
|
|
3322
|
-
const
|
|
3106
|
+
const result = await runProcess(runner.command, args, {
|
|
3323
3107
|
env: runner.env,
|
|
3324
3108
|
cwd,
|
|
3325
3109
|
stdio: "collect"
|
|
3326
3110
|
});
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
}
|
|
3331
|
-
}
|
|
3332
|
-
/** Bundle a Node runtime package entry: ESM for Node behind `#!/usr/bin/env node`. */
|
|
3333
|
-
async function execNodeBuild(entryPath, outfilePath, minify, envFiles, cwd, bunPlugins = [], runner = resolveBunBuildRunner()) {
|
|
3334
|
-
await execScriptBuild("node", entryPath, outfilePath, minify, envFiles, cwd, bunPlugins, runner);
|
|
3111
|
+
const output = processOutput(result);
|
|
3112
|
+
if (result.exitCode !== 0) throw new Error(`Build failed for ${outfilePath}${output ? `:\n${output}` : ""}`);
|
|
3113
|
+
return output;
|
|
3335
3114
|
}
|
|
3336
3115
|
/**
|
|
3337
|
-
* Bundle a
|
|
3338
|
-
*
|
|
3116
|
+
* Bundle a runtime package entry as ESM behind `#!/usr/bin/env <target>`: for
|
|
3117
|
+
* Node, or Bun-targeted so the installed bin launches with Bun rather than Node.
|
|
3339
3118
|
*/
|
|
3340
|
-
async function
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
async function execScriptBuild(target, entryPath, outfilePath, minify, envFiles, cwd, bunPlugins, runner) {
|
|
3344
|
-
if (bunPlugins.length > 0) await runBunPluginDriver({
|
|
3119
|
+
async function execScriptBuild(target, entryPath, outfilePath, settings, runner = resolveBunBuildRunner()) {
|
|
3120
|
+
const { cwd, minify, envFiles, external } = settings;
|
|
3121
|
+
if (settings.bunPlugins.length > 0) await runBunPluginDriver({
|
|
3345
3122
|
entrypoints: [entryPath],
|
|
3346
3123
|
minify,
|
|
3347
3124
|
env: "PUBLIC_*",
|
|
3348
3125
|
target,
|
|
3349
|
-
format: "esm"
|
|
3350
|
-
|
|
3126
|
+
format: "esm",
|
|
3127
|
+
external
|
|
3128
|
+
}, outfilePath, settings, runner);
|
|
3351
3129
|
else await runBuildProcess(runner, [
|
|
3352
3130
|
"build",
|
|
3353
3131
|
...toBunEnvFileArgs(envFiles),
|
|
@@ -3358,6 +3136,7 @@ async function execScriptBuild(target, entryPath, outfilePath, minify, envFiles,
|
|
|
3358
3136
|
target,
|
|
3359
3137
|
"--format",
|
|
3360
3138
|
"esm",
|
|
3139
|
+
...external.flatMap((name) => ["--external", name]),
|
|
3361
3140
|
"--outfile",
|
|
3362
3141
|
outfilePath,
|
|
3363
3142
|
...minify ? ["--minify"] : [],
|
|
@@ -3371,191 +3150,17 @@ async function execScriptBuild(target, entryPath, outfilePath, minify, envFiles,
|
|
|
3371
3150
|
async function execDenoBuild(entryPath, outfilePath, target, cwd, runner = resolveDenoBuildRunner()) {
|
|
3372
3151
|
await runBuildProcess(runner, createDenoCompileArgs(entryPath, outfilePath, target), outfilePath, cwd);
|
|
3373
3152
|
}
|
|
3374
|
-
/** tsdown's executable target for a Node target, embedding exactly `nodeVersion`. */
|
|
3375
|
-
function nodeExeTarget(target, nodeVersion) {
|
|
3376
|
-
const { os, cpu } = NODE_TARGETS.info[target];
|
|
3377
|
-
return {
|
|
3378
|
-
platform: os === "win32" ? "win" : os,
|
|
3379
|
-
arch: cpu,
|
|
3380
|
-
nodeVersion
|
|
3381
|
-
};
|
|
3382
|
-
}
|
|
3383
3153
|
/**
|
|
3384
|
-
*
|
|
3385
|
-
*
|
|
3386
|
-
*
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
const { build } = await import(pathToFileURL(createRequire(options.tsdownPackageJson).resolve("tsdown")).href);
|
|
3395
|
-
await build({
|
|
3396
|
-
config: false,
|
|
3397
|
-
cwd: options.cwd,
|
|
3398
|
-
entry: { app: options.entry },
|
|
3399
|
-
outDir: options.bundleDir,
|
|
3400
|
-
format: "esm",
|
|
3401
|
-
platform: "node",
|
|
3402
|
-
dts: false,
|
|
3403
|
-
// Rolldown reprints the Bun bundle, so it minifies again.
|
|
3404
|
-
minify: options.minify,
|
|
3405
|
-
logLevel: "warn",
|
|
3406
|
-
report: false,
|
|
3407
|
-
// Bun already inlined the PUBLIC_* constants; inline nothing from this environment.
|
|
3408
|
-
envPrefix: [],
|
|
3409
|
-
// Bun leaves only Node built-ins as imports; fail on anything else, which
|
|
3410
|
-
// an executable shipping no node_modules could not resolve.
|
|
3411
|
-
deps: { alwaysBundle: () => true, onlyBundle: false, onlyImport: [] },
|
|
3412
|
-
exe: { outDir: options.exeDir, fileName: "app", targets: [options.target] },
|
|
3413
|
-
});
|
|
3414
|
-
`;
|
|
3415
|
-
}
|
|
3416
|
-
/**
|
|
3417
|
-
* Build a single entry file into a Node standalone executable: Bun first
|
|
3418
|
-
* bundles it for Node exactly as a Node runtime package ({@link execNodeBuild}:
|
|
3419
|
-
* `PUBLIC_*` constants from `envFiles` and the environment, each module's own
|
|
3420
|
-
* `import.meta.main` and `require.main === module`, CommonJS and ESM inputs),
|
|
3421
|
-
* then tsdown's executable builder (Node SEA), run by the selected external
|
|
3422
|
-
* Node, turns that bundle into an executable embedding exactly its version
|
|
3423
|
-
* for `target`. tsdown takes a single entry per executable, so callers build
|
|
3424
|
-
* every command separately. Matching official Node binaries for targets are
|
|
3425
|
-
* downloaded, checksum-verified, and cached by @tsdown/exe. Builds for darwin
|
|
3426
|
-
* are only code-signed on a macOS host.
|
|
3427
|
-
*
|
|
3428
|
-
* @param compiler - The selected Node compiler (`resolveNodeBinaryCompiler`)
|
|
3429
|
-
* @param bunRunner - The Bun that bundles (`resolveBunBuildRunner`)
|
|
3430
|
-
* @param onWarning - Receives the backend's warnings (such as a skipped code
|
|
3431
|
-
* signature) after a successful build
|
|
3432
|
-
* @throws {Error} If the build fails
|
|
3433
|
-
*/
|
|
3434
|
-
async function execNodeBinaryBuild(entryPath, outfilePath, minify, target, envFiles, cwd, compiler, bunRunner, onWarning = () => {}) {
|
|
3435
|
-
const workDir = await mkdtemp(join(tmpdir(), "crust-node-exe-"));
|
|
3436
|
-
try {
|
|
3437
|
-
const scriptPath = join(workDir, "build.mjs");
|
|
3438
|
-
const bunBundle = join(workDir, "app.mjs");
|
|
3439
|
-
const exeDir = join(workDir, "exe");
|
|
3440
|
-
await execNodeBuild(resolve(cwd, entryPath), bunBundle, minify, envFiles, cwd, [], bunRunner);
|
|
3441
|
-
await writeFile(scriptPath, createNodeExeBuildScript({
|
|
3442
|
-
tsdownPackageJson: compiler.backend.packageJsonPath,
|
|
3443
|
-
cwd,
|
|
3444
|
-
entry: bunBundle,
|
|
3445
|
-
bundleDir: join(workDir, "bundle"),
|
|
3446
|
-
exeDir,
|
|
3447
|
-
minify,
|
|
3448
|
-
target: nodeExeTarget(target, compiler.version)
|
|
3449
|
-
}));
|
|
3450
|
-
const { exitCode, stdout, stderr } = await runProcess(compiler.runner.command, [scriptPath], {
|
|
3451
|
-
env: compiler.runner.env,
|
|
3452
|
-
cwd,
|
|
3453
|
-
stdio: "collect"
|
|
3454
|
-
});
|
|
3455
|
-
const output = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
3456
|
-
if (exitCode !== 0) throw new Error(`Build failed for ${outfilePath}${output ? `:\n${output}` : ""}`);
|
|
3457
|
-
const produced = await readdir(exeDir);
|
|
3458
|
-
if (produced.length !== 1) throw new Error(`Build failed for ${outfilePath}: expected one executable from tsdown, found ${JSON.stringify(produced)}.`);
|
|
3459
|
-
await copyFile(join(exeDir, produced[0]), outfilePath);
|
|
3460
|
-
if (output) onWarning(output);
|
|
3461
|
-
} finally {
|
|
3462
|
-
await rm(workDir, {
|
|
3463
|
-
recursive: true,
|
|
3464
|
-
force: true,
|
|
3465
|
-
maxRetries: 3,
|
|
3466
|
-
retryDelay: 100
|
|
3467
|
-
});
|
|
3468
|
-
}
|
|
3469
|
-
}
|
|
3470
|
-
function createNodeExeProvisionScript(tsdownPackageJson, targets) {
|
|
3471
|
-
return `// Generated by crust build to provision Node binaries for targets; deleted when it exits.
|
|
3472
|
-
import { realpathSync } from "node:fs";
|
|
3473
|
-
import { createRequire } from "node:module";
|
|
3474
|
-
import { pathToFileURL } from "node:url";
|
|
3475
|
-
|
|
3476
|
-
const exe = createRequire(realpathSync(${JSON.stringify(tsdownPackageJson)})).resolve("@tsdown/exe");
|
|
3477
|
-
const { resolveNodeBinary } = await import(pathToFileURL(exe).href);
|
|
3478
|
-
for (const { name, target } of ${JSON.stringify(targets)}) {
|
|
3479
|
-
try {
|
|
3480
|
-
await resolveNodeBinary(target, {});
|
|
3481
|
-
} catch (error) {
|
|
3482
|
-
console.error(name + ": " + error.message + (error.cause?.message ? "\\n" + error.cause.message : ""));
|
|
3483
|
-
process.exit(1);
|
|
3484
|
-
}
|
|
3485
|
-
}
|
|
3486
|
-
`;
|
|
3487
|
-
}
|
|
3488
|
-
/**
|
|
3489
|
-
* Provisions the Node binary every target embeds before anything is staged,
|
|
3490
|
-
* with the selected Node and the call tsdown's executable builder makes per
|
|
3491
|
-
* target (@tsdown/exe's `resolveNodeBinary`): an uncached official archive of
|
|
3492
|
-
* exactly `compiler.version` is downloaded, checksum-verified, unpacked with
|
|
3493
|
-
* the tool that target's archive needs on this host, and cached, where
|
|
3494
|
-
* {@link execNodeBinaryBuild} then finds it. A missing unpacking tool, a failed
|
|
3495
|
-
* download, or a checksum mismatch therefore fails here, before `.crust/` is
|
|
3496
|
-
* replaced; cached targets need nothing.
|
|
3497
|
-
*
|
|
3498
|
-
* @throws {Error} If a target cannot be provisioned
|
|
3499
|
-
*/
|
|
3500
|
-
async function provisionNodeExeTargets(targets, cwd, compiler) {
|
|
3501
|
-
const workDir = await mkdtemp(join(tmpdir(), "crust-node-exe-provision-"));
|
|
3502
|
-
try {
|
|
3503
|
-
const scriptPath = join(workDir, "provision.mjs");
|
|
3504
|
-
await writeFile(scriptPath, createNodeExeProvisionScript(compiler.backend.packageJsonPath, targets.map((name) => ({
|
|
3505
|
-
name,
|
|
3506
|
-
target: nodeExeTarget(name, compiler.version)
|
|
3507
|
-
}))));
|
|
3508
|
-
const { exitCode, stdout, stderr } = await runProcess(compiler.runner.command, [scriptPath], {
|
|
3509
|
-
env: compiler.runner.env,
|
|
3510
|
-
cwd,
|
|
3511
|
-
stdio: "collect"
|
|
3512
|
-
});
|
|
3513
|
-
if (exitCode !== 0) {
|
|
3514
|
-
const output = [stderr.trim(), stdout.trim()].filter(Boolean).join("\n");
|
|
3515
|
-
throw new Error(`Could not provision the Node ${compiler.version} binary that node standalone binaries embed${output ? `:\n${output}` : "."}\n tsdown downloads each target's official Node archive once and unpacks it with tar (GNU tar also needs xz for linux targets), or unzip for win targets on non-Windows hosts.
|
|
3516
|
-
Install the missing tool, or build only targets this machine can provision (--target).`);
|
|
3517
|
-
}
|
|
3518
|
-
} finally {
|
|
3519
|
-
await rm(workDir, {
|
|
3520
|
-
recursive: true,
|
|
3521
|
-
force: true
|
|
3522
|
-
});
|
|
3523
|
-
}
|
|
3524
|
-
}
|
|
3525
|
-
/**
|
|
3526
|
-
* Oldest `deno` accepted to bundle a Deno runtime package. Deno 2.4 resolves a
|
|
3527
|
-
* package.json project's npm dependencies from the registry instead of its
|
|
3528
|
-
* installed node_modules, so it can bundle a different Crust than the project
|
|
3529
|
-
* installed. Tested on Linux x64 with Deno 2.5.0 through 2.9.6.
|
|
3530
|
-
*/
|
|
3531
|
-
const DENO_PACKAGE_MIN_VERSION = "2.5.0";
|
|
3532
|
-
/**
|
|
3533
|
-
* Select the external deno that bundles a Deno runtime package, reading its
|
|
3534
|
-
* version in the project directory `cwd`. Call it before staging is replaced.
|
|
3535
|
-
* A runtime package embeds no Deno, so `engines.deno` stays a consumer
|
|
3536
|
-
* requirement and is not checked against this bundler.
|
|
3537
|
-
*/
|
|
3538
|
-
async function resolveDenoPackageBundler(cwd, runner = resolveDenoBuildRunner()) {
|
|
3539
|
-
const version = await readCompilerVersion("deno", runner, cwd);
|
|
3540
|
-
if (!(0, import_satisfies.default)(version, `>=2.5.0`, { includePrerelease: true })) throw new Error(`Deno ${version} (${runner.command}) cannot bundle a Deno runtime package; deno ${DENO_PACKAGE_MIN_VERSION} or newer is required.\n Older deno bundle resolves package.json dependencies from the npm registry instead of the installed node_modules.
|
|
3541
|
-
crust does not install or upgrade Deno; put a newer deno first on PATH (e.g. with your version manager).`);
|
|
3542
|
-
return {
|
|
3543
|
-
runtime: "deno",
|
|
3544
|
-
runner,
|
|
3545
|
-
version
|
|
3546
|
-
};
|
|
3547
|
-
}
|
|
3548
|
-
/**
|
|
3549
|
-
* Bundle a Deno runtime package entry with native `deno bundle`, which resolves
|
|
3550
|
-
* dependencies through the project's own Deno setup (deno.json imports,
|
|
3551
|
-
* package.json with its installed node_modules) and inlines them into one file.
|
|
3552
|
-
*
|
|
3553
|
-
* Bundle the declared command directly so Deno preserves its `import.meta.main`
|
|
3554
|
-
* semantics without making dependencies main. Prepend the packaged-build marker
|
|
3555
|
-
* (`PACKAGED_BUILD_KEY`) to the bundled code so it runs before the inlined
|
|
3556
|
-
* application graph. The output has no shebang or permission flags: consumers
|
|
3557
|
-
* choose grants with `deno run` / `deno install`.
|
|
3558
|
-
* No env files or plugins: `deno bundle` has no PUBLIC_* filter or Bun plugins.
|
|
3154
|
+
* Bundle a Deno runtime package entry with native `deno bundle`, which resolves
|
|
3155
|
+
* dependencies through the project's own Deno setup (deno.json imports,
|
|
3156
|
+
* package.json with its installed node_modules) and inlines them into one file.
|
|
3157
|
+
*
|
|
3158
|
+
* Bundle the declared command directly so Deno preserves its `import.meta.main`
|
|
3159
|
+
* semantics without making dependencies main. Prepend the packaged-build marker
|
|
3160
|
+
* (`PACKAGED_BUILD_KEY`) to the bundled code so it runs before the inlined
|
|
3161
|
+
* application graph. The output has no shebang or permission flags: consumers
|
|
3162
|
+
* choose grants with `deno run` / `deno install`.
|
|
3163
|
+
* No env files or plugins: `deno bundle` has no PUBLIC_* filter or Bun plugins.
|
|
3559
3164
|
*/
|
|
3560
3165
|
async function execDenoPackageBuild(entryPath, outfilePath, cwd, runner = resolveDenoBuildRunner()) {
|
|
3561
3166
|
await runBuildProcess(runner, [
|
|
@@ -3569,147 +3174,104 @@ async function execDenoPackageBuild(entryPath, outfilePath, cwd, runner = resolv
|
|
|
3569
3174
|
const output = await readFile(outfilePath, "utf8");
|
|
3570
3175
|
await writeFile(outfilePath, `globalThis[Symbol.for(${JSON.stringify(PACKAGED_BUILD_KEY)})] = true;\n` + output.replace(/^#![^\n]*(?:\n|$)/, ""));
|
|
3571
3176
|
}
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
"
|
|
3577
|
-
"SIGQUIT"
|
|
3578
|
-
];
|
|
3579
|
-
function isBuildReport(value) {
|
|
3580
|
-
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"));
|
|
3177
|
+
//#endregion
|
|
3178
|
+
//#region src/launcher.ts
|
|
3179
|
+
/** Platform binary filename: `<command>-<target>`, `.exe` on Windows. */
|
|
3180
|
+
function binaryFilename(command, target) {
|
|
3181
|
+
return `${command}-${target.target}${target.os === "win32" ? ".exe" : ""}`;
|
|
3581
3182
|
}
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
const buildReportPath = join(snapshotDir, "build-report.json");
|
|
3183
|
+
function generateLauncher(command, targets) {
|
|
3184
|
+
const targetMap = Object.fromEntries(targets.map((target) => [target.platformKey, {
|
|
3185
|
+
packagePathSegment: target.packagePathSegment,
|
|
3186
|
+
packageName: target.packageName,
|
|
3187
|
+
targetAlias: target.targetAlias,
|
|
3188
|
+
binaryFilename: binaryFilename(command, target)
|
|
3189
|
+
}]));
|
|
3190
|
+
const supportedPlatforms = targets.map((target) => target.targetAlias).join(", ");
|
|
3191
|
+
return `#!/usr/bin/env node
|
|
3192
|
+
// Auto-generated by crust build -- do not edit
|
|
3193
|
+
import { spawn } from "node:child_process";
|
|
3194
|
+
import { chmodSync, existsSync } from "node:fs";
|
|
3195
|
+
import { dirname, resolve } from "node:path";
|
|
3196
|
+
import process from "node:process";
|
|
3197
|
+
import { fileURLToPath } from "node:url";
|
|
3198
|
+
|
|
3199
|
+
const NAME = ${JSON.stringify(command)};
|
|
3200
|
+
const PLATFORMS = ${JSON.stringify(targetMap, null, " ")};
|
|
3201
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
3202
|
+
|
|
3203
|
+
// Same check as Bun's own npm installer: glibc reports its version, musl does not.
|
|
3204
|
+
function isMusl() {
|
|
3605
3205
|
try {
|
|
3606
|
-
const
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
let snapshot;
|
|
3673
|
-
try {
|
|
3674
|
-
snapshot = JSON.parse(serialized);
|
|
3675
|
-
} catch (error) {
|
|
3676
|
-
throw new Error(`Entry produced an invalid Command Snapshot.\n Ensure ${absoluteEntry} uses a compatible @crustjs/core version.`, { cause: error });
|
|
3677
|
-
}
|
|
3678
|
-
let serializedBuild;
|
|
3679
|
-
try {
|
|
3680
|
-
serializedBuild = await readFile(buildReportPath, "utf8");
|
|
3681
|
-
} catch (error) {
|
|
3682
|
-
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 });
|
|
3683
|
-
throw error;
|
|
3684
|
-
}
|
|
3685
|
-
try {
|
|
3686
|
-
const build = JSON.parse(serializedBuild);
|
|
3687
|
-
if (!isBuildReport(build)) throw new Error("Expected extensions with string ids and files arrays.");
|
|
3688
|
-
for (const extension of build.extensions) {
|
|
3689
|
-
defineExtensionId(extension.id);
|
|
3690
|
-
for (const file of extension.files) {
|
|
3691
|
-
const path = resolve(outDir, file);
|
|
3692
|
-
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}`);
|
|
3693
|
-
}
|
|
3694
|
-
}
|
|
3695
|
-
return {
|
|
3696
|
-
snapshot,
|
|
3697
|
-
build
|
|
3698
|
-
};
|
|
3699
|
-
} catch (error) {
|
|
3700
|
-
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 });
|
|
3701
|
-
}
|
|
3702
|
-
} finally {
|
|
3703
|
-
await rm(snapshotDir, {
|
|
3704
|
-
recursive: true,
|
|
3705
|
-
force: true,
|
|
3706
|
-
maxRetries: 3,
|
|
3707
|
-
retryDelay: 100
|
|
3708
|
-
});
|
|
3709
|
-
}
|
|
3206
|
+
const report = process.report?.getReport();
|
|
3207
|
+
if (report?.header) return report.header.glibcVersionRuntime === undefined;
|
|
3208
|
+
} catch {}
|
|
3209
|
+
return existsSync("/etc/alpine-release");
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
const platformKey =
|
|
3213
|
+
\`\${process.platform}-\${process.arch}\` + (process.platform === "linux" && isMusl() ? "-musl" : "");
|
|
3214
|
+
const target = PLATFORMS[platformKey];
|
|
3215
|
+
|
|
3216
|
+
if (!target) {
|
|
3217
|
+
\tconsole.error("[" + NAME + "] Unsupported platform: " + platformKey);
|
|
3218
|
+
\tconsole.error("[" + NAME + "] Supported platforms: ${supportedPlatforms}");
|
|
3219
|
+
\tprocess.exit(1);
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
const candidates = [
|
|
3223
|
+
\t// Hoisted install: the platform package beside this one in node_modules.
|
|
3224
|
+
\tresolve(dir, "..", "..", target.packagePathSegment, "bin", target.binaryFilename),
|
|
3225
|
+
\t// Nested install: the platform package under this package's node_modules.
|
|
3226
|
+
\tresolve(dir, "..", "node_modules", target.packageName, "bin", target.binaryFilename),
|
|
3227
|
+
\t// In place: the .crust/ tree crust build staged, before any install.
|
|
3228
|
+
\tresolve(dir, "..", "..", target.targetAlias, "bin", target.binaryFilename),
|
|
3229
|
+
];
|
|
3230
|
+
const binPath = candidates.find((candidate) => existsSync(candidate));
|
|
3231
|
+
|
|
3232
|
+
if (!binPath) {
|
|
3233
|
+
\tconsole.error("[" + NAME + "] Missing platform package for " + platformKey);
|
|
3234
|
+
\tconsole.error("[" + NAME + "] Tried:");
|
|
3235
|
+
\tfor (const candidate of candidates) console.error(" " + candidate);
|
|
3236
|
+
\tconsole.error(
|
|
3237
|
+
\t\t"[" + NAME + "] Reinstall dependencies on this platform and ensure optional dependencies are enabled.",
|
|
3238
|
+
\t);
|
|
3239
|
+
\tprocess.exit(1);
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
if (process.platform !== "win32") {
|
|
3243
|
+
\ttry {
|
|
3244
|
+
\t\tchmodSync(binPath, 0o755);
|
|
3245
|
+
\t} catch {
|
|
3246
|
+
\t\t// Ignore permission adjustment failures and let spawn surface real errors.
|
|
3247
|
+
\t}
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
const child = spawn(binPath, process.argv.slice(2), {
|
|
3251
|
+
\tstdio: "inherit",
|
|
3252
|
+
});
|
|
3253
|
+
|
|
3254
|
+
child.on("error", (error) => {
|
|
3255
|
+
\tconsole.error("[" + NAME + "] Failed to launch binary: " + error.message);
|
|
3256
|
+
\tprocess.exit(1);
|
|
3257
|
+
});
|
|
3258
|
+
|
|
3259
|
+
child.on("exit", (code, signal) => {
|
|
3260
|
+
\tif (signal) {
|
|
3261
|
+
\t\ttry {
|
|
3262
|
+
\t\t\tprocess.kill(process.pid, signal);
|
|
3263
|
+
\t\t} catch {
|
|
3264
|
+
\t\t\tprocess.exit(1);
|
|
3265
|
+
\t\t}
|
|
3266
|
+
\t\treturn;
|
|
3267
|
+
\t}
|
|
3268
|
+
|
|
3269
|
+
\tprocess.exit(code ?? 0);
|
|
3270
|
+
});
|
|
3271
|
+
`;
|
|
3710
3272
|
}
|
|
3711
3273
|
//#endregion
|
|
3712
|
-
//#region src/
|
|
3274
|
+
//#region src/distribute.ts
|
|
3713
3275
|
/** Project-relative directory that `crust build` owns: wiped per build, read by `crust publish`. */
|
|
3714
3276
|
const CRUST_DIR = ".crust";
|
|
3715
3277
|
/**
|
|
@@ -3731,15 +3293,9 @@ const METADATA_KEYS = [
|
|
|
3731
3293
|
"funding",
|
|
3732
3294
|
"engines"
|
|
3733
3295
|
];
|
|
3734
|
-
function readPackageJson(cwd, packageJson) {
|
|
3735
|
-
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.`);
|
|
3736
|
-
if (!isJsonObject(packageJson)) throw new Error(`package.json in ${cwd} must contain a JSON object.`);
|
|
3737
|
-
validatePackageIdentity(packageJson, "package.json");
|
|
3738
|
-
return packageJson;
|
|
3739
|
-
}
|
|
3740
3296
|
/** Only identity is interpreted here; this is not a complete npm schema validator. */
|
|
3741
3297
|
function validatePackageIdentity(value, source) {
|
|
3742
|
-
if (
|
|
3298
|
+
if (!isJsonObject(value)) throw new Error(`${source} must contain a JSON object.`);
|
|
3743
3299
|
if (typeof value.name !== "string" || value.name.trim() === "") throw new Error(`${source} name field must be a non-empty string.`);
|
|
3744
3300
|
if (typeof value.version !== "string" || value.version.trim() === "") throw new Error(`${source} version field must be a non-empty string.`);
|
|
3745
3301
|
}
|
|
@@ -3754,10 +3310,6 @@ function derivePlatformPackageName(rootPackageName, targetAlias) {
|
|
|
3754
3310
|
function getPackagePathSegment(packageName) {
|
|
3755
3311
|
return packageName.startsWith("@") ? packageName.split("/")[1] ?? packageName : packageName;
|
|
3756
3312
|
}
|
|
3757
|
-
/** Platform binary filename: `<command>-<target>`, `.exe` on Windows. */
|
|
3758
|
-
function binaryFilename(command, target) {
|
|
3759
|
-
return `${command}-${target.target}${target.os === "win32" ? ".exe" : ""}`;
|
|
3760
|
-
}
|
|
3761
3313
|
function platformBinMap(commands, target) {
|
|
3762
3314
|
return Object.fromEntries(commands.map((command) => [command, `bin/${binaryFilename(command, target)}`]));
|
|
3763
3315
|
}
|
|
@@ -3769,6 +3321,7 @@ function buildDistributionRootPackageJson(metadata, commands, targets, { artifac
|
|
|
3769
3321
|
type: "module",
|
|
3770
3322
|
files: ["bin", ...artifactDirs],
|
|
3771
3323
|
bin: Object.fromEntries(commands.map((command) => [command, `bin/${command}.js`])),
|
|
3324
|
+
dependencies: metadata.dependencies,
|
|
3772
3325
|
...targets.length > 0 ? { optionalDependencies: Object.fromEntries(targets.map((target) => [target.packageName, metadata.version])) } : {},
|
|
3773
3326
|
...manPages.length > 0 ? { man: manPages.map((page) => `./man/${page}`) } : {},
|
|
3774
3327
|
exports: metadata.exports,
|
|
@@ -3834,16 +3387,23 @@ function validateStagedExports(exports, rootDir, sourceType) {
|
|
|
3834
3387
|
checkTarget(exports, "\".\"");
|
|
3835
3388
|
}
|
|
3836
3389
|
/**
|
|
3390
|
+
* The staged manifests go to npm as written, so a range the registry cannot
|
|
3391
|
+
* resolve (a workspace, catalog, or local path protocol) would leak into it.
|
|
3392
|
+
* Bare paths and scheme-less tarball names are local too, except GitHub
|
|
3393
|
+
* owner/repo[#ref] shorthand: npm resolves hosted Git before its file fallback.
|
|
3394
|
+
*/
|
|
3395
|
+
function assertPublishableRange(range, label) {
|
|
3396
|
+
if (range === void 0 || !isString$1(range) || /^(?:(?:workspace|catalog|file|link|portal):|[.]|~[/\\]|[/\\]|[a-zA-Z]:)/.test(range) || !/^[a-z][a-z+]*:/i.test(range) && !/^[^@\s/:#\\]+\/[^@\s/:#\\]+(?:#.*)?$/.test(range) && /[.](?:tgz|tar\.gz|tar)$/i.test(range)) throw new Error(`${label} must be a publishable range, not ${JSON.stringify(range)}.\n crust build publishes the staged root package as written; workspace:, catalog:, file:, link:, portal:, and local path ranges are never rewritten.`);
|
|
3397
|
+
}
|
|
3398
|
+
/**
|
|
3837
3399
|
* Carries `peerDependencies` (and `peerDependenciesMeta`) into the root package
|
|
3838
3400
|
* so a library `exports` entry can declare what its published types import.
|
|
3839
|
-
* Ranges must be publishable as written: the staged manifests go to npm
|
|
3840
|
-
* directly, so `workspace:` and `catalog:` ranges would leak into the registry.
|
|
3841
3401
|
*/
|
|
3842
3402
|
function validatePeerDependencies(peerDependencies, peerDependenciesMeta) {
|
|
3843
3403
|
if (!isJsonObject(peerDependencies)) throw new Error("package.json peerDependencies must be an object of package names to ranges.");
|
|
3844
3404
|
const ranges = {};
|
|
3845
3405
|
for (const [name, range] of Object.entries(peerDependencies)) {
|
|
3846
|
-
|
|
3406
|
+
assertPublishableRange(range, `package.json peerDependencies[${JSON.stringify(name)}]`);
|
|
3847
3407
|
ranges[name] = range;
|
|
3848
3408
|
}
|
|
3849
3409
|
if (peerDependenciesMeta === void 0) return { peerDependencies: ranges };
|
|
@@ -3873,130 +3433,49 @@ function pickRootMetadata(pkgJson) {
|
|
|
3873
3433
|
};
|
|
3874
3434
|
for (const key of METADATA_KEYS) {
|
|
3875
3435
|
const value = pkgJson[key];
|
|
3876
|
-
if (value !== void 0)
|
|
3436
|
+
if (value !== void 0) metadata[key] = value;
|
|
3877
3437
|
}
|
|
3878
3438
|
return metadata;
|
|
3879
3439
|
}
|
|
3880
3440
|
function validatePackageNameLength(packageName) {
|
|
3881
3441
|
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.`);
|
|
3882
3442
|
}
|
|
3883
|
-
|
|
3884
|
-
|
|
3443
|
+
/** `crust.external` ranges, already validated by `planBuild`; re-asserted for their type. */
|
|
3444
|
+
function externalDependencies(pkgJson, external) {
|
|
3445
|
+
const dependencies = pkgJson.dependencies !== void 0 && isJsonObject(pkgJson.dependencies) ? pkgJson.dependencies : {};
|
|
3446
|
+
return Object.fromEntries(external.map((name) => {
|
|
3447
|
+
const range = dependencies[name];
|
|
3448
|
+
assertPublishableRange(range, `package.json dependencies[${JSON.stringify(name)}]`);
|
|
3449
|
+
return [name, range];
|
|
3450
|
+
}));
|
|
3451
|
+
}
|
|
3452
|
+
function resolveDistributionMetadata(pkgJson, external) {
|
|
3885
3453
|
validatePackageNameLength(pkgJson.name);
|
|
3886
3454
|
return {
|
|
3887
3455
|
rootPackageName: pkgJson.name,
|
|
3888
3456
|
version: pkgJson.version,
|
|
3889
3457
|
rootPackageJson: pickRootMetadata(pkgJson),
|
|
3890
|
-
sourceType: pkgJson.type,
|
|
3891
|
-
...pkgJson.exports !== void 0 ? { exports: pkgJson.exports } : {},
|
|
3892
|
-
...pkgJson.peerDependencies !== void 0 ? validatePeerDependencies(pkgJson.peerDependencies, pkgJson.peerDependenciesMeta) : {}
|
|
3893
|
-
|
|
3894
|
-
}
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
const
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
}
|
|
3911
|
-
function generateDistributionJsResolver(command, targets) {
|
|
3912
|
-
const targetMap = Object.fromEntries(targets.map((target) => [target.platformKey, {
|
|
3913
|
-
packagePathSegment: target.packagePathSegment,
|
|
3914
|
-
packageName: target.packageName,
|
|
3915
|
-
targetAlias: target.targetAlias,
|
|
3916
|
-
binaryFilename: binaryFilename(command, target)
|
|
3917
|
-
}]));
|
|
3918
|
-
const supportedPlatforms = targets.map((target) => target.targetAlias).join(", ");
|
|
3919
|
-
return `#!/usr/bin/env node
|
|
3920
|
-
// Auto-generated by crust build -- do not edit
|
|
3921
|
-
import { spawn } from "node:child_process";
|
|
3922
|
-
import { chmodSync, existsSync } from "node:fs";
|
|
3923
|
-
import { dirname, resolve } from "node:path";
|
|
3924
|
-
import process from "node:process";
|
|
3925
|
-
import { fileURLToPath } from "node:url";
|
|
3926
|
-
|
|
3927
|
-
const NAME = ${JSON.stringify(command)};
|
|
3928
|
-
const PLATFORMS = ${JSON.stringify(targetMap, null, " ")};
|
|
3929
|
-
const dir = dirname(fileURLToPath(import.meta.url));
|
|
3930
|
-
|
|
3931
|
-
// Same check as Bun's own npm installer: glibc reports its version, musl does not.
|
|
3932
|
-
function isMusl() {
|
|
3933
|
-
try {
|
|
3934
|
-
const report = process.report?.getReport();
|
|
3935
|
-
if (report?.header) return report.header.glibcVersionRuntime === undefined;
|
|
3936
|
-
} catch {}
|
|
3937
|
-
return existsSync("/etc/alpine-release");
|
|
3938
|
-
}
|
|
3939
|
-
|
|
3940
|
-
const platformKey =
|
|
3941
|
-
\`\${process.platform}-\${process.arch}\` + (process.platform === "linux" && isMusl() ? "-musl" : "");
|
|
3942
|
-
const target = PLATFORMS[platformKey];
|
|
3943
|
-
|
|
3944
|
-
if (!target) {
|
|
3945
|
-
\tconsole.error("[" + NAME + "] Unsupported platform: " + platformKey);
|
|
3946
|
-
\tconsole.error("[" + NAME + "] Supported platforms: ${supportedPlatforms}");
|
|
3947
|
-
\tprocess.exit(1);
|
|
3948
|
-
}
|
|
3949
|
-
|
|
3950
|
-
const candidates = [
|
|
3951
|
-
\t// Hoisted install: the platform package beside this one in node_modules.
|
|
3952
|
-
\tresolve(dir, "..", "..", target.packagePathSegment, "bin", target.binaryFilename),
|
|
3953
|
-
\t// Nested install: the platform package under this package's node_modules.
|
|
3954
|
-
\tresolve(dir, "..", "node_modules", target.packageName, "bin", target.binaryFilename),
|
|
3955
|
-
\t// In place: the .crust/ tree crust build staged, before any install.
|
|
3956
|
-
\tresolve(dir, "..", "..", target.targetAlias, "bin", target.binaryFilename),
|
|
3957
|
-
];
|
|
3958
|
-
const binPath = candidates.find((candidate) => existsSync(candidate));
|
|
3959
|
-
|
|
3960
|
-
if (!binPath) {
|
|
3961
|
-
\tconsole.error("[" + NAME + "] Missing platform package for " + platformKey);
|
|
3962
|
-
\tconsole.error("[" + NAME + "] Tried:");
|
|
3963
|
-
\tfor (const candidate of candidates) console.error(" " + candidate);
|
|
3964
|
-
\tconsole.error(
|
|
3965
|
-
\t\t"[" + NAME + "] Reinstall dependencies on this platform and ensure optional dependencies are enabled.",
|
|
3966
|
-
\t);
|
|
3967
|
-
\tprocess.exit(1);
|
|
3968
|
-
}
|
|
3969
|
-
|
|
3970
|
-
if (process.platform !== "win32") {
|
|
3971
|
-
\ttry {
|
|
3972
|
-
\t\tchmodSync(binPath, 0o755);
|
|
3973
|
-
\t} catch {
|
|
3974
|
-
\t\t// Ignore permission adjustment failures and let spawn surface real errors.
|
|
3975
|
-
\t}
|
|
3976
|
-
}
|
|
3977
|
-
|
|
3978
|
-
const child = spawn(binPath, process.argv.slice(2), {
|
|
3979
|
-
\tstdio: "inherit",
|
|
3980
|
-
});
|
|
3981
|
-
|
|
3982
|
-
child.on("error", (error) => {
|
|
3983
|
-
\tconsole.error("[" + NAME + "] Failed to launch binary: " + error.message);
|
|
3984
|
-
\tprocess.exit(1);
|
|
3985
|
-
});
|
|
3986
|
-
|
|
3987
|
-
child.on("exit", (code, signal) => {
|
|
3988
|
-
\tif (signal) {
|
|
3989
|
-
\t\ttry {
|
|
3990
|
-
\t\t\tprocess.kill(process.pid, signal);
|
|
3991
|
-
\t\t} catch {
|
|
3992
|
-
\t\t\tprocess.exit(1);
|
|
3993
|
-
\t\t}
|
|
3994
|
-
\t\treturn;
|
|
3995
|
-
\t}
|
|
3996
|
-
|
|
3997
|
-
\tprocess.exit(code ?? 0);
|
|
3998
|
-
});
|
|
3999
|
-
`;
|
|
3458
|
+
sourceType: pkgJson.type,
|
|
3459
|
+
...pkgJson.exports !== void 0 ? { exports: pkgJson.exports } : {},
|
|
3460
|
+
...pkgJson.peerDependencies !== void 0 ? validatePeerDependencies(pkgJson.peerDependencies, pkgJson.peerDependenciesMeta) : {},
|
|
3461
|
+
...external.length > 0 ? { dependencies: externalDependencies(pkgJson, external) } : {}
|
|
3462
|
+
};
|
|
3463
|
+
}
|
|
3464
|
+
function resolveDistributionTarget(table, stageDir, rootPackageName, target) {
|
|
3465
|
+
const info = table.info[target];
|
|
3466
|
+
const packageName = derivePlatformPackageName(rootPackageName, info.alias);
|
|
3467
|
+
validatePackageNameLength(packageName);
|
|
3468
|
+
return {
|
|
3469
|
+
target,
|
|
3470
|
+
platformKey: info.platformKey,
|
|
3471
|
+
targetAlias: info.alias,
|
|
3472
|
+
packageName,
|
|
3473
|
+
packagePathSegment: getPackagePathSegment(packageName),
|
|
3474
|
+
packageDir: resolve(stageDir, info.alias),
|
|
3475
|
+
os: info.os,
|
|
3476
|
+
cpu: info.cpu,
|
|
3477
|
+
...info.libc ? { libc: info.libc } : {}
|
|
3478
|
+
};
|
|
4000
3479
|
}
|
|
4001
3480
|
function writeJson(path, value) {
|
|
4002
3481
|
writeFileSync(path, `${JSON.stringify(value, null, " ")}\n`);
|
|
@@ -4037,13 +3516,14 @@ function writeDistributionManifest(stageDir, metadata, identity, commands, targe
|
|
|
4037
3516
|
publishOrder: [...targets.map((target) => target.targetAlias), "root"],
|
|
4038
3517
|
...build ? { build } : {}
|
|
4039
3518
|
};
|
|
4040
|
-
|
|
4041
|
-
|
|
3519
|
+
const manifestPath = join(stageDir, "manifest.json");
|
|
3520
|
+
writeJson(manifestPath, manifest);
|
|
3521
|
+
return manifestPath;
|
|
4042
3522
|
}
|
|
4043
3523
|
/** `pkg` without `engines.node`, dropping `engines` once nothing else is left. */
|
|
4044
3524
|
function omitNodeEngine(pkg) {
|
|
4045
3525
|
const { engines, ...rest } = pkg;
|
|
4046
|
-
if (engines
|
|
3526
|
+
if (engines === void 0 || !isJsonObject(engines) || engines.node === void 0) return pkg;
|
|
4047
3527
|
const others = Object.entries(engines).filter(([runtime]) => runtime !== "node");
|
|
4048
3528
|
return others.length > 0 ? {
|
|
4049
3529
|
...rest,
|
|
@@ -4068,7 +3548,7 @@ function stageDistributionPackages(cwd, stageDir, metadata, platformMetadata, co
|
|
|
4068
3548
|
* Returns every generated package.json, launcher, and compiled command in staging order.
|
|
4069
3549
|
*/
|
|
4070
3550
|
async function runDistributeBuild(plan, distribution, io, build) {
|
|
4071
|
-
const sourceMetadata = resolveDistributionMetadata(plan.
|
|
3551
|
+
const sourceMetadata = resolveDistributionMetadata(plan.userPackageJson, plan.external);
|
|
4072
3552
|
const commands = plan.entries.map((entry) => entry.command);
|
|
4073
3553
|
const table = distribution.table;
|
|
4074
3554
|
const platformMetadata = plan.runtime === "node" && table ? {
|
|
@@ -4117,7 +3597,7 @@ async function runDistributeBuild(plan, distribution, io, build) {
|
|
|
4117
3597
|
if (table) {
|
|
4118
3598
|
for (const { command } of plan.entries) {
|
|
4119
3599
|
const launcherPath = join(rootBinDir, `${command}.js`);
|
|
4120
|
-
writeFileSync(launcherPath,
|
|
3600
|
+
writeFileSync(launcherPath, generateLauncher(command, distributionTargets), { mode: 493 });
|
|
4121
3601
|
produced.push({
|
|
4122
3602
|
kind: "launcher",
|
|
4123
3603
|
path: launcherPath,
|
|
@@ -4145,7 +3625,7 @@ async function runDistributeBuild(plan, distribution, io, build) {
|
|
|
4145
3625
|
command
|
|
4146
3626
|
});
|
|
4147
3627
|
}
|
|
4148
|
-
writeDistributionManifest(plan.stageDir, metadata, table ? {
|
|
3628
|
+
const manifestPath = writeDistributionManifest(plan.stageDir, metadata, table ? {
|
|
4149
3629
|
runtime: plan.runtime,
|
|
4150
3630
|
artifact: "binary",
|
|
4151
3631
|
embeddedRuntimeVersion: distribution.embeddedRuntimeVersion
|
|
@@ -4153,116 +3633,470 @@ async function runDistributeBuild(plan, distribution, io, build) {
|
|
|
4153
3633
|
runtime: plan.runtime,
|
|
4154
3634
|
artifact: "package"
|
|
4155
3635
|
}, commands, distributionTargets, build);
|
|
4156
|
-
const manifestPath = join(plan.stageDir, "manifest.json");
|
|
4157
3636
|
io.stdout(`\n${green("✓")} Staged ${bold(`${distributionTargets.length + 1}`)} npm package(s) successfully:`);
|
|
4158
3637
|
io.stdout(` ${rootDir}`);
|
|
4159
3638
|
for (const targetPackage of distributionTargets) io.stdout(` ${targetPackage.packageDir}`);
|
|
4160
3639
|
io.stdout(`\n${dim("Manifest:")} ${manifestPath}`);
|
|
4161
3640
|
return produced;
|
|
4162
3641
|
}
|
|
3642
|
+
//#endregion
|
|
3643
|
+
//#region package.json
|
|
3644
|
+
var optionalDependencies = {
|
|
3645
|
+
"@tsdown/exe": "0.23.0",
|
|
3646
|
+
"tsdown": "0.23.0"
|
|
3647
|
+
};
|
|
3648
|
+
//#endregion
|
|
3649
|
+
//#region src/node-exe.ts
|
|
4163
3650
|
/**
|
|
4164
|
-
*
|
|
4165
|
-
* staged
|
|
3651
|
+
* Crust's own optional dependencies that build Node standalone binaries.
|
|
3652
|
+
* Crust's staged root and platform packages declare them
|
|
3653
|
+
* (scripts/stage-node-exe-dependencies.ts) so an installed crust resolves them
|
|
3654
|
+
* next to itself; an install that skipped them only affects Node binaries.
|
|
4166
3655
|
*/
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
3656
|
+
const NODE_EXE_BACKEND_PACKAGES = ["tsdown", "@tsdown/exe"];
|
|
3657
|
+
/**
|
|
3658
|
+
* A file of Crust's own installation that the backend is resolved from, never
|
|
3659
|
+
* the project or PATH. A compiled crust serves its modules from Bun's virtual
|
|
3660
|
+
* filesystem (`/$bunfs/`, `B:/~BUN/` on Windows), so there it is the
|
|
3661
|
+
* executable, inside its platform package.
|
|
3662
|
+
*/
|
|
3663
|
+
function crustInstallPath(moduleUrl = import.meta.url, execPath = process.execPath) {
|
|
3664
|
+
return /^file:\/\/\/(?:\$bunfs|[A-Za-z]:\/~BUN)\//.test(moduleUrl) ? execPath : fileURLToPath(moduleUrl);
|
|
3665
|
+
}
|
|
3666
|
+
/**
|
|
3667
|
+
* `<dir>/node_modules/<name>/package.json` for the nearest `dir` at or above
|
|
3668
|
+
* `from`: Node's node_modules lookup without its NODE_PATH and global-folder
|
|
3669
|
+
* fallbacks, so only an installation next to `from` can supply the package.
|
|
3670
|
+
* Package managers hoist crust's backend into the project's node_modules, so
|
|
3671
|
+
* the found package may be the project's own: callers check its version.
|
|
3672
|
+
*/
|
|
3673
|
+
function findInstalledPackageJson(from, name) {
|
|
3674
|
+
for (let dir = from;; dir = dirname(dir)) {
|
|
3675
|
+
const candidate = join(dir, "node_modules", name, "package.json");
|
|
3676
|
+
if (existsSync(candidate)) return candidate;
|
|
3677
|
+
if (dirname(dir) === dir) return null;
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
function isVersionedPackageJson(value) {
|
|
3681
|
+
return isJsonObject(value) && typeof value.version === "string";
|
|
3682
|
+
}
|
|
3683
|
+
function isTsdownPackageJson(value) {
|
|
3684
|
+
return isVersionedPackageJson(value) && value.engines !== void 0 && isJsonObject(value.engines) && value.engines.node !== void 0 && isVersionRange(value.engines.node);
|
|
3685
|
+
}
|
|
3686
|
+
function createNodeExeProbeScript(tsdownPackageJson) {
|
|
3687
|
+
return `// Generated by crust build to read tsdown's executable minimum; deleted when it exits.
|
|
3688
|
+
import { createRequire } from "node:module";
|
|
3689
|
+
import { pathToFileURL } from "node:url";
|
|
3690
|
+
|
|
3691
|
+
const internal = createRequire(${JSON.stringify(tsdownPackageJson)}).resolve("tsdown/internal");
|
|
3692
|
+
console.log((await import(pathToFileURL(internal).href)).NODE_SEA_MIN_VERSION);
|
|
3693
|
+
`;
|
|
3694
|
+
}
|
|
3695
|
+
const NODE_TOOLCHAIN_GUIDANCE = " Binaries embed the selected node's version; crust does not install or upgrade it.\n Put a supported node first on PATH (e.g. with your version manager).";
|
|
3696
|
+
/**
|
|
3697
|
+
* Checks the selected Node against the installed backend's two separate
|
|
3698
|
+
* requirements: tsdown's package `engines.node` and its executable builder's
|
|
3699
|
+
* minimum (skipped while unknown). Both come from the installed tsdown, not
|
|
3700
|
+
* from this file.
|
|
3701
|
+
*/
|
|
3702
|
+
function assertNodeExeBackendSupports(compiler, backend) {
|
|
3703
|
+
const selected = `Node ${compiler.version} (${compiler.runner.command})`;
|
|
3704
|
+
if (!(0, import_satisfies.default)(compiler.version, backend.engines)) throw new Error(`${selected} is not supported by tsdown ${backend.version}, which builds node standalone binaries: its package requires Node "${backend.engines}".\n${NODE_TOOLCHAIN_GUIDANCE}`);
|
|
3705
|
+
if (backend.seaMinVersion !== void 0 && !(0, import_satisfies.default)(compiler.version, `>=${backend.seaMinVersion}`)) throw new Error(`${selected} cannot build standalone executables: tsdown ${backend.version}'s executable builder requires Node ${backend.seaMinVersion} or later.\n${NODE_TOOLCHAIN_GUIDANCE}`);
|
|
3706
|
+
}
|
|
3707
|
+
/**
|
|
3708
|
+
* Finds Crust's installed tsdown and @tsdown/exe from `installPath`, at the
|
|
3709
|
+
* versions its package.json `optionalDependencies` pin, and validates the
|
|
3710
|
+
* selected Node against tsdown's requirements, loading the executable
|
|
3711
|
+
* builder's minimum under that Node. `cwd` is the project
|
|
3712
|
+
* directory, as for every other compiler step.
|
|
3713
|
+
*/
|
|
3714
|
+
async function resolveNodeExeBackend(compiler, cwd, installPath = crustInstallPath()) {
|
|
3715
|
+
const reinstall = ` @crustjs/crust ships ${NODE_EXE_BACKEND_PACKAGES.join(" and ")} as optional dependencies, which package managers skip when optional dependencies are disabled or the installing node does not satisfy tsdown's engines.\n Reinstall @crustjs/crust with optional dependencies enabled under a node that can build Node binaries (e.g. the one on PATH here).`;
|
|
3716
|
+
const packageJsonPath = findInstalledPackageJson(dirname(installPath), "tsdown");
|
|
3717
|
+
if (packageJsonPath === null) throw new Error(`tsdown, which builds node standalone binaries, is not installed with crust (${installPath}).\n${reinstall}`);
|
|
3718
|
+
const exePackageJsonPath = findInstalledPackageJson(dirname(realpathSync(packageJsonPath)), "@tsdown/exe");
|
|
3719
|
+
if (exePackageJsonPath === null) throw new Error(`@tsdown/exe is not installed beside crust's tsdown (${packageJsonPath}).\n${reinstall}`);
|
|
3720
|
+
const readPackage = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
3721
|
+
const tsdownPackage = readPackage(packageJsonPath);
|
|
3722
|
+
for (const [name, path] of [["tsdown", packageJsonPath], ["@tsdown/exe", exePackageJsonPath]]) {
|
|
3723
|
+
const installed = name === "tsdown" ? tsdownPackage : readPackage(path);
|
|
3724
|
+
const version = isVersionedPackageJson(installed) ? installed.version : "without a version";
|
|
3725
|
+
const pinned = optionalDependencies[name];
|
|
3726
|
+
if (!(0, import_satisfies.default)(version, pinned)) throw new Error(`${name} ${version} (${path}) is not the ${name} ${pinned} that crust builds node standalone binaries with.\n${reinstall}`);
|
|
3727
|
+
}
|
|
3728
|
+
if (!isTsdownPackageJson(tsdownPackage)) throw new Error(`Unexpected tsdown package metadata in ${packageJsonPath}.\n${reinstall}`);
|
|
3729
|
+
const backend = {
|
|
3730
|
+
packageJsonPath,
|
|
3731
|
+
version: tsdownPackage.version,
|
|
3732
|
+
engines: tsdownPackage.engines.node
|
|
3733
|
+
};
|
|
3734
|
+
assertNodeExeBackendSupports(compiler, backend);
|
|
3735
|
+
const probe = await runGeneratedScript(compiler.runner, "probe", createNodeExeProbeScript(packageJsonPath), cwd);
|
|
3736
|
+
const seaMinVersion = probe.exitCode === 0 ? (0, import_valid.default)(probe.stdout.trim()) : null;
|
|
3737
|
+
if (seaMinVersion === null) {
|
|
3738
|
+
const output = processOutput(probe);
|
|
3739
|
+
throw new Error(`tsdown ${backend.version}'s executable builder could not be loaded with ${compiler.runner.command} (exit ${probe.exitCode}).\n${reinstall}${output ? `\n${output}` : ""}`);
|
|
3740
|
+
}
|
|
3741
|
+
const resolved = {
|
|
3742
|
+
...backend,
|
|
3743
|
+
seaMinVersion
|
|
3744
|
+
};
|
|
3745
|
+
assertNodeExeBackendSupports(compiler, resolved);
|
|
3746
|
+
return resolved;
|
|
3747
|
+
}
|
|
3748
|
+
/**
|
|
3749
|
+
* Select the Node binary compiler once for a build: the external `node`, its
|
|
3750
|
+
* version read in the project directory `cwd`, validated against the
|
|
3751
|
+
* project's `engines.node` and then the installed tsdown's requirements.
|
|
3752
|
+
* Pass it to {@link execNodeBinaryBuild} for every command and target.
|
|
3753
|
+
*/
|
|
3754
|
+
async function resolveNodeBinaryCompiler(userPackageJson, cwd) {
|
|
3755
|
+
const runner = resolveNodeBuildRunner();
|
|
3756
|
+
const compiler = {
|
|
3757
|
+
runtime: "node",
|
|
3758
|
+
runner,
|
|
3759
|
+
version: await readCompilerVersion("node", runner, cwd)
|
|
3760
|
+
};
|
|
3761
|
+
assertCompilerSatisfiesEngines(compiler, userPackageJson);
|
|
3762
|
+
return {
|
|
3763
|
+
...compiler,
|
|
3764
|
+
backend: await resolveNodeExeBackend(compiler, cwd)
|
|
3765
|
+
};
|
|
3766
|
+
}
|
|
3767
|
+
/** tsdown's executable target for a Node target, embedding exactly `nodeVersion`. */
|
|
3768
|
+
function nodeExeTarget(target, nodeVersion) {
|
|
3769
|
+
const { os, cpu } = NODE_TARGETS.info[target];
|
|
3770
|
+
return {
|
|
3771
|
+
platform: os === "win32" ? "win" : os,
|
|
3772
|
+
arch: cpu,
|
|
3773
|
+
nodeVersion
|
|
3774
|
+
};
|
|
3775
|
+
}
|
|
3776
|
+
/**
|
|
3777
|
+
* Script the selected Node runs to turn one Bun bundle into one target's
|
|
3778
|
+
* executable with tsdown's programmatic `build`. Every value is embedded via
|
|
3779
|
+
* JSON.stringify.
|
|
3780
|
+
*/
|
|
3781
|
+
function createNodeExeBuildScript(options) {
|
|
3782
|
+
return `// Generated by crust build for a Node standalone binary; deleted when the build finishes.
|
|
3783
|
+
import { createRequire } from "node:module";
|
|
3784
|
+
import { pathToFileURL } from "node:url";
|
|
3785
|
+
|
|
3786
|
+
const options = ${JSON.stringify(options)};
|
|
3787
|
+
const { build } = await import(pathToFileURL(createRequire(options.tsdownPackageJson).resolve("tsdown")).href);
|
|
3788
|
+
await build({
|
|
3789
|
+
config: false,
|
|
3790
|
+
cwd: options.cwd,
|
|
3791
|
+
entry: { app: options.entry },
|
|
3792
|
+
outDir: options.bundleDir,
|
|
3793
|
+
format: "esm",
|
|
3794
|
+
platform: "node",
|
|
3795
|
+
dts: false,
|
|
3796
|
+
// Rolldown reprints the Bun bundle, so it minifies again.
|
|
3797
|
+
minify: options.minify,
|
|
3798
|
+
logLevel: "warn",
|
|
3799
|
+
report: false,
|
|
3800
|
+
// Bun already inlined the PUBLIC_* constants; inline nothing from this environment.
|
|
3801
|
+
envPrefix: [],
|
|
3802
|
+
// Bun leaves only Node built-ins as imports; fail on anything else, which
|
|
3803
|
+
// an executable shipping no node_modules could not resolve.
|
|
3804
|
+
deps: { alwaysBundle: () => true, onlyBundle: false, onlyImport: [] },
|
|
3805
|
+
exe: { outDir: options.exeDir, fileName: "app", targets: [options.target] },
|
|
3806
|
+
});
|
|
3807
|
+
`;
|
|
3808
|
+
}
|
|
3809
|
+
/**
|
|
3810
|
+
* Build a single entry file into a Node standalone executable: Bun first
|
|
3811
|
+
* bundles it for Node exactly as a Node runtime package ({@link execScriptBuild}:
|
|
3812
|
+
* `PUBLIC_*` constants from `envFiles` and the environment, each module's own
|
|
3813
|
+
* `import.meta.main` and `require.main === module`, CommonJS and ESM inputs),
|
|
3814
|
+
* then tsdown's executable builder (Node SEA), run by the selected external
|
|
3815
|
+
* Node, turns that bundle into an executable embedding exactly its version
|
|
3816
|
+
* for `target`. tsdown takes a single entry per executable, so callers build
|
|
3817
|
+
* every command separately. Matching official Node binaries for targets are
|
|
3818
|
+
* downloaded, checksum-verified, and cached by @tsdown/exe. Builds for darwin
|
|
3819
|
+
* are only code-signed on a macOS host.
|
|
3820
|
+
*
|
|
3821
|
+
* @param compiler - The selected Node compiler (`resolveNodeBinaryCompiler`)
|
|
3822
|
+
* @param bunRunner - The Bun that bundles (`resolveBunBuildRunner`)
|
|
3823
|
+
* @param onWarning - Receives the backend's warnings (such as a skipped code
|
|
3824
|
+
* signature) after a successful build
|
|
3825
|
+
* @throws {Error} If the build fails
|
|
3826
|
+
*/
|
|
3827
|
+
async function execNodeBinaryBuild(entryPath, outfilePath, target, settings, compiler, bunRunner, onWarning = () => {}) {
|
|
3828
|
+
const { cwd, minify } = settings;
|
|
3829
|
+
const workDir = await mkdtemp(join(tmpdir(), "crust-node-exe-"));
|
|
3830
|
+
try {
|
|
3831
|
+
const scriptPath = join(workDir, "build.mjs");
|
|
3832
|
+
const bunBundle = join(workDir, "app.mjs");
|
|
3833
|
+
const exeDir = join(workDir, "exe");
|
|
3834
|
+
await execScriptBuild("node", resolve(cwd, entryPath), bunBundle, {
|
|
3835
|
+
...settings,
|
|
3836
|
+
bunPlugins: [],
|
|
3837
|
+
external: []
|
|
3838
|
+
}, bunRunner);
|
|
3839
|
+
await writeFile(scriptPath, createNodeExeBuildScript({
|
|
3840
|
+
tsdownPackageJson: compiler.backend.packageJsonPath,
|
|
3841
|
+
cwd,
|
|
3842
|
+
entry: bunBundle,
|
|
3843
|
+
bundleDir: join(workDir, "bundle"),
|
|
3844
|
+
exeDir,
|
|
3845
|
+
minify,
|
|
3846
|
+
target: nodeExeTarget(target, compiler.version)
|
|
3847
|
+
}));
|
|
3848
|
+
const output = await runBuildProcess(compiler.runner, [scriptPath], outfilePath, cwd);
|
|
3849
|
+
const produced = await readdir(exeDir);
|
|
3850
|
+
if (produced.length !== 1) throw new Error(`Build failed for ${outfilePath}: expected one executable from tsdown, found ${JSON.stringify(produced)}.`);
|
|
3851
|
+
await copyFile(join(exeDir, produced[0]), outfilePath);
|
|
3852
|
+
if (output) onWarning(output);
|
|
3853
|
+
} finally {
|
|
3854
|
+
await rm(workDir, {
|
|
3855
|
+
recursive: true,
|
|
3856
|
+
force: true,
|
|
3857
|
+
maxRetries: 3,
|
|
3858
|
+
retryDelay: 100
|
|
3859
|
+
});
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
function createNodeExeProvisionScript(tsdownPackageJson, targets) {
|
|
3863
|
+
return `// Generated by crust build to provision Node binaries for targets; deleted when it exits.
|
|
3864
|
+
import { realpathSync } from "node:fs";
|
|
3865
|
+
import { createRequire } from "node:module";
|
|
3866
|
+
import { pathToFileURL } from "node:url";
|
|
3867
|
+
|
|
3868
|
+
const exe = createRequire(realpathSync(${JSON.stringify(tsdownPackageJson)})).resolve("@tsdown/exe");
|
|
3869
|
+
const { resolveNodeBinary } = await import(pathToFileURL(exe).href);
|
|
3870
|
+
for (const { name, target } of ${JSON.stringify(targets)}) {
|
|
3871
|
+
try {
|
|
3872
|
+
await resolveNodeBinary(target, {});
|
|
3873
|
+
} catch (error) {
|
|
3874
|
+
console.error(name + ": " + error.message + (error.cause?.message ? "\\n" + error.cause.message : ""));
|
|
3875
|
+
process.exit(1);
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
`;
|
|
3879
|
+
}
|
|
3880
|
+
/**
|
|
3881
|
+
* Provisions the Node binary every target embeds before anything is staged,
|
|
3882
|
+
* with the selected Node and the call tsdown's executable builder makes per
|
|
3883
|
+
* target (@tsdown/exe's `resolveNodeBinary`): an uncached official archive of
|
|
3884
|
+
* exactly `compiler.version` is downloaded, checksum-verified, unpacked with
|
|
3885
|
+
* the tool that target's archive needs on this host, and cached, where
|
|
3886
|
+
* {@link execNodeBinaryBuild} then finds it. A missing unpacking tool, a failed
|
|
3887
|
+
* download, or a checksum mismatch therefore fails here, before `.crust/` is
|
|
3888
|
+
* replaced; cached targets need nothing.
|
|
3889
|
+
*
|
|
3890
|
+
* @throws {Error} If a target cannot be provisioned
|
|
3891
|
+
*/
|
|
3892
|
+
async function provisionNodeExeTargets(targets, cwd, compiler) {
|
|
3893
|
+
const provision = await runGeneratedScript(compiler.runner, "provision", createNodeExeProvisionScript(compiler.backend.packageJsonPath, targets.map((name) => ({
|
|
3894
|
+
name,
|
|
3895
|
+
target: nodeExeTarget(name, compiler.version)
|
|
3896
|
+
}))), cwd);
|
|
3897
|
+
if (provision.exitCode !== 0) {
|
|
3898
|
+
const output = processOutput(provision);
|
|
3899
|
+
throw new Error(`Could not provision the Node ${compiler.version} binary that node standalone binaries embed${output ? `:\n${output}` : "."}\n tsdown downloads each target's official Node archive once and unpacks it with tar (GNU tar also needs xz for linux targets), or unzip for win targets on non-Windows hosts.
|
|
3900
|
+
Install the missing tool, or build only targets this machine can provision (--target).`);
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
/** Runs a generated `<name>.mjs` with `runner` from a temporary directory that is removed afterwards. */
|
|
3904
|
+
async function runGeneratedScript(runner, name, source, cwd) {
|
|
3905
|
+
const workDir = await mkdtemp(join(tmpdir(), `crust-node-exe-${name}-`));
|
|
3906
|
+
try {
|
|
3907
|
+
const scriptPath = join(workDir, `${name}.mjs`);
|
|
3908
|
+
await writeFile(scriptPath, source);
|
|
3909
|
+
return await runProcess(runner.command, [scriptPath], {
|
|
3910
|
+
env: runner.env,
|
|
3911
|
+
cwd,
|
|
3912
|
+
stdio: "collect"
|
|
3913
|
+
});
|
|
3914
|
+
} finally {
|
|
3915
|
+
await rm(workDir, {
|
|
3916
|
+
recursive: true,
|
|
3917
|
+
force: true
|
|
4183
3918
|
});
|
|
4184
|
-
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).`);
|
|
4185
|
-
names.push(posixName);
|
|
4186
|
-
includeDirs.push(posixName);
|
|
4187
3919
|
}
|
|
4188
|
-
return includeDirs;
|
|
4189
3920
|
}
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
3921
|
+
//#endregion
|
|
3922
|
+
//#region ../core/dist/index.js
|
|
3923
|
+
/** Mint an Extension identity from any non-blank, trimmed string. */
|
|
3924
|
+
function defineExtensionId(id) {
|
|
3925
|
+
if (!id || id !== id.trim()) throw new CrustError("DEFINITION", "Extension id must be a non-empty, trimmed string", {
|
|
3926
|
+
subject: "extension",
|
|
3927
|
+
reason: "empty-id"
|
|
3928
|
+
});
|
|
3929
|
+
return id;
|
|
3930
|
+
}
|
|
3931
|
+
//#endregion
|
|
3932
|
+
//#region src/snapshot.ts
|
|
3933
|
+
const SNAPSHOT_TIMEOUT_MS = 3e4;
|
|
3934
|
+
const SNAPSHOT_FORWARDED_SIGNALS = [
|
|
3935
|
+
"SIGINT",
|
|
3936
|
+
"SIGTERM",
|
|
3937
|
+
"SIGHUP",
|
|
3938
|
+
"SIGQUIT"
|
|
3939
|
+
];
|
|
3940
|
+
function isBuildReport(value) {
|
|
3941
|
+
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"));
|
|
3942
|
+
}
|
|
3943
|
+
/** Checks only the root `meta.name`, the one snapshot field crust reads. */
|
|
3944
|
+
function isCommandSnapshot(value) {
|
|
3945
|
+
return isJsonObject(value) && value.meta !== void 0 && isJsonObject(value.meta) && typeof value.meta.name === "string";
|
|
4205
3946
|
}
|
|
4206
3947
|
/**
|
|
4207
|
-
*
|
|
4208
|
-
*
|
|
4209
|
-
*
|
|
4210
|
-
*
|
|
4211
|
-
*
|
|
4212
|
-
*
|
|
3948
|
+
* Prepare a CLI entry's Command Snapshot in the user's project context.
|
|
3949
|
+
*
|
|
3950
|
+
* The entry runs as a subprocess with `CRUST_INTERNAL_SNAPSHOT_PATH` pointing
|
|
3951
|
+
* to a temporary file. `.execute()` validates and writes the command graph and
|
|
3952
|
+
* adjacent Build Report, then exits before any following entrypoint code can run.
|
|
3953
|
+
*
|
|
3954
|
+
* Runs with the same bun as compilation (`resolveBunBuildRunner`, or the
|
|
3955
|
+
* pinned Bun `runner`): bun on PATH, or a compiled standalone crust executable
|
|
3956
|
+
* as `BUN_BE_BUN=1`, so arbitrary `.ts` entries run without a separate `bun`
|
|
3957
|
+
* install.
|
|
3958
|
+
*
|
|
3959
|
+
* After `timeoutMs`, the entry's POSIX process group or live Windows process
|
|
3960
|
+
* tree is killed and preparation fails, even if a descendant still holds stderr.
|
|
3961
|
+
* On POSIX, SIGINT, SIGTERM, SIGHUP, SIGQUIT or a synchronous exit of this
|
|
3962
|
+
* process also kills the entry's group, and whatever remains of the group is
|
|
3963
|
+
* killed once preparation settles.
|
|
4213
3964
|
*/
|
|
4214
|
-
function
|
|
4215
|
-
const
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
3965
|
+
async function buildEntrypoint(entryPath, outDir, envFiles, io, cwd, runner = resolveBunBuildRunner(), timeoutMs = SNAPSHOT_TIMEOUT_MS) {
|
|
3966
|
+
const absoluteEntry = resolve(entryPath);
|
|
3967
|
+
const snapshotDir = await mkdtemp(join(tmpdir(), "crust-snapshot-"));
|
|
3968
|
+
const snapshotPath = join(snapshotDir, "command.json");
|
|
3969
|
+
const buildReportPath = join(snapshotDir, "build-report.json");
|
|
3970
|
+
try {
|
|
3971
|
+
const detached = process.platform !== "win32";
|
|
3972
|
+
const proc = spawn(runner.command, [...toBunEnvFileArgs(envFiles), absoluteEntry], {
|
|
3973
|
+
env: {
|
|
3974
|
+
...runner.env,
|
|
3975
|
+
[SNAPSHOT_PATH_ENV]: snapshotPath,
|
|
3976
|
+
[BUILD_OUT_DIR_ENV]: resolve(outDir)
|
|
3977
|
+
},
|
|
3978
|
+
cwd,
|
|
3979
|
+
stdio: [
|
|
3980
|
+
"ignore",
|
|
3981
|
+
"ignore",
|
|
3982
|
+
"pipe"
|
|
3983
|
+
],
|
|
3984
|
+
detached
|
|
3985
|
+
});
|
|
3986
|
+
let stop;
|
|
3987
|
+
const stopped = new Promise((_, reject) => {
|
|
3988
|
+
stop = (message) => {
|
|
3989
|
+
let failure = new Error(message);
|
|
3990
|
+
try {
|
|
3991
|
+
killProcessTree(proc);
|
|
3992
|
+
} catch (cause) {
|
|
3993
|
+
failure = new Error(`${message}\n Process-tree cleanup failed.`, { cause });
|
|
3994
|
+
}
|
|
3995
|
+
proc.stderr.destroy();
|
|
3996
|
+
reject(failure);
|
|
3997
|
+
};
|
|
3998
|
+
});
|
|
3999
|
+
const timer = setTimeout(() => stop(`Command Snapshot preparation timed out after ${timeoutMs / 1e3}s.\n An Extension build hook may be hanging. Use --no-validate to skip entry preparation and build hooks.`), timeoutMs);
|
|
4000
|
+
const onSignal = (signal) => {
|
|
4001
|
+
removeLifetimeListeners();
|
|
4002
|
+
stop(`Command Snapshot preparation was interrupted by ${signal}.`);
|
|
4003
|
+
if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
|
|
4004
|
+
};
|
|
4005
|
+
const killRemainingGroup = () => {
|
|
4006
|
+
try {
|
|
4007
|
+
killProcessTree(proc);
|
|
4008
|
+
} catch {}
|
|
4009
|
+
};
|
|
4010
|
+
const removeLifetimeListeners = () => {
|
|
4011
|
+
for (const signal of SNAPSHOT_FORWARDED_SIGNALS) process.removeListener(signal, onSignal);
|
|
4012
|
+
process.removeListener("exit", killRemainingGroup);
|
|
4013
|
+
};
|
|
4014
|
+
if (detached) {
|
|
4015
|
+
for (const signal of SNAPSHOT_FORWARDED_SIGNALS) process.prependListener(signal, onSignal);
|
|
4016
|
+
process.on("exit", killRemainingGroup);
|
|
4017
|
+
}
|
|
4018
|
+
const [rawStderr, [exitCode]] = await Promise.race([Promise.all([text(proc.stderr), once(proc, "close")]), stopped]).finally(() => {
|
|
4019
|
+
clearTimeout(timer);
|
|
4020
|
+
removeLifetimeListeners();
|
|
4021
|
+
if (detached) killRemainingGroup();
|
|
4022
|
+
});
|
|
4023
|
+
const stderr = rawStderr.trim();
|
|
4024
|
+
if (proc.signalCode !== null) throw new Error(`Command Snapshot preparation was killed by ${proc.signalCode}.${stderr ? `\n${stderr}` : ""}`);
|
|
4025
|
+
if (exitCode !== 0) throw new Error(stderr || "Command Snapshot preparation failed");
|
|
4026
|
+
if (stderr) {
|
|
4027
|
+
const styled = stderr.split("\n").map((line) => line.startsWith("Warning:") ? `${yellow("Warning:")}${line.slice(8)}` : line).join("\n");
|
|
4028
|
+
io.stderr(styled);
|
|
4029
|
+
}
|
|
4030
|
+
let serialized;
|
|
4031
|
+
try {
|
|
4032
|
+
serialized = await readFile(snapshotPath, "utf8");
|
|
4033
|
+
} catch (error) {
|
|
4034
|
+
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 });
|
|
4035
|
+
throw error;
|
|
4036
|
+
}
|
|
4037
|
+
let snapshot;
|
|
4038
|
+
try {
|
|
4039
|
+
const parsed = JSON.parse(serialized);
|
|
4040
|
+
if (!isCommandSnapshot(parsed)) throw new Error("Expected a root command with a string meta.name.");
|
|
4041
|
+
snapshot = parsed;
|
|
4042
|
+
} catch (error) {
|
|
4043
|
+
throw new Error(`Entry produced an invalid Command Snapshot.\n Ensure ${absoluteEntry} uses a compatible @crustjs/core version.`, { cause: error });
|
|
4044
|
+
}
|
|
4045
|
+
let serializedBuild;
|
|
4046
|
+
try {
|
|
4047
|
+
serializedBuild = await readFile(buildReportPath, "utf8");
|
|
4048
|
+
} catch (error) {
|
|
4049
|
+
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 });
|
|
4050
|
+
throw error;
|
|
4051
|
+
}
|
|
4052
|
+
try {
|
|
4053
|
+
const build = JSON.parse(serializedBuild);
|
|
4054
|
+
if (!isBuildReport(build)) throw new Error("Expected extensions with string ids and files arrays.");
|
|
4055
|
+
for (const extension of build.extensions) {
|
|
4056
|
+
defineExtensionId(extension.id);
|
|
4057
|
+
for (const file of extension.files) {
|
|
4058
|
+
const path = resolve(outDir, file);
|
|
4059
|
+
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}`);
|
|
4060
|
+
}
|
|
4236
4061
|
}
|
|
4062
|
+
return {
|
|
4063
|
+
snapshot,
|
|
4064
|
+
build
|
|
4065
|
+
};
|
|
4066
|
+
} catch (error) {
|
|
4067
|
+
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 });
|
|
4237
4068
|
}
|
|
4238
|
-
}
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
names: [],
|
|
4247
|
-
manPages: []
|
|
4248
|
-
};
|
|
4249
|
-
const names = readdirSync(artifactOutDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
4250
|
-
const binName = names.find((name) => name.toLowerCase() === "bin");
|
|
4251
|
-
if (binName !== void 0) throw new Error(`Artifact directory "${binName}" in ${artifactOutDir} conflicts with the generated npm bin directory.\n Emit build artifacts under a different top-level name.`);
|
|
4252
|
-
return {
|
|
4253
|
-
names,
|
|
4254
|
-
manPages: names.includes("man") ? readdirSync(join(artifactOutDir, "man"), { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => entry.name).sort() : []
|
|
4255
|
-
};
|
|
4069
|
+
} finally {
|
|
4070
|
+
await rm(snapshotDir, {
|
|
4071
|
+
recursive: true,
|
|
4072
|
+
force: true,
|
|
4073
|
+
maxRetries: 3,
|
|
4074
|
+
retryDelay: 100
|
|
4075
|
+
});
|
|
4076
|
+
}
|
|
4256
4077
|
}
|
|
4257
4078
|
//#endregion
|
|
4258
|
-
//#region src/
|
|
4079
|
+
//#region src/build.ts
|
|
4080
|
+
function readUserPackageJson(cwd) {
|
|
4081
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
4082
|
+
if (!existsSync(packageJsonPath)) throw new Error(`package.json not found in ${cwd}\n crust build requires a package.json with name and version fields.`);
|
|
4083
|
+
let packageJson;
|
|
4084
|
+
try {
|
|
4085
|
+
packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
4086
|
+
} catch (error) {
|
|
4087
|
+
throw new Error(`Failed to parse package.json in ${cwd}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
4088
|
+
}
|
|
4089
|
+
if (!isJsonObject(packageJson)) throw new Error(`package.json in ${cwd} must contain a JSON object.`);
|
|
4090
|
+
return packageJson;
|
|
4091
|
+
}
|
|
4259
4092
|
/** Also mirrored by `schema/package.json`; build.test.ts guards against drift. */
|
|
4260
4093
|
const CRUST_CONFIG_KEYS = [
|
|
4261
4094
|
"runtime",
|
|
4262
4095
|
"artifact",
|
|
4263
4096
|
"targets",
|
|
4264
4097
|
"bunPlugins",
|
|
4265
|
-
"include"
|
|
4098
|
+
"include",
|
|
4099
|
+
"external"
|
|
4266
4100
|
];
|
|
4267
4101
|
function isBuildRuntime(value) {
|
|
4268
4102
|
return typeof value === "string" && BUILD_RUNTIMES.some((runtime) => runtime === value);
|
|
@@ -4277,7 +4111,7 @@ function isStringArray(value) {
|
|
|
4277
4111
|
return Array.isArray(value) && value.every(isString);
|
|
4278
4112
|
}
|
|
4279
4113
|
function readCrustConfig(pkg) {
|
|
4280
|
-
if (pkg
|
|
4114
|
+
if (pkg.crust === void 0) return {};
|
|
4281
4115
|
const crust = pkg.crust;
|
|
4282
4116
|
const allowed = `Allowed keys: ${CRUST_CONFIG_KEYS.join(", ")}`;
|
|
4283
4117
|
if (!isJsonObject(crust)) throw new Error(`package.json crust must be an object. ${allowed}`);
|
|
@@ -4304,12 +4138,54 @@ function readCrustConfig(pkg) {
|
|
|
4304
4138
|
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\"].");
|
|
4305
4139
|
config.include = crust.include;
|
|
4306
4140
|
}
|
|
4141
|
+
const external = crust.external;
|
|
4142
|
+
if (external !== void 0) {
|
|
4143
|
+
if (!isStringArray(external)) throw new Error("package.json crust.external must be an array of package names from dependencies, e.g. [\"better-sqlite3\"].");
|
|
4144
|
+
const duplicate = external.find((name, index) => external.indexOf(name) !== index);
|
|
4145
|
+
if (duplicate !== void 0) throw new Error(`package.json crust.external lists ${JSON.stringify(duplicate)} more than once.`);
|
|
4146
|
+
config.external = external;
|
|
4147
|
+
}
|
|
4307
4148
|
return config;
|
|
4308
4149
|
}
|
|
4309
4150
|
function hasDependency(pkg, name) {
|
|
4310
|
-
if (!isJsonObject(pkg)) return false;
|
|
4311
4151
|
return [pkg.dependencies, pkg.devDependencies].some((deps) => deps !== void 0 && isJsonObject(deps) && name in deps);
|
|
4312
4152
|
}
|
|
4153
|
+
const NON_DEPENDENCY_SECTIONS = [
|
|
4154
|
+
"devDependencies",
|
|
4155
|
+
"optionalDependencies",
|
|
4156
|
+
"peerDependencies"
|
|
4157
|
+
];
|
|
4158
|
+
/**
|
|
4159
|
+
* `crust.external` names ship as root `dependencies` of a Node or Bun runtime
|
|
4160
|
+
* package, so each needs a publishable `dependencies` range. Crust packages
|
|
4161
|
+
* stay bundled: core reads `process.env.CRUST_INTERNAL_BUILD`, which the
|
|
4162
|
+
* bundler defines only in bundled code, and an external `@crustjs/*` package
|
|
4163
|
+
* would load a second copy of core.
|
|
4164
|
+
*/
|
|
4165
|
+
function validateExternal(external, pkg, runtime, artifact) {
|
|
4166
|
+
if (external.length === 0) return;
|
|
4167
|
+
if (artifact === "binary") throw new Error("package.json crust.external is not supported for standalone binaries (artifact binary).\n A binary bundles every dependency; remove crust.external or use artifact package.");
|
|
4168
|
+
if (runtime === "deno") throw new Error("package.json crust.external is not supported with the deno runtime.\n crust.external applies to Node and Bun runtime packages; remove crust.external or set crust.runtime to node or bun.");
|
|
4169
|
+
const dependencies = pkg.dependencies !== void 0 && isJsonObject(pkg.dependencies) ? pkg.dependencies : {};
|
|
4170
|
+
const rejectCrustPackage = (quoted, detail) => {
|
|
4171
|
+
throw new Error(`package.json crust.external cannot name ${quoted}${detail}.\n Crust packages must stay bundled: core detects a packaged build through a define the bundler injects, and an external Crust package would load a second copy of core.`);
|
|
4172
|
+
};
|
|
4173
|
+
for (const name of external) {
|
|
4174
|
+
const quoted = JSON.stringify(name);
|
|
4175
|
+
if (name.startsWith("@crustjs/")) rejectCrustPackage(quoted, "");
|
|
4176
|
+
if (!Object.hasOwn(dependencies, name)) {
|
|
4177
|
+
const sections = NON_DEPENDENCY_SECTIONS.filter((section) => {
|
|
4178
|
+
const deps = pkg[section];
|
|
4179
|
+
return deps !== void 0 && isJsonObject(deps) && Object.hasOwn(deps, name);
|
|
4180
|
+
});
|
|
4181
|
+
throw new Error(sections.length > 0 ? `package.json crust.external entry ${quoted} is in ${sections.join(" and ")}, not dependencies.\n Only dependencies ship with the staged package; move ${quoted} to dependencies.` : `package.json crust.external entry ${quoted} is not in package.json dependencies.\n crust.external names packages from dependencies, which ship with the staged package; add ${quoted} to dependencies.`);
|
|
4182
|
+
}
|
|
4183
|
+
const range = dependencies[name];
|
|
4184
|
+
assertPublishableRange(range, `package.json dependencies[${quoted}]`);
|
|
4185
|
+
const aliased = /^npm:(@crustjs\/[^@]+)/.exec(range)?.[1];
|
|
4186
|
+
if (aliased !== void 0) rejectCrustPackage(quoted, ` (an alias of ${aliased})`);
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4313
4189
|
const DENO_CONFIG_FILES = ["deno.json", "deno.jsonc"];
|
|
4314
4190
|
/**
|
|
4315
4191
|
* package.json `crust.runtime` > inference > Bun. Inference uses only
|
|
@@ -4327,7 +4203,7 @@ function resolveBuildRuntime(pkg, config, cwd) {
|
|
|
4327
4203
|
runtime: "deno",
|
|
4328
4204
|
source: `inferred from ${denoConfig}`
|
|
4329
4205
|
};
|
|
4330
|
-
if (
|
|
4206
|
+
if (hasDependency(pkg, "@types/node") && !hasDependency(pkg, "@types/bun")) return {
|
|
4331
4207
|
runtime: "node",
|
|
4332
4208
|
source: "inferred from @types/node"
|
|
4333
4209
|
};
|
|
@@ -4357,12 +4233,6 @@ function resolveEnvFilePaths(cwd, envFiles) {
|
|
|
4357
4233
|
}
|
|
4358
4234
|
/** Entry built when package.json has no `bin`. */
|
|
4359
4235
|
const DEFAULT_ENTRY = "src/cli.ts";
|
|
4360
|
-
/**
|
|
4361
|
-
* Command names become `bin/<command>.js`, `<command>-<target>` binary
|
|
4362
|
-
* filenames, and launcher text, so they are restricted to a filename-safe
|
|
4363
|
-
* subset of what npm accepts: no separators, dots-only names, or leading `.`/`-`.
|
|
4364
|
-
*/
|
|
4365
|
-
const COMMAND_NAME_PATTERN = /^[A-Za-z0-9_~][A-Za-z0-9._~-]*$/;
|
|
4366
4236
|
const BIN_EXAMPLE = `{ "my-cli": ${JSON.stringify(DEFAULT_ENTRY)} }`;
|
|
4367
4237
|
/**
|
|
4368
4238
|
* A `bin` value as an absolute path. The path must be relative and lexically
|
|
@@ -4388,19 +4258,18 @@ function resolveEntryPath(cwd, command, source) {
|
|
|
4388
4258
|
* `tool` would be the same `bin/` file on a case-insensitive filesystem.
|
|
4389
4259
|
*/
|
|
4390
4260
|
function resolveBinEntries(cwd, pkg) {
|
|
4391
|
-
const
|
|
4392
|
-
const bin = packageJson.bin;
|
|
4261
|
+
const bin = pkg.bin;
|
|
4393
4262
|
let declared;
|
|
4394
4263
|
if (bin === void 0 || isString(bin)) {
|
|
4395
|
-
const name =
|
|
4264
|
+
const name = pkg.name;
|
|
4396
4265
|
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}.`);
|
|
4397
|
-
declared = [[name.replace(/^@[^/]+\//, ""), bin ??
|
|
4266
|
+
declared = [[name.replace(/^@[^/]+\//, ""), bin ?? DEFAULT_ENTRY]];
|
|
4398
4267
|
} else if (isJsonObject(bin) && Object.keys(bin).length > 0) declared = Object.entries(bin);
|
|
4399
4268
|
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}.`);
|
|
4400
4269
|
const commandByLowerName = /* @__PURE__ */ new Map();
|
|
4401
4270
|
const commandByRealPath = /* @__PURE__ */ new Map();
|
|
4402
4271
|
return declared.map(([command, source]) => {
|
|
4403
|
-
if (!
|
|
4272
|
+
if (!isInstalledCommandName(command)) throw new Error(`package.json bin key ${JSON.stringify(command)} is not a valid command name.\n Use ${INSTALLED_COMMAND_NAME_RULE}.`);
|
|
4404
4273
|
const sameName = commandByLowerName.get(command.toLowerCase());
|
|
4405
4274
|
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.`);
|
|
4406
4275
|
commandByLowerName.set(command.toLowerCase(), command);
|
|
@@ -4433,27 +4302,26 @@ function resolveArtifact(option, config, runtime) {
|
|
|
4433
4302
|
binary: standalone executables in per-platform npm packages behind a Node launcher.
|
|
4434
4303
|
Earlier versions built a ${previous} for the ${runtime} runtime implicitly; set "artifact": "${previous}" to keep that output.`);
|
|
4435
4304
|
}
|
|
4436
|
-
return
|
|
4437
|
-
runtime,
|
|
4438
|
-
artifact
|
|
4439
|
-
};
|
|
4305
|
+
return artifact;
|
|
4440
4306
|
}
|
|
4441
4307
|
function planBuild(options, cwd) {
|
|
4442
4308
|
const userPackageJson = readUserPackageJson(cwd);
|
|
4443
4309
|
const config = readCrustConfig(userPackageJson);
|
|
4444
4310
|
const { runtime, source: runtimeSource } = resolveBuildRuntime(userPackageJson, config, cwd);
|
|
4445
|
-
const
|
|
4311
|
+
const artifact = resolveArtifact(options.artifact, config, runtime);
|
|
4446
4312
|
const entries = resolveBinEntries(cwd, userPackageJson);
|
|
4447
4313
|
validatePackageIdentity(userPackageJson, "package.json");
|
|
4448
4314
|
const envFiles = resolveEnvFilePaths(cwd, options.envFiles);
|
|
4449
4315
|
const bunPlugins = config.bunPlugins ?? [];
|
|
4450
|
-
|
|
4451
|
-
if (
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
if (runtime === "deno" &&
|
|
4316
|
+
const external = config.external ?? [];
|
|
4317
|
+
if (artifact === "package" && options.targets?.length) throw new Error("--target cannot be used with runtime packages (artifact package).\n A runtime package is one portable JavaScript bundle; drop --target or use artifact binary.");
|
|
4318
|
+
if (artifact === "package" && config.targets !== void 0) throw new Error("package.json crust.targets is not supported for runtime packages (artifact package).\n A runtime package is one portable JavaScript bundle; remove crust.targets or set \"artifact\": \"binary\".");
|
|
4319
|
+
const denoTool = artifact === "binary" ? "deno compile" : "deno bundle";
|
|
4320
|
+
if (runtime === "deno" && options.minify) throw new Error(artifact === "binary" ? "--minify is not supported with the deno runtime.\n deno compile has no minification step; drop the flag." : "--minify is not supported with the deno runtime.\n crust does not minify Deno runtime packages; drop the flag.");
|
|
4321
|
+
if (runtime === "deno" && envFiles.length > 0) throw new Error(artifact === "binary" ? "--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)." : "--env-file is not supported with the deno runtime.\n deno bundle has no PUBLIC_* build-time constants. Load configuration at runtime instead (e.g. deno run --env-file).");
|
|
4455
4322
|
if (runtime === "deno" && bunPlugins.length > 0) throw new Error(`package.json crust.bunPlugins is not supported with the deno runtime.\n ${denoTool} has no Bun bundler; remove crust.bunPlugins or set crust.runtime to bun.`);
|
|
4456
|
-
if (runtime === "node" &&
|
|
4323
|
+
if (runtime === "node" && artifact === "binary" && bunPlugins.length > 0) throw new Error("package.json crust.bunPlugins is not supported for node standalone binaries.\n Remove crust.bunPlugins, or use artifact package or the bun runtime.");
|
|
4324
|
+
validateExternal(external, userPackageJson, runtime, artifact);
|
|
4457
4325
|
const stageDir = resolve(cwd, CRUST_DIR);
|
|
4458
4326
|
const common = {
|
|
4459
4327
|
cwd,
|
|
@@ -4463,33 +4331,33 @@ function planBuild(options, cwd) {
|
|
|
4463
4331
|
envFiles,
|
|
4464
4332
|
bunPlugins,
|
|
4465
4333
|
include: config.include ?? [],
|
|
4334
|
+
external,
|
|
4466
4335
|
outDir: join(stageDir, "artifacts"),
|
|
4467
4336
|
stageDir,
|
|
4468
4337
|
validate: options.validate ?? true,
|
|
4469
4338
|
minify: runtime === "deno" ? false : options.minify ?? true
|
|
4470
4339
|
};
|
|
4471
|
-
if (
|
|
4340
|
+
if (artifact === "package") return {
|
|
4472
4341
|
...common,
|
|
4473
|
-
runtime
|
|
4474
|
-
artifact
|
|
4342
|
+
runtime,
|
|
4343
|
+
artifact
|
|
4475
4344
|
};
|
|
4476
4345
|
const targetInputs = options.targets?.length ? options.targets : config.targets;
|
|
4477
|
-
|
|
4478
|
-
if (binaryRuntime === "bun") return {
|
|
4346
|
+
if (runtime === "bun") return {
|
|
4479
4347
|
...common,
|
|
4480
|
-
runtime
|
|
4348
|
+
runtime,
|
|
4481
4349
|
artifact,
|
|
4482
4350
|
targets: resolveTargets(BUN_TARGETS, targetInputs)
|
|
4483
4351
|
};
|
|
4484
|
-
if (
|
|
4352
|
+
if (runtime === "node") return {
|
|
4485
4353
|
...common,
|
|
4486
|
-
runtime
|
|
4354
|
+
runtime,
|
|
4487
4355
|
artifact,
|
|
4488
4356
|
targets: resolveTargets(NODE_TARGETS, targetInputs)
|
|
4489
4357
|
};
|
|
4490
4358
|
return {
|
|
4491
4359
|
...common,
|
|
4492
|
-
runtime
|
|
4360
|
+
runtime,
|
|
4493
4361
|
artifact,
|
|
4494
4362
|
targets: resolveTargets(DENO_TARGETS, targetInputs)
|
|
4495
4363
|
};
|
|
@@ -4516,10 +4384,10 @@ async function selectCompilers(plan, io) {
|
|
|
4516
4384
|
}
|
|
4517
4385
|
if (plan.artifact === "package") {
|
|
4518
4386
|
const runner = resolveBunBuildRunner();
|
|
4519
|
-
const
|
|
4387
|
+
const target = plan.runtime === "bun" ? "bun" : "node";
|
|
4520
4388
|
return {
|
|
4521
4389
|
snapshotRunner: runner,
|
|
4522
|
-
stage: (reports) => runDistributeBuild(plan, { execute: (entry, outfile) =>
|
|
4390
|
+
stage: (reports) => runDistributeBuild(plan, { execute: (entry, outfile) => execScriptBuild(target, entry, outfile, plan, runner) }, io, reports)
|
|
4523
4391
|
};
|
|
4524
4392
|
}
|
|
4525
4393
|
const printCompiler = (compiler) => io.stdout(`${dim("Compiler:")} ${compiler.runtime} ${compiler.version} ${dim(`(${compiler.runner.command})`)}`);
|
|
@@ -4532,7 +4400,7 @@ async function selectCompilers(plan, io) {
|
|
|
4532
4400
|
table: NODE_TARGETS,
|
|
4533
4401
|
targets: plan.targets,
|
|
4534
4402
|
embeddedRuntimeVersion: compiler.version,
|
|
4535
|
-
execute: (entry, outfile, target) => execNodeBinaryBuild(entry, outfile,
|
|
4403
|
+
execute: (entry, outfile, target) => execNodeBinaryBuild(entry, outfile, target, plan, compiler, bunRunner, io.stderr)
|
|
4536
4404
|
};
|
|
4537
4405
|
return {
|
|
4538
4406
|
snapshotRunner: bunRunner,
|
|
@@ -4542,12 +4410,12 @@ async function selectCompilers(plan, io) {
|
|
|
4542
4410
|
const compiler = await resolveBinaryCompiler(plan.runtime, plan.userPackageJson, plan.cwd);
|
|
4543
4411
|
printCompiler(compiler);
|
|
4544
4412
|
if (plan.runtime === "bun") {
|
|
4545
|
-
assertTargetsBuildableWithoutBun(plan.targets,
|
|
4413
|
+
assertTargetsBuildableWithoutBun(plan.targets, compiler.runner);
|
|
4546
4414
|
const distribution = {
|
|
4547
4415
|
table: BUN_TARGETS,
|
|
4548
4416
|
targets: plan.targets,
|
|
4549
4417
|
embeddedRuntimeVersion: compiler.version,
|
|
4550
|
-
execute: (entry, outfile, target) => execBuild(entry, outfile,
|
|
4418
|
+
execute: (entry, outfile, target) => execBuild(entry, outfile, target, plan, compiler.runner)
|
|
4551
4419
|
};
|
|
4552
4420
|
return {
|
|
4553
4421
|
snapshotRunner: compiler.runner,
|
|
@@ -4598,12 +4466,13 @@ async function prepareEntries(plan, io, runner) {
|
|
|
4598
4466
|
* `bin` entry, each the command's bundle (artifact `package`) or a Node
|
|
4599
4467
|
* launcher (artifact `binary`), plus for binaries one platform package per
|
|
4600
4468
|
* target holding one executable per command. Command names and source entries
|
|
4601
|
-
* come from `bin`; the runtime, artifact, Bun plugins,
|
|
4602
|
-
* from package.json `crust`. Bundling and Command
|
|
4603
|
-
* subprocesses (bun on PATH, or the running Bun
|
|
4604
|
-
* under Node as well when Bun is installed; Deno
|
|
4605
|
-
* packages (experimental, deno 2.5.0 or newer) need
|
|
4606
|
-
* binaries a node on PATH that Crust's tsdown can build
|
|
4469
|
+
* come from `bin`; the runtime, artifact, Bun plugins, extra directories, and
|
|
4470
|
+
* external dependencies from package.json `crust`. Bundling and Command
|
|
4471
|
+
* Snapshots run in bun subprocesses (bun on PATH, or the running Bun
|
|
4472
|
+
* executable), so this works under Node as well when Bun is installed; Deno
|
|
4473
|
+
* binaries and Deno runtime packages (experimental, deno 2.5.0 or newer) need
|
|
4474
|
+
* deno on PATH, and Node binaries a node on PATH that Crust's tsdown can build
|
|
4475
|
+
* executables with.
|
|
4607
4476
|
*
|
|
4608
4477
|
* Throws on any failure. Planning and compiler-selection failures (bad
|
|
4609
4478
|
* options or package.json, a missing or hung compiler, an engines mismatch) leave the
|