sproutboat 0.4.11 → 0.6.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/src/wrap.ts CHANGED
@@ -35,15 +35,73 @@ export type Bindings = {
35
35
  assets: string;
36
36
  };
37
37
 
38
- export const EMPTY_BINDINGS: Bindings = { kv: [], secrets: [], outbound: [], d1: [], r2: [], queues: [], analytics: [], do: [], crons: [], assets: "" };
38
+ export const EMPTY_BINDINGS: Bindings = {
39
+ kv: [],
40
+ secrets: [],
41
+ outbound: [],
42
+ d1: [],
43
+ r2: [],
44
+ queues: [],
45
+ analytics: [],
46
+ do: [],
47
+ crons: [],
48
+ assets: "",
49
+ };
39
50
 
40
51
  function hasBindings(b: Bindings): boolean {
41
52
  return (
42
- b.kv.length > 0 || b.secrets.length > 0 || b.outbound.length > 0 || b.d1.length > 0 || b.r2.length > 0 ||
43
- b.queues.length > 0 || b.analytics.length > 0 || b.do.length > 0 || b.assets !== ""
53
+ b.kv.length > 0 ||
54
+ b.secrets.length > 0 ||
55
+ b.outbound.length > 0 ||
56
+ b.d1.length > 0 ||
57
+ b.r2.length > 0 ||
58
+ b.queues.length > 0 ||
59
+ b.analytics.length > 0 ||
60
+ b.do.length > 0 ||
61
+ b.assets !== ""
44
62
  );
45
63
  }
46
64
 
65
+ /**
66
+ * Turn the module's exports into plain top-level declarations, so the handler
67
+ * object is reachable as `__sbHandlers` and Durable Object classes stay
68
+ * addressable by name.
69
+ *
70
+ * Two shapes reach us. A hand-written file exports inline
71
+ * (`export default { fetch }`), while a bundled one declares everything first
72
+ * and re-exports at the end (`export { src_default as default, Counter }`) —
73
+ * #89 made the second shape the normal case. Returns null when neither matches.
74
+ */
75
+ export function neutraliseExports(source: string): string | null {
76
+ if (/\bexport\s+default\s*\{/.test(source)) {
77
+ return source
78
+ .replace(/^(\s*)export\s+default\s*/m, "$1const __sbHandlers = ")
79
+ .replace(/^export\s+(async\s+function|function|class|const|let|var)\b/gm, "$1");
80
+ }
81
+ // Not anchored to a line: a minified bundle puts the whole module on one
82
+ // line. Bundlers emit exactly one such block, at the end.
83
+ const blocks = [...source.matchAll(/export\s*\{([^}]*)\}\s*;?/g)];
84
+ const block = blocks[blocks.length - 1];
85
+ if (block === undefined) return null;
86
+ let handler: string | null = null;
87
+ const aliases: string[] = [];
88
+ for (const entry of block[1]
89
+ .split(",")
90
+ .map((part) => part.trim())
91
+ .filter(Boolean)) {
92
+ const parts = entry.match(/^(\S+)(?:\s+as\s+(\S+))?$/);
93
+ if (parts === null) continue;
94
+ const local = parts[1];
95
+ const exported = parts[2] ?? local;
96
+ if (exported === "default") handler = local;
97
+ // `export { Counter as Counter }` needs no alias; a renamed one does, so
98
+ // `durable_objects` in the config can still name the class it expects.
99
+ else if (exported !== local) aliases.push(`const ${exported} = ${local};`);
100
+ }
101
+ if (handler === null) return null;
102
+ return source.replace(block[0], [`const __sbHandlers = ${handler};`, ...aliases].join("\n"));
103
+ }
104
+
47
105
  /**
48
106
  * Build the final native-fetch module: the prelude (Web API shims + the broker
49
107
  * binding shim + the trigger dispatcher), then `const env = {…}` with the baked
@@ -70,15 +128,10 @@ export function wrapNativeFetchHandler(
70
128
  bindings: Bindings = EMPTY_BINDINGS,
71
129
  port: number = DEFAULT_PORT,
72
130
  ): string {
73
- if (!/\bexport\s+default\s*\{/.test(source) || !/\bfetch\s*\(/.test(source)) {
131
+ const neutralised = neutraliseExports(source);
132
+ if (neutralised === null || !/\bfetch\s*\(/.test(source)) {
74
133
  throw new Error("handler must default-export an object with a fetch(request) method");
75
134
  }
76
- // Neutralise the module's exports: its default object becomes `__sbHandlers`,
77
- // and any `export class`/`function`/`const` (Durable Object classes, helpers)
78
- // becomes a plain top-level declaration. Imports are already rejected upstream.
79
- const neutralised = source
80
- .replace(/^(\s*)export\s+default\s*/m, "$1const __sbHandlers = ")
81
- .replace(/^export\s+(async\s+function|function|class|const|let|var)\b/gm, "$1");
82
135
 
83
136
  const env = `const env = ${JSON.stringify(vars)};\nglobalThis.env = env;\n`;
84
137
  const wire = hasBindings(bindings) ? `__sbInstallBindings(env, ${JSON.stringify(bindings)});\n` : "";
@@ -105,15 +158,16 @@ function isVarsString(value: VarsJson): value is string {
105
158
  /** `SPROUTBOAT_VARS_JSON` (set by the build) → a validated flat string map. */
106
159
  export function readVarsFromEnv(): Record<string, string> {
107
160
  const raw = process.env.SPROUTBOAT_VARS_JSON;
108
- const vars: Record<string, string> = {};
109
- if (!raw) return vars;
161
+ if (!raw) return {};
110
162
  const parsed: VarsJson = JSON.parse(raw);
111
163
  if (!isVarsObject(parsed)) throw new Error("SPROUTBOAT_VARS_JSON must be a JSON object");
112
- for (const [key, value] of Object.entries(parsed)) {
113
- if (!/^[A-Z][A-Z0-9_]*$/.test(key) || !isVarsString(value)) throw new Error(`SPROUTBOAT_VARS_JSON.${key} must map an UPPER_SNAKE name to a string`);
114
- vars[key] = value;
115
- }
116
- return vars;
164
+ return Object.fromEntries(
165
+ Object.entries(parsed).map(([key, value]): [string, string] => {
166
+ if (!/^[A-Z][A-Z0-9_]*$/.test(key) || !isVarsString(value))
167
+ throw new Error(`SPROUTBOAT_VARS_JSON.${key} must map an UPPER_SNAKE name to a string`);
168
+ return [key, value];
169
+ }),
170
+ );
117
171
  }
118
172
 
119
173
  /**