sproutboat 0.11.11 → 0.11.13

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/src/bundle.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * #89 — bundle the handler before Porffor sees it.
3
3
  *
4
- * Porffor compiles one self-contained module, so until now a project was one
5
- * file with no imports: no router, no validation library, no SDK, no splitting
6
- * a codebase in two. Bundling first lifts that without touching the compiler.
4
+ * Porffor alpha 9 supports ESM imports. We still bundle first so capability
5
+ * validation sees every dependency and the runtime wrapper receives one
6
+ * controlled default export.
7
7
  *
8
8
  * Bun's bundler resolves relative imports across the project and bare
9
9
  * specifiers out of the project's own `node_modules`, then emits a single ESM
@@ -12,6 +12,7 @@
12
12
  * reaching for a Node API fails exactly as user code would.
13
13
  */
14
14
  import { relative } from "node:path";
15
+ import { NODE_SHIMS } from "./node-shims";
15
16
 
16
17
  export type BundleResult = {
17
18
  /** One self-contained ESM module: what gets validated, hashed, and compiled. */
@@ -27,6 +28,32 @@ export class BundleError extends Error {
27
28
 
28
29
  const entryLabel = (entryPath: string, projectDir: string): string => relative(projectDir, entryPath) || entryPath;
29
30
 
31
+ const shimSpecifiers = new RegExp(
32
+ `^(${Object.keys(NODE_SHIMS)
33
+ .map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
34
+ .join("|")})$`,
35
+ );
36
+
37
+ /**
38
+ * Bun already bundles working polyfills for most node:* built-ins under
39
+ * `target: "browser"` (verified for node:url, node:querystring). This
40
+ * plugin only overrides the ones in NODE_SHIMS — currently just node:path,
41
+ * whose bundled resolve() otherwise falls back to `process.cwd()`.
42
+ */
43
+ const nodeShimsPlugin: import("bun").BunPlugin = {
44
+ name: "sproutboat-node-shims",
45
+ setup(build) {
46
+ build.onResolve({ filter: shimSpecifiers }, (args) => ({ path: args.path, namespace: "sb-node-shim" }));
47
+ build.onLoad({ filter: /.*/, namespace: "sb-node-shim" }, (args) => ({
48
+ // SAFETY: onResolve above only ever routes a specifier matched by
49
+ // shimSpecifiers (built from NODE_SHIMS's own keys) into this
50
+ // namespace, so args.path is always one of NODE_SHIMS's keys here.
51
+ contents: (NODE_SHIMS as Record<string, string>)[args.path],
52
+ loader: "js",
53
+ }));
54
+ },
55
+ };
56
+
30
57
  /** Bun reports resolution failures on `AggregateError.errors`; its own message is just "Bundle failed". */
31
58
  function bundleDetail(cause: unknown): string {
32
59
  const errors = cause instanceof AggregateError ? cause.errors : [];
@@ -51,6 +78,7 @@ export async function bundleHandler(entryPath: string, projectDir: string): Prom
51
78
  minify: false,
52
79
  splitting: false,
53
80
  sourcemap: "none",
81
+ plugins: [nodeShimsPlugin],
54
82
  });
55
83
  } catch (cause) {
56
84
  // An unresolvable specifier arrives as an AggregateError whose `errors`
@@ -0,0 +1,98 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { chmod, mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
4
+
5
+ type CacheRecord = { key: string; hash: string; size: number };
6
+
7
+ function digest(bytes: Uint8Array): string {
8
+ return createHash("sha256").update(bytes).digest("hex");
9
+ }
10
+
11
+ async function verified(entry: string, key: string): Promise<Buffer | null> {
12
+ try {
13
+ // SAFETY: cache.json is private cache data; the key, metadata and binary
14
+ // digest are all checked before this entry can be used.
15
+ const record = JSON.parse(await readFile(resolve(entry, "cache.json"), "utf8")) as CacheRecord;
16
+ if (record.key !== key) return null;
17
+ const path = resolve(entry, "sprout");
18
+ const info = await stat(path);
19
+ if (!info.isFile() || (info.mode & 0o111) === 0 || info.size !== record.size) return null;
20
+ const bytes = await readFile(path);
21
+ return digest(bytes) === record.hash ? bytes : null;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ async function publish(bytes: Uint8Array, outPath: string): Promise<void> {
28
+ await mkdir(dirname(outPath), { recursive: true });
29
+ const candidate = `${outPath}.${randomUUID()}.tmp`;
30
+ try {
31
+ await writeFile(candidate, bytes);
32
+ await chmod(candidate, 0o555);
33
+ await rename(candidate, outPath);
34
+ } finally {
35
+ await rm(candidate, { force: true });
36
+ }
37
+ }
38
+
39
+ /** A per-project binary cache. The lock covers compilation and publication, so
40
+ * another process can only read a complete, checksum-verified entry. */
41
+ export async function compileCached(
42
+ cacheRoot: string,
43
+ key: string,
44
+ outPath: string,
45
+ compile: (candidatePath: string) => Promise<void>,
46
+ ): Promise<"hit" | "miss"> {
47
+ const entry = resolve(cacheRoot, key);
48
+ const cached = await verified(entry, key);
49
+ if (cached) {
50
+ await publish(cached, outPath);
51
+ return "hit";
52
+ }
53
+ await mkdir(cacheRoot, { recursive: true });
54
+ const lock = `${entry}.lock`;
55
+ const deadline = Date.now() + 12 * 60_000;
56
+ while (true) {
57
+ try {
58
+ await mkdir(lock);
59
+ break;
60
+ } catch (error) {
61
+ // SAFETY: Node filesystem errors expose code; unrelated failures are
62
+ // rethrown instead of being treated as a held lock.
63
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
64
+ const ready = await verified(entry, key);
65
+ if (ready) {
66
+ await publish(ready, outPath);
67
+ return "hit";
68
+ }
69
+ if (Date.now() > deadline) throw new Error(`timed out waiting for compile cache lock ${lock}`);
70
+ const lockInfo = await stat(lock).catch(() => null);
71
+ if (lockInfo && Date.now() - lockInfo.mtimeMs > 15 * 60_000) await rm(lock, { recursive: true, force: true });
72
+ else await Bun.sleep(100);
73
+ }
74
+ }
75
+ let stage: string | undefined;
76
+ try {
77
+ const ready = await verified(entry, key);
78
+ if (ready) {
79
+ await publish(ready, outPath);
80
+ return "hit";
81
+ }
82
+ await rm(entry, { recursive: true, force: true });
83
+ stage = await mkdtemp(resolve(cacheRoot, `.candidate-${key}-`));
84
+ const candidate = resolve(stage, "sprout");
85
+ await compile(candidate);
86
+ const bytes = await readFile(candidate);
87
+ const record: CacheRecord = { key, hash: digest(bytes), size: bytes.byteLength };
88
+ await chmod(candidate, 0o555);
89
+ await writeFile(resolve(stage, "cache.json"), JSON.stringify(record));
90
+ await rename(stage, entry);
91
+ stage = undefined;
92
+ await publish(bytes, outPath);
93
+ return "miss";
94
+ } finally {
95
+ if (stage) await rm(stage, { recursive: true, force: true });
96
+ await rm(lock, { recursive: true, force: true });
97
+ }
98
+ }
package/src/compile.ts CHANGED
@@ -10,7 +10,6 @@
10
10
  * unusable it stops with its specific integrity or archive error. Contributors
11
11
  * may explicitly opt into Porffor's source fallback.
12
12
  */
13
- import { existsSync } from "node:fs";
14
13
  import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
15
14
  import { dirname, resolve } from "node:path";
16
15
  import { ensurePorfforPatched } from "./patch-porffor";
@@ -50,8 +49,12 @@ const COMPILE_TIMEOUT_MS = Number(process.env.SPROUTBOAT_COMPILE_TIMEOUT_MS || 6
50
49
 
51
50
  export type CompileInput = {
52
51
  sourcePath: string;
52
+ /** Stable compiler input for dev rebuilds, independent of the candidate binary path. */
53
+ generatedPath?: string;
53
54
  /** The bundled module (#89). Falls back to reading `sourcePath` verbatim. */
54
55
  source?: string;
56
+ /** The exact wrapped module used to compute a verified compile-cache key. */
57
+ generatedSource?: string;
55
58
  outPath: string;
56
59
  vars: Record<string, string>;
57
60
  bindings?: Bindings;
@@ -195,26 +198,29 @@ export async function compileSprout(input: CompileInput): Promise<void> {
195
198
  // OS may still be executing. The linker cannot overwrite either, so clear it
196
199
  // first rather than failing with "can't write output file".
197
200
  await rm(input.outPath, { force: true });
198
- const generatedPath = resolve(outDir, "sprout.generated.js");
199
- const [source, prelude] = await Promise.all([
200
- input.source === undefined ? readFile(input.sourcePath, "utf8") : Promise.resolve(input.source),
201
- loadPrelude(input.transport ?? "broker"),
202
- ]);
203
- await writeFile(
204
- generatedPath,
205
- wrapNativeFetchHandler(
206
- source,
207
- prelude,
208
- input.vars,
209
- input.bindings ?? EMPTY_BINDINGS,
210
- undefined,
211
- input.compatibilityDate,
212
- input.appName,
213
- input.assets,
214
- input.transport,
215
- input.versionMetadata,
216
- ),
217
- );
201
+ const generatedPath = input.generatedPath ?? resolve(outDir, "sprout.generated.js");
202
+ await mkdir(dirname(generatedPath), { recursive: true });
203
+ const generatedSource =
204
+ input.generatedSource ??
205
+ (await (async () => {
206
+ const [source, prelude] = await Promise.all([
207
+ input.source === undefined ? readFile(input.sourcePath, "utf8") : Promise.resolve(input.source),
208
+ loadPrelude(input.transport ?? "broker"),
209
+ ]);
210
+ return wrapNativeFetchHandler(
211
+ source,
212
+ prelude,
213
+ input.vars,
214
+ input.bindings ?? EMPTY_BINDINGS,
215
+ undefined,
216
+ input.compatibilityDate,
217
+ input.appName,
218
+ input.assets,
219
+ input.transport,
220
+ input.versionMetadata,
221
+ );
222
+ })());
223
+ await writeFile(generatedPath, generatedSource);
218
224
 
219
225
  const launcher = resolve(porffor, "runtime/index.js");
220
226
  // A musl build shells `zig` bare (Porffor hardcodes the literal command), so
@@ -222,22 +228,7 @@ export async function compileSprout(input: CompileInput): Promise<void> {
222
228
  // below instead and has no zigBin to contribute here.
223
229
  const zigDir = input.zigBin ? `${dirname(input.zigBin)}:` : "";
224
230
  const hostCompiler = input.target === "host" ? await resolveHostCompiler() : undefined;
225
- // The per-platform binary ships esbuild next to it; a compiled build finds it
226
- // there. Running from the npm package under Bun, `process.execPath` is Bun
227
- // itself and `npm i -g` puts no dependency `.bin` on PATH, so resolve the
228
- // esbuild dependency directly. `Bun.which` is the last resort.
229
- const packagedEsbuild = resolve(dirname(process.execPath), "esbuild");
230
- let esbuild: string | null = existsSync(packagedEsbuild) ? packagedEsbuild : null;
231
- if (!esbuild) {
232
- try {
233
- esbuild = Bun.resolveSync("esbuild/bin/esbuild", import.meta.dir);
234
- } catch {
235
- esbuild = null;
236
- }
237
- }
238
- esbuild ??= Bun.which("esbuild");
239
- if (!esbuild) throw new Error("esbuild could not be located; install it or reinstall sproutboat");
240
- const path = `${zigDir}${dirname(esbuild)}:${process.env.PATH ?? ""}`;
231
+ const path = `${zigDir}${process.env.PATH ?? ""}`;
241
232
  const command = PACKAGED
242
233
  ? [process.execPath, "__porffor", launcher]
243
234
  : [process.execPath, resolve(import.meta.dir, "main.ts"), "__porffor", launcher];
package/src/dev.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  * project, one port.
12
12
  */
13
13
  import { existsSync, watch, type FSWatcher } from "node:fs";
14
+ import { randomUUID } from "node:crypto";
14
15
  import { mkdir, readFile, rm } from "node:fs/promises";
15
16
  import { connect } from "node:net";
16
17
  import { dirname, resolve } from "node:path";
@@ -21,6 +22,7 @@ import { amber, dim, leaf, ok } from "./style";
21
22
  import type { SproutboatConfig } from "./config";
22
23
 
23
24
  const RESTART_DEBOUNCE_MS = 120;
25
+ const DEV_COMPILE_SESSION = randomUUID();
24
26
 
25
27
  export type DevInput = {
26
28
  projectDir: string;
@@ -121,6 +123,14 @@ async function start(input: DevInput, port: number): Promise<Running> {
121
123
  optimize: "dev",
122
124
  reuseSproutPath: input.reuseSproutPath,
123
125
  outputDirectory: candidateDir,
126
+ // Alpha 9 keys its C-unit cache by the entry's absolute path. Keep that
127
+ // path fixed across saves while each candidate keeps its own binary.
128
+ generatedPath: resolve(
129
+ input.projectDir,
130
+ ".sproutboat/dev-compile",
131
+ `${process.pid}-${DEV_COMPILE_SESSION}`,
132
+ "sprout.generated.js",
133
+ ),
124
134
  });
125
135
  } catch (error) {
126
136
  await rm(candidateDir, { recursive: true, force: true });
package/src/main.ts CHANGED
@@ -319,6 +319,7 @@ async function build(directory?: string, target: "linux-x86_64" | "host" = "linu
319
319
  target,
320
320
  });
321
321
  console.log(ok(`built ${project.config.name}`));
322
+ console.log(dim(` compile ${artifact.compileMs} ms · binary cache ${artifact.compileCache}`));
322
323
  if (target === "host")
323
324
  console.log(dim(" host build — runs here, not deployable; drop --target host to build for a box"));
324
325
  console.log(artifact.artifactDir);
@@ -0,0 +1,378 @@
1
+ /**
2
+ * Bun's bundler ships full Node-compatible polyfills for node:path,
3
+ * node:url, and node:querystring under `target: "browser"`, but each has
4
+ * exactly one thing wrong with it for a Porffor-compiled handler — found
5
+ * by auditing bundled output against the capability check (source.ts) and,
6
+ * for node:url, by an actual failure through the real Porffor compiler.
7
+ * Auditing the bundle alone was not enough:
8
+ *
9
+ * - node:path's bundled `resolve()` falls back to `process.cwd()`, which
10
+ * trips the `process.` ban in `../packages/runtime/src/source.ts`.
11
+ * - node:querystring's bundled polyfill pulls in Bun's full Buffer
12
+ * implementation internally (for percent-decoding), tripping the
13
+ * `Buffer.` ban.
14
+ * - node:url's bundled `URL`/`URLSearchParams` exports are
15
+ * `var { URL, URLSearchParams } = globalThis;` — passes every capability
16
+ * check (no banned pattern in it) but Porffor's compiler rejects it at
17
+ * parse time as `Identifier 'URL' has already been declared`: Porffor
18
+ * pre-binds `URL` as a global, and a second top-level declaration of
19
+ * that exact name is a compile error, confirmed by actually building a
20
+ * handler through `sproutboat build` rather than just checking the
21
+ * bundle. `URL`/`URLSearchParams` are already global in a compiled
22
+ * handler (see `docs/concepts/handler.mdx`), so nothing needs importing
23
+ * — the fix is a shim that re-exports the globals under aliased local
24
+ * names instead of declaring anything literally named `URL`.
25
+ *
26
+ * None of these are the capability check being wrong — every case is a
27
+ * real thing (`process.cwd()`, `Buffer`, a name Porffor itself reserves)
28
+ * that doesn't exist the way a compiled handler needs it to, so the shims
29
+ * replace it rather than the check being loosened.
30
+ */
31
+ const NODE_PATH_SHIM = `
32
+ function assertPath(path) {
33
+ if (typeof path !== "string")
34
+ throw TypeError("Path must be a string. Received " + JSON.stringify(path));
35
+ }
36
+ function normalizeStringPosix(path, allowAboveRoot) {
37
+ var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code;
38
+ for (var i = 0; i <= path.length; ++i) {
39
+ if (i < path.length) code = path.charCodeAt(i);
40
+ else if (code === 47) break;
41
+ else code = 47;
42
+ if (code === 47) {
43
+ if (lastSlash === i - 1 || dots === 1) {
44
+ } else if (lastSlash !== i - 1 && dots === 2) {
45
+ if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) {
46
+ if (res.length > 2) {
47
+ var lastSlashIndex = res.lastIndexOf("/");
48
+ if (lastSlashIndex !== res.length - 1) {
49
+ if (lastSlashIndex === -1) res = "", lastSegmentLength = 0;
50
+ else res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
51
+ lastSlash = i, dots = 0;
52
+ continue;
53
+ }
54
+ } else if (res.length === 2 || res.length === 1) {
55
+ res = "", lastSegmentLength = 0, lastSlash = i, dots = 0;
56
+ continue;
57
+ }
58
+ }
59
+ if (allowAboveRoot) {
60
+ if (res.length > 0) res += "/..";
61
+ else res = "..";
62
+ lastSegmentLength = 2;
63
+ }
64
+ } else {
65
+ if (res.length > 0) res += "/" + path.slice(lastSlash + 1, i);
66
+ else res = path.slice(lastSlash + 1, i);
67
+ lastSegmentLength = i - lastSlash - 1;
68
+ }
69
+ lastSlash = i, dots = 0;
70
+ } else if (code === 46 && dots !== -1) ++dots;
71
+ else dots = -1;
72
+ }
73
+ return res;
74
+ }
75
+ function _format(sep, pathObject) {
76
+ var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "");
77
+ if (!dir) return base;
78
+ if (dir === pathObject.root) return dir + base;
79
+ return dir + sep + base;
80
+ }
81
+ function resolve() {
82
+ // Node falls back to process.cwd() here; a compiled handler has no real
83
+ // filesystem or cwd, so an all-relative resolve() resolves against a
84
+ // fixed virtual root instead.
85
+ var resolvedPath = "", resolvedAbsolute = false, cwd;
86
+ for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
87
+ var path;
88
+ if (i >= 0) path = arguments[i];
89
+ else {
90
+ if (cwd === undefined) cwd = "/";
91
+ path = cwd;
92
+ }
93
+ if (assertPath(path), path.length === 0) continue;
94
+ resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47;
95
+ }
96
+ if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute)
97
+ if (resolvedPath.length > 0) return "/" + resolvedPath;
98
+ else return "/";
99
+ else if (resolvedPath.length > 0) return resolvedPath;
100
+ else return ".";
101
+ }
102
+ function normalize(path) {
103
+ if (assertPath(path), path.length === 0) return ".";
104
+ var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47;
105
+ if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute) path = ".";
106
+ if (path.length > 0 && trailingSeparator) path += "/";
107
+ if (isAbsolute) return "/" + path;
108
+ return path;
109
+ }
110
+ function isAbsolute(path) {
111
+ return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47;
112
+ }
113
+ function join() {
114
+ if (arguments.length === 0) return ".";
115
+ var joined;
116
+ for (var i = 0; i < arguments.length; ++i) {
117
+ var arg = arguments[i];
118
+ if (assertPath(arg), arg.length > 0)
119
+ if (joined === undefined) joined = arg;
120
+ else joined += "/" + arg;
121
+ }
122
+ if (joined === undefined) return ".";
123
+ return normalize(joined);
124
+ }
125
+ function relative(from, to) {
126
+ if (assertPath(from), assertPath(to), from === to) return "";
127
+ if (from = resolve(from), to = resolve(to), from === to) return "";
128
+ var fromStart = 1;
129
+ for (; fromStart < from.length; ++fromStart) if (from.charCodeAt(fromStart) !== 47) break;
130
+ var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1;
131
+ for (; toStart < to.length; ++toStart) if (to.charCodeAt(toStart) !== 47) break;
132
+ var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0;
133
+ for (; i <= length; ++i) {
134
+ if (i === length) {
135
+ if (toLen > length) {
136
+ if (to.charCodeAt(toStart + i) === 47) return to.slice(toStart + i + 1);
137
+ else if (i === 0) return to.slice(toStart + i);
138
+ } else if (fromLen > length) {
139
+ if (from.charCodeAt(fromStart + i) === 47) lastCommonSep = i;
140
+ else if (i === 0) lastCommonSep = 0;
141
+ }
142
+ break;
143
+ }
144
+ var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i);
145
+ if (fromCode !== toCode) break;
146
+ else if (fromCode === 47) lastCommonSep = i;
147
+ }
148
+ var out = "";
149
+ for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i)
150
+ if (i === fromEnd || from.charCodeAt(i) === 47)
151
+ if (out.length === 0) out += "..";
152
+ else out += "/..";
153
+ if (out.length > 0) return out + to.slice(toStart + lastCommonSep);
154
+ else {
155
+ if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47) ++toStart;
156
+ return to.slice(toStart);
157
+ }
158
+ }
159
+ function dirname(path) {
160
+ if (assertPath(path), path.length === 0) return ".";
161
+ var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true;
162
+ for (var i = path.length - 1; i >= 1; --i)
163
+ if (code = path.charCodeAt(i), code === 47) {
164
+ if (!matchedSlash) { end = i; break; }
165
+ } else matchedSlash = false;
166
+ if (end === -1) return hasRoot ? "/" : ".";
167
+ if (hasRoot && end === 1) return "//";
168
+ return path.slice(0, end);
169
+ }
170
+ function basename(path, ext) {
171
+ if (ext !== undefined && typeof ext !== "string") throw TypeError('"ext" argument must be a string');
172
+ assertPath(path);
173
+ var start = 0, end = -1, matchedSlash = true, i;
174
+ if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
175
+ if (ext.length === path.length && ext === path) return "";
176
+ var extIdx = ext.length - 1, firstNonSlashEnd = -1;
177
+ for (i = path.length - 1; i >= 0; --i) {
178
+ var code = path.charCodeAt(i);
179
+ if (code === 47) {
180
+ if (!matchedSlash) { start = i + 1; break; }
181
+ } else {
182
+ if (firstNonSlashEnd === -1) matchedSlash = false, firstNonSlashEnd = i + 1;
183
+ if (extIdx >= 0)
184
+ if (code === ext.charCodeAt(extIdx)) { if (--extIdx === -1) end = i; }
185
+ else extIdx = -1, end = firstNonSlashEnd;
186
+ }
187
+ }
188
+ if (start === end) end = firstNonSlashEnd;
189
+ else if (end === -1) end = path.length;
190
+ return path.slice(start, end);
191
+ } else {
192
+ for (i = path.length - 1; i >= 0; --i)
193
+ if (path.charCodeAt(i) === 47) {
194
+ if (!matchedSlash) { start = i + 1; break; }
195
+ } else if (end === -1) matchedSlash = false, end = i + 1;
196
+ if (end === -1) return "";
197
+ return path.slice(start, end);
198
+ }
199
+ }
200
+ function extname(path) {
201
+ assertPath(path);
202
+ var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0;
203
+ for (var i = path.length - 1; i >= 0; --i) {
204
+ var code = path.charCodeAt(i);
205
+ if (code === 47) {
206
+ if (!matchedSlash) { startPart = i + 1; break; }
207
+ continue;
208
+ }
209
+ if (end === -1) matchedSlash = false, end = i + 1;
210
+ if (code === 46) { if (startDot === -1) startDot = i; else if (preDotState !== 1) preDotState = 1; }
211
+ else if (startDot !== -1) preDotState = -1;
212
+ }
213
+ if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) return "";
214
+ return path.slice(startDot, end);
215
+ }
216
+ function format(pathObject) {
217
+ if (pathObject === null || typeof pathObject !== "object")
218
+ throw TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject);
219
+ return _format("/", pathObject);
220
+ }
221
+ function parse(path) {
222
+ assertPath(path);
223
+ var ret = { root: "", dir: "", base: "", ext: "", name: "" };
224
+ if (path.length === 0) return ret;
225
+ var code = path.charCodeAt(0), isAbsolute2 = code === 47, start;
226
+ if (isAbsolute2) ret.root = "/", start = 1;
227
+ else start = 0;
228
+ var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0;
229
+ for (; i >= start; --i) {
230
+ if (code = path.charCodeAt(i), code === 47) {
231
+ if (!matchedSlash) { startPart = i + 1; break; }
232
+ continue;
233
+ }
234
+ if (end === -1) matchedSlash = false, end = i + 1;
235
+ if (code === 46) { if (startDot === -1) startDot = i; else if (preDotState !== 1) preDotState = 1; }
236
+ else if (startDot !== -1) preDotState = -1;
237
+ }
238
+ if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
239
+ if (end !== -1)
240
+ if (startPart === 0 && isAbsolute2) ret.base = ret.name = path.slice(1, end);
241
+ else ret.base = ret.name = path.slice(startPart, end);
242
+ } else {
243
+ if (startPart === 0 && isAbsolute2) ret.name = path.slice(1, startDot), ret.base = path.slice(1, end);
244
+ else ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end);
245
+ ret.ext = path.slice(startDot, end);
246
+ }
247
+ if (startPart > 0) ret.dir = path.slice(0, startPart - 1);
248
+ else if (isAbsolute2) ret.dir = "/";
249
+ return ret;
250
+ }
251
+ var sep = "/";
252
+ var delimiter = ":";
253
+ var path = { resolve, normalize, isAbsolute, join, relative, dirname, basename, extname, format, parse, sep, delimiter };
254
+ path.posix = path;
255
+ path.win32 = path;
256
+ export default path;
257
+ export { resolve, normalize, isAbsolute, join, relative, dirname, basename, extname, format, parse, sep, delimiter };
258
+ `;
259
+
260
+ /**
261
+ * node:querystring's bundled polyfill pulls in Bun's full Buffer
262
+ * implementation (used internally for percent-decoding), which the
263
+ * capability check correctly rejects — Buffer isn't something Porffor's
264
+ * compiled output can rely on. This shim implements parse/stringify/
265
+ * escape/unescape (plus the encode/decode aliases Node exports) over
266
+ * plain string ops and encodeURIComponent/decodeURIComponent instead.
267
+ */
268
+ const NODE_QUERYSTRING_SHIM = `
269
+ function qsUnescape(str) {
270
+ try { return decodeURIComponent(String(str).replace(/\\+/g, " ")); }
271
+ catch { return String(str); }
272
+ }
273
+ function qsEscape(str) {
274
+ return encodeURIComponent(String(str));
275
+ }
276
+ function parse(qs, sep, eq, options) {
277
+ sep = sep || "&";
278
+ eq = eq || "=";
279
+ var obj = Object.create(null);
280
+ if (typeof qs !== "string" || qs.length === 0) return obj;
281
+ var decode = (options && options.decodeURIComponent) || qsUnescape;
282
+ var maxKeys = options && typeof options.maxKeys === "number" ? options.maxKeys : 1000;
283
+ var pairs = qs.split(sep);
284
+ var limit = maxKeys > 0 ? Math.min(pairs.length, maxKeys) : pairs.length;
285
+ for (var i = 0; i < limit; i++) {
286
+ var pair = pairs[i];
287
+ var eqIdx = pair.indexOf(eq);
288
+ var key, value;
289
+ if (eqIdx >= 0) { key = pair.slice(0, eqIdx); value = pair.slice(eqIdx + eq.length); }
290
+ else { key = pair; value = ""; }
291
+ key = decode(key);
292
+ value = decode(value);
293
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
294
+ if (Array.isArray(obj[key])) obj[key].push(value);
295
+ else obj[key] = [obj[key], value];
296
+ } else obj[key] = value;
297
+ }
298
+ return obj;
299
+ }
300
+ function stringify(obj, sep, eq, options) {
301
+ sep = sep || "&";
302
+ eq = eq || "=";
303
+ obj = obj || {};
304
+ var encode = (options && options.encodeURIComponent) || qsEscape;
305
+ var parts = [];
306
+ for (var key in obj) {
307
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
308
+ var value = obj[key];
309
+ var encodedKey = encode(key);
310
+ if (Array.isArray(value)) {
311
+ for (var i = 0; i < value.length; i++) parts.push(encodedKey + eq + encode(value[i]));
312
+ } else {
313
+ parts.push(encodedKey + eq + encode(value));
314
+ }
315
+ }
316
+ return parts.join(sep);
317
+ }
318
+ export { parse, stringify, qsEscape as escape, qsUnescape as unescape, stringify as encode, parse as decode };
319
+ export default { parse, stringify, escape: qsEscape, unescape: qsUnescape, encode: stringify, decode: parse };
320
+ `;
321
+
322
+ /**
323
+ * `URL`/`URLSearchParams` are re-exported under aliased local names
324
+ * (`NodeURL`/`NodeURLSearchParams`) rather than declared as top-level
325
+ * `URL`/`URLSearchParams` — Porffor pre-binds `URL` as a global and
326
+ * rejects a second top-level declaration of that name. legacy
327
+ * parse/format/resolve are hand-written over the WHATWG URL rather than
328
+ * Bun's internal legacy-Url implementation, which isn't something this
329
+ * shim can import from; good enough for the common absolute-URL case,
330
+ * not a byte-for-byte port of Node's edge cases (relative/protocol-less
331
+ * input, unusual schemes).
332
+ */
333
+ const NODE_URL_SHIM = `
334
+ var NodeURL = globalThis.URL;
335
+ var NodeURLSearchParams = globalThis.URLSearchParams;
336
+ function urlFormat(urlObject) {
337
+ if (typeof urlObject === "string") return urlObject;
338
+ if (urlObject instanceof NodeURL) return urlObject.href;
339
+ var protocol = urlObject.protocol || "";
340
+ var slashes = urlObject.slashes !== false && !!(urlObject.host || urlObject.hostname);
341
+ var auth = urlObject.auth ? urlObject.auth + "@" : "";
342
+ var host = urlObject.host || ((urlObject.hostname || "") + (urlObject.port ? ":" + urlObject.port : ""));
343
+ var pathname = urlObject.pathname || "";
344
+ var search = urlObject.search || (urlObject.query ? "?" + (typeof urlObject.query === "string" ? urlObject.query : "") : "");
345
+ var hash = urlObject.hash || "";
346
+ return protocol + (slashes ? "//" : "") + auth + host + pathname + search + hash;
347
+ }
348
+ function legacyFromURL(u) {
349
+ return {
350
+ protocol: u.protocol,
351
+ slashes: true,
352
+ auth: u.username ? u.username + (u.password ? ":" + u.password : "") : null,
353
+ host: u.host,
354
+ port: u.port || null,
355
+ hostname: u.hostname,
356
+ hash: u.hash || null,
357
+ search: u.search || null,
358
+ query: u.search ? u.search.slice(1) : null,
359
+ pathname: u.pathname,
360
+ path: u.pathname + (u.search || ""),
361
+ href: u.href,
362
+ };
363
+ }
364
+ function urlParse(input) {
365
+ return legacyFromURL(new NodeURL(input));
366
+ }
367
+ function urlResolve(from, to) {
368
+ return new NodeURL(to, new NodeURL(from)).href;
369
+ }
370
+ export { NodeURL as URL, NodeURLSearchParams as URLSearchParams, urlFormat as format, urlParse as parse, urlResolve as resolve };
371
+ export default { URL: NodeURL, URLSearchParams: NodeURLSearchParams, format: urlFormat, parse: urlParse, resolve: urlResolve };
372
+ `;
373
+
374
+ export const NODE_SHIMS = {
375
+ "node:path": NODE_PATH_SHIM,
376
+ "node:querystring": NODE_QUERYSTRING_SHIM,
377
+ "node:url": NODE_URL_SHIM,
378
+ } satisfies Record<string, string>;