wawesome 0.0.15 → 0.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/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.1.0";
185
724
  //#endregion
186
725
  //#region src/prompt.ts
187
726
  /**
@@ -726,6 +1265,65 @@ 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";
731
1329
  const SUBTREE_NOTE = "Every path beneath this address reaches the Function.";
@@ -785,10 +1383,28 @@ async function deploy(entryInput, options) {
785
1383
  process.exit(1);
786
1384
  }
787
1385
  if (isVerbose) console.log(`[wawesome:verbose] Bundle size: ${(Buffer.byteLength(jsCode) / 1024).toFixed(2)} KB`);
1386
+ if (options.skipBuild) {
1387
+ const findings = scanBundle(jsCode, { bundlePath });
1388
+ const refused = refusesDeploy(findings);
1389
+ for (const line of surfaceReportLines(findings)) (refused ? console.error : console.warn)(line);
1390
+ if (refused) process.exit(1);
1391
+ }
1392
+ const assets = config.assets ? collectAssets(path.resolve(config.assets)) : [];
1393
+ if (assets.length > 0) await uploadAssets(creds, app, funcName, jsCode, assets, isVerbose);
788
1394
  console.log(`[wawesome] Uploading code for ${app}/${funcName}...`);
789
1395
  const uploadUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/code`;
790
1396
  if (isVerbose) console.log(`[wawesome:verbose] POST ${uploadUrl}`);
791
- const uploadRes = await fetch(uploadUrl, {
1397
+ const uploadRes = assets.length > 0 ? await fetch(uploadUrl, {
1398
+ method: "POST",
1399
+ headers: {
1400
+ Authorization: `Bearer ${creds.tenant_jwt}`,
1401
+ "Content-Type": "application/json"
1402
+ },
1403
+ body: JSON.stringify({
1404
+ code: jsCode,
1405
+ assets: manifestOf(assets)
1406
+ })
1407
+ }) : await fetch(uploadUrl, {
792
1408
  method: "POST",
793
1409
  headers: {
794
1410
  Authorization: `Bearer ${creds.tenant_jwt}`,
@@ -853,6 +1469,7 @@ async function deploy(entryInput, options) {
853
1469
  console.log(`\n App: ${app}`);
854
1470
  console.log(` Function: ${funcName}`);
855
1471
  if (version !== void 0) console.log(` Version: ${version}`);
1472
+ if (assets.length > 0) console.log(` Assets: ${assets.length}`);
856
1473
  if (address) {
857
1474
  console.log(`\n URL: \x1b[36m${address}\x1b[0m`);
858
1475
  console.log(` ${SUBTREE_NOTE}`);
@@ -869,6 +1486,69 @@ async function deploy(entryInput, options) {
869
1486
  address
870
1487
  };
871
1488
  }
1489
+ /**
1490
+ * Ask the gateway which of this deploy's files it does not hold, and send only
1491
+ * those.
1492
+ *
1493
+ * The identity of the whole deploy goes with the question, so a redeploy that
1494
+ * changed nothing at all is refused here — before a byte of it has moved.
1495
+ */
1496
+ async function uploadAssets(creds, app, funcName, bundle, assets, isVerbose) {
1497
+ const manifestUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/assets/manifest`;
1498
+ const manifestRes = await fetch(manifestUrl, {
1499
+ method: "POST",
1500
+ headers: {
1501
+ Authorization: `Bearer ${creds.tenant_jwt}`,
1502
+ "Content-Type": "application/json"
1503
+ },
1504
+ body: JSON.stringify({
1505
+ deploy_digest: deployDigest(Buffer.from(bundle, "utf-8"), assets),
1506
+ assets: manifestOf(assets)
1507
+ })
1508
+ });
1509
+ if (!manifestRes.ok) {
1510
+ const errorBody = await manifestRes.text();
1511
+ if (manifestRes.status === 401) console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
1512
+ else if (manifestRes.status === 409) {
1513
+ const refusal = rejectionOf(errorBody, manifestRes.status, "This deploy is already deployed.");
1514
+ console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m`);
1515
+ console.error("[wawesome] Nothing changed since the last deploy, so nothing was uploaded.\n");
1516
+ } else {
1517
+ const refusal = rejectionOf(errorBody, manifestRes.status, `Asset manifest failed (HTTP ${manifestRes.status}).`);
1518
+ console.error(`[wawesome] Error: ${refusal.message}`);
1519
+ if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
1520
+ }
1521
+ process.exit(1);
1522
+ }
1523
+ const { missing = [] } = await manifestRes.json();
1524
+ const toUpload = assets.filter((asset) => missing.includes(asset.content_hash));
1525
+ if (toUpload.length === 0) {
1526
+ console.log(`[wawesome] ✅ ${assets.length} asset(s) already on the platform, nothing to upload.`);
1527
+ return;
1528
+ }
1529
+ console.log(`[wawesome] Uploading ${toUpload.length} of ${assets.length} asset(s)...`);
1530
+ for (const asset of toUpload) {
1531
+ if (isVerbose) console.log(`[wawesome:verbose] PUT ${asset.path} (${asset.size_bytes} bytes)`);
1532
+ const res = await fetch(`${creds.gateway_url}/v1/assets/${asset.content_hash}`, {
1533
+ method: "PUT",
1534
+ headers: {
1535
+ Authorization: `Bearer ${creds.tenant_jwt}`,
1536
+ "Content-Type": "application/octet-stream",
1537
+ "Content-Length": String(asset.size_bytes)
1538
+ },
1539
+ body: Readable.toWeb(fs.createReadStream(asset.source)),
1540
+ duplex: "half"
1541
+ });
1542
+ if (!res.ok) {
1543
+ const errorBody = await res.text();
1544
+ const refusal = rejectionOf(errorBody, res.status, `Upload of '${asset.path}' failed (HTTP ${res.status}).`);
1545
+ console.error(`[wawesome] Error: ${refusal.message}`);
1546
+ if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
1547
+ process.exit(1);
1548
+ }
1549
+ }
1550
+ console.log(`[wawesome] ✅ ${toUpload.length} asset(s) uploaded.`);
1551
+ }
872
1552
  //#endregion
873
1553
  //#region src/billing.ts
874
1554
  function billingPageUrl() {
@@ -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.1.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"