wawesome 0.0.15 → 0.2.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/README.md CHANGED
@@ -114,6 +114,26 @@ npx wawesome deploy
114
114
 
115
115
  ---
116
116
 
117
+ ## 🧭 Unsupported Globals
118
+
119
+ Every build scans the bundle it just produced against the platform's declared guest surface, and says
120
+ nothing unless it finds something.
121
+
122
+ - **`Intl` is not provided.** Where the bundle reaches it as it loads — your own module scope, or a
123
+ dependency's — the build is refused before a deploy uploads anything: that bundle would not
124
+ evaluate on the platform. Where the reference sits inside a function that may never be called,
125
+ behind a `typeof` check, or inside a `try`, you get a warning and the deploy proceeds.
126
+ - **`toLocaleString`, `toLocaleDateString`, `toLocaleTimeString`, `toLocaleLowerCase` and
127
+ `toLocaleUpperCase` ignore their locale argument.** They run and return an unlocalised answer, so
128
+ these warn.
129
+
130
+ Each message names the global, the file and line in *your* source, and what to do about it. Declaring
131
+ an `Intl` polyfill in your `package.json` — the same declaration [local parity
132
+ reads](#testing-against-the-guests-javascript-surface) — silences the report, as does installing one
133
+ on `globalThis` in the bundle itself. The scan reads static references only: a global reached through
134
+ `globalThis['Intl']` is invisible to it, so it never refuses a deploy on a guess. `deploy
135
+ --skip-build` scans the bundle it found on disk before uploading it.
136
+
117
137
  ## 📜 Invocation Logs
118
138
 
119
139
  Inspect past function runs or view raw `stdout` / `stderr` log outputs directly in your terminal.
@@ -200,6 +220,22 @@ Every project directory includes a `wawesome-function.json` file generated durin
200
220
  `app` is the App this Function is deployed into, and it is client-facing — every deploy from this
201
221
  directory is scoped to it.
202
222
 
223
+ Add `"assets"` to deploy static files beside your code:
224
+
225
+ ```json
226
+ {
227
+ "app": "my-app",
228
+ "function": "hello-world",
229
+ "entry": "src/index.ts",
230
+ "assets": "dist/client"
231
+ }
232
+ ```
233
+
234
+ Everything under that directory is deployed with the version, addressed by its path from the
235
+ directory root. The CLI hashes each file and asks the platform which of them it does not already
236
+ hold, so a redeploy that changed one chunk uploads one chunk — and a deploy that changed nothing at
237
+ all is refused before a byte moves. Assets are not served yet.
238
+
203
239
  ### Reserved headers
204
240
 
205
241
  `x-wawesome-*` belongs to the platform in both directions. It is stripped off the request before your
@@ -1,5 +1,4 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
1
+ import { a as unsupportedGlobals, i as shimmedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-CXON0L5V.mjs";
3
2
  //#region ../../server/guest-surface/message-channel.js
4
3
  const ENTANGLED = Symbol("entangled");
5
4
  const QUEUE = Symbol("queue");
@@ -116,80 +115,6 @@ function resolve(scope, target) {
116
115
  return target.split(".").reduce((current, part) => current == null ? current : current[part], scope);
117
116
  }
118
117
  //#endregion
119
- //#region ../../server/guest-surface/surface.json
120
- var globals = [
121
- {
122
- "name": "MessageChannel",
123
- "status": "shim",
124
- "shim": "message-channel",
125
- "why": "The engine has none. react-dom/server.browser constructs one at module scope, so a bundle that reaches for it does not evaluate at all."
126
- },
127
- {
128
- "name": "MessagePort",
129
- "status": "shim",
130
- "shim": "message-channel",
131
- "why": "The other half of the pair: a port handed to code that checks what it received has to be a real constructor."
132
- },
133
- {
134
- "name": "Intl",
135
- "status": "unsupported",
136
- "remedy": "bundle an Intl polyfill, for example @formatjs/intl-numberformat",
137
- "providedBy": [
138
- "intl",
139
- "full-icu",
140
- "@formatjs/intl",
141
- "@formatjs/intl-*",
142
- "intl-pluralrules",
143
- "intl-locales-supported",
144
- "intl-segmenter-polyfill"
145
- ]
146
- }
147
- ];
148
- var methods = [
149
- {
150
- "target": "Number.prototype",
151
- "name": "toLocaleString"
152
- },
153
- {
154
- "target": "Date.prototype",
155
- "name": "toLocaleString"
156
- },
157
- {
158
- "target": "Date.prototype",
159
- "name": "toLocaleDateString"
160
- },
161
- {
162
- "target": "Date.prototype",
163
- "name": "toLocaleTimeString"
164
- },
165
- {
166
- "target": "String.prototype",
167
- "name": "toLocaleLowerCase"
168
- },
169
- {
170
- "target": "String.prototype",
171
- "name": "toLocaleUpperCase"
172
- }
173
- ];
174
- var methodRemedy$1 = "the engine carries no ICU, so it ignores the locale and returns an unlocalised string; format the value yourself, or bundle a formatting library and call it directly";
175
- //#endregion
176
- //#region src/guest-surface.ts
177
- function declaredGlobals() {
178
- return globals;
179
- }
180
- function declaredMethods() {
181
- return methods;
182
- }
183
- function methodRemedy() {
184
- return methodRemedy$1;
185
- }
186
- function unsupportedGlobals() {
187
- return declaredGlobals().filter((entry) => entry.status === "unsupported");
188
- }
189
- function shimmedGlobals() {
190
- return declaredGlobals().filter((entry) => entry.status === "shim");
191
- }
192
- //#endregion
193
118
  //#region src/guest-parity.ts
194
119
  const SHIMS = {
195
120
  MessageChannel,
@@ -197,12 +122,12 @@ const SHIMS = {
197
122
  };
198
123
  function applyGuestParity(options = {}) {
199
124
  const scope = globalThis;
200
- const declared = declaredPackages(options.projectDir ?? process.cwd());
125
+ const polyfilled = polyfilledGlobals(options.projectDir ?? process.cwd());
201
126
  const undo = [];
202
127
  const removed = [];
203
128
  const exempted = [];
204
129
  for (const entry of unsupportedGlobals()) {
205
- if (isPolyfilled(entry, declared)) {
130
+ if (polyfilled.has(entry.name)) {
206
131
  exempted.push(entry.name);
207
132
  continue;
208
133
  }
@@ -239,34 +164,5 @@ function applyGuestParity(options = {}) {
239
164
  }
240
165
  };
241
166
  }
242
- /**
243
- * Read from the manifest the polyfill is already declared in, so parity has no
244
- * switch of its own that can drift from what the project actually bundles.
245
- */
246
- function isPolyfilled(entry, declared) {
247
- return (entry.providedBy ?? []).some((pattern) => declared.some((name) => matches(pattern, name)));
248
- }
249
- function matches(pattern, name) {
250
- if (!pattern.endsWith("*")) return pattern === name;
251
- return name.startsWith(pattern.slice(0, -1));
252
- }
253
- function declaredPackages(projectDir) {
254
- const manifest = path.join(projectDir, "package.json");
255
- if (!fs.existsSync(manifest)) return [];
256
- let parsed;
257
- try {
258
- parsed = JSON.parse(fs.readFileSync(manifest, "utf-8"));
259
- } catch {
260
- return [];
261
- }
262
- return [
263
- "dependencies",
264
- "devDependencies",
265
- "optionalDependencies"
266
- ].flatMap((field) => {
267
- const deps = parsed[field];
268
- return typeof deps === "object" && deps !== null ? Object.keys(deps) : [];
269
- });
270
- }
271
167
  //#endregion
272
168
  export { applyGuestParity as t };
@@ -1,2 +1,2 @@
1
- import { t as applyGuestParity } from "./guest-parity-CWuYJPbS.mjs";
1
+ import { t as applyGuestParity } from "./guest-parity-Df4rlLDW.mjs";
2
2
  export { applyGuestParity };
@@ -0,0 +1,107 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ //#region ../../server/guest-surface/surface.json
4
+ var globals = [
5
+ {
6
+ "name": "MessageChannel",
7
+ "status": "shim",
8
+ "shim": "message-channel",
9
+ "why": "The engine has none. react-dom/server.browser constructs one at module scope, so a bundle that reaches for it does not evaluate at all."
10
+ },
11
+ {
12
+ "name": "MessagePort",
13
+ "status": "shim",
14
+ "shim": "message-channel",
15
+ "why": "The other half of the pair: a port handed to code that checks what it received has to be a real constructor."
16
+ },
17
+ {
18
+ "name": "Intl",
19
+ "status": "unsupported",
20
+ "remedy": "bundle an Intl polyfill, for example @formatjs/intl-numberformat",
21
+ "providedBy": [
22
+ "intl",
23
+ "full-icu",
24
+ "@formatjs/intl",
25
+ "@formatjs/intl-*",
26
+ "intl-pluralrules",
27
+ "intl-locales-supported",
28
+ "intl-segmenter-polyfill"
29
+ ]
30
+ }
31
+ ];
32
+ var methods = [
33
+ {
34
+ "target": "Number.prototype",
35
+ "name": "toLocaleString"
36
+ },
37
+ {
38
+ "target": "Date.prototype",
39
+ "name": "toLocaleString"
40
+ },
41
+ {
42
+ "target": "Date.prototype",
43
+ "name": "toLocaleDateString"
44
+ },
45
+ {
46
+ "target": "Date.prototype",
47
+ "name": "toLocaleTimeString"
48
+ },
49
+ {
50
+ "target": "String.prototype",
51
+ "name": "toLocaleLowerCase"
52
+ },
53
+ {
54
+ "target": "String.prototype",
55
+ "name": "toLocaleUpperCase"
56
+ }
57
+ ];
58
+ var methodRemedy$1 = "the engine carries no ICU, so it ignores the locale and returns an unlocalised string; format the value yourself, or bundle a formatting library and call it directly";
59
+ //#endregion
60
+ //#region src/guest-surface.ts
61
+ function declaredGlobals() {
62
+ return globals;
63
+ }
64
+ function declaredMethods() {
65
+ return methods;
66
+ }
67
+ function methodRemedy() {
68
+ return methodRemedy$1;
69
+ }
70
+ function unsupportedGlobals() {
71
+ return declaredGlobals().filter((entry) => entry.status === "unsupported");
72
+ }
73
+ function shimmedGlobals() {
74
+ return declaredGlobals().filter((entry) => entry.status === "shim");
75
+ }
76
+ /**
77
+ * Globals a project already carries a polyfill for, read from the manifest the
78
+ * polyfill is declared in so nothing here can drift from what is bundled.
79
+ */
80
+ function polyfilledGlobals(projectDir) {
81
+ const declared = declaredPackages(projectDir);
82
+ return new Set(declaredGlobals().filter((entry) => (entry.providedBy ?? []).some((pattern) => declared.some((name) => matches(pattern, name)))).map((entry) => entry.name));
83
+ }
84
+ function matches(pattern, name) {
85
+ if (!pattern.endsWith("*")) return pattern === name;
86
+ return name.startsWith(pattern.slice(0, -1));
87
+ }
88
+ function declaredPackages(projectDir) {
89
+ const manifest = path.join(projectDir, "package.json");
90
+ if (!fs.existsSync(manifest)) return [];
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(fs.readFileSync(manifest, "utf-8"));
94
+ } catch {
95
+ return [];
96
+ }
97
+ return [
98
+ "dependencies",
99
+ "devDependencies",
100
+ "optionalDependencies"
101
+ ].flatMap((field) => {
102
+ const deps = parsed[field];
103
+ return typeof deps === "object" && deps !== null ? Object.keys(deps) : [];
104
+ });
105
+ }
106
+ //#endregion
107
+ export { unsupportedGlobals as a, shimmedGlobals as i, methodRemedy as n, polyfilledGlobals as r, declaredMethods as t };
package/dist/index.mjs CHANGED
@@ -1,10 +1,14 @@
1
+ import { a as unsupportedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-CXON0L5V.mjs";
1
2
  import cac from "cac";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
4
5
  import { build } from "esbuild";
5
6
  import os from "node:os";
7
+ import { parse } from "acorn";
6
8
  import http from "node:http";
7
9
  import readline from "node:readline";
10
+ import { Readable } from "node:stream";
11
+ import crypto from "node:crypto";
8
12
  import { confirm, select } from "@inquirer/prompts";
9
13
  import { spawnSync } from "node:child_process";
10
14
  import zlib from "node:zlib";
@@ -119,6 +123,522 @@ function readFunctionConfig(projectDir) {
119
123
  }
120
124
  }
121
125
  //#endregion
126
+ //#region src/sourcemap.ts
127
+ const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
128
+ const NO_POSITION = () => null;
129
+ function originalPositionLookup(mapJson) {
130
+ if (!mapJson) return NO_POSITION;
131
+ let map;
132
+ try {
133
+ map = JSON.parse(mapJson);
134
+ } catch {
135
+ return NO_POSITION;
136
+ }
137
+ const sources = Array.isArray(map.sources) ? map.sources.map(String) : [];
138
+ const root = typeof map.sourceRoot === "string" && map.sourceRoot !== "" ? `${map.sourceRoot.replace(/\/+$/, "")}/` : "";
139
+ if (typeof map.mappings !== "string" || sources.length === 0) return NO_POSITION;
140
+ const lines = decodeMappings(map.mappings);
141
+ return (line, column) => {
142
+ const segments = lines[line - 1];
143
+ if (!segments) return null;
144
+ let found;
145
+ for (const segment of segments) {
146
+ if (segment[0] > column) break;
147
+ found = segment;
148
+ }
149
+ if (!found) return null;
150
+ const source = sources[found[1]];
151
+ if (source === void 0) return null;
152
+ return {
153
+ source: `${root}${source}`,
154
+ line: found[2] + 1,
155
+ column: found[3]
156
+ };
157
+ };
158
+ }
159
+ function decodeMappings(mappings) {
160
+ const lines = [];
161
+ let source = 0;
162
+ let sourceLine = 0;
163
+ let sourceColumn = 0;
164
+ for (const encodedLine of mappings.split(";")) {
165
+ const segments = [];
166
+ let generatedColumn = 0;
167
+ for (const encoded of encodedLine.split(",")) {
168
+ if (encoded === "") continue;
169
+ const fields = decodeVlq(encoded);
170
+ if (fields === null) continue;
171
+ generatedColumn += fields[0] ?? 0;
172
+ if (fields.length < 4) continue;
173
+ source += fields[1];
174
+ sourceLine += fields[2];
175
+ sourceColumn += fields[3];
176
+ segments.push([
177
+ generatedColumn,
178
+ source,
179
+ sourceLine,
180
+ sourceColumn
181
+ ]);
182
+ }
183
+ lines.push(segments);
184
+ }
185
+ return lines;
186
+ }
187
+ function decodeVlq(encoded) {
188
+ const values = [];
189
+ let value = 0;
190
+ let shift = 0;
191
+ for (const character of encoded) {
192
+ const digit = BASE64.indexOf(character);
193
+ if (digit === -1) return null;
194
+ value += (digit & 31) << shift;
195
+ if (digit & 32) {
196
+ shift += 5;
197
+ continue;
198
+ }
199
+ const negative = value & 1;
200
+ value >>>= 1;
201
+ values.push(negative ? -value : value);
202
+ value = 0;
203
+ shift = 0;
204
+ }
205
+ return values;
206
+ }
207
+ //#endregion
208
+ //#region src/guest-surface-scan.ts
209
+ const MAX_LOCATIONS = 3;
210
+ const ABSENT_PROBLEM = "the guest does not define it — evaluating this reference throws a ReferenceError";
211
+ const METHOD_PROBLEM = "the guest carries no ICU, so it ignores the locale argument";
212
+ const GUARDED_PROBLEM = "the guest does not define it — this bundle guards the reference, so the path it takes when the global is missing is the one that runs";
213
+ const DEFERRED_PROBLEM = "the guest does not define it — the code holding this reference throws a ReferenceError if it ever runs";
214
+ const GLOBAL_OBJECTS = /* @__PURE__ */ new Set([
215
+ "globalThis",
216
+ "window",
217
+ "self",
218
+ "global"
219
+ ]);
220
+ const FUNCTIONS = /* @__PURE__ */ new Set([
221
+ "FunctionDeclaration",
222
+ "FunctionExpression",
223
+ "ArrowFunctionExpression"
224
+ ]);
225
+ /** Everything it is unsure of warns: a wrong refusal costs a deploy that works. */
226
+ function scanBundle(code, options = {}) {
227
+ let program;
228
+ try {
229
+ program = parse(code, {
230
+ ecmaVersion: "latest",
231
+ sourceType: "module",
232
+ locations: true
233
+ });
234
+ } catch {
235
+ return [];
236
+ }
237
+ const bound = /* @__PURE__ */ new Set();
238
+ const boundNodes = /* @__PURE__ */ new Set();
239
+ const guarded = /* @__PURE__ */ new Set();
240
+ const replacedMethods = /* @__PURE__ */ new Set();
241
+ const references = [];
242
+ const methodUses = [];
243
+ const scopes = /* @__PURE__ */ new Map();
244
+ const functionValues = /* @__PURE__ */ new Map();
245
+ const pendingBodies = [];
246
+ const scopeOf = (owner) => {
247
+ const existing = scopes.get(owner);
248
+ if (existing) return existing;
249
+ const scope = {
250
+ calls: [],
251
+ bodies: /* @__PURE__ */ new Map(),
252
+ locals: /* @__PURE__ */ new Set(),
253
+ immediate: []
254
+ };
255
+ scopes.set(owner, scope);
256
+ return scope;
257
+ };
258
+ scopeOf(null);
259
+ try {
260
+ walk(program, (node, parent, key, owner, inTry) => {
261
+ collectBindings(node, bound, boundNodes, scopeOf(owner), functionValues, pendingBodies);
262
+ collectGuards(node, guarded);
263
+ collectReplacedMethods(node, replacedMethods);
264
+ if (node.type === "CallExpression") {
265
+ const callee = unwrap(node.callee);
266
+ if (callee.type === "Identifier") scopeOf(owner).calls.push(String(callee.name));
267
+ const invoked = invokedFunction(callee);
268
+ if (invoked) {
269
+ scopeOf(owner).immediate.push(invoked);
270
+ if (invoked === callee) bindArguments(invoked, node.arguments, scopeOf(invoked));
271
+ }
272
+ }
273
+ if (node.type === "MemberExpression" && node.computed === false) methodUses.push(node.property);
274
+ if (node.type === "Identifier") {
275
+ if (boundNodes.has(node)) scopeOf(owner).locals.add(String(node.name));
276
+ if (isReference(node, parent, key)) references.push({
277
+ node,
278
+ owner,
279
+ guarded: inTry
280
+ });
281
+ }
282
+ });
283
+ } catch {
284
+ return [];
285
+ }
286
+ const absent = absentGlobals(options.projectDir ?? process.cwd());
287
+ const divergent = new Set(declaredMethods().map((method) => method.name));
288
+ resolveWrappedBodies(functionValues, pendingBodies);
289
+ const evaluated = evaluatedOnLoad(scopes);
290
+ const position = locator(options);
291
+ const findings = /* @__PURE__ */ new Map();
292
+ const classified = references.flatMap((reference) => {
293
+ const name = String(reference.node.name);
294
+ if (boundNodes.has(reference.node) || bound.has(name)) return [];
295
+ const gap = absent.get(name);
296
+ if (!gap) return [];
297
+ if (reference.guarded || guarded.has(name)) return [{
298
+ node: reference.node,
299
+ gap: {
300
+ ...gap,
301
+ problem: GUARDED_PROBLEM
302
+ },
303
+ severity: "warn"
304
+ }];
305
+ if (!evaluated.has(reference.owner)) return [{
306
+ node: reference.node,
307
+ gap: {
308
+ ...gap,
309
+ problem: DEFERRED_PROBLEM
310
+ },
311
+ severity: "warn"
312
+ }];
313
+ return [{
314
+ node: reference.node,
315
+ gap,
316
+ severity: "refuse"
317
+ }];
318
+ });
319
+ const refused = new Set(classified.filter((entry) => entry.severity === "refuse").map((entry) => entry.gap.name));
320
+ for (const entry of classified) {
321
+ if (entry.severity === "warn" && refused.has(entry.gap.name)) continue;
322
+ record(findings, entry.gap, entry.severity, position(entry.node));
323
+ }
324
+ for (const use of methodUses) {
325
+ const name = String(use.name);
326
+ if (replacedMethods.has(name) || !divergent.has(name)) continue;
327
+ record(findings, {
328
+ name,
329
+ problem: METHOD_PROBLEM,
330
+ remedy: methodRemedy()
331
+ }, "warn", position(use));
332
+ }
333
+ return [...findings.values()].sort((a, b) => a.severity === b.severity ? a.name.localeCompare(b.name) : a.severity === "refuse" ? -1 : 1);
334
+ }
335
+ /** The declared surface, less anything this project's own manifest says it bundles. */
336
+ function absentGlobals(projectDir) {
337
+ const polyfilled = polyfilledGlobals(projectDir);
338
+ return new Map(unsupportedGlobals().filter((entry) => !polyfilled.has(entry.name)).map((entry) => [entry.name, {
339
+ name: entry.name,
340
+ problem: ABSENT_PROBLEM,
341
+ remedy: entry.remedy ?? ""
342
+ }]));
343
+ }
344
+ function refusesDeploy(findings) {
345
+ return findings.some((finding) => finding.severity === "refuse");
346
+ }
347
+ function surfaceReportLines(findings) {
348
+ const lines = [];
349
+ for (const finding of findings) {
350
+ const headline = finding.severity === "refuse" ? "Refusing to deploy" : "Warning";
351
+ lines.push(`[wawesome] ${headline}: \`${finding.name}\` — ${finding.problem}.`);
352
+ for (const location of finding.locations) lines.push(`[wawesome] at ${location.file}:${location.line}:${location.column}`);
353
+ if (finding.undisplayedLocations > 0) lines.push(`[wawesome] and ${finding.undisplayedLocations} more reference${finding.undisplayedLocations === 1 ? "" : "s"}`);
354
+ lines.push(`[wawesome] Remedy: ${finding.remedy}`);
355
+ }
356
+ if (refusesDeploy(findings)) lines.push("[wawesome] The guest cannot evaluate this bundle, so nothing has been uploaded.");
357
+ return lines;
358
+ }
359
+ /**
360
+ * A bundled CommonJS dependency is a function esbuild calls as the bundle
361
+ * evaluates, so "runs on load" is not the same question as "sits outside every
362
+ * function".
363
+ */
364
+ function evaluatedOnLoad(scopes) {
365
+ const program = scopes.get(null);
366
+ const evaluated = /* @__PURE__ */ new Set([null]);
367
+ const pending = [null];
368
+ const run = (body) => {
369
+ if (evaluated.has(body)) return;
370
+ evaluated.add(body);
371
+ pending.push(body);
372
+ };
373
+ while (pending.length > 0) {
374
+ const owner = pending.shift();
375
+ const scope = scopes.get(owner);
376
+ if (!scope) continue;
377
+ for (const body of scope.immediate) run(body);
378
+ for (const call of scope.calls) {
379
+ const bodies = scope.bodies.get(call) ?? (scope.locals.has(call) ? [] : program.bodies.get(call) ?? []);
380
+ for (const body of bodies) run(body);
381
+ }
382
+ }
383
+ return evaluated;
384
+ }
385
+ function record(findings, gap, severity, location) {
386
+ const existing = findings.get(gap.name);
387
+ if (!existing) {
388
+ findings.set(gap.name, {
389
+ ...gap,
390
+ severity,
391
+ locations: [location],
392
+ undisplayedLocations: 0
393
+ });
394
+ return;
395
+ }
396
+ if (existing.locations.some((known) => known.file === location.file && known.line === location.line && known.column === location.column)) return;
397
+ if (existing.locations.length < MAX_LOCATIONS) existing.locations.push(location);
398
+ else existing.undisplayedLocations += 1;
399
+ }
400
+ function locator(options) {
401
+ const bundleFile = options.bundlePath ? displayPath(options.bundlePath) : "the bundle";
402
+ const lookup = originalPositionLookup(options.map);
403
+ const baseDir = options.mapBaseDir ?? path.dirname(options.bundlePath ?? ".");
404
+ return (node) => {
405
+ const loc = node.loc;
406
+ const original = lookup(loc.start.line, loc.start.column);
407
+ if (original) return {
408
+ file: displayPath(path.resolve(baseDir, original.source)),
409
+ line: original.line,
410
+ column: original.column + 1
411
+ };
412
+ return {
413
+ file: bundleFile,
414
+ line: loc.start.line,
415
+ column: loc.start.column + 1
416
+ };
417
+ };
418
+ }
419
+ function displayPath(target) {
420
+ const relative = path.relative(process.cwd(), path.resolve(target));
421
+ return relative === "" || relative.startsWith("..") ? target : relative;
422
+ }
423
+ function isReference(node, parent, key) {
424
+ if (!parent) return true;
425
+ switch (parent.type) {
426
+ case "MemberExpression": return key !== "property" || parent.computed === true;
427
+ case "Property":
428
+ case "PropertyDefinition":
429
+ case "MethodDefinition": return key !== "key" || parent.computed === true;
430
+ case "UnaryExpression": return parent.operator !== "typeof";
431
+ case "ImportSpecifier":
432
+ case "ExportSpecifier":
433
+ case "ImportDefaultSpecifier":
434
+ case "ImportNamespaceSpecifier": return false;
435
+ case "LabeledStatement":
436
+ case "BreakStatement":
437
+ case "ContinueStatement": return key !== "label";
438
+ default: return true;
439
+ }
440
+ }
441
+ /** `typeof Intl`, `globalThis.Intl` — a bundle that asks has an answer for "no". */
442
+ function collectGuards(node, guarded) {
443
+ if (node.type === "UnaryExpression" && node.operator === "typeof") {
444
+ const argument = node.argument;
445
+ if (argument.type === "Identifier") guarded.add(String(argument.name));
446
+ return;
447
+ }
448
+ if (node.type !== "MemberExpression") return;
449
+ const name = globalObjectProperty(node);
450
+ if (name !== null) guarded.add(name);
451
+ }
452
+ function collectReplacedMethods(node, replaced) {
453
+ if (node.type === "AssignmentExpression") {
454
+ const target = node.left;
455
+ if (target.type === "MemberExpression" && target.computed === false && onPrototype(target)) replaced.add(String(target.property.name));
456
+ return;
457
+ }
458
+ if (node.type !== "CallExpression") return;
459
+ const callee = node.callee;
460
+ const owner = node.arguments[0];
461
+ const property = node.arguments[1];
462
+ if (callee.type === "MemberExpression" && callee.computed === false && String(callee.property.name) === "defineProperty" && owner && isPrototype(owner) && property?.type === "Literal" && typeof property.value === "string") replaced.add(property.value);
463
+ }
464
+ function onPrototype(member) {
465
+ return isPrototype(member.object);
466
+ }
467
+ function isPrototype(node) {
468
+ return node.type === "MemberExpression" && node.computed === false && String(node.property.name) === "prototype";
469
+ }
470
+ function globalObjectProperty(member) {
471
+ const object = member.object;
472
+ if (object.type !== "Identifier" || !GLOBAL_OBJECTS.has(String(object.name))) return null;
473
+ const property = member.property;
474
+ if (member.computed === false && property.type === "Identifier") return String(property.name);
475
+ if (member.computed === true && property.type === "Literal" && typeof property.value === "string") return property.value;
476
+ return null;
477
+ }
478
+ function collectBindings(node, bound, boundNodes, scope, functionValues, pending) {
479
+ switch (node.type) {
480
+ case "VariableDeclarator":
481
+ bindPattern(node.id, bound, boundNodes);
482
+ bindBody(node.id, node.init, scope, functionValues, pending);
483
+ break;
484
+ case "FunctionDeclaration":
485
+ case "FunctionExpression":
486
+ case "ArrowFunctionExpression":
487
+ if (node.id) bindPattern(node.id, bound, boundNodes);
488
+ if (node.type === "FunctionDeclaration" && node.id) scope.bodies.set(String(node.id.name), [node]);
489
+ for (const param of node.params) bindPattern(param, bound, boundNodes);
490
+ break;
491
+ case "ClassDeclaration":
492
+ case "ClassExpression":
493
+ if (node.id) bindPattern(node.id, bound, boundNodes);
494
+ break;
495
+ case "CatchClause":
496
+ if (node.param) bindPattern(node.param, bound, boundNodes);
497
+ break;
498
+ case "ImportSpecifier":
499
+ case "ImportDefaultSpecifier":
500
+ case "ImportNamespaceSpecifier":
501
+ bindPattern(node.local, bound, boundNodes);
502
+ break;
503
+ case "AssignmentExpression":
504
+ bindAssignmentTarget(node.left, bound);
505
+ bindBody(node.left, node.right, scope, functionValues, pending);
506
+ break;
507
+ default: break;
508
+ }
509
+ }
510
+ function bindBody(target, value, scope, functionValues, pending) {
511
+ if (!value || target.type !== "Identifier") return;
512
+ const name = String(target.name);
513
+ if (FUNCTIONS.has(value.type)) {
514
+ scope.bodies.set(name, [value]);
515
+ functionValues.set(name, value);
516
+ return;
517
+ }
518
+ if (value.type !== "CallExpression") return;
519
+ const callee = unwrap(value.callee);
520
+ if (callee.type !== "Identifier") return;
521
+ const bodies = [];
522
+ for (const argument of value.arguments) {
523
+ if (FUNCTIONS.has(argument.type)) bodies.push(argument);
524
+ if (argument.type === "ObjectExpression") for (const property of argument.properties) {
525
+ const propertyValue = property.value;
526
+ if (propertyValue && FUNCTIONS.has(propertyValue.type)) bodies.push(propertyValue);
527
+ }
528
+ }
529
+ if (bodies.length > 0) pending.push({
530
+ scope,
531
+ name,
532
+ callee: String(callee.name),
533
+ bodies
534
+ });
535
+ }
536
+ /**
537
+ * A bundled dependency arrives as a function handed to one of esbuild's wrapper
538
+ * helpers, whose name minification has already rewritten. The helper is
539
+ * recognised by what it does — hand back a function that invokes what it was
540
+ * given — so an ordinary call handed an ordinary callback is left alone.
541
+ */
542
+ function resolveWrappedBodies(functionValues, pending) {
543
+ const wrappers = new Set([...functionValues].filter(([, value]) => wrapsItsArgument(value)).map(([name]) => name));
544
+ for (const entry of pending) if (wrappers.has(entry.callee)) entry.scope.bodies.set(entry.name, entry.bodies);
545
+ }
546
+ function wrapsItsArgument(fn) {
547
+ const first = fn.params[0];
548
+ if (first?.type !== "Identifier") return false;
549
+ const returned = returnedFunction(fn);
550
+ if (!returned) return false;
551
+ let invokes = false;
552
+ walk(returned, (node) => {
553
+ if (node.type !== "CallExpression") return;
554
+ if (rootObject(unwrap(node.callee)) === String(first.name)) invokes = true;
555
+ });
556
+ return invokes;
557
+ }
558
+ function returnedFunction(fn) {
559
+ const body = fn.body;
560
+ if (FUNCTIONS.has(body.type)) return body;
561
+ if (body.type !== "BlockStatement") return null;
562
+ for (const statement of body.body) if (statement.type === "ReturnStatement" && statement.argument) {
563
+ const returned = statement.argument;
564
+ if (FUNCTIONS.has(returned.type)) return returned;
565
+ }
566
+ return null;
567
+ }
568
+ function rootObject(node) {
569
+ let current = node;
570
+ while (current.type === "MemberExpression") current = unwrap(current.object);
571
+ return current.type === "Identifier" ? String(current.name) : null;
572
+ }
573
+ /** `(0, fn)(...)` and `(fn)(...)` reach the same place as `fn(...)`. */
574
+ function unwrap(node) {
575
+ if (node.type !== "SequenceExpression") return node;
576
+ const expressions = node.expressions;
577
+ return unwrap(expressions[expressions.length - 1]);
578
+ }
579
+ /** The one call site of an IIFE says exactly what its parameters hold. */
580
+ function bindArguments(fn, args, scope) {
581
+ fn.params.forEach((param, index) => {
582
+ const argument = args[index];
583
+ if (param.type === "Identifier" && argument && FUNCTIONS.has(argument.type)) scope.bodies.set(String(param.name), [argument]);
584
+ });
585
+ }
586
+ function invokedFunction(callee) {
587
+ if (FUNCTIONS.has(callee.type)) return callee;
588
+ if (callee.type === "MemberExpression" && callee.computed === false) {
589
+ const property = String(callee.property.name);
590
+ const object = unwrap(callee.object);
591
+ if ((property === "call" || property === "apply") && FUNCTIONS.has(object.type)) return object;
592
+ }
593
+ return null;
594
+ }
595
+ function bindPattern(node, bound, boundNodes) {
596
+ if (!node) return;
597
+ switch (node.type) {
598
+ case "Identifier":
599
+ bound.add(String(node.name));
600
+ boundNodes.add(node);
601
+ break;
602
+ case "ObjectPattern":
603
+ for (const property of node.properties) bindPattern(property.value ?? property.argument, bound, boundNodes);
604
+ break;
605
+ case "ArrayPattern":
606
+ for (const element of node.elements) bindPattern(element, bound, boundNodes);
607
+ break;
608
+ case "RestElement":
609
+ bindPattern(node.argument, bound, boundNodes);
610
+ break;
611
+ case "AssignmentPattern":
612
+ bindPattern(node.left, bound, boundNodes);
613
+ break;
614
+ default: break;
615
+ }
616
+ }
617
+ function bindAssignmentTarget(target, bound) {
618
+ if (target.type === "Identifier") {
619
+ bound.add(String(target.name));
620
+ return;
621
+ }
622
+ if (target.type !== "MemberExpression") return;
623
+ const name = globalObjectProperty(target);
624
+ if (name !== null) bound.add(name);
625
+ }
626
+ function walk(node, visit, parent = null, key = "", owner = null, inTry = false) {
627
+ visit(node, parent, key, owner, inTry);
628
+ for (const childKey of Object.keys(node)) {
629
+ if (childKey === "loc") continue;
630
+ const value = node[childKey];
631
+ const childOwner = FUNCTIONS.has(node.type) || node.type === "PropertyDefinition" && node.static !== true ? node : owner;
632
+ const childInTry = inTry || node.type === "TryStatement" && childKey === "block";
633
+ if (Array.isArray(value)) {
634
+ for (const child of value) if (isNode(child)) walk(child, visit, node, childKey, childOwner, childInTry);
635
+ } else if (isNode(value)) walk(value, visit, node, childKey, childOwner, childInTry);
636
+ }
637
+ }
638
+ function isNode(value) {
639
+ return typeof value === "object" && value !== null && typeof value.type === "string";
640
+ }
641
+ //#endregion
122
642
  //#region src/build.ts
123
643
  /**
124
644
  * Bundles user TS/JS entry point into a single optimized ESM JavaScript file using esbuild.
@@ -138,8 +658,9 @@ async function buildJs(entryInput, options) {
138
658
  console.log(`[wawesome:verbose] Entry point resolved: ${path.resolve(entry)}`);
139
659
  console.log(`[wawesome:verbose] Target output path: ${outPath}`);
140
660
  }
661
+ let result;
141
662
  try {
142
- await build({
663
+ result = await build({
143
664
  entryPoints: [entry],
144
665
  absWorkingDir: process.cwd(),
145
666
  bundle: true,
@@ -156,12 +677,10 @@ async function buildJs(entryInput, options) {
156
677
  minify: true,
157
678
  keepNames: true,
158
679
  legalComments: "none",
159
- logLevel: isVerbose ? "info" : "silent"
680
+ logLevel: isVerbose ? "info" : "silent",
681
+ sourcemap: "external",
682
+ write: false
160
683
  });
161
- const sizeKb = (fs.statSync(outPath).size / 1024).toFixed(2);
162
- const elapsed = Date.now() - startTime;
163
- console.log(`[wawesome] Successfully built ${options.out} (${sizeKb} KB) in ${elapsed}ms!`);
164
- return outPath;
165
684
  } catch (err) {
166
685
  console.error("[wawesome] Build failed!");
167
686
  if (isVerbose && err instanceof Error) console.error(err.stack || err.message);
@@ -169,6 +688,26 @@ async function buildJs(entryInput, options) {
169
688
  else console.error(err);
170
689
  process.exit(1);
171
690
  }
691
+ const bundle = result.outputFiles.find((file) => file.path === outPath);
692
+ if (!bundle) {
693
+ console.error(`[wawesome] Build failed! esbuild produced no output at ${outPath}.`);
694
+ process.exit(1);
695
+ }
696
+ const map = result.outputFiles.find((file) => file.path === `${outPath}.map`);
697
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
698
+ fs.writeFileSync(outPath, bundle.contents);
699
+ const findings = scanBundle(bundle.text, {
700
+ bundlePath: outPath,
701
+ map: map?.text ?? null,
702
+ mapBaseDir: path.dirname(outPath)
703
+ });
704
+ const refused = refusesDeploy(findings);
705
+ for (const line of surfaceReportLines(findings)) (refused ? console.error : console.warn)(line);
706
+ if (refused) process.exit(1);
707
+ const sizeKb = (bundle.contents.byteLength / 1024).toFixed(2);
708
+ const elapsed = Date.now() - startTime;
709
+ console.log(`[wawesome] Successfully built ${options.out} (${sizeKb} KB) in ${elapsed}ms!`);
710
+ return outPath;
172
711
  }
173
712
  //#endregion
174
713
  //#region src/cli-version.ts
@@ -181,7 +720,7 @@ async function buildJs(entryInput, options) {
181
720
  * that has to name this version — `--version`, the dependency a scaffolded
182
721
  * project pins — reads it here, so a release bumps one file.
183
722
  */
184
- const CLI_VERSION = "0.0.15";
723
+ const CLI_VERSION = "0.2.0";
185
724
  //#endregion
186
725
  //#region src/prompt.ts
187
726
  /**
@@ -726,20 +1265,146 @@ function widest(values) {
726
1265
  return values.reduce((longest, value) => Math.max(longest, value.length), 0);
727
1266
  }
728
1267
  //#endregion
1268
+ //#region src/assets.ts
1269
+ /**
1270
+ * The files under `dir`, hashed.
1271
+ *
1272
+ * Hashes are read off disk in a stream rather than by reading each file whole:
1273
+ * a client build's largest chunk is not something to hold in memory just to
1274
+ * find out the platform already has it.
1275
+ */
1276
+ function collectAssets(dir) {
1277
+ if (!fs.existsSync(dir)) return [];
1278
+ const assets = [];
1279
+ const walk = (current, prefix) => {
1280
+ for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
1281
+ const absolute = path.join(current, entry.name);
1282
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
1283
+ if (entry.isDirectory()) walk(absolute, relative);
1284
+ else if (entry.isFile()) assets.push({
1285
+ path: relative,
1286
+ content_hash: hashFile(absolute),
1287
+ size_bytes: fs.statSync(absolute).size,
1288
+ source: absolute
1289
+ });
1290
+ }
1291
+ };
1292
+ walk(dir, "");
1293
+ return assets;
1294
+ }
1295
+ function hashFile(file) {
1296
+ return sha256(fs.readFileSync(file));
1297
+ }
1298
+ function sha256(bytes) {
1299
+ return crypto.createHash("sha256").update(bytes).digest("hex");
1300
+ }
1301
+ /**
1302
+ * What this deploy *is*: the server bundle together with the sorted set of
1303
+ * asset path and content-hash pairs.
1304
+ *
1305
+ * Computed here, before anything is uploaded, so a deploy that changed nothing
1306
+ * is refused before a byte moves. Mirrors `deploy_digest` in
1307
+ * `server/crates/core/src/features/assets/identity.rs` — the gateway recomputes
1308
+ * it from what actually arrives, so the two have to agree exactly.
1309
+ */
1310
+ const DEPLOY_DIGEST_DOMAIN = "wawesome-deploy-v1";
1311
+ function deployDigest(bundle, assets) {
1312
+ const digest = crypto.createHash("sha256");
1313
+ digest.update(`${DEPLOY_DIGEST_DOMAIN}\n`);
1314
+ digest.update(`${sha256(bundle)}\n`);
1315
+ for (const asset of [...assets].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) digest.update(`${asset.path}\0${asset.content_hash}\n`);
1316
+ return digest.digest("hex");
1317
+ }
1318
+ /** The manifest as the gateway reads it — the bytes on disk are the CLI's business. */
1319
+ function manifestOf(assets) {
1320
+ return assets.map(({ path, content_hash, size_bytes }) => ({
1321
+ path,
1322
+ content_hash,
1323
+ size_bytes
1324
+ }));
1325
+ }
1326
+ //#endregion
729
1327
  //#region ../shared/public-address.ts
730
1328
  const INVOCATION_PREFIX = "/x";
1329
+ const SLUG_SEPARATOR = "--";
1330
+ const SURFACE_ROUTE = "/v1/invocation-surface";
731
1331
  const SUBTREE_NOTE = "Every path beneath this address reaches the Function.";
732
- function mountBase(origin, tenantSlug) {
733
- return `${origin.replace(/\/+$/, "")}${INVOCATION_PREFIX}/${encodeURIComponent(tenantSlug)}`;
1332
+ /**
1333
+ * A gateway that serves no form of the address — and the value a client uses for
1334
+ * one that did not answer at all, deliberately the same: both name no address,
1335
+ * and there is nothing else either could honestly do.
1336
+ */
1337
+ const NO_INVOCATION_SURFACE = {
1338
+ contentOrigin: null,
1339
+ pathFormOrigin: null
1340
+ };
1341
+ async function fetchInvocationSurface(gatewayUrl) {
1342
+ const origin = trimTrailingSlashes(gatewayUrl);
1343
+ const res = await fetch(`${origin}${SURFACE_ROUTE}`);
1344
+ if (!res.ok) throw new Error(`Failed to read the gateway's address forms (HTTP ${res.status}).`);
1345
+ const body = await res.json();
1346
+ return {
1347
+ contentOrigin: body.content_origin ?? null,
1348
+ pathFormOrigin: body.path_form ? origin : null
1349
+ };
734
1350
  }
735
- function publicAddress(origin, tenantSlug, appSlug, functionSlug) {
736
- const base = `${mountBase(origin, tenantSlug)}/${encodeURIComponent(appSlug)}`;
1351
+ /** The one address the Function answers at, `null` where it has none. */
1352
+ function publicAddress(surface, tenantSlug, appSlug, functionSlug) {
1353
+ const base = appBase(surface, tenantSlug, appSlug);
1354
+ if (base === null) return null;
737
1355
  if (functionSlug === "root") return base;
738
1356
  return `${base}/${encodeURIComponent(functionSlug)}`;
739
1357
  }
740
1358
  /** The same address with the app and function still to be chosen. */
741
- function publicAddressTemplate(origin, tenantSlug) {
742
- return `${mountBase(origin, tenantSlug)}/<app>/<function>`;
1359
+ function publicAddressTemplate(surface, tenantSlug) {
1360
+ if (surface.contentOrigin) {
1361
+ if (!isLegalDnsLabel(tenantSlug)) return null;
1362
+ const hostname = appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}<app>`);
1363
+ return hostname === null ? null : `${hostname}/<function>`;
1364
+ }
1365
+ if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/<app>/<function>`;
1366
+ return null;
1367
+ }
1368
+ function appBase(surface, tenantSlug, appSlug) {
1369
+ if (surface.contentOrigin) {
1370
+ if (!isLegalDnsLabel(tenantSlug) || !isLegalDnsLabel(appSlug)) return null;
1371
+ return appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}${appSlug}`);
1372
+ }
1373
+ if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/${encodeURIComponent(appSlug)}`;
1374
+ return null;
1375
+ }
1376
+ /**
1377
+ * The App's own hostname, built the way the gateway rebuilds it from its own
1378
+ * configuration: the label one level beneath the content domain. Callers check
1379
+ * the slugs the label is made of, since a slug the gateway would refuse to read
1380
+ * a `Host` for names no App there.
1381
+ */
1382
+ function appHostname(contentOrigin, label) {
1383
+ let origin;
1384
+ try {
1385
+ origin = new URL(contentOrigin);
1386
+ } catch {
1387
+ return null;
1388
+ }
1389
+ return `${origin.protocol}//${label}.${origin.host}`;
1390
+ }
1391
+ /** The surface, or none where the gateway did not answer. */
1392
+ async function invocationSurfaceOrNone(gatewayUrl) {
1393
+ try {
1394
+ return await fetchInvocationSurface(gatewayUrl);
1395
+ } catch {
1396
+ return NO_INVOCATION_SURFACE;
1397
+ }
1398
+ }
1399
+ function pathFormBase(origin, tenantSlug) {
1400
+ return `${trimTrailingSlashes(origin)}${INVOCATION_PREFIX}/${encodeURIComponent(tenantSlug)}`;
1401
+ }
1402
+ /** Must match `dns_label::is_legal` in `server/crates/core/src/value_objects.rs`. */
1403
+ function isLegalDnsLabel(candidate) {
1404
+ return candidate.length > 0 && candidate.length <= 63 && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
1405
+ }
1406
+ function trimTrailingSlashes(origin) {
1407
+ return origin.replace(/\/+$/, "");
743
1408
  }
744
1409
  //#endregion
745
1410
  //#region src/deploy.ts
@@ -785,10 +1450,28 @@ async function deploy(entryInput, options) {
785
1450
  process.exit(1);
786
1451
  }
787
1452
  if (isVerbose) console.log(`[wawesome:verbose] Bundle size: ${(Buffer.byteLength(jsCode) / 1024).toFixed(2)} KB`);
1453
+ if (options.skipBuild) {
1454
+ const findings = scanBundle(jsCode, { bundlePath });
1455
+ const refused = refusesDeploy(findings);
1456
+ for (const line of surfaceReportLines(findings)) (refused ? console.error : console.warn)(line);
1457
+ if (refused) process.exit(1);
1458
+ }
1459
+ const assets = config.assets ? collectAssets(path.resolve(config.assets)) : [];
1460
+ if (assets.length > 0) await uploadAssets(creds, app, funcName, jsCode, assets, isVerbose);
788
1461
  console.log(`[wawesome] Uploading code for ${app}/${funcName}...`);
789
1462
  const uploadUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/code`;
790
1463
  if (isVerbose) console.log(`[wawesome:verbose] POST ${uploadUrl}`);
791
- const uploadRes = await fetch(uploadUrl, {
1464
+ const uploadRes = assets.length > 0 ? await fetch(uploadUrl, {
1465
+ method: "POST",
1466
+ headers: {
1467
+ Authorization: `Bearer ${creds.tenant_jwt}`,
1468
+ "Content-Type": "application/json"
1469
+ },
1470
+ body: JSON.stringify({
1471
+ code: jsCode,
1472
+ assets: manifestOf(assets)
1473
+ })
1474
+ }) : await fetch(uploadUrl, {
792
1475
  method: "POST",
793
1476
  headers: {
794
1477
  Authorization: `Bearer ${creds.tenant_jwt}`,
@@ -835,9 +1518,11 @@ async function deploy(entryInput, options) {
835
1518
  process.exit(1);
836
1519
  }
837
1520
  let address = null;
1521
+ let surface = null;
838
1522
  try {
839
1523
  const { slug } = await resolveWorkspace(creds);
840
- address = publicAddress(creds.gateway_url, slug, app, funcName);
1524
+ surface = await fetchInvocationSurface(creds.gateway_url);
1525
+ address = publicAddress(surface, slug, app, funcName);
841
1526
  } catch (err) {
842
1527
  if (isVerbose) console.log(`[wawesome:verbose] Could not resolve the workspace address: ${err instanceof Error ? err.message : err}`);
843
1528
  }
@@ -853,9 +1538,14 @@ async function deploy(entryInput, options) {
853
1538
  console.log(`\n App: ${app}`);
854
1539
  console.log(` Function: ${funcName}`);
855
1540
  if (version !== void 0) console.log(` Version: ${version}`);
1541
+ if (assets.length > 0) console.log(` Assets: ${assets.length}`);
856
1542
  if (address) {
857
1543
  console.log(`\n URL: \x1b[36m${address}\x1b[0m`);
858
1544
  console.log(` ${SUBTREE_NOTE}`);
1545
+ } else if (surface) {
1546
+ console.log("\n URL: none — this gateway serves no public address form.");
1547
+ console.log(" Set CONTENT_ORIGIN on it, or ALLOW_PATH_INVOCATION_FORM");
1548
+ console.log(" for local development.");
859
1549
  }
860
1550
  if (headroom) {
861
1551
  console.log("");
@@ -869,6 +1559,69 @@ async function deploy(entryInput, options) {
869
1559
  address
870
1560
  };
871
1561
  }
1562
+ /**
1563
+ * Ask the gateway which of this deploy's files it does not hold, and send only
1564
+ * those.
1565
+ *
1566
+ * The identity of the whole deploy goes with the question, so a redeploy that
1567
+ * changed nothing at all is refused here — before a byte of it has moved.
1568
+ */
1569
+ async function uploadAssets(creds, app, funcName, bundle, assets, isVerbose) {
1570
+ const manifestUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/assets/manifest`;
1571
+ const manifestRes = await fetch(manifestUrl, {
1572
+ method: "POST",
1573
+ headers: {
1574
+ Authorization: `Bearer ${creds.tenant_jwt}`,
1575
+ "Content-Type": "application/json"
1576
+ },
1577
+ body: JSON.stringify({
1578
+ deploy_digest: deployDigest(Buffer.from(bundle, "utf-8"), assets),
1579
+ assets: manifestOf(assets)
1580
+ })
1581
+ });
1582
+ if (!manifestRes.ok) {
1583
+ const errorBody = await manifestRes.text();
1584
+ if (manifestRes.status === 401) console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
1585
+ else if (manifestRes.status === 409) {
1586
+ const refusal = rejectionOf(errorBody, manifestRes.status, "This deploy is already deployed.");
1587
+ console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m`);
1588
+ console.error("[wawesome] Nothing changed since the last deploy, so nothing was uploaded.\n");
1589
+ } else {
1590
+ const refusal = rejectionOf(errorBody, manifestRes.status, `Asset manifest failed (HTTP ${manifestRes.status}).`);
1591
+ console.error(`[wawesome] Error: ${refusal.message}`);
1592
+ if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
1593
+ }
1594
+ process.exit(1);
1595
+ }
1596
+ const { missing = [] } = await manifestRes.json();
1597
+ const toUpload = assets.filter((asset) => missing.includes(asset.content_hash));
1598
+ if (toUpload.length === 0) {
1599
+ console.log(`[wawesome] ✅ ${assets.length} asset(s) already on the platform, nothing to upload.`);
1600
+ return;
1601
+ }
1602
+ console.log(`[wawesome] Uploading ${toUpload.length} of ${assets.length} asset(s)...`);
1603
+ for (const asset of toUpload) {
1604
+ if (isVerbose) console.log(`[wawesome:verbose] PUT ${asset.path} (${asset.size_bytes} bytes)`);
1605
+ const res = await fetch(`${creds.gateway_url}/v1/assets/${asset.content_hash}`, {
1606
+ method: "PUT",
1607
+ headers: {
1608
+ Authorization: `Bearer ${creds.tenant_jwt}`,
1609
+ "Content-Type": "application/octet-stream",
1610
+ "Content-Length": String(asset.size_bytes)
1611
+ },
1612
+ body: Readable.toWeb(fs.createReadStream(asset.source)),
1613
+ duplex: "half"
1614
+ });
1615
+ if (!res.ok) {
1616
+ const errorBody = await res.text();
1617
+ const refusal = rejectionOf(errorBody, res.status, `Upload of '${asset.path}' failed (HTTP ${res.status}).`);
1618
+ console.error(`[wawesome] Error: ${refusal.message}`);
1619
+ if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
1620
+ process.exit(1);
1621
+ }
1622
+ }
1623
+ console.log(`[wawesome] ✅ ${toUpload.length} asset(s) uploaded.`);
1624
+ }
872
1625
  //#endregion
873
1626
  //#region src/billing.ts
874
1627
  function billingPageUrl() {
@@ -1733,9 +2486,11 @@ async function ensureSession(options) {
1733
2486
  /** Bound on the rename loop, so a name the gateway keeps refusing ends the offer. */
1734
2487
  const MAX_RENAME_ATTEMPTS = 3;
1735
2488
  /** Show the address the Function will answer on, once it is deployed. */
1736
- function announceUrl(creds, slug, appSlug, functionName) {
2489
+ function announceUrl(surface, slug, appSlug, functionName) {
2490
+ const address = publicAddress(surface, slug, appSlug, functionName);
2491
+ if (!address) return;
1737
2492
  console.log("\n[wawesome] Your Function will answer on:\n");
1738
- console.log(` \x1b[36m${publicAddress(creds.gateway_url, slug, appSlug, functionName)}\x1b[0m`);
2493
+ console.log(` \x1b[36m${address}\x1b[0m`);
1739
2494
  console.log(` ${SUBTREE_NOTE}\n`);
1740
2495
  }
1741
2496
  /**
@@ -1755,7 +2510,8 @@ function announceUrl(creds, slug, appSlug, functionName) {
1755
2510
  async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionName) {
1756
2511
  const current = tenant?.tenant_slug ?? creds.tenant_slug;
1757
2512
  if (!current) return;
1758
- announceUrl(creds, current, appSlug, functionName);
2513
+ const surface = await invocationSurfaceOrNone(creds.gateway_url);
2514
+ announceUrl(surface, current, appSlug, functionName);
1759
2515
  if (!tenant) {
1760
2516
  console.log(` '${current}' is your workspace address. Whether it can still be changed`);
1761
2517
  console.log(" is a question for the gateway, which did not answer.\n");
@@ -1779,7 +2535,7 @@ async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionNa
1779
2535
  try {
1780
2536
  const renamed = await renameTenantSlug(creds, answer);
1781
2537
  console.log(`\n[wawesome] ✅ Workspace renamed to '${renamed.tenant_slug}'.`);
1782
- announceUrl(creds, renamed.tenant_slug, appSlug, functionName);
2538
+ announceUrl(surface, renamed.tenant_slug, appSlug, functionName);
1783
2539
  return;
1784
2540
  } catch (err) {
1785
2541
  console.log(`[wawesome] ${errorText(err)}`);
@@ -2715,8 +3471,11 @@ async function showWorkspace(options) {
2715
3471
  console.log(` Address: ${tenant.tenant_slug}`);
2716
3472
  console.log(` Tenant: ${tenant.id}`);
2717
3473
  if (options.verbose) {
2718
- console.log(` URLs: ${publicAddressTemplate(creds.gateway_url, tenant.tenant_slug)}`);
2719
- console.log(` ${SUBTREE_NOTE}`);
3474
+ const template = publicAddressTemplate(await invocationSurfaceOrNone(creds.gateway_url), tenant.tenant_slug);
3475
+ if (template) {
3476
+ console.log(` URLs: ${template}`);
3477
+ console.log(` ${SUBTREE_NOTE}`);
3478
+ }
2720
3479
  }
2721
3480
  if (tenant.slug_locked) console.log("\n 🔒 The address is fixed — a Function version has been promoted and live URLs carry it.");
2722
3481
  else {
@@ -1,4 +1,4 @@
1
- import { t as applyGuestParity } from "./guest-parity-CWuYJPbS.mjs";
1
+ import { t as applyGuestParity } from "./guest-parity-Df4rlLDW.mjs";
2
2
  //#region src/vitest-setup.ts
3
3
  applyGuestParity();
4
4
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wawesome",
3
- "version": "0.0.15",
3
+ "version": "0.2.0",
4
4
  "description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,6 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@inquirer/prompts": "^8.5.2",
40
+ "acorn": "^8.18.0",
40
41
  "cac": "^6.7.14",
41
42
  "esbuild": "^0.28.0",
42
43
  "open": "^10.0.0"