@crustjs/crust-linux-arm64 0.5.3 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/crust-bun-linux-arm64 +1 -1
- package/bin/dist/index.d.ts +2 -3
- package/bin/dist/index.js +930 -1130
- 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,30 @@ 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 } = settings;
|
|
3121
|
+
if (settings.bunPlugins.length > 0) await runBunPluginDriver({
|
|
3345
3122
|
entrypoints: [entryPath],
|
|
3346
3123
|
minify,
|
|
3347
3124
|
env: "PUBLIC_*",
|
|
3348
3125
|
target,
|
|
3349
3126
|
format: "esm"
|
|
3350
|
-
}, outfilePath,
|
|
3127
|
+
}, outfilePath, settings, runner);
|
|
3351
3128
|
else await runBuildProcess(runner, [
|
|
3352
3129
|
"build",
|
|
3353
3130
|
...toBunEnvFileArgs(envFiles),
|
|
@@ -3371,191 +3148,17 @@ async function execScriptBuild(target, entryPath, outfilePath, minify, envFiles,
|
|
|
3371
3148
|
async function execDenoBuild(entryPath, outfilePath, target, cwd, runner = resolveDenoBuildRunner()) {
|
|
3372
3149
|
await runBuildProcess(runner, createDenoCompileArgs(entryPath, outfilePath, target), outfilePath, cwd);
|
|
3373
3150
|
}
|
|
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
3151
|
/**
|
|
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.
|
|
3152
|
+
* Bundle a Deno runtime package entry with native `deno bundle`, which resolves
|
|
3153
|
+
* dependencies through the project's own Deno setup (deno.json imports,
|
|
3154
|
+
* package.json with its installed node_modules) and inlines them into one file.
|
|
3155
|
+
*
|
|
3156
|
+
* Bundle the declared command directly so Deno preserves its `import.meta.main`
|
|
3157
|
+
* semantics without making dependencies main. Prepend the packaged-build marker
|
|
3158
|
+
* (`PACKAGED_BUILD_KEY`) to the bundled code so it runs before the inlined
|
|
3159
|
+
* application graph. The output has no shebang or permission flags: consumers
|
|
3160
|
+
* choose grants with `deno run` / `deno install`.
|
|
3161
|
+
* No env files or plugins: `deno bundle` has no PUBLIC_* filter or Bun plugins.
|
|
3559
3162
|
*/
|
|
3560
3163
|
async function execDenoPackageBuild(entryPath, outfilePath, cwd, runner = resolveDenoBuildRunner()) {
|
|
3561
3164
|
await runBuildProcess(runner, [
|
|
@@ -3569,147 +3172,104 @@ async function execDenoPackageBuild(entryPath, outfilePath, cwd, runner = resolv
|
|
|
3569
3172
|
const output = await readFile(outfilePath, "utf8");
|
|
3570
3173
|
await writeFile(outfilePath, `globalThis[Symbol.for(${JSON.stringify(PACKAGED_BUILD_KEY)})] = true;\n` + output.replace(/^#![^\n]*(?:\n|$)/, ""));
|
|
3571
3174
|
}
|
|
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"));
|
|
3175
|
+
//#endregion
|
|
3176
|
+
//#region src/launcher.ts
|
|
3177
|
+
/** Platform binary filename: `<command>-<target>`, `.exe` on Windows. */
|
|
3178
|
+
function binaryFilename(command, target) {
|
|
3179
|
+
return `${command}-${target.target}${target.os === "win32" ? ".exe" : ""}`;
|
|
3581
3180
|
}
|
|
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");
|
|
3181
|
+
function generateLauncher(command, targets) {
|
|
3182
|
+
const targetMap = Object.fromEntries(targets.map((target) => [target.platformKey, {
|
|
3183
|
+
packagePathSegment: target.packagePathSegment,
|
|
3184
|
+
packageName: target.packageName,
|
|
3185
|
+
targetAlias: target.targetAlias,
|
|
3186
|
+
binaryFilename: binaryFilename(command, target)
|
|
3187
|
+
}]));
|
|
3188
|
+
const supportedPlatforms = targets.map((target) => target.targetAlias).join(", ");
|
|
3189
|
+
return `#!/usr/bin/env node
|
|
3190
|
+
// Auto-generated by crust build -- do not edit
|
|
3191
|
+
import { spawn } from "node:child_process";
|
|
3192
|
+
import { chmodSync, existsSync } from "node:fs";
|
|
3193
|
+
import { dirname, resolve } from "node:path";
|
|
3194
|
+
import process from "node:process";
|
|
3195
|
+
import { fileURLToPath } from "node:url";
|
|
3196
|
+
|
|
3197
|
+
const NAME = ${JSON.stringify(command)};
|
|
3198
|
+
const PLATFORMS = ${JSON.stringify(targetMap, null, " ")};
|
|
3199
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
3200
|
+
|
|
3201
|
+
// Same check as Bun's own npm installer: glibc reports its version, musl does not.
|
|
3202
|
+
function isMusl() {
|
|
3605
3203
|
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
|
-
}
|
|
3204
|
+
const report = process.report?.getReport();
|
|
3205
|
+
if (report?.header) return report.header.glibcVersionRuntime === undefined;
|
|
3206
|
+
} catch {}
|
|
3207
|
+
return existsSync("/etc/alpine-release");
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
const platformKey =
|
|
3211
|
+
\`\${process.platform}-\${process.arch}\` + (process.platform === "linux" && isMusl() ? "-musl" : "");
|
|
3212
|
+
const target = PLATFORMS[platformKey];
|
|
3213
|
+
|
|
3214
|
+
if (!target) {
|
|
3215
|
+
\tconsole.error("[" + NAME + "] Unsupported platform: " + platformKey);
|
|
3216
|
+
\tconsole.error("[" + NAME + "] Supported platforms: ${supportedPlatforms}");
|
|
3217
|
+
\tprocess.exit(1);
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
const candidates = [
|
|
3221
|
+
\t// Hoisted install: the platform package beside this one in node_modules.
|
|
3222
|
+
\tresolve(dir, "..", "..", target.packagePathSegment, "bin", target.binaryFilename),
|
|
3223
|
+
\t// Nested install: the platform package under this package's node_modules.
|
|
3224
|
+
\tresolve(dir, "..", "node_modules", target.packageName, "bin", target.binaryFilename),
|
|
3225
|
+
\t// In place: the .crust/ tree crust build staged, before any install.
|
|
3226
|
+
\tresolve(dir, "..", "..", target.targetAlias, "bin", target.binaryFilename),
|
|
3227
|
+
];
|
|
3228
|
+
const binPath = candidates.find((candidate) => existsSync(candidate));
|
|
3229
|
+
|
|
3230
|
+
if (!binPath) {
|
|
3231
|
+
\tconsole.error("[" + NAME + "] Missing platform package for " + platformKey);
|
|
3232
|
+
\tconsole.error("[" + NAME + "] Tried:");
|
|
3233
|
+
\tfor (const candidate of candidates) console.error(" " + candidate);
|
|
3234
|
+
\tconsole.error(
|
|
3235
|
+
\t\t"[" + NAME + "] Reinstall dependencies on this platform and ensure optional dependencies are enabled.",
|
|
3236
|
+
\t);
|
|
3237
|
+
\tprocess.exit(1);
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
if (process.platform !== "win32") {
|
|
3241
|
+
\ttry {
|
|
3242
|
+
\t\tchmodSync(binPath, 0o755);
|
|
3243
|
+
\t} catch {
|
|
3244
|
+
\t\t// Ignore permission adjustment failures and let spawn surface real errors.
|
|
3245
|
+
\t}
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
const child = spawn(binPath, process.argv.slice(2), {
|
|
3249
|
+
\tstdio: "inherit",
|
|
3250
|
+
});
|
|
3251
|
+
|
|
3252
|
+
child.on("error", (error) => {
|
|
3253
|
+
\tconsole.error("[" + NAME + "] Failed to launch binary: " + error.message);
|
|
3254
|
+
\tprocess.exit(1);
|
|
3255
|
+
});
|
|
3256
|
+
|
|
3257
|
+
child.on("exit", (code, signal) => {
|
|
3258
|
+
\tif (signal) {
|
|
3259
|
+
\t\ttry {
|
|
3260
|
+
\t\t\tprocess.kill(process.pid, signal);
|
|
3261
|
+
\t\t} catch {
|
|
3262
|
+
\t\t\tprocess.exit(1);
|
|
3263
|
+
\t\t}
|
|
3264
|
+
\t\treturn;
|
|
3265
|
+
\t}
|
|
3266
|
+
|
|
3267
|
+
\tprocess.exit(code ?? 0);
|
|
3268
|
+
});
|
|
3269
|
+
`;
|
|
3710
3270
|
}
|
|
3711
3271
|
//#endregion
|
|
3712
|
-
//#region src/
|
|
3272
|
+
//#region src/distribute.ts
|
|
3713
3273
|
/** Project-relative directory that `crust build` owns: wiped per build, read by `crust publish`. */
|
|
3714
3274
|
const CRUST_DIR = ".crust";
|
|
3715
3275
|
/**
|
|
@@ -3731,15 +3291,9 @@ const METADATA_KEYS = [
|
|
|
3731
3291
|
"funding",
|
|
3732
3292
|
"engines"
|
|
3733
3293
|
];
|
|
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
3294
|
/** Only identity is interpreted here; this is not a complete npm schema validator. */
|
|
3741
3295
|
function validatePackageIdentity(value, source) {
|
|
3742
|
-
if (
|
|
3296
|
+
if (!isJsonObject(value)) throw new Error(`${source} must contain a JSON object.`);
|
|
3743
3297
|
if (typeof value.name !== "string" || value.name.trim() === "") throw new Error(`${source} name field must be a non-empty string.`);
|
|
3744
3298
|
if (typeof value.version !== "string" || value.version.trim() === "") throw new Error(`${source} version field must be a non-empty string.`);
|
|
3745
3299
|
}
|
|
@@ -3754,10 +3308,6 @@ function derivePlatformPackageName(rootPackageName, targetAlias) {
|
|
|
3754
3308
|
function getPackagePathSegment(packageName) {
|
|
3755
3309
|
return packageName.startsWith("@") ? packageName.split("/")[1] ?? packageName : packageName;
|
|
3756
3310
|
}
|
|
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
3311
|
function platformBinMap(commands, target) {
|
|
3762
3312
|
return Object.fromEntries(commands.map((command) => [command, `bin/${binaryFilename(command, target)}`]));
|
|
3763
3313
|
}
|
|
@@ -3872,131 +3422,40 @@ function pickRootMetadata(pkgJson) {
|
|
|
3872
3422
|
version: pkgJson.version
|
|
3873
3423
|
};
|
|
3874
3424
|
for (const key of METADATA_KEYS) {
|
|
3875
|
-
const value = pkgJson[key];
|
|
3876
|
-
if (value !== void 0)
|
|
3877
|
-
}
|
|
3878
|
-
return metadata;
|
|
3879
|
-
}
|
|
3880
|
-
function validatePackageNameLength(packageName) {
|
|
3881
|
-
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
|
-
}
|
|
3883
|
-
function resolveDistributionMetadata(
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
...pkgJson.
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
const
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
packageName,
|
|
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
|
-
`;
|
|
3425
|
+
const value = pkgJson[key];
|
|
3426
|
+
if (value !== void 0) metadata[key] = value;
|
|
3427
|
+
}
|
|
3428
|
+
return metadata;
|
|
3429
|
+
}
|
|
3430
|
+
function validatePackageNameLength(packageName) {
|
|
3431
|
+
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.`);
|
|
3432
|
+
}
|
|
3433
|
+
function resolveDistributionMetadata(pkgJson) {
|
|
3434
|
+
validatePackageNameLength(pkgJson.name);
|
|
3435
|
+
return {
|
|
3436
|
+
rootPackageName: pkgJson.name,
|
|
3437
|
+
version: pkgJson.version,
|
|
3438
|
+
rootPackageJson: pickRootMetadata(pkgJson),
|
|
3439
|
+
sourceType: pkgJson.type,
|
|
3440
|
+
...pkgJson.exports !== void 0 ? { exports: pkgJson.exports } : {},
|
|
3441
|
+
...pkgJson.peerDependencies !== void 0 ? validatePeerDependencies(pkgJson.peerDependencies, pkgJson.peerDependenciesMeta) : {}
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
function resolveDistributionTarget(table, stageDir, rootPackageName, target) {
|
|
3445
|
+
const info = table.info[target];
|
|
3446
|
+
const packageName = derivePlatformPackageName(rootPackageName, info.alias);
|
|
3447
|
+
validatePackageNameLength(packageName);
|
|
3448
|
+
return {
|
|
3449
|
+
target,
|
|
3450
|
+
platformKey: info.platformKey,
|
|
3451
|
+
targetAlias: info.alias,
|
|
3452
|
+
packageName,
|
|
3453
|
+
packagePathSegment: getPackagePathSegment(packageName),
|
|
3454
|
+
packageDir: resolve(stageDir, info.alias),
|
|
3455
|
+
os: info.os,
|
|
3456
|
+
cpu: info.cpu,
|
|
3457
|
+
...info.libc ? { libc: info.libc } : {}
|
|
3458
|
+
};
|
|
4000
3459
|
}
|
|
4001
3460
|
function writeJson(path, value) {
|
|
4002
3461
|
writeFileSync(path, `${JSON.stringify(value, null, " ")}\n`);
|
|
@@ -4037,13 +3496,14 @@ function writeDistributionManifest(stageDir, metadata, identity, commands, targe
|
|
|
4037
3496
|
publishOrder: [...targets.map((target) => target.targetAlias), "root"],
|
|
4038
3497
|
...build ? { build } : {}
|
|
4039
3498
|
};
|
|
4040
|
-
|
|
4041
|
-
|
|
3499
|
+
const manifestPath = join(stageDir, "manifest.json");
|
|
3500
|
+
writeJson(manifestPath, manifest);
|
|
3501
|
+
return manifestPath;
|
|
4042
3502
|
}
|
|
4043
3503
|
/** `pkg` without `engines.node`, dropping `engines` once nothing else is left. */
|
|
4044
3504
|
function omitNodeEngine(pkg) {
|
|
4045
3505
|
const { engines, ...rest } = pkg;
|
|
4046
|
-
if (engines
|
|
3506
|
+
if (engines === void 0 || !isJsonObject(engines) || engines.node === void 0) return pkg;
|
|
4047
3507
|
const others = Object.entries(engines).filter(([runtime]) => runtime !== "node");
|
|
4048
3508
|
return others.length > 0 ? {
|
|
4049
3509
|
...rest,
|
|
@@ -4068,7 +3528,7 @@ function stageDistributionPackages(cwd, stageDir, metadata, platformMetadata, co
|
|
|
4068
3528
|
* Returns every generated package.json, launcher, and compiled command in staging order.
|
|
4069
3529
|
*/
|
|
4070
3530
|
async function runDistributeBuild(plan, distribution, io, build) {
|
|
4071
|
-
const sourceMetadata = resolveDistributionMetadata(plan.
|
|
3531
|
+
const sourceMetadata = resolveDistributionMetadata(plan.userPackageJson);
|
|
4072
3532
|
const commands = plan.entries.map((entry) => entry.command);
|
|
4073
3533
|
const table = distribution.table;
|
|
4074
3534
|
const platformMetadata = plan.runtime === "node" && table ? {
|
|
@@ -4117,7 +3577,7 @@ async function runDistributeBuild(plan, distribution, io, build) {
|
|
|
4117
3577
|
if (table) {
|
|
4118
3578
|
for (const { command } of plan.entries) {
|
|
4119
3579
|
const launcherPath = join(rootBinDir, `${command}.js`);
|
|
4120
|
-
writeFileSync(launcherPath,
|
|
3580
|
+
writeFileSync(launcherPath, generateLauncher(command, distributionTargets), { mode: 493 });
|
|
4121
3581
|
produced.push({
|
|
4122
3582
|
kind: "launcher",
|
|
4123
3583
|
path: launcherPath,
|
|
@@ -4145,7 +3605,7 @@ async function runDistributeBuild(plan, distribution, io, build) {
|
|
|
4145
3605
|
command
|
|
4146
3606
|
});
|
|
4147
3607
|
}
|
|
4148
|
-
writeDistributionManifest(plan.stageDir, metadata, table ? {
|
|
3608
|
+
const manifestPath = writeDistributionManifest(plan.stageDir, metadata, table ? {
|
|
4149
3609
|
runtime: plan.runtime,
|
|
4150
3610
|
artifact: "binary",
|
|
4151
3611
|
embeddedRuntimeVersion: distribution.embeddedRuntimeVersion
|
|
@@ -4153,109 +3613,461 @@ async function runDistributeBuild(plan, distribution, io, build) {
|
|
|
4153
3613
|
runtime: plan.runtime,
|
|
4154
3614
|
artifact: "package"
|
|
4155
3615
|
}, commands, distributionTargets, build);
|
|
4156
|
-
const manifestPath = join(plan.stageDir, "manifest.json");
|
|
4157
3616
|
io.stdout(`\n${green("✓")} Staged ${bold(`${distributionTargets.length + 1}`)} npm package(s) successfully:`);
|
|
4158
3617
|
io.stdout(` ${rootDir}`);
|
|
4159
3618
|
for (const targetPackage of distributionTargets) io.stdout(` ${targetPackage.packageDir}`);
|
|
4160
3619
|
io.stdout(`\n${dim("Manifest:")} ${manifestPath}`);
|
|
4161
3620
|
return produced;
|
|
4162
3621
|
}
|
|
3622
|
+
//#endregion
|
|
3623
|
+
//#region package.json
|
|
3624
|
+
var optionalDependencies = {
|
|
3625
|
+
"@tsdown/exe": "0.23.0",
|
|
3626
|
+
"tsdown": "0.23.0"
|
|
3627
|
+
};
|
|
3628
|
+
//#endregion
|
|
3629
|
+
//#region src/node-exe.ts
|
|
4163
3630
|
/**
|
|
4164
|
-
*
|
|
4165
|
-
* staged
|
|
3631
|
+
* Crust's own optional dependencies that build Node standalone binaries.
|
|
3632
|
+
* Crust's staged root and platform packages declare them
|
|
3633
|
+
* (scripts/stage-node-exe-dependencies.ts) so an installed crust resolves them
|
|
3634
|
+
* next to itself; an install that skipped them only affects Node binaries.
|
|
4166
3635
|
*/
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
3636
|
+
const NODE_EXE_BACKEND_PACKAGES = ["tsdown", "@tsdown/exe"];
|
|
3637
|
+
/**
|
|
3638
|
+
* A file of Crust's own installation that the backend is resolved from, never
|
|
3639
|
+
* the project or PATH. A compiled crust serves its modules from Bun's virtual
|
|
3640
|
+
* filesystem (`/$bunfs/`, `B:/~BUN/` on Windows), so there it is the
|
|
3641
|
+
* executable, inside its platform package.
|
|
3642
|
+
*/
|
|
3643
|
+
function crustInstallPath(moduleUrl = import.meta.url, execPath = process.execPath) {
|
|
3644
|
+
return /^file:\/\/\/(?:\$bunfs|[A-Za-z]:\/~BUN)\//.test(moduleUrl) ? execPath : fileURLToPath(moduleUrl);
|
|
3645
|
+
}
|
|
3646
|
+
/**
|
|
3647
|
+
* `<dir>/node_modules/<name>/package.json` for the nearest `dir` at or above
|
|
3648
|
+
* `from`: Node's node_modules lookup without its NODE_PATH and global-folder
|
|
3649
|
+
* fallbacks, so only an installation next to `from` can supply the package.
|
|
3650
|
+
* Package managers hoist crust's backend into the project's node_modules, so
|
|
3651
|
+
* the found package may be the project's own: callers check its version.
|
|
3652
|
+
*/
|
|
3653
|
+
function findInstalledPackageJson(from, name) {
|
|
3654
|
+
for (let dir = from;; dir = dirname(dir)) {
|
|
3655
|
+
const candidate = join(dir, "node_modules", name, "package.json");
|
|
3656
|
+
if (existsSync(candidate)) return candidate;
|
|
3657
|
+
if (dirname(dir) === dir) return null;
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
function isVersionedPackageJson(value) {
|
|
3661
|
+
return isJsonObject(value) && typeof value.version === "string";
|
|
3662
|
+
}
|
|
3663
|
+
function isTsdownPackageJson(value) {
|
|
3664
|
+
return isVersionedPackageJson(value) && value.engines !== void 0 && isJsonObject(value.engines) && value.engines.node !== void 0 && isVersionRange(value.engines.node);
|
|
3665
|
+
}
|
|
3666
|
+
function createNodeExeProbeScript(tsdownPackageJson) {
|
|
3667
|
+
return `// Generated by crust build to read tsdown's executable minimum; deleted when it exits.
|
|
3668
|
+
import { createRequire } from "node:module";
|
|
3669
|
+
import { pathToFileURL } from "node:url";
|
|
3670
|
+
|
|
3671
|
+
const internal = createRequire(${JSON.stringify(tsdownPackageJson)}).resolve("tsdown/internal");
|
|
3672
|
+
console.log((await import(pathToFileURL(internal).href)).NODE_SEA_MIN_VERSION);
|
|
3673
|
+
`;
|
|
3674
|
+
}
|
|
3675
|
+
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).";
|
|
3676
|
+
/**
|
|
3677
|
+
* Checks the selected Node against the installed backend's two separate
|
|
3678
|
+
* requirements: tsdown's package `engines.node` and its executable builder's
|
|
3679
|
+
* minimum (skipped while unknown). Both come from the installed tsdown, not
|
|
3680
|
+
* from this file.
|
|
3681
|
+
*/
|
|
3682
|
+
function assertNodeExeBackendSupports(compiler, backend) {
|
|
3683
|
+
const selected = `Node ${compiler.version} (${compiler.runner.command})`;
|
|
3684
|
+
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}`);
|
|
3685
|
+
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}`);
|
|
3686
|
+
}
|
|
3687
|
+
/**
|
|
3688
|
+
* Finds Crust's installed tsdown and @tsdown/exe from `installPath`, at the
|
|
3689
|
+
* versions its package.json `optionalDependencies` pin, and validates the
|
|
3690
|
+
* selected Node against tsdown's requirements, loading the executable
|
|
3691
|
+
* builder's minimum under that Node. `cwd` is the project
|
|
3692
|
+
* directory, as for every other compiler step.
|
|
3693
|
+
*/
|
|
3694
|
+
async function resolveNodeExeBackend(compiler, cwd, installPath = crustInstallPath()) {
|
|
3695
|
+
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).`;
|
|
3696
|
+
const packageJsonPath = findInstalledPackageJson(dirname(installPath), "tsdown");
|
|
3697
|
+
if (packageJsonPath === null) throw new Error(`tsdown, which builds node standalone binaries, is not installed with crust (${installPath}).\n${reinstall}`);
|
|
3698
|
+
const exePackageJsonPath = findInstalledPackageJson(dirname(realpathSync(packageJsonPath)), "@tsdown/exe");
|
|
3699
|
+
if (exePackageJsonPath === null) throw new Error(`@tsdown/exe is not installed beside crust's tsdown (${packageJsonPath}).\n${reinstall}`);
|
|
3700
|
+
const readPackage = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
3701
|
+
const tsdownPackage = readPackage(packageJsonPath);
|
|
3702
|
+
for (const [name, path] of [["tsdown", packageJsonPath], ["@tsdown/exe", exePackageJsonPath]]) {
|
|
3703
|
+
const installed = name === "tsdown" ? tsdownPackage : readPackage(path);
|
|
3704
|
+
const version = isVersionedPackageJson(installed) ? installed.version : "without a version";
|
|
3705
|
+
const pinned = optionalDependencies[name];
|
|
3706
|
+
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}`);
|
|
3707
|
+
}
|
|
3708
|
+
if (!isTsdownPackageJson(tsdownPackage)) throw new Error(`Unexpected tsdown package metadata in ${packageJsonPath}.\n${reinstall}`);
|
|
3709
|
+
const backend = {
|
|
3710
|
+
packageJsonPath,
|
|
3711
|
+
version: tsdownPackage.version,
|
|
3712
|
+
engines: tsdownPackage.engines.node
|
|
3713
|
+
};
|
|
3714
|
+
assertNodeExeBackendSupports(compiler, backend);
|
|
3715
|
+
const probe = await runGeneratedScript(compiler.runner, "probe", createNodeExeProbeScript(packageJsonPath), cwd);
|
|
3716
|
+
const seaMinVersion = probe.exitCode === 0 ? (0, import_valid.default)(probe.stdout.trim()) : null;
|
|
3717
|
+
if (seaMinVersion === null) {
|
|
3718
|
+
const output = processOutput(probe);
|
|
3719
|
+
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}` : ""}`);
|
|
3720
|
+
}
|
|
3721
|
+
const resolved = {
|
|
3722
|
+
...backend,
|
|
3723
|
+
seaMinVersion
|
|
3724
|
+
};
|
|
3725
|
+
assertNodeExeBackendSupports(compiler, resolved);
|
|
3726
|
+
return resolved;
|
|
3727
|
+
}
|
|
3728
|
+
/**
|
|
3729
|
+
* Select the Node binary compiler once for a build: the external `node`, its
|
|
3730
|
+
* version read in the project directory `cwd`, validated against the
|
|
3731
|
+
* project's `engines.node` and then the installed tsdown's requirements.
|
|
3732
|
+
* Pass it to {@link execNodeBinaryBuild} for every command and target.
|
|
3733
|
+
*/
|
|
3734
|
+
async function resolveNodeBinaryCompiler(userPackageJson, cwd) {
|
|
3735
|
+
const runner = resolveNodeBuildRunner();
|
|
3736
|
+
const compiler = {
|
|
3737
|
+
runtime: "node",
|
|
3738
|
+
runner,
|
|
3739
|
+
version: await readCompilerVersion("node", runner, cwd)
|
|
3740
|
+
};
|
|
3741
|
+
assertCompilerSatisfiesEngines(compiler, userPackageJson);
|
|
3742
|
+
return {
|
|
3743
|
+
...compiler,
|
|
3744
|
+
backend: await resolveNodeExeBackend(compiler, cwd)
|
|
3745
|
+
};
|
|
3746
|
+
}
|
|
3747
|
+
/** tsdown's executable target for a Node target, embedding exactly `nodeVersion`. */
|
|
3748
|
+
function nodeExeTarget(target, nodeVersion) {
|
|
3749
|
+
const { os, cpu } = NODE_TARGETS.info[target];
|
|
3750
|
+
return {
|
|
3751
|
+
platform: os === "win32" ? "win" : os,
|
|
3752
|
+
arch: cpu,
|
|
3753
|
+
nodeVersion
|
|
3754
|
+
};
|
|
3755
|
+
}
|
|
3756
|
+
/**
|
|
3757
|
+
* Script the selected Node runs to turn one Bun bundle into one target's
|
|
3758
|
+
* executable with tsdown's programmatic `build`. Every value is embedded via
|
|
3759
|
+
* JSON.stringify.
|
|
3760
|
+
*/
|
|
3761
|
+
function createNodeExeBuildScript(options) {
|
|
3762
|
+
return `// Generated by crust build for a Node standalone binary; deleted when the build finishes.
|
|
3763
|
+
import { createRequire } from "node:module";
|
|
3764
|
+
import { pathToFileURL } from "node:url";
|
|
3765
|
+
|
|
3766
|
+
const options = ${JSON.stringify(options)};
|
|
3767
|
+
const { build } = await import(pathToFileURL(createRequire(options.tsdownPackageJson).resolve("tsdown")).href);
|
|
3768
|
+
await build({
|
|
3769
|
+
config: false,
|
|
3770
|
+
cwd: options.cwd,
|
|
3771
|
+
entry: { app: options.entry },
|
|
3772
|
+
outDir: options.bundleDir,
|
|
3773
|
+
format: "esm",
|
|
3774
|
+
platform: "node",
|
|
3775
|
+
dts: false,
|
|
3776
|
+
// Rolldown reprints the Bun bundle, so it minifies again.
|
|
3777
|
+
minify: options.minify,
|
|
3778
|
+
logLevel: "warn",
|
|
3779
|
+
report: false,
|
|
3780
|
+
// Bun already inlined the PUBLIC_* constants; inline nothing from this environment.
|
|
3781
|
+
envPrefix: [],
|
|
3782
|
+
// Bun leaves only Node built-ins as imports; fail on anything else, which
|
|
3783
|
+
// an executable shipping no node_modules could not resolve.
|
|
3784
|
+
deps: { alwaysBundle: () => true, onlyBundle: false, onlyImport: [] },
|
|
3785
|
+
exe: { outDir: options.exeDir, fileName: "app", targets: [options.target] },
|
|
3786
|
+
});
|
|
3787
|
+
`;
|
|
3788
|
+
}
|
|
3789
|
+
/**
|
|
3790
|
+
* Build a single entry file into a Node standalone executable: Bun first
|
|
3791
|
+
* bundles it for Node exactly as a Node runtime package ({@link execScriptBuild}:
|
|
3792
|
+
* `PUBLIC_*` constants from `envFiles` and the environment, each module's own
|
|
3793
|
+
* `import.meta.main` and `require.main === module`, CommonJS and ESM inputs),
|
|
3794
|
+
* then tsdown's executable builder (Node SEA), run by the selected external
|
|
3795
|
+
* Node, turns that bundle into an executable embedding exactly its version
|
|
3796
|
+
* for `target`. tsdown takes a single entry per executable, so callers build
|
|
3797
|
+
* every command separately. Matching official Node binaries for targets are
|
|
3798
|
+
* downloaded, checksum-verified, and cached by @tsdown/exe. Builds for darwin
|
|
3799
|
+
* are only code-signed on a macOS host.
|
|
3800
|
+
*
|
|
3801
|
+
* @param compiler - The selected Node compiler (`resolveNodeBinaryCompiler`)
|
|
3802
|
+
* @param bunRunner - The Bun that bundles (`resolveBunBuildRunner`)
|
|
3803
|
+
* @param onWarning - Receives the backend's warnings (such as a skipped code
|
|
3804
|
+
* signature) after a successful build
|
|
3805
|
+
* @throws {Error} If the build fails
|
|
3806
|
+
*/
|
|
3807
|
+
async function execNodeBinaryBuild(entryPath, outfilePath, target, settings, compiler, bunRunner, onWarning = () => {}) {
|
|
3808
|
+
const { cwd, minify } = settings;
|
|
3809
|
+
const workDir = await mkdtemp(join(tmpdir(), "crust-node-exe-"));
|
|
3810
|
+
try {
|
|
3811
|
+
const scriptPath = join(workDir, "build.mjs");
|
|
3812
|
+
const bunBundle = join(workDir, "app.mjs");
|
|
3813
|
+
const exeDir = join(workDir, "exe");
|
|
3814
|
+
await execScriptBuild("node", resolve(cwd, entryPath), bunBundle, {
|
|
3815
|
+
...settings,
|
|
3816
|
+
bunPlugins: []
|
|
3817
|
+
}, bunRunner);
|
|
3818
|
+
await writeFile(scriptPath, createNodeExeBuildScript({
|
|
3819
|
+
tsdownPackageJson: compiler.backend.packageJsonPath,
|
|
3820
|
+
cwd,
|
|
3821
|
+
entry: bunBundle,
|
|
3822
|
+
bundleDir: join(workDir, "bundle"),
|
|
3823
|
+
exeDir,
|
|
3824
|
+
minify,
|
|
3825
|
+
target: nodeExeTarget(target, compiler.version)
|
|
3826
|
+
}));
|
|
3827
|
+
const output = await runBuildProcess(compiler.runner, [scriptPath], outfilePath, cwd);
|
|
3828
|
+
const produced = await readdir(exeDir);
|
|
3829
|
+
if (produced.length !== 1) throw new Error(`Build failed for ${outfilePath}: expected one executable from tsdown, found ${JSON.stringify(produced)}.`);
|
|
3830
|
+
await copyFile(join(exeDir, produced[0]), outfilePath);
|
|
3831
|
+
if (output) onWarning(output);
|
|
3832
|
+
} finally {
|
|
3833
|
+
await rm(workDir, {
|
|
3834
|
+
recursive: true,
|
|
3835
|
+
force: true,
|
|
3836
|
+
maxRetries: 3,
|
|
3837
|
+
retryDelay: 100
|
|
4183
3838
|
});
|
|
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
3839
|
}
|
|
4188
|
-
|
|
3840
|
+
}
|
|
3841
|
+
function createNodeExeProvisionScript(tsdownPackageJson, targets) {
|
|
3842
|
+
return `// Generated by crust build to provision Node binaries for targets; deleted when it exits.
|
|
3843
|
+
import { realpathSync } from "node:fs";
|
|
3844
|
+
import { createRequire } from "node:module";
|
|
3845
|
+
import { pathToFileURL } from "node:url";
|
|
3846
|
+
|
|
3847
|
+
const exe = createRequire(realpathSync(${JSON.stringify(tsdownPackageJson)})).resolve("@tsdown/exe");
|
|
3848
|
+
const { resolveNodeBinary } = await import(pathToFileURL(exe).href);
|
|
3849
|
+
for (const { name, target } of ${JSON.stringify(targets)}) {
|
|
3850
|
+
try {
|
|
3851
|
+
await resolveNodeBinary(target, {});
|
|
3852
|
+
} catch (error) {
|
|
3853
|
+
console.error(name + ": " + error.message + (error.cause?.message ? "\\n" + error.cause.message : ""));
|
|
3854
|
+
process.exit(1);
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
`;
|
|
4189
3858
|
}
|
|
4190
3859
|
/**
|
|
4191
|
-
*
|
|
4192
|
-
*
|
|
3860
|
+
* Provisions the Node binary every target embeds before anything is staged,
|
|
3861
|
+
* with the selected Node and the call tsdown's executable builder makes per
|
|
3862
|
+
* target (@tsdown/exe's `resolveNodeBinary`): an uncached official archive of
|
|
3863
|
+
* exactly `compiler.version` is downloaded, checksum-verified, unpacked with
|
|
3864
|
+
* the tool that target's archive needs on this host, and cached, where
|
|
3865
|
+
* {@link execNodeBinaryBuild} then finds it. A missing unpacking tool, a failed
|
|
3866
|
+
* download, or a checksum mismatch therefore fails here, before `.crust/` is
|
|
3867
|
+
* replaced; cached targets need nothing.
|
|
3868
|
+
*
|
|
3869
|
+
* @throws {Error} If a target cannot be provisioned
|
|
4193
3870
|
*/
|
|
4194
|
-
function
|
|
4195
|
-
const
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
}
|
|
4204
|
-
|
|
3871
|
+
async function provisionNodeExeTargets(targets, cwd, compiler) {
|
|
3872
|
+
const provision = await runGeneratedScript(compiler.runner, "provision", createNodeExeProvisionScript(compiler.backend.packageJsonPath, targets.map((name) => ({
|
|
3873
|
+
name,
|
|
3874
|
+
target: nodeExeTarget(name, compiler.version)
|
|
3875
|
+
}))), cwd);
|
|
3876
|
+
if (provision.exitCode !== 0) {
|
|
3877
|
+
const output = processOutput(provision);
|
|
3878
|
+
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.
|
|
3879
|
+
Install the missing tool, or build only targets this machine can provision (--target).`);
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
/** Runs a generated `<name>.mjs` with `runner` from a temporary directory that is removed afterwards. */
|
|
3883
|
+
async function runGeneratedScript(runner, name, source, cwd) {
|
|
3884
|
+
const workDir = await mkdtemp(join(tmpdir(), `crust-node-exe-${name}-`));
|
|
3885
|
+
try {
|
|
3886
|
+
const scriptPath = join(workDir, `${name}.mjs`);
|
|
3887
|
+
await writeFile(scriptPath, source);
|
|
3888
|
+
return await runProcess(runner.command, [scriptPath], {
|
|
3889
|
+
env: runner.env,
|
|
3890
|
+
cwd,
|
|
3891
|
+
stdio: "collect"
|
|
3892
|
+
});
|
|
3893
|
+
} finally {
|
|
3894
|
+
await rm(workDir, {
|
|
3895
|
+
recursive: true,
|
|
3896
|
+
force: true
|
|
3897
|
+
});
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
//#endregion
|
|
3901
|
+
//#region ../core/dist/index.js
|
|
3902
|
+
/** Mint an Extension identity from any non-blank, trimmed string. */
|
|
3903
|
+
function defineExtensionId(id) {
|
|
3904
|
+
if (!id || id !== id.trim()) throw new CrustError("DEFINITION", "Extension id must be a non-empty, trimmed string", {
|
|
3905
|
+
subject: "extension",
|
|
3906
|
+
reason: "empty-id"
|
|
3907
|
+
});
|
|
3908
|
+
return id;
|
|
3909
|
+
}
|
|
3910
|
+
//#endregion
|
|
3911
|
+
//#region src/snapshot.ts
|
|
3912
|
+
const SNAPSHOT_TIMEOUT_MS = 3e4;
|
|
3913
|
+
const SNAPSHOT_FORWARDED_SIGNALS = [
|
|
3914
|
+
"SIGINT",
|
|
3915
|
+
"SIGTERM",
|
|
3916
|
+
"SIGHUP",
|
|
3917
|
+
"SIGQUIT"
|
|
3918
|
+
];
|
|
3919
|
+
function isBuildReport(value) {
|
|
3920
|
+
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"));
|
|
3921
|
+
}
|
|
3922
|
+
/** Checks only the root `meta.name`, the one snapshot field crust reads. */
|
|
3923
|
+
function isCommandSnapshot(value) {
|
|
3924
|
+
return isJsonObject(value) && value.meta !== void 0 && isJsonObject(value.meta) && typeof value.meta.name === "string";
|
|
4205
3925
|
}
|
|
4206
3926
|
/**
|
|
4207
|
-
*
|
|
4208
|
-
*
|
|
4209
|
-
*
|
|
4210
|
-
*
|
|
4211
|
-
*
|
|
4212
|
-
*
|
|
3927
|
+
* Prepare a CLI entry's Command Snapshot in the user's project context.
|
|
3928
|
+
*
|
|
3929
|
+
* The entry runs as a subprocess with `CRUST_INTERNAL_SNAPSHOT_PATH` pointing
|
|
3930
|
+
* to a temporary file. `.execute()` validates and writes the command graph and
|
|
3931
|
+
* adjacent Build Report, then exits before any following entrypoint code can run.
|
|
3932
|
+
*
|
|
3933
|
+
* Runs with the same bun as compilation (`resolveBunBuildRunner`, or the
|
|
3934
|
+
* pinned Bun `runner`): bun on PATH, or a compiled standalone crust executable
|
|
3935
|
+
* as `BUN_BE_BUN=1`, so arbitrary `.ts` entries run without a separate `bun`
|
|
3936
|
+
* install.
|
|
3937
|
+
*
|
|
3938
|
+
* After `timeoutMs`, the entry's POSIX process group or live Windows process
|
|
3939
|
+
* tree is killed and preparation fails, even if a descendant still holds stderr.
|
|
3940
|
+
* On POSIX, SIGINT, SIGTERM, SIGHUP, SIGQUIT or a synchronous exit of this
|
|
3941
|
+
* process also kills the entry's group, and whatever remains of the group is
|
|
3942
|
+
* killed once preparation settles.
|
|
4213
3943
|
*/
|
|
4214
|
-
function
|
|
4215
|
-
const
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
3944
|
+
async function buildEntrypoint(entryPath, outDir, envFiles, io, cwd, runner = resolveBunBuildRunner(), timeoutMs = SNAPSHOT_TIMEOUT_MS) {
|
|
3945
|
+
const absoluteEntry = resolve(entryPath);
|
|
3946
|
+
const snapshotDir = await mkdtemp(join(tmpdir(), "crust-snapshot-"));
|
|
3947
|
+
const snapshotPath = join(snapshotDir, "command.json");
|
|
3948
|
+
const buildReportPath = join(snapshotDir, "build-report.json");
|
|
3949
|
+
try {
|
|
3950
|
+
const detached = process.platform !== "win32";
|
|
3951
|
+
const proc = spawn(runner.command, [...toBunEnvFileArgs(envFiles), absoluteEntry], {
|
|
3952
|
+
env: {
|
|
3953
|
+
...runner.env,
|
|
3954
|
+
[SNAPSHOT_PATH_ENV]: snapshotPath,
|
|
3955
|
+
[BUILD_OUT_DIR_ENV]: resolve(outDir)
|
|
3956
|
+
},
|
|
3957
|
+
cwd,
|
|
3958
|
+
stdio: [
|
|
3959
|
+
"ignore",
|
|
3960
|
+
"ignore",
|
|
3961
|
+
"pipe"
|
|
3962
|
+
],
|
|
3963
|
+
detached
|
|
3964
|
+
});
|
|
3965
|
+
let stop;
|
|
3966
|
+
const stopped = new Promise((_, reject) => {
|
|
3967
|
+
stop = (message) => {
|
|
3968
|
+
let failure = new Error(message);
|
|
3969
|
+
try {
|
|
3970
|
+
killProcessTree(proc);
|
|
3971
|
+
} catch (cause) {
|
|
3972
|
+
failure = new Error(`${message}\n Process-tree cleanup failed.`, { cause });
|
|
3973
|
+
}
|
|
3974
|
+
proc.stderr.destroy();
|
|
3975
|
+
reject(failure);
|
|
3976
|
+
};
|
|
3977
|
+
});
|
|
3978
|
+
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);
|
|
3979
|
+
const onSignal = (signal) => {
|
|
3980
|
+
removeLifetimeListeners();
|
|
3981
|
+
stop(`Command Snapshot preparation was interrupted by ${signal}.`);
|
|
3982
|
+
if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
|
|
3983
|
+
};
|
|
3984
|
+
const killRemainingGroup = () => {
|
|
3985
|
+
try {
|
|
3986
|
+
killProcessTree(proc);
|
|
3987
|
+
} catch {}
|
|
3988
|
+
};
|
|
3989
|
+
const removeLifetimeListeners = () => {
|
|
3990
|
+
for (const signal of SNAPSHOT_FORWARDED_SIGNALS) process.removeListener(signal, onSignal);
|
|
3991
|
+
process.removeListener("exit", killRemainingGroup);
|
|
3992
|
+
};
|
|
3993
|
+
if (detached) {
|
|
3994
|
+
for (const signal of SNAPSHOT_FORWARDED_SIGNALS) process.prependListener(signal, onSignal);
|
|
3995
|
+
process.on("exit", killRemainingGroup);
|
|
3996
|
+
}
|
|
3997
|
+
const [rawStderr, [exitCode]] = await Promise.race([Promise.all([text(proc.stderr), once(proc, "close")]), stopped]).finally(() => {
|
|
3998
|
+
clearTimeout(timer);
|
|
3999
|
+
removeLifetimeListeners();
|
|
4000
|
+
if (detached) killRemainingGroup();
|
|
4001
|
+
});
|
|
4002
|
+
const stderr = rawStderr.trim();
|
|
4003
|
+
if (proc.signalCode !== null) throw new Error(`Command Snapshot preparation was killed by ${proc.signalCode}.${stderr ? `\n${stderr}` : ""}`);
|
|
4004
|
+
if (exitCode !== 0) throw new Error(stderr || "Command Snapshot preparation failed");
|
|
4005
|
+
if (stderr) {
|
|
4006
|
+
const styled = stderr.split("\n").map((line) => line.startsWith("Warning:") ? `${yellow("Warning:")}${line.slice(8)}` : line).join("\n");
|
|
4007
|
+
io.stderr(styled);
|
|
4008
|
+
}
|
|
4009
|
+
let serialized;
|
|
4010
|
+
try {
|
|
4011
|
+
serialized = await readFile(snapshotPath, "utf8");
|
|
4012
|
+
} catch (error) {
|
|
4013
|
+
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 });
|
|
4014
|
+
throw error;
|
|
4015
|
+
}
|
|
4016
|
+
let snapshot;
|
|
4017
|
+
try {
|
|
4018
|
+
const parsed = JSON.parse(serialized);
|
|
4019
|
+
if (!isCommandSnapshot(parsed)) throw new Error("Expected a root command with a string meta.name.");
|
|
4020
|
+
snapshot = parsed;
|
|
4021
|
+
} catch (error) {
|
|
4022
|
+
throw new Error(`Entry produced an invalid Command Snapshot.\n Ensure ${absoluteEntry} uses a compatible @crustjs/core version.`, { cause: error });
|
|
4023
|
+
}
|
|
4024
|
+
let serializedBuild;
|
|
4025
|
+
try {
|
|
4026
|
+
serializedBuild = await readFile(buildReportPath, "utf8");
|
|
4027
|
+
} catch (error) {
|
|
4028
|
+
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 });
|
|
4029
|
+
throw error;
|
|
4030
|
+
}
|
|
4031
|
+
try {
|
|
4032
|
+
const build = JSON.parse(serializedBuild);
|
|
4033
|
+
if (!isBuildReport(build)) throw new Error("Expected extensions with string ids and files arrays.");
|
|
4034
|
+
for (const extension of build.extensions) {
|
|
4035
|
+
defineExtensionId(extension.id);
|
|
4036
|
+
for (const file of extension.files) {
|
|
4037
|
+
const path = resolve(outDir, file);
|
|
4038
|
+
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}`);
|
|
4039
|
+
}
|
|
4236
4040
|
}
|
|
4041
|
+
return {
|
|
4042
|
+
snapshot,
|
|
4043
|
+
build
|
|
4044
|
+
};
|
|
4045
|
+
} catch (error) {
|
|
4046
|
+
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
4047
|
}
|
|
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
|
-
};
|
|
4048
|
+
} finally {
|
|
4049
|
+
await rm(snapshotDir, {
|
|
4050
|
+
recursive: true,
|
|
4051
|
+
force: true,
|
|
4052
|
+
maxRetries: 3,
|
|
4053
|
+
retryDelay: 100
|
|
4054
|
+
});
|
|
4055
|
+
}
|
|
4256
4056
|
}
|
|
4257
4057
|
//#endregion
|
|
4258
|
-
//#region src/
|
|
4058
|
+
//#region src/build.ts
|
|
4059
|
+
function readUserPackageJson(cwd) {
|
|
4060
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
4061
|
+
if (!existsSync(packageJsonPath)) throw new Error(`package.json not found in ${cwd}\n crust build requires a package.json with name and version fields.`);
|
|
4062
|
+
let packageJson;
|
|
4063
|
+
try {
|
|
4064
|
+
packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
4065
|
+
} catch (error) {
|
|
4066
|
+
throw new Error(`Failed to parse package.json in ${cwd}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
4067
|
+
}
|
|
4068
|
+
if (!isJsonObject(packageJson)) throw new Error(`package.json in ${cwd} must contain a JSON object.`);
|
|
4069
|
+
return packageJson;
|
|
4070
|
+
}
|
|
4259
4071
|
/** Also mirrored by `schema/package.json`; build.test.ts guards against drift. */
|
|
4260
4072
|
const CRUST_CONFIG_KEYS = [
|
|
4261
4073
|
"runtime",
|
|
@@ -4277,7 +4089,7 @@ function isStringArray(value) {
|
|
|
4277
4089
|
return Array.isArray(value) && value.every(isString);
|
|
4278
4090
|
}
|
|
4279
4091
|
function readCrustConfig(pkg) {
|
|
4280
|
-
if (pkg
|
|
4092
|
+
if (pkg.crust === void 0) return {};
|
|
4281
4093
|
const crust = pkg.crust;
|
|
4282
4094
|
const allowed = `Allowed keys: ${CRUST_CONFIG_KEYS.join(", ")}`;
|
|
4283
4095
|
if (!isJsonObject(crust)) throw new Error(`package.json crust must be an object. ${allowed}`);
|
|
@@ -4307,7 +4119,6 @@ function readCrustConfig(pkg) {
|
|
|
4307
4119
|
return config;
|
|
4308
4120
|
}
|
|
4309
4121
|
function hasDependency(pkg, name) {
|
|
4310
|
-
if (!isJsonObject(pkg)) return false;
|
|
4311
4122
|
return [pkg.dependencies, pkg.devDependencies].some((deps) => deps !== void 0 && isJsonObject(deps) && name in deps);
|
|
4312
4123
|
}
|
|
4313
4124
|
const DENO_CONFIG_FILES = ["deno.json", "deno.jsonc"];
|
|
@@ -4327,7 +4138,7 @@ function resolveBuildRuntime(pkg, config, cwd) {
|
|
|
4327
4138
|
runtime: "deno",
|
|
4328
4139
|
source: `inferred from ${denoConfig}`
|
|
4329
4140
|
};
|
|
4330
|
-
if (
|
|
4141
|
+
if (hasDependency(pkg, "@types/node") && !hasDependency(pkg, "@types/bun")) return {
|
|
4331
4142
|
runtime: "node",
|
|
4332
4143
|
source: "inferred from @types/node"
|
|
4333
4144
|
};
|
|
@@ -4357,12 +4168,6 @@ function resolveEnvFilePaths(cwd, envFiles) {
|
|
|
4357
4168
|
}
|
|
4358
4169
|
/** Entry built when package.json has no `bin`. */
|
|
4359
4170
|
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
4171
|
const BIN_EXAMPLE = `{ "my-cli": ${JSON.stringify(DEFAULT_ENTRY)} }`;
|
|
4367
4172
|
/**
|
|
4368
4173
|
* A `bin` value as an absolute path. The path must be relative and lexically
|
|
@@ -4388,19 +4193,18 @@ function resolveEntryPath(cwd, command, source) {
|
|
|
4388
4193
|
* `tool` would be the same `bin/` file on a case-insensitive filesystem.
|
|
4389
4194
|
*/
|
|
4390
4195
|
function resolveBinEntries(cwd, pkg) {
|
|
4391
|
-
const
|
|
4392
|
-
const bin = packageJson.bin;
|
|
4196
|
+
const bin = pkg.bin;
|
|
4393
4197
|
let declared;
|
|
4394
4198
|
if (bin === void 0 || isString(bin)) {
|
|
4395
|
-
const name =
|
|
4199
|
+
const name = pkg.name;
|
|
4396
4200
|
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 ??
|
|
4201
|
+
declared = [[name.replace(/^@[^/]+\//, ""), bin ?? DEFAULT_ENTRY]];
|
|
4398
4202
|
} else if (isJsonObject(bin) && Object.keys(bin).length > 0) declared = Object.entries(bin);
|
|
4399
4203
|
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
4204
|
const commandByLowerName = /* @__PURE__ */ new Map();
|
|
4401
4205
|
const commandByRealPath = /* @__PURE__ */ new Map();
|
|
4402
4206
|
return declared.map(([command, source]) => {
|
|
4403
|
-
if (!
|
|
4207
|
+
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
4208
|
const sameName = commandByLowerName.get(command.toLowerCase());
|
|
4405
4209
|
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
4210
|
commandByLowerName.set(command.toLowerCase(), command);
|
|
@@ -4433,27 +4237,24 @@ function resolveArtifact(option, config, runtime) {
|
|
|
4433
4237
|
binary: standalone executables in per-platform npm packages behind a Node launcher.
|
|
4434
4238
|
Earlier versions built a ${previous} for the ${runtime} runtime implicitly; set "artifact": "${previous}" to keep that output.`);
|
|
4435
4239
|
}
|
|
4436
|
-
return
|
|
4437
|
-
runtime,
|
|
4438
|
-
artifact
|
|
4439
|
-
};
|
|
4240
|
+
return artifact;
|
|
4440
4241
|
}
|
|
4441
4242
|
function planBuild(options, cwd) {
|
|
4442
4243
|
const userPackageJson = readUserPackageJson(cwd);
|
|
4443
4244
|
const config = readCrustConfig(userPackageJson);
|
|
4444
4245
|
const { runtime, source: runtimeSource } = resolveBuildRuntime(userPackageJson, config, cwd);
|
|
4445
|
-
const
|
|
4246
|
+
const artifact = resolveArtifact(options.artifact, config, runtime);
|
|
4446
4247
|
const entries = resolveBinEntries(cwd, userPackageJson);
|
|
4447
4248
|
validatePackageIdentity(userPackageJson, "package.json");
|
|
4448
4249
|
const envFiles = resolveEnvFilePaths(cwd, options.envFiles);
|
|
4449
4250
|
const bunPlugins = config.bunPlugins ?? [];
|
|
4450
|
-
if (
|
|
4451
|
-
if (
|
|
4452
|
-
const denoTool =
|
|
4453
|
-
if (runtime === "deno" && options.minify) throw new Error(
|
|
4454
|
-
if (runtime === "deno" && envFiles.length > 0) throw new Error(
|
|
4251
|
+
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.");
|
|
4252
|
+
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\".");
|
|
4253
|
+
const denoTool = artifact === "binary" ? "deno compile" : "deno bundle";
|
|
4254
|
+
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.");
|
|
4255
|
+
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
4256
|
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" &&
|
|
4257
|
+
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.");
|
|
4457
4258
|
const stageDir = resolve(cwd, CRUST_DIR);
|
|
4458
4259
|
const common = {
|
|
4459
4260
|
cwd,
|
|
@@ -4468,28 +4269,27 @@ function planBuild(options, cwd) {
|
|
|
4468
4269
|
validate: options.validate ?? true,
|
|
4469
4270
|
minify: runtime === "deno" ? false : options.minify ?? true
|
|
4470
4271
|
};
|
|
4471
|
-
if (
|
|
4272
|
+
if (artifact === "package") return {
|
|
4472
4273
|
...common,
|
|
4473
|
-
runtime
|
|
4474
|
-
artifact
|
|
4274
|
+
runtime,
|
|
4275
|
+
artifact
|
|
4475
4276
|
};
|
|
4476
4277
|
const targetInputs = options.targets?.length ? options.targets : config.targets;
|
|
4477
|
-
|
|
4478
|
-
if (binaryRuntime === "bun") return {
|
|
4278
|
+
if (runtime === "bun") return {
|
|
4479
4279
|
...common,
|
|
4480
|
-
runtime
|
|
4280
|
+
runtime,
|
|
4481
4281
|
artifact,
|
|
4482
4282
|
targets: resolveTargets(BUN_TARGETS, targetInputs)
|
|
4483
4283
|
};
|
|
4484
|
-
if (
|
|
4284
|
+
if (runtime === "node") return {
|
|
4485
4285
|
...common,
|
|
4486
|
-
runtime
|
|
4286
|
+
runtime,
|
|
4487
4287
|
artifact,
|
|
4488
4288
|
targets: resolveTargets(NODE_TARGETS, targetInputs)
|
|
4489
4289
|
};
|
|
4490
4290
|
return {
|
|
4491
4291
|
...common,
|
|
4492
|
-
runtime
|
|
4292
|
+
runtime,
|
|
4493
4293
|
artifact,
|
|
4494
4294
|
targets: resolveTargets(DENO_TARGETS, targetInputs)
|
|
4495
4295
|
};
|
|
@@ -4516,10 +4316,10 @@ async function selectCompilers(plan, io) {
|
|
|
4516
4316
|
}
|
|
4517
4317
|
if (plan.artifact === "package") {
|
|
4518
4318
|
const runner = resolveBunBuildRunner();
|
|
4519
|
-
const
|
|
4319
|
+
const target = plan.runtime === "bun" ? "bun" : "node";
|
|
4520
4320
|
return {
|
|
4521
4321
|
snapshotRunner: runner,
|
|
4522
|
-
stage: (reports) => runDistributeBuild(plan, { execute: (entry, outfile) =>
|
|
4322
|
+
stage: (reports) => runDistributeBuild(plan, { execute: (entry, outfile) => execScriptBuild(target, entry, outfile, plan, runner) }, io, reports)
|
|
4523
4323
|
};
|
|
4524
4324
|
}
|
|
4525
4325
|
const printCompiler = (compiler) => io.stdout(`${dim("Compiler:")} ${compiler.runtime} ${compiler.version} ${dim(`(${compiler.runner.command})`)}`);
|
|
@@ -4532,7 +4332,7 @@ async function selectCompilers(plan, io) {
|
|
|
4532
4332
|
table: NODE_TARGETS,
|
|
4533
4333
|
targets: plan.targets,
|
|
4534
4334
|
embeddedRuntimeVersion: compiler.version,
|
|
4535
|
-
execute: (entry, outfile, target) => execNodeBinaryBuild(entry, outfile,
|
|
4335
|
+
execute: (entry, outfile, target) => execNodeBinaryBuild(entry, outfile, target, plan, compiler, bunRunner, io.stderr)
|
|
4536
4336
|
};
|
|
4537
4337
|
return {
|
|
4538
4338
|
snapshotRunner: bunRunner,
|
|
@@ -4542,12 +4342,12 @@ async function selectCompilers(plan, io) {
|
|
|
4542
4342
|
const compiler = await resolveBinaryCompiler(plan.runtime, plan.userPackageJson, plan.cwd);
|
|
4543
4343
|
printCompiler(compiler);
|
|
4544
4344
|
if (plan.runtime === "bun") {
|
|
4545
|
-
assertTargetsBuildableWithoutBun(plan.targets,
|
|
4345
|
+
assertTargetsBuildableWithoutBun(plan.targets, compiler.runner);
|
|
4546
4346
|
const distribution = {
|
|
4547
4347
|
table: BUN_TARGETS,
|
|
4548
4348
|
targets: plan.targets,
|
|
4549
4349
|
embeddedRuntimeVersion: compiler.version,
|
|
4550
|
-
execute: (entry, outfile, target) => execBuild(entry, outfile,
|
|
4350
|
+
execute: (entry, outfile, target) => execBuild(entry, outfile, target, plan, compiler.runner)
|
|
4551
4351
|
};
|
|
4552
4352
|
return {
|
|
4553
4353
|
snapshotRunner: compiler.runner,
|