@oh-my-pi/pi-utils 18.0.10 → 18.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/types/env.d.ts +9 -0
- package/dist/types/path.d.ts +2 -0
- package/dist/types/postmortem.d.ts +4 -0
- package/dist/types/runtime-install.d.ts +8 -1
- package/package.json +6 -6
- package/src/acp/connection.ts +6 -10
- package/src/ar/entries.ts +2 -2
- package/src/ar/iso.ts +4 -4
- package/src/ar/rar/rar4-decoder.ts +4 -4
- package/src/ar/rar/rar5-decoder.ts +4 -4
- package/src/ar/sevenzip.ts +17 -2
- package/src/ar/unix-ar.ts +2 -2
- package/src/browsers.ts +11 -5
- package/src/dates.ts +1 -1
- package/src/docx/converter.ts +2 -2
- package/src/dom/core.ts +4 -4
- package/src/env.ts +12 -0
- package/src/format.ts +1 -1
- package/src/frontmatter.ts +1 -0
- package/src/marked/core.ts +8 -1
- package/src/path.ts +14 -0
- package/src/postmortem.ts +33 -6
- package/src/prompt.ts +1 -0
- package/src/ring.ts +1 -0
- package/src/runtime-install.ts +77 -26
- package/src/template.ts +2 -2
- package/src/tls-fetch.ts +1 -1
- package/src/turndown/html.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.1.0] - 2026-09-01
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `postmortem.fatal` for terminal-safe top-level failure reporting.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed sub-second duration formatting so it no longer exposes floating-point precision noise.
|
|
14
|
+
- Managed Chrome-for-Testing downloads now reject unsupported Linux ARM64 hosts instead of installing an incompatible x86_64 browser.
|
|
15
|
+
- Fixed Markdown reference-link labels that match built-in `Object.prototype` names, such as `constructor` and `__proto__`, so they are no longer misidentified as definitions or emitted with an undefined URL ([#10283](https://github.com/can1357/oh-my-pi/issues/10283)).
|
|
16
|
+
|
|
17
|
+
## [18.0.11] - 2026-08-29
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Fixed runtime installation getting stuck for up to 60 seconds after an installer crash or forced termination, allowing subsequent installation attempts to proceed normally.
|
|
22
|
+
|
|
5
23
|
## [18.0.10] - 2026-08-28
|
|
6
24
|
|
|
7
25
|
### Added
|
package/dist/types/env.d.ts
CHANGED
|
@@ -16,6 +16,15 @@ export declare function isValidEnvName(name: string): boolean;
|
|
|
16
16
|
export declare function isSafeEnvName(name: string): boolean;
|
|
17
17
|
export declare function isSafeEnvValue(value: string): boolean;
|
|
18
18
|
export declare function isMacosMallocStackLoggingEnvName(name: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* True when running inside a WSL (Windows Subsystem for Linux) distribution.
|
|
21
|
+
*
|
|
22
|
+
* WSL reports `linux` for `process.platform`, so the only reliable signal is
|
|
23
|
+
* the `WSL_DISTRO_NAME`/`WSL_INTEROP` variables the interop layer injects.
|
|
24
|
+
* Callers use this to translate Windows drive paths to their `/mnt/<drive>`
|
|
25
|
+
* mounts and to route clipboard access through `powershell.exe`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isWsl(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): boolean;
|
|
19
28
|
export declare function filterProcessEnv(env: Record<string, string | undefined>): Record<string, string>;
|
|
20
29
|
/** Filters process env for child shells without launch-cwd dotenv values. */
|
|
21
30
|
export declare function filterChildShellEnv(env: Record<string, string | undefined>, cwd?: string): Record<string, string>;
|
package/dist/types/path.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
/** Maps an absolute Windows drive path to the drive's default WSL mount. */
|
|
2
|
+
export declare function windowsPathToWslMount(filePath: string): string | undefined;
|
|
1
3
|
/** Removes Win32 extended-length prefixes before passing paths to Bun APIs. */
|
|
2
4
|
export declare function stripWindowsExtendedLengthPathPrefix(filePath: string, platform?: NodeJS.Platform): string;
|
|
@@ -123,6 +123,10 @@ export declare function interceptUnhandledRejections(interceptor: (reason: unkno
|
|
|
123
123
|
* through an uncaught exception or unhandled rejection.
|
|
124
124
|
*/
|
|
125
125
|
export declare function registerFatalRecoveryHint(provider: FatalRecoveryHintProvider): () => void;
|
|
126
|
+
/**
|
|
127
|
+
* Reports a caught top-level failure after terminal owners restore their display, then exits.
|
|
128
|
+
*/
|
|
129
|
+
export declare function fatal(error: unknown): Promise<never>;
|
|
126
130
|
/** Controls when a registered cleanup callback participates in cleanup passes. */
|
|
127
131
|
export interface CleanupRegistrationOptions {
|
|
128
132
|
/**
|
|
@@ -73,6 +73,13 @@ export interface EnsureRuntimeInstalledOptions {
|
|
|
73
73
|
export declare function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void>;
|
|
74
74
|
/**
|
|
75
75
|
* Materialize a pinned dependency set into `runtimeDir` (idempotent,
|
|
76
|
-
* cross-process safe
|
|
76
|
+
* cross-process safe). Returns `runtimeDir`.
|
|
77
|
+
*
|
|
78
|
+
* Serialization uses the OS-backed {@link withFileLock} at
|
|
79
|
+
* `${runtimeDir}.install.lock`, which the kernel releases on process death, so
|
|
80
|
+
* a crashed installer cannot wedge later attempts (issue #10120). The path is
|
|
81
|
+
* deliberately distinct from the legacy `${runtimeDir}.lock` mkdir directory;
|
|
82
|
+
* {@link withLegacyInstallLock} atomically reserves that namespace during the
|
|
83
|
+
* new install so older processes cannot cross the migration boundary.
|
|
77
84
|
*/
|
|
78
85
|
export declare function ensureRuntimeInstalled(options: EnsureRuntimeInstalledOptions): Promise<string>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "18.0
|
|
4
|
+
"version": "18.1.0",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Stencil Labs, Inc.",
|
|
@@ -23,15 +23,15 @@
|
|
|
23
23
|
"main": "./src/index.ts",
|
|
24
24
|
"types": "./dist/types/index.d.ts",
|
|
25
25
|
"scripts": {
|
|
26
|
-
"check": "
|
|
26
|
+
"check": "oxlint . && oxfmt --check --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts' && bun run check:types",
|
|
27
27
|
"check:types": "tsgo -p tsconfig.json --noEmit",
|
|
28
|
-
"lint": "
|
|
28
|
+
"lint": "oxlint .",
|
|
29
29
|
"test": "bun test --parallel",
|
|
30
|
-
"fix": "
|
|
31
|
-
"fmt": "
|
|
30
|
+
"fix": "oxlint --fix --fix-suggestions . && bun run fmt",
|
|
31
|
+
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-natives": "18.0
|
|
34
|
+
"@oh-my-pi/pi-natives": "18.1.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/bun": "^1.3.14"
|
package/src/acp/connection.ts
CHANGED
|
@@ -67,13 +67,11 @@ export class AgentSideConnection {
|
|
|
67
67
|
#connection: RpcConnection;
|
|
68
68
|
|
|
69
69
|
constructor(toAgent: (connection: AgentSideConnection) => Agent, stream: Stream) {
|
|
70
|
-
let agent: Agent | undefined;
|
|
71
70
|
this.#connection = new RpcConnection(stream, async (method, params, notification) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return dispatchAgent(target, method, params, notification);
|
|
71
|
+
if (!agent) throw RequestError.internalError(undefined, "Agent is not initialized");
|
|
72
|
+
return dispatchAgent(agent, method, params, notification);
|
|
75
73
|
});
|
|
76
|
-
agent = toAgent(this);
|
|
74
|
+
const agent = toAgent(this);
|
|
77
75
|
}
|
|
78
76
|
|
|
79
77
|
/** Signal aborted when the transport closes. */
|
|
@@ -170,13 +168,11 @@ export class ClientSideConnection {
|
|
|
170
168
|
#connection: RpcConnection;
|
|
171
169
|
|
|
172
170
|
constructor(toClient: (connection: ClientSideConnection) => Client, stream: Stream) {
|
|
173
|
-
let client: Client | undefined;
|
|
174
171
|
this.#connection = new RpcConnection(stream, async (method, params, notification) => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return dispatchClient(target, method, params, notification);
|
|
172
|
+
if (!client) throw RequestError.internalError(undefined, "Client is not initialized");
|
|
173
|
+
return dispatchClient(client, method, params, notification);
|
|
178
174
|
});
|
|
179
|
-
client = toClient(this);
|
|
175
|
+
const client = toClient(this);
|
|
180
176
|
}
|
|
181
177
|
/** Signal aborted when the transport closes. */
|
|
182
178
|
get signal(): AbortSignal {
|
package/src/ar/entries.ts
CHANGED
|
@@ -45,7 +45,7 @@ export function upsertArchiveEntry(
|
|
|
45
45
|
*/
|
|
46
46
|
export function ensureParentDirectories(map: Map<string, ArchiveIndexEntry>, limits: ArchiveLimits): void {
|
|
47
47
|
assertEntryCount(map.size, limits);
|
|
48
|
-
for (const entry of
|
|
48
|
+
for (const entry of Array.from(map.values())) {
|
|
49
49
|
const parts = entry.path.split("/");
|
|
50
50
|
const stop = parts.length - 1;
|
|
51
51
|
for (let index = 1; index <= stop; index++) {
|
|
@@ -73,7 +73,7 @@ export function resolveArchiveLinkPath(
|
|
|
73
73
|
): string {
|
|
74
74
|
let resolvedPath = archivePath;
|
|
75
75
|
const seen = new Set<string>();
|
|
76
|
-
for (let rewrites = 0; !seen.has(resolvedPath);
|
|
76
|
+
for (let rewrites = 0; !seen.has(resolvedPath);) {
|
|
77
77
|
seen.add(resolvedPath);
|
|
78
78
|
let replacement: string | undefined;
|
|
79
79
|
for (let end = resolvedPath.length; end > 0; end = resolvedPath.lastIndexOf("/", end - 1)) {
|
package/src/ar/iso.ts
CHANGED
|
@@ -324,7 +324,7 @@ function decodeIdentifier(identifier: Uint8Array, joliet: boolean): string {
|
|
|
324
324
|
|
|
325
325
|
function findSuspSkip(record: IsoRecord): number | undefined {
|
|
326
326
|
const bytes = record.systemUse;
|
|
327
|
-
for (let offset = 0; offset + 7 <= bytes.byteLength;
|
|
327
|
+
for (let offset = 0; offset + 7 <= bytes.byteLength;) {
|
|
328
328
|
const length = bytes[offset + 2]!;
|
|
329
329
|
if (length < 4 || offset + length > bytes.byteLength) return undefined;
|
|
330
330
|
if (
|
|
@@ -359,7 +359,7 @@ async function parseSusp(
|
|
|
359
359
|
let relocation: SuspData["relocation"];
|
|
360
360
|
for (let queueIndex = 0; queueIndex < queue.length; queueIndex++) {
|
|
361
361
|
const area = queue[queueIndex]!;
|
|
362
|
-
for (let offset = 0; offset + 4 <= area.byteLength;
|
|
362
|
+
for (let offset = 0; offset + 4 <= area.byteLength;) {
|
|
363
363
|
const signature = String.fromCharCode(area[offset]!, area[offset + 1]!);
|
|
364
364
|
const length = area[offset + 2]!;
|
|
365
365
|
const version = area[offset + 3]!;
|
|
@@ -466,7 +466,7 @@ async function readDirectoryRecords(
|
|
|
466
466
|
const end = safeAdd(start, record.size, "directory extent range");
|
|
467
467
|
const bytes = await budget.read(source, start, end, "directory extent");
|
|
468
468
|
const records: IsoRecord[] = [];
|
|
469
|
-
for (let offset = 0; offset < bytes.byteLength;
|
|
469
|
+
for (let offset = 0; offset < bytes.byteLength;) {
|
|
470
470
|
const sectorRemaining = blockSize - (offset % blockSize);
|
|
471
471
|
const length = bytes[offset]!;
|
|
472
472
|
if (length === 0) {
|
|
@@ -586,7 +586,7 @@ async function readIsoImpl(source: ByteSource, options: Parameters<FormatReader>
|
|
|
586
586
|
) {
|
|
587
587
|
suspSkip = findSuspSkip(records[0]!);
|
|
588
588
|
}
|
|
589
|
-
for (let index = 0; index < records.length;
|
|
589
|
+
for (let index = 0; index < records.length;) {
|
|
590
590
|
const record = records[index]!;
|
|
591
591
|
if (record.identifier.byteLength === 1 && (record.identifier[0] === 0 || record.identifier[0] === 1)) {
|
|
592
592
|
index++;
|
|
@@ -45,7 +45,7 @@ class Bits {
|
|
|
45
45
|
read(count: number): number {
|
|
46
46
|
if (count === 0) return 0;
|
|
47
47
|
let value = 0;
|
|
48
|
-
for (let left = count; left > 0;
|
|
48
|
+
for (let left = count; left > 0;) {
|
|
49
49
|
if (this.pos >= this.bytes.byteLength - 4) corrupt("truncated compressed data");
|
|
50
50
|
const take = Math.min(left, 8 - this.bit);
|
|
51
51
|
value = value * 2 ** take + ((this.bytes[this.pos]! >>> (8 - this.bit - take)) & (2 ** take - 1));
|
|
@@ -340,7 +340,7 @@ class VmBits {
|
|
|
340
340
|
|
|
341
341
|
read(count: number): number {
|
|
342
342
|
let value = 0;
|
|
343
|
-
for (let left = count; left > 0;
|
|
343
|
+
for (let left = count; left > 0;) {
|
|
344
344
|
const bytePosition = this.#position >>> 3;
|
|
345
345
|
if (bytePosition >= this.#bytes.byteLength) corrupt("truncated RarVM code");
|
|
346
346
|
const bitPosition = this.#position & 7;
|
|
@@ -393,7 +393,7 @@ function applyFilters(output: Uint8Array, filters: PendingFilter[]): void {
|
|
|
393
393
|
continue;
|
|
394
394
|
}
|
|
395
395
|
const compareOpcode = filter.type === 2 ? 0xe9 : 0xe8;
|
|
396
|
-
for (let position = 0; position + 4 < data.byteLength;
|
|
396
|
+
for (let position = 0; position + 4 < data.byteLength;) {
|
|
397
397
|
const opcode = data[position++]!;
|
|
398
398
|
if (opcode !== 0xe8 && opcode !== compareOpcode) continue;
|
|
399
399
|
const offset = position + (filter.fileOffset >>> 0);
|
|
@@ -437,7 +437,7 @@ function readLengthTable(bits: Bits, previous: Uint8Array, size: number): Uint8A
|
|
|
437
437
|
const preTable = new Huffman(20);
|
|
438
438
|
preTable.build(preLengths, 0, 20);
|
|
439
439
|
const lengths = new Uint8Array(size);
|
|
440
|
-
for (let position = 0; position < size;
|
|
440
|
+
for (let position = 0; position < size;) {
|
|
441
441
|
const symbol = preTable.decode(bits);
|
|
442
442
|
if (symbol < 16) {
|
|
443
443
|
lengths[position] = (symbol + previous[position]!) & 15;
|
|
@@ -33,7 +33,7 @@ class Bits {
|
|
|
33
33
|
if (n === 0) return 0;
|
|
34
34
|
if (n < 0 || n > 31) fail("invalid bit count");
|
|
35
35
|
let value = 0;
|
|
36
|
-
for (let left = n; left > 0;
|
|
36
|
+
for (let left = n; left > 0;) {
|
|
37
37
|
if (this.pos >= this.bytes.byteLength - 16) fail("truncated compressed data");
|
|
38
38
|
const take = Math.min(left, 8 - this.bit);
|
|
39
39
|
value = value * 2 ** take + ((this.bytes[this.pos]! >>> (8 - this.bit - take)) & (2 ** take - 1));
|
|
@@ -211,7 +211,7 @@ export class Rar5Decoder {
|
|
|
211
211
|
if ((flags & 0x80) === 0) return;
|
|
212
212
|
|
|
213
213
|
const levelLengths = new Uint8Array(20);
|
|
214
|
-
for (let i = 0; i < 20;
|
|
214
|
+
for (let i = 0; i < 20;) {
|
|
215
215
|
const length = bits.read(4);
|
|
216
216
|
if (length === 15) {
|
|
217
217
|
const zeros = bits.read(4);
|
|
@@ -226,7 +226,7 @@ export class Rar5Decoder {
|
|
|
226
226
|
level.build(levelLengths);
|
|
227
227
|
const tableLength = version === 1 ? TABLE_SIZE : TABLE_SIZE - 16;
|
|
228
228
|
const lengthsCompact = new Uint8Array(tableLength);
|
|
229
|
-
for (let i = 0; i < tableLength;
|
|
229
|
+
for (let i = 0; i < tableLength;) {
|
|
230
230
|
const symbol = level.decode(bits);
|
|
231
231
|
if (symbol < 16) {
|
|
232
232
|
lengthsCompact[i++] = symbol;
|
|
@@ -358,7 +358,7 @@ function applyFilters(output: Uint8Array, filters: Filter[]): void {
|
|
|
358
358
|
}
|
|
359
359
|
} else if (filter.type === 1 || filter.type === 2) {
|
|
360
360
|
const fileSize = 1 << 24;
|
|
361
|
-
for (let pos = 0; pos + 4 < data.byteLength;
|
|
361
|
+
for (let pos = 0; pos + 4 < data.byteLength;) {
|
|
362
362
|
const opcode = data[pos++]!;
|
|
363
363
|
if (opcode !== 0xe8 && (filter.type === 1 || opcode !== 0xe9)) continue;
|
|
364
364
|
const offset = (filter.start + pos) & (fileSize - 1);
|
package/src/ar/sevenzip.ts
CHANGED
|
@@ -107,6 +107,7 @@ interface FileMetadata {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
function readBoolVector(reader: HeaderReader, count: number): boolean[] {
|
|
110
|
+
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
110
111
|
const result = new Array<boolean>(count);
|
|
111
112
|
let byte = 0;
|
|
112
113
|
let mask = 0;
|
|
@@ -122,12 +123,16 @@ function readBoolVector(reader: HeaderReader, count: number): boolean[] {
|
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
function readDefinedVector(reader: HeaderReader, count: number): boolean[] {
|
|
125
|
-
if (reader.readByte() !== 0)
|
|
126
|
+
if (reader.readByte() !== 0) {
|
|
127
|
+
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
128
|
+
return new Array<boolean>(count).fill(true);
|
|
129
|
+
}
|
|
126
130
|
return readBoolVector(reader, count);
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
function readDigests(reader: HeaderReader, count: number): Digests {
|
|
130
134
|
const defined = readDefinedVector(reader, count);
|
|
135
|
+
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
131
136
|
const values = new Array<number | undefined>(count);
|
|
132
137
|
for (let index = 0; index < count; index++) if (defined[index]) values[index] = reader.readUInt32();
|
|
133
138
|
return { defined, values };
|
|
@@ -229,7 +234,10 @@ function parsePackInfo(reader: HeaderReader, streams: SevenZipStreams, options:
|
|
|
229
234
|
} else reader.skipSizedProperty();
|
|
230
235
|
}
|
|
231
236
|
if (streams.packSizes.length !== count) throw new ArchiveError("Invalid 7z PackInfo without complete sizes");
|
|
232
|
-
if (streams.packCrcs.length === 0)
|
|
237
|
+
if (streams.packCrcs.length === 0) {
|
|
238
|
+
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
239
|
+
streams.packCrcs = new Array(count).fill(undefined);
|
|
240
|
+
}
|
|
233
241
|
}
|
|
234
242
|
|
|
235
243
|
function parseUnpackInfo(reader: HeaderReader, streams: SevenZipStreams, options: FormatReadOptions): void {
|
|
@@ -254,6 +262,7 @@ function parseUnpackInfo(reader: HeaderReader, streams: SevenZipStreams, options
|
|
|
254
262
|
}
|
|
255
263
|
|
|
256
264
|
function parseSubStreamsInfo(reader: HeaderReader, streams: SevenZipStreams): void {
|
|
265
|
+
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
257
266
|
const counts = new Array<number>(streams.folders.length).fill(1);
|
|
258
267
|
let sizes: number[] | undefined;
|
|
259
268
|
let rawDigests: Digests | undefined;
|
|
@@ -398,6 +407,7 @@ function parseNames(reader: HeaderReader, count: number, external: Uint8Array[],
|
|
|
398
407
|
function parseTimes(reader: HeaderReader, count: number, external: Uint8Array[]): Array<number | undefined> {
|
|
399
408
|
const defined = readDefinedVector(reader, count);
|
|
400
409
|
const values = selectPropertyStream(reader, external);
|
|
410
|
+
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
401
411
|
const result = new Array<number | undefined>(count);
|
|
402
412
|
for (let index = 0; index < count; index++)
|
|
403
413
|
if (defined[index]) {
|
|
@@ -411,6 +421,7 @@ function parseTimes(reader: HeaderReader, count: number, external: Uint8Array[])
|
|
|
411
421
|
function parseAttributes(reader: HeaderReader, count: number, external: Uint8Array[]): Array<number | undefined> {
|
|
412
422
|
const defined = readDefinedVector(reader, count);
|
|
413
423
|
const values = selectPropertyStream(reader, external);
|
|
424
|
+
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
414
425
|
const result = new Array<number | undefined>(count);
|
|
415
426
|
for (let index = 0; index < count; index++) if (defined[index]) result[index] = values.readUInt32();
|
|
416
427
|
return result;
|
|
@@ -420,11 +431,15 @@ function parseFilesInfo(reader: HeaderReader, external: Uint8Array[], options: F
|
|
|
420
431
|
const count = reader.readNumber("file count");
|
|
421
432
|
assertEntryCount(count, options.limits);
|
|
422
433
|
const metadata: FileMetadata = {
|
|
434
|
+
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
423
435
|
names: new Array<string>(count).fill(""),
|
|
436
|
+
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
424
437
|
emptyStreams: new Array<boolean>(count).fill(false),
|
|
425
438
|
emptyFiles: [],
|
|
426
439
|
antiFiles: [],
|
|
440
|
+
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
427
441
|
mtimes: new Array<number | undefined>(count),
|
|
442
|
+
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
428
443
|
attributes: new Array<number | undefined>(count),
|
|
429
444
|
};
|
|
430
445
|
for (;;) {
|
package/src/ar/unix-ar.ts
CHANGED
|
@@ -197,7 +197,7 @@ export function readUnixArEntriesFromBuffer(bytes: Uint8Array, options: FormatRe
|
|
|
197
197
|
const records: RawArMember[] = [];
|
|
198
198
|
let longNames: Uint8Array | undefined;
|
|
199
199
|
let metadataSize = 0;
|
|
200
|
-
for (let position = SIGNATURE.length; position < bytes.byteLength;
|
|
200
|
+
for (let position = SIGNATURE.length; position < bytes.byteLength;) {
|
|
201
201
|
if (bytes.byteLength - position < HEADER_SIZE)
|
|
202
202
|
throw new ArchiveError("Invalid ar archive: truncated member header");
|
|
203
203
|
const header = parseHeader(readMemoryRange(bytes, position, position + HEADER_SIZE));
|
|
@@ -255,7 +255,7 @@ async function readUnixArImpl(source: ByteSource, options: FormatReadOptions): P
|
|
|
255
255
|
const records: RawArMember[] = [];
|
|
256
256
|
let longNames: Uint8Array | undefined;
|
|
257
257
|
let metadataSize = 0;
|
|
258
|
-
for (let position = SIGNATURE.length; position < source.size;
|
|
258
|
+
for (let position = SIGNATURE.length; position < source.size;) {
|
|
259
259
|
const headerBytes = await readExact(source, position, position + HEADER_SIZE, "member header");
|
|
260
260
|
const header = parseHeader(headerBytes);
|
|
261
261
|
metadataSize += HEADER_SIZE;
|
package/src/browsers.ts
CHANGED
|
@@ -39,6 +39,13 @@ export enum BrowserPlatform {
|
|
|
39
39
|
WIN32 = "win32",
|
|
40
40
|
WIN64 = "win64",
|
|
41
41
|
}
|
|
42
|
+
type ChromeForTestingPlatform = Exclude<BrowserPlatform, BrowserPlatform.LINUX_ARM>;
|
|
43
|
+
|
|
44
|
+
function requireChromeForTestingPlatform(platform: BrowserPlatform): ChromeForTestingPlatform {
|
|
45
|
+
if (platform === BrowserPlatform.LINUX_ARM)
|
|
46
|
+
throw new Error("Chrome for Testing does not provide linux/arm64 builds");
|
|
47
|
+
return platform;
|
|
48
|
+
}
|
|
42
49
|
|
|
43
50
|
const BROWSERS = [
|
|
44
51
|
Browser.CHROME,
|
|
@@ -163,13 +170,13 @@ export function getDownloadUrl(
|
|
|
163
170
|
|
|
164
171
|
/** Compute the executable path in Puppeteer's cache layout. */
|
|
165
172
|
export function computeExecutablePath(options: ComputeExecutablePathOptions): string {
|
|
166
|
-
const
|
|
167
|
-
if (!
|
|
173
|
+
const detectedPlatform = options.platform ?? detectBrowserPlatform();
|
|
174
|
+
if (!detectedPlatform) throw new Error("Cannot determine a browser platform for this host");
|
|
168
175
|
if (options.browser !== Browser.CHROME) throw new Error(`Unsupported browser executable: ${options.browser}`);
|
|
176
|
+
const platform = requireChromeForTestingPlatform(detectedPlatform);
|
|
169
177
|
const installDir = installationDir(options.cacheDir, options.browser, platform, options.buildId);
|
|
170
178
|
switch (platform) {
|
|
171
179
|
case BrowserPlatform.LINUX:
|
|
172
|
-
case BrowserPlatform.LINUX_ARM:
|
|
173
180
|
return path.join(installDir, "chrome-linux64", "chrome");
|
|
174
181
|
case BrowserPlatform.MAC:
|
|
175
182
|
return path.join(
|
|
@@ -292,9 +299,8 @@ async function fetchMetadata<T>(filename: string): Promise<T> {
|
|
|
292
299
|
}
|
|
293
300
|
|
|
294
301
|
function chromeArchivePlatform(platform: BrowserPlatform): string {
|
|
295
|
-
switch (platform) {
|
|
302
|
+
switch (requireChromeForTestingPlatform(platform)) {
|
|
296
303
|
case BrowserPlatform.LINUX:
|
|
297
|
-
case BrowserPlatform.LINUX_ARM:
|
|
298
304
|
return "linux64";
|
|
299
305
|
case BrowserPlatform.MAC:
|
|
300
306
|
return "mac-x64";
|
package/src/dates.ts
CHANGED
|
@@ -100,7 +100,7 @@ export function format(value: Date | number, pattern: string): string {
|
|
|
100
100
|
const date = asDate(value);
|
|
101
101
|
let result = "";
|
|
102
102
|
|
|
103
|
-
for (let index = 0; index < pattern.length;
|
|
103
|
+
for (let index = 0; index < pattern.length;) {
|
|
104
104
|
if (pattern[index] === "'") {
|
|
105
105
|
if (pattern[index + 1] === "'") {
|
|
106
106
|
result += "'";
|
package/src/docx/converter.ts
CHANGED
|
@@ -530,7 +530,7 @@ function renderList(
|
|
|
530
530
|
|
|
531
531
|
function renderBlocks(blocks: readonly Block[]): string {
|
|
532
532
|
let html = "";
|
|
533
|
-
for (let index = 0; index < blocks.length;
|
|
533
|
+
for (let index = 0; index < blocks.length;) {
|
|
534
534
|
const block = blocks[index];
|
|
535
535
|
if (block.kind === "html") {
|
|
536
536
|
html += block.html;
|
|
@@ -549,7 +549,7 @@ function renderBlocks(blocks: readonly Block[]): string {
|
|
|
549
549
|
listItems.push(candidate);
|
|
550
550
|
index++;
|
|
551
551
|
}
|
|
552
|
-
for (let itemIndex = 0; itemIndex < listItems.length;
|
|
552
|
+
for (let itemIndex = 0; itemIndex < listItems.length;) {
|
|
553
553
|
const rendered = renderList(listItems, itemIndex, listItems[itemIndex].list?.level ?? 0);
|
|
554
554
|
html += rendered.html;
|
|
555
555
|
itemIndex = rendered.next;
|
package/src/dom/core.ts
CHANGED
|
@@ -105,7 +105,7 @@ export class EventTarget {
|
|
|
105
105
|
dispatchEvent(event: Event): boolean {
|
|
106
106
|
if (!event.target) event.target = this;
|
|
107
107
|
event.currentTarget = this;
|
|
108
|
-
for (const listener of
|
|
108
|
+
for (const listener of Array.from(this.#listeners.get(event.type) ?? [])) {
|
|
109
109
|
if (typeof listener === "function") listener.call(this, event);
|
|
110
110
|
else listener.handleEvent(event);
|
|
111
111
|
if (event.propagationStopped) break;
|
|
@@ -196,7 +196,7 @@ export class Node extends EventTarget {
|
|
|
196
196
|
const node: Node = child;
|
|
197
197
|
if (node === this || node.contains(this)) throw new Error("The new child is an ancestor of this node");
|
|
198
198
|
if (child instanceof DocumentFragment) {
|
|
199
|
-
for (const nested of
|
|
199
|
+
for (const nested of Array.from(child.childNodes)) this.appendChild(nested);
|
|
200
200
|
return child;
|
|
201
201
|
}
|
|
202
202
|
child.parentNode?.removeChild(child);
|
|
@@ -212,7 +212,7 @@ export class Node extends EventTarget {
|
|
|
212
212
|
const index = this.childNodes.indexOf(reference);
|
|
213
213
|
if (index < 0) throw new Error("The reference node is not a child of this node");
|
|
214
214
|
if (child instanceof DocumentFragment) {
|
|
215
|
-
for (const nested of
|
|
215
|
+
for (const nested of Array.from(child.childNodes)) this.insertBefore(nested, reference);
|
|
216
216
|
return child;
|
|
217
217
|
}
|
|
218
218
|
child.parentNode?.removeChild(child);
|
|
@@ -681,7 +681,7 @@ export class Element extends Node {
|
|
|
681
681
|
const parent = this.parentNode;
|
|
682
682
|
if (!parent) return;
|
|
683
683
|
const fragment = parseFragment(value, this.documentForCreation(), this.parentElement?.localName);
|
|
684
|
-
for (const child of
|
|
684
|
+
for (const child of Array.from(fragment.childNodes)) parent.insertBefore(child, this);
|
|
685
685
|
parent.removeChild(this);
|
|
686
686
|
}
|
|
687
687
|
|
package/src/env.ts
CHANGED
|
@@ -36,6 +36,18 @@ export function isMacosMallocStackLoggingEnvName(name: string): boolean {
|
|
|
36
36
|
return name === "MallocStackLogging" || name === "MallocStackLoggingNoCompact";
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* True when running inside a WSL (Windows Subsystem for Linux) distribution.
|
|
41
|
+
*
|
|
42
|
+
* WSL reports `linux` for `process.platform`, so the only reliable signal is
|
|
43
|
+
* the `WSL_DISTRO_NAME`/`WSL_INTEROP` variables the interop layer injects.
|
|
44
|
+
* Callers use this to translate Windows drive paths to their `/mnt/<drive>`
|
|
45
|
+
* mounts and to route clipboard access through `powershell.exe`.
|
|
46
|
+
*/
|
|
47
|
+
export function isWsl(platform: NodeJS.Platform = process.platform, env: NodeJS.ProcessEnv = process.env): boolean {
|
|
48
|
+
return platform === "linux" && Boolean(env.WSL_DISTRO_NAME || env.WSL_INTEROP);
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
export function filterProcessEnv(env: Record<string, string | undefined>): Record<string, string> {
|
|
40
52
|
const result: Record<string, string> = {};
|
|
41
53
|
for (const key in env) {
|
package/src/format.ts
CHANGED
|
@@ -9,7 +9,7 @@ const DAY = 24 * HOUR;
|
|
|
9
9
|
*/
|
|
10
10
|
export function formatDuration(ms: number): string {
|
|
11
11
|
if (!Number.isFinite(ms) || ms <= 0) return "0ms";
|
|
12
|
-
if (ms < SEC) return `${ms}ms`;
|
|
12
|
+
if (ms < SEC) return `${Math.floor(ms)}ms`;
|
|
13
13
|
if (ms < MIN) return `${(ms / SEC).toFixed(1)}s`;
|
|
14
14
|
if (ms < HOUR) {
|
|
15
15
|
const mins = Math.floor(ms / MIN);
|
package/src/frontmatter.ts
CHANGED
|
@@ -22,6 +22,7 @@ export function normalizeFrontmatterKeys<T>(obj: T): T {
|
|
|
22
22
|
if (obj === null || typeof obj !== "object") return obj;
|
|
23
23
|
if (Array.isArray(obj)) {
|
|
24
24
|
let changed = false;
|
|
25
|
+
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
25
26
|
const out: unknown[] = new Array(obj.length);
|
|
26
27
|
for (let i = 0; i < obj.length; i++) {
|
|
27
28
|
const v = obj[i];
|
package/src/marked/core.ts
CHANGED
|
@@ -343,8 +343,15 @@ const DEFAULTS: MarkedOptions = {
|
|
|
343
343
|
};
|
|
344
344
|
const PUNCTUATION = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/;
|
|
345
345
|
|
|
346
|
-
function tokenList(links: Links =
|
|
346
|
+
function tokenList(links: Links = Object.create(null)): TokensList {
|
|
347
347
|
const list = [] as unknown as TokensList;
|
|
348
|
+
// The reference-definition map is keyed by user-controlled labels. A plain
|
|
349
|
+
// `{}` inherits `Object.prototype`, so a reference-style link whose label is
|
|
350
|
+
// an inherited member (`[x][constructor]`, `[x][__proto__]`) resolves to a
|
|
351
|
+
// truthy non-definition and yields a link token with `href: undefined`
|
|
352
|
+
// (issue #10283). A null-prototype map makes such lookups miss, so the link
|
|
353
|
+
// correctly falls back to literal text, and label writes cannot pollute the
|
|
354
|
+
// prototype.
|
|
348
355
|
list.links = links;
|
|
349
356
|
return list;
|
|
350
357
|
}
|
package/src/path.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
3
|
+
const WINDOWS_DRIVE_PATH = /^([A-Za-z]):[\\/](.*)$/;
|
|
4
|
+
|
|
5
|
+
/** Maps an absolute Windows drive path to the drive's default WSL mount. */
|
|
6
|
+
export function windowsPathToWslMount(filePath: string): string | undefined {
|
|
7
|
+
const normalized = path.win32.normalize(filePath.trim());
|
|
8
|
+
const match = WINDOWS_DRIVE_PATH.exec(normalized);
|
|
9
|
+
if (!match) return undefined;
|
|
10
|
+
const [, drive, rest] = match;
|
|
11
|
+
const segments = rest.split("\\").filter(Boolean);
|
|
12
|
+
return path.posix.join("/mnt", drive!.toLowerCase(), ...segments);
|
|
13
|
+
}
|
|
14
|
+
|
|
1
15
|
const WINDOWS_DRIVE_EXTENDED_PREFIX = /^\\\\[?]\\([A-Za-z]:[\\/].*)$/;
|
|
2
16
|
const WINDOWS_UNC_EXTENDED_PREFIX = /^\\\\[?]\\UNC[\\/]([^\\/]+)[\\/](.+)$/i;
|
|
3
17
|
const WINDOWS_DRIVE_EXTENDED_FORWARD_PREFIX = /^\/\/[?]\/([A-Za-z]:\/.*)$/;
|
package/src/postmortem.ts
CHANGED
|
@@ -222,7 +222,7 @@ export function isInternalSocketClosedError(err: unknown): boolean {
|
|
|
222
222
|
const internal = frames.every(frame => {
|
|
223
223
|
const trimmed = frame.trim();
|
|
224
224
|
if (trimmed === "" || trimmed === "at unknown" || trimmed === "at native") return true;
|
|
225
|
-
if (!/\(node:[^)]*\)$/.test(trimmed) &&
|
|
225
|
+
if (!/\(node:[^)]*\)$/.test(trimmed) && !trimmed.startsWith("at node:")) return false;
|
|
226
226
|
hasNetFrame ||= trimmed.includes("node:net:");
|
|
227
227
|
return true;
|
|
228
228
|
});
|
|
@@ -395,23 +395,40 @@ function formatFatalError(label: string, err: Error): string {
|
|
|
395
395
|
return `\n[${label}] ${name}: ${message}${formattedStack}\n`;
|
|
396
396
|
}
|
|
397
397
|
|
|
398
|
-
async function exitAfterFatal(
|
|
398
|
+
async function exitAfterFatal(output: string, logMessage: string, err: Error, reason: Reason): Promise<never> {
|
|
399
399
|
const forcedExit = setTimeout(() => exitProcess(1), CLEANUP_DEADLINE_MS);
|
|
400
400
|
try {
|
|
401
|
+
// Cleanup callbacks are invoked synchronously before runCleanup returns its
|
|
402
|
+
// completion promise. TUI owners therefore hand the cursor back before the
|
|
403
|
+
// fatal report is written, while slower resource cleanup continues afterward.
|
|
404
|
+
const cleanup = runCleanup(reason);
|
|
401
405
|
restoreTerminalStderr();
|
|
402
406
|
// A revoked terminal can make stream writes raise another fatal error. Use
|
|
403
407
|
// the descriptor directly so failure stays synchronous and contained.
|
|
404
408
|
try {
|
|
405
|
-
fs.writeSync(2,
|
|
409
|
+
fs.writeSync(2, output);
|
|
406
410
|
} catch {}
|
|
407
411
|
logger.error(logMessage, { err });
|
|
408
|
-
await
|
|
412
|
+
await cleanup;
|
|
409
413
|
} finally {
|
|
410
414
|
clearTimeout(forcedExit);
|
|
411
415
|
exitProcess(1);
|
|
412
416
|
}
|
|
413
417
|
}
|
|
414
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Reports a caught top-level failure after terminal owners restore their display, then exits.
|
|
421
|
+
*/
|
|
422
|
+
export async function fatal(error: unknown): Promise<never> {
|
|
423
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
424
|
+
const output = `${Bun.inspect(error, { colors: process.stderr.isTTY === true })}\n${formatFatalRecoveryHints()}`;
|
|
425
|
+
if (!isMainThread) {
|
|
426
|
+
process.stderr.write(output);
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
return exitAfterFatal(output, "Fatal error", err, Reason.UNHANDLED_REJECTION);
|
|
430
|
+
}
|
|
431
|
+
|
|
415
432
|
if (isMainThread) {
|
|
416
433
|
process
|
|
417
434
|
.on("SIGINT", async () => {
|
|
@@ -461,7 +478,12 @@ if (isMainThread) {
|
|
|
461
478
|
});
|
|
462
479
|
return;
|
|
463
480
|
}
|
|
464
|
-
await exitAfterFatal(
|
|
481
|
+
await exitAfterFatal(
|
|
482
|
+
`${formatFatalError("Uncaught Exception", err)}${formatFatalRecoveryHints()}`,
|
|
483
|
+
"Uncaught exception",
|
|
484
|
+
err,
|
|
485
|
+
Reason.UNCAUGHT_EXCEPTION,
|
|
486
|
+
);
|
|
465
487
|
})
|
|
466
488
|
.on("unhandledRejection", async reason => {
|
|
467
489
|
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
@@ -497,7 +519,12 @@ if (isMainThread) {
|
|
|
497
519
|
});
|
|
498
520
|
}
|
|
499
521
|
}
|
|
500
|
-
await exitAfterFatal(
|
|
522
|
+
await exitAfterFatal(
|
|
523
|
+
`${formatFatalError("Unhandled Rejection", err)}${formatFatalRecoveryHints()}`,
|
|
524
|
+
"Unhandled rejection",
|
|
525
|
+
err,
|
|
526
|
+
Reason.UNHANDLED_REJECTION,
|
|
527
|
+
);
|
|
501
528
|
})
|
|
502
529
|
.on("exit", async () => {
|
|
503
530
|
void runCleanup(Reason.EXIT); // fire and forget (exit imminent)
|
package/src/prompt.ts
CHANGED
|
@@ -196,6 +196,7 @@ export function format(content: string, options: PromptFormatOptions = {}): stri
|
|
|
196
196
|
} = options;
|
|
197
197
|
const isPreRender = renderPhase === "pre-render";
|
|
198
198
|
const lines = content.split("\n");
|
|
199
|
+
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
199
200
|
const result: string[] = new Array(lines.length);
|
|
200
201
|
let n = 0; // logical length of `result` (pops are n--)
|
|
201
202
|
let inCodeBlock = false;
|
package/src/ring.ts
CHANGED
|
@@ -15,6 +15,7 @@ export class RingBuffer<T> {
|
|
|
15
15
|
* @param capacity - The maximum number of elements the buffer can hold. Must be positive.
|
|
16
16
|
*/
|
|
17
17
|
constructor(public readonly capacity: number) {
|
|
18
|
+
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
18
19
|
this.#buf = new Array(capacity);
|
|
19
20
|
}
|
|
20
21
|
|
package/src/runtime-install.ts
CHANGED
|
@@ -2,6 +2,8 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as fsp from "node:fs/promises";
|
|
3
3
|
import * as Module from "node:module";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { withFileLock } from "./file-lock";
|
|
6
|
+
import { isEexist, isEnoent } from "./fs-error";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* On-demand runtime dependency support for native-heavy optional packages
|
|
@@ -303,25 +305,62 @@ export interface EnsureRuntimeInstalledOptions {
|
|
|
303
305
|
lockSleepMs?: number;
|
|
304
306
|
}
|
|
305
307
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
308
|
+
/** No runtime install plausibly runs this long, so older legacy lock directories are crash orphans. */
|
|
309
|
+
const STALE_LEGACY_LOCK_MS = 10 * 60_000;
|
|
309
310
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Run `fn` while reserving the pre-crash-safe `${runtimeDir}.lock` namespace.
|
|
313
|
+
*
|
|
314
|
+
* Versions through 18.0.10 serialized installs with a bare lock *directory*
|
|
315
|
+
* that only its creator removed; an installer killed outside that window
|
|
316
|
+
* (SIGKILL/OOM/Ctrl-C) left it unreleasable, wedging every later install for
|
|
317
|
+
* the full wait envelope (issue #10120). During an in-flight upgrade a legacy
|
|
318
|
+
* process may still legitimately own this directory, so poll until it is
|
|
319
|
+
* released and only force-reclaim once the directory is older than any
|
|
320
|
+
* plausible install ({@link STALE_LEGACY_LOCK_MS}) — never merely because a
|
|
321
|
+
* retry budget elapsed, which would delete a still-active legacy lock and let
|
|
322
|
+
* two installers race the same tree. Once the namespace is free, atomically
|
|
323
|
+
* create and retain a regular file through `fn`: an older process cannot
|
|
324
|
+
* acquire it between the handoff check and the new install. A file left by a
|
|
325
|
+
* crashed new installer can be reused immediately because the outer OS lock
|
|
326
|
+
* proves its owner is gone, unlike a legacy directory whose owner is unknown.
|
|
327
|
+
*/
|
|
328
|
+
async function withLegacyInstallLock<T>(runtimeDir: string, sleepMs: number, fn: () => Promise<T>): Promise<T> {
|
|
329
|
+
const legacy = `${runtimeDir}.lock`;
|
|
330
|
+
for (;;) {
|
|
314
331
|
try {
|
|
315
|
-
await fsp.
|
|
316
|
-
|
|
317
|
-
await fsp.rm(lockDir, { recursive: true, force: true });
|
|
318
|
-
};
|
|
332
|
+
const reservation = await fsp.open(legacy, "wx");
|
|
333
|
+
await reservation.close();
|
|
319
334
|
} catch (error) {
|
|
320
|
-
if (!
|
|
321
|
-
|
|
335
|
+
if (!isEexist(error)) throw error;
|
|
336
|
+
let stat: fs.Stats;
|
|
337
|
+
try {
|
|
338
|
+
stat = await fsp.stat(legacy);
|
|
339
|
+
} catch (statError) {
|
|
340
|
+
if (isEnoent(statError)) continue; // released between open and stat; retry
|
|
341
|
+
throw statError;
|
|
342
|
+
}
|
|
343
|
+
// A non-directory is a reservation left by a newer installer. The
|
|
344
|
+
// outer OS lock proves that installer is gone, so reuse it at once.
|
|
345
|
+
if (!stat.isDirectory()) break;
|
|
346
|
+
// A fresh directory may still belong to a live pre-18.x installer, so
|
|
347
|
+
// wait for it to finish; only a crash orphan (older than any plausible
|
|
348
|
+
// install) is force-reclaimed.
|
|
349
|
+
if (Date.now() - stat.mtimeMs > STALE_LEGACY_LOCK_MS) {
|
|
350
|
+
await fsp.rm(legacy, { recursive: true, force: true });
|
|
351
|
+
} else {
|
|
352
|
+
await Bun.sleep(sleepMs);
|
|
353
|
+
}
|
|
354
|
+
continue;
|
|
322
355
|
}
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
// Retain the regular-file reservation across the install.
|
|
359
|
+
try {
|
|
360
|
+
return await fn();
|
|
361
|
+
} finally {
|
|
362
|
+
await fsp.rm(legacy, { force: true });
|
|
323
363
|
}
|
|
324
|
-
throw new Error(`Timed out waiting for runtime install lock: ${lockDir}`);
|
|
325
364
|
}
|
|
326
365
|
|
|
327
366
|
export async function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void> {
|
|
@@ -363,7 +402,14 @@ async function runRuntimeInstall(runtimeDir: string): Promise<void> {
|
|
|
363
402
|
|
|
364
403
|
/**
|
|
365
404
|
* Materialize a pinned dependency set into `runtimeDir` (idempotent,
|
|
366
|
-
* cross-process safe
|
|
405
|
+
* cross-process safe). Returns `runtimeDir`.
|
|
406
|
+
*
|
|
407
|
+
* Serialization uses the OS-backed {@link withFileLock} at
|
|
408
|
+
* `${runtimeDir}.install.lock`, which the kernel releases on process death, so
|
|
409
|
+
* a crashed installer cannot wedge later attempts (issue #10120). The path is
|
|
410
|
+
* deliberately distinct from the legacy `${runtimeDir}.lock` mkdir directory;
|
|
411
|
+
* {@link withLegacyInstallLock} atomically reserves that namespace during the
|
|
412
|
+
* new install so older processes cannot cross the migration boundary.
|
|
367
413
|
*/
|
|
368
414
|
export async function ensureRuntimeInstalled(options: EnsureRuntimeInstalledOptions): Promise<string> {
|
|
369
415
|
const { runtimeDir, install, onPhase, lockAttempts = 240, lockSleepMs = 250 } = options;
|
|
@@ -379,15 +425,20 @@ export async function ensureRuntimeInstalled(options: EnsureRuntimeInstalledOpti
|
|
|
379
425
|
if (await probeManifest.exists()) return runtimeDir;
|
|
380
426
|
|
|
381
427
|
onPhase?.("initiate");
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
428
|
+
// withFileLock does not create parent directories; the runtime cache dir may
|
|
429
|
+
// not exist yet on the very first install.
|
|
430
|
+
await fsp.mkdir(path.dirname(runtimeDir), { recursive: true });
|
|
431
|
+
return withFileLock(
|
|
432
|
+
`${runtimeDir}.install`,
|
|
433
|
+
() =>
|
|
434
|
+
withLegacyInstallLock(runtimeDir, lockSleepMs, async () => {
|
|
435
|
+
if (await probeManifest.exists()) return runtimeDir;
|
|
436
|
+
await writeRuntimeManifest(runtimeDir, install);
|
|
437
|
+
onPhase?.("download");
|
|
438
|
+
await runRuntimeInstall(runtimeDir);
|
|
439
|
+
onPhase?.("done");
|
|
440
|
+
return runtimeDir;
|
|
441
|
+
}),
|
|
442
|
+
{ retries: lockAttempts, retryDelayMs: lockSleepMs },
|
|
443
|
+
);
|
|
393
444
|
}
|
package/src/template.ts
CHANGED
|
@@ -549,7 +549,7 @@ export function compile<T = unknown>(source: string, options: CompileOptions = {
|
|
|
549
549
|
const nodes = parseTemplate(stripStandalone(source));
|
|
550
550
|
return (context, runtime = {}) => {
|
|
551
551
|
const root = context ?? {};
|
|
552
|
-
const frame: Frame = { context: root, parents: [], root, data: { root, ...
|
|
552
|
+
const frame: Frame = { context: root, parents: [], root, data: { root, ...runtime.data } };
|
|
553
553
|
return renderNodes(nodes, frame, { helpers, partials, options, runtime });
|
|
554
554
|
};
|
|
555
555
|
}
|
|
@@ -579,7 +579,7 @@ export class TemplateEngine {
|
|
|
579
579
|
const nodes = parseTemplate(stripStandalone(source));
|
|
580
580
|
return (context, runtime = {}) => {
|
|
581
581
|
const root = context ?? {};
|
|
582
|
-
const frame: Frame = { context: root, parents: [], root, data: { root, ...
|
|
582
|
+
const frame: Frame = { context: root, parents: [], root, data: { root, ...runtime.data } };
|
|
583
583
|
return renderNodes(nodes, frame, { helpers: this.#helpers, partials: this.#partials, options, runtime });
|
|
584
584
|
};
|
|
585
585
|
}
|
package/src/tls-fetch.ts
CHANGED
|
@@ -174,5 +174,5 @@ export function withExtraCaFetch<T extends { fetch?: FetchImpl } | undefined>(op
|
|
|
174
174
|
const fetchImpl = options?.fetch ?? (globalThis.fetch as FetchImpl);
|
|
175
175
|
const wrapped = wrapFetchForExtraCa(fetchImpl);
|
|
176
176
|
if (wrapped === fetchImpl && options?.fetch !== undefined) return options;
|
|
177
|
-
return { ...
|
|
177
|
+
return { ...options, fetch: wrapped } as T;
|
|
178
178
|
}
|
package/src/turndown/html.ts
CHANGED
|
@@ -222,7 +222,7 @@ export function parseHtmlFragment(html: string): TurndownNode {
|
|
|
222
222
|
const stack: HtmlNode[] = [root];
|
|
223
223
|
for (const token of htmlTokens(html)) {
|
|
224
224
|
const parent = stack[stack.length - 1] ?? root;
|
|
225
|
-
if (token.startsWith("<!--") ||
|
|
225
|
+
if (token.startsWith("<!--") || token.startsWith("<!")) continue;
|
|
226
226
|
if (!token.startsWith("<")) {
|
|
227
227
|
parent.append(new HtmlText(decodeEntities(token)));
|
|
228
228
|
continue;
|