wawesome 0.0.14 → 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 +63 -0
- package/dist/guest-parity-Df4rlLDW.mjs +168 -0
- package/dist/guest-parity.d.mts +16 -0
- package/dist/guest-parity.mjs +2 -0
- package/dist/guest-surface-CXON0L5V.mjs +107 -0
- package/dist/index.mjs +688 -8
- package/dist/vitest-setup.d.mts +1 -0
- package/dist/vitest-setup.mjs +5 -0
- package/package.json +17 -1
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
|
|
@@ -215,6 +251,33 @@ Three headers arrive or leave on it, and the stripping is what makes them worth
|
|
|
215
251
|
| `x-wawesome-invocation-id` | outbound | The id of this run — the key to fetch its logs with `npx wawesome logs --invocation <id>`. |
|
|
216
252
|
| `x-wawesome-error` | outbound | Present only when the platform failed, never when your Function did. Its *absence* means the status on the wire is yours — up to the moment your response is committed, and no further. |
|
|
217
253
|
|
|
254
|
+
### Testing against the guest's JavaScript surface
|
|
255
|
+
|
|
256
|
+
Your Function does not run on Node. The engine has no `Intl`, and its `toLocaleString` ignores the
|
|
257
|
+
locale you pass — `(1234.5).toLocaleString('en-US')` comes back as `"1234.5"`, not `"1,234.50"`. On
|
|
258
|
+
Node both work, which is how a green suite ships a Function that throws in production, or renders
|
|
259
|
+
markup the browser then refuses to hydrate.
|
|
260
|
+
|
|
261
|
+
Point your test suite at the guest's surface instead:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
// vitest.config.ts
|
|
265
|
+
import { defineConfig } from "vitest/config";
|
|
266
|
+
|
|
267
|
+
export default defineConfig({
|
|
268
|
+
test: { setupFiles: ["wawesome/vitest-setup"] },
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Templates scaffolded with `wawesome init --template` ship this already. With it in place `Intl` is
|
|
273
|
+
gone, `MessageChannel` is the platform's own implementation rather than Node's, and the
|
|
274
|
+
locale-sensitive methods throw with a message naming the remedy — they throw rather than return the
|
|
275
|
+
engine's unlocalised answer because the platform declares them unsupported, and a wrong string that
|
|
276
|
+
fails nowhere is the thing this is here to stop you shipping.
|
|
277
|
+
|
|
278
|
+
If you bundle an `Intl` polyfill, declare it in your `package.json` as you normally would — a
|
|
279
|
+
dependency that provides `Intl` is left in place rather than stripped out from under you.
|
|
280
|
+
|
|
218
281
|
### Local Development / Gateway Overrides
|
|
219
282
|
|
|
220
283
|
If you are running a local gateway or self-hosted instance, you can configure your CLI Gateway URL using any of the
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { a as unsupportedGlobals, i as shimmedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-CXON0L5V.mjs";
|
|
2
|
+
//#region ../../server/guest-surface/message-channel.js
|
|
3
|
+
const ENTANGLED = Symbol("entangled");
|
|
4
|
+
const QUEUE = Symbol("queue");
|
|
5
|
+
const STARTED = Symbol("started");
|
|
6
|
+
const CLOSED = Symbol("closed");
|
|
7
|
+
const LISTENERS = Symbol("listeners");
|
|
8
|
+
const ONMESSAGE = Symbol("onmessage");
|
|
9
|
+
var MessagePort = class {
|
|
10
|
+
constructor() {
|
|
11
|
+
this[ENTANGLED] = null;
|
|
12
|
+
this[QUEUE] = [];
|
|
13
|
+
this[STARTED] = false;
|
|
14
|
+
this[CLOSED] = false;
|
|
15
|
+
this[LISTENERS] = [];
|
|
16
|
+
this[ONMESSAGE] = null;
|
|
17
|
+
this.onmessageerror = null;
|
|
18
|
+
}
|
|
19
|
+
get onmessage() {
|
|
20
|
+
return this[ONMESSAGE];
|
|
21
|
+
}
|
|
22
|
+
set onmessage(handler) {
|
|
23
|
+
this[ONMESSAGE] = handler;
|
|
24
|
+
if (handler) this.start();
|
|
25
|
+
}
|
|
26
|
+
start() {
|
|
27
|
+
if (this[STARTED]) return;
|
|
28
|
+
this[STARTED] = true;
|
|
29
|
+
drain(this);
|
|
30
|
+
}
|
|
31
|
+
close() {
|
|
32
|
+
this[CLOSED] = true;
|
|
33
|
+
const peer = this[ENTANGLED];
|
|
34
|
+
this[ENTANGLED] = null;
|
|
35
|
+
if (peer) peer[ENTANGLED] = null;
|
|
36
|
+
}
|
|
37
|
+
postMessage(data) {
|
|
38
|
+
const peer = this[ENTANGLED];
|
|
39
|
+
if (!peer || peer[CLOSED]) return;
|
|
40
|
+
peer[QUEUE].push(data);
|
|
41
|
+
if (peer[STARTED]) drain(peer);
|
|
42
|
+
}
|
|
43
|
+
addEventListener(type, listener) {
|
|
44
|
+
if (type !== "message" || !listener) return;
|
|
45
|
+
this[LISTENERS].push(listener);
|
|
46
|
+
}
|
|
47
|
+
removeEventListener(type, listener) {
|
|
48
|
+
if (type !== "message") return;
|
|
49
|
+
const at = this[LISTENERS].indexOf(listener);
|
|
50
|
+
if (at !== -1) this[LISTENERS].splice(at, 1);
|
|
51
|
+
}
|
|
52
|
+
dispatchEvent(event) {
|
|
53
|
+
deliver(this, event);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function drain(port) {
|
|
58
|
+
if (port[QUEUE].length === 0) return;
|
|
59
|
+
const data = port[QUEUE].shift();
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
if (port[CLOSED]) return;
|
|
62
|
+
deliver(port, {
|
|
63
|
+
type: "message",
|
|
64
|
+
data,
|
|
65
|
+
target: port,
|
|
66
|
+
ports: []
|
|
67
|
+
});
|
|
68
|
+
drain(port);
|
|
69
|
+
}, 0);
|
|
70
|
+
}
|
|
71
|
+
function deliver(port, event) {
|
|
72
|
+
if (typeof port.onmessage === "function") port.onmessage(event);
|
|
73
|
+
for (const listener of port[LISTENERS].slice()) if (typeof listener === "function") listener.call(port, event);
|
|
74
|
+
else if (listener && typeof listener.handleEvent === "function") listener.handleEvent(event);
|
|
75
|
+
}
|
|
76
|
+
var MessageChannel = class {
|
|
77
|
+
constructor() {
|
|
78
|
+
this.port1 = new MessagePort();
|
|
79
|
+
this.port2 = new MessagePort();
|
|
80
|
+
this.port1[ENTANGLED] = this.port2;
|
|
81
|
+
this.port2[ENTANGLED] = this.port1;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region ../../server/guest-surface/locale-methods.js
|
|
86
|
+
function installUnsupportedLocaleMethods(scope, methods, remedy) {
|
|
87
|
+
const replaced = [];
|
|
88
|
+
for (const { target, name } of methods) {
|
|
89
|
+
const owner = resolve(scope, target);
|
|
90
|
+
if (!owner || typeof owner[name] !== "function") continue;
|
|
91
|
+
const message = `wawesome: ${target}.${name} is not supported — ${remedy}.`;
|
|
92
|
+
const thrower = function() {
|
|
93
|
+
throw new TypeError(message);
|
|
94
|
+
};
|
|
95
|
+
Object.defineProperty(thrower, "name", {
|
|
96
|
+
value: name,
|
|
97
|
+
configurable: true
|
|
98
|
+
});
|
|
99
|
+
replaced.push({
|
|
100
|
+
target,
|
|
101
|
+
name,
|
|
102
|
+
owner,
|
|
103
|
+
previous: Object.getOwnPropertyDescriptor(owner, name)
|
|
104
|
+
});
|
|
105
|
+
Object.defineProperty(owner, name, {
|
|
106
|
+
value: thrower,
|
|
107
|
+
writable: true,
|
|
108
|
+
enumerable: false,
|
|
109
|
+
configurable: true
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return replaced;
|
|
113
|
+
}
|
|
114
|
+
function resolve(scope, target) {
|
|
115
|
+
return target.split(".").reduce((current, part) => current == null ? current : current[part], scope);
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/guest-parity.ts
|
|
119
|
+
const SHIMS = {
|
|
120
|
+
MessageChannel,
|
|
121
|
+
MessagePort
|
|
122
|
+
};
|
|
123
|
+
function applyGuestParity(options = {}) {
|
|
124
|
+
const scope = globalThis;
|
|
125
|
+
const polyfilled = polyfilledGlobals(options.projectDir ?? process.cwd());
|
|
126
|
+
const undo = [];
|
|
127
|
+
const removed = [];
|
|
128
|
+
const exempted = [];
|
|
129
|
+
for (const entry of unsupportedGlobals()) {
|
|
130
|
+
if (polyfilled.has(entry.name)) {
|
|
131
|
+
exempted.push(entry.name);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const descriptor = Object.getOwnPropertyDescriptor(scope, entry.name);
|
|
135
|
+
if (!descriptor) continue;
|
|
136
|
+
undo.push(() => Object.defineProperty(scope, entry.name, descriptor));
|
|
137
|
+
delete scope[entry.name];
|
|
138
|
+
removed.push(entry.name);
|
|
139
|
+
}
|
|
140
|
+
const shimmed = [];
|
|
141
|
+
for (const entry of shimmedGlobals()) {
|
|
142
|
+
const replacement = SHIMS[entry.name];
|
|
143
|
+
if (!replacement) throw new Error(`wawesome: the guest surface declares ${entry.name} as a platform shim, and no implementation is registered for it.`);
|
|
144
|
+
const descriptor = Object.getOwnPropertyDescriptor(scope, entry.name);
|
|
145
|
+
undo.push(() => {
|
|
146
|
+
if (descriptor) Object.defineProperty(scope, entry.name, descriptor);
|
|
147
|
+
else delete scope[entry.name];
|
|
148
|
+
});
|
|
149
|
+
scope[entry.name] = replacement;
|
|
150
|
+
shimmed.push(entry.name);
|
|
151
|
+
}
|
|
152
|
+
const replaced = installUnsupportedLocaleMethods(scope, declaredMethods(), methodRemedy());
|
|
153
|
+
undo.push(() => {
|
|
154
|
+
for (const { owner, name, previous } of replaced) if (previous) Object.defineProperty(owner, name, previous);
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
removed,
|
|
158
|
+
shimmed,
|
|
159
|
+
replacedMethods: replaced.map(({ target, name }) => `${target}.${name}`),
|
|
160
|
+
exempted,
|
|
161
|
+
restore() {
|
|
162
|
+
for (const step of undo.reverse()) step();
|
|
163
|
+
undo.length = 0;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
export { applyGuestParity as t };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/guest-parity.d.ts
|
|
2
|
+
interface GuestParityOptions {
|
|
3
|
+
/** Where the polyfills are declared. Defaults to the working directory. */
|
|
4
|
+
projectDir?: string;
|
|
5
|
+
}
|
|
6
|
+
interface GuestParity {
|
|
7
|
+
removed: string[];
|
|
8
|
+
shimmed: string[];
|
|
9
|
+
replacedMethods: string[];
|
|
10
|
+
/** Left in place because the project bundles a polyfill for them. */
|
|
11
|
+
exempted: string[];
|
|
12
|
+
restore(): void;
|
|
13
|
+
}
|
|
14
|
+
declare function applyGuestParity(options?: GuestParityOptions): GuestParity;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { GuestParity, GuestParityOptions, 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
|
|
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() {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wawesome",
|
|
3
|
-
"version": "0.0
|
|
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": {
|
|
@@ -8,6 +8,21 @@
|
|
|
8
8
|
},
|
|
9
9
|
"main": "./dist/index.mjs",
|
|
10
10
|
"types": "./dist/index.d.mts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.mts",
|
|
14
|
+
"default": "./dist/index.mjs"
|
|
15
|
+
},
|
|
16
|
+
"./guest-parity": {
|
|
17
|
+
"types": "./dist/guest-parity.d.mts",
|
|
18
|
+
"default": "./dist/guest-parity.mjs"
|
|
19
|
+
},
|
|
20
|
+
"./vitest-setup": {
|
|
21
|
+
"types": "./dist/vitest-setup.d.mts",
|
|
22
|
+
"default": "./dist/vitest-setup.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
11
26
|
"files": [
|
|
12
27
|
"bin",
|
|
13
28
|
"dist"
|
|
@@ -22,6 +37,7 @@
|
|
|
22
37
|
},
|
|
23
38
|
"dependencies": {
|
|
24
39
|
"@inquirer/prompts": "^8.5.2",
|
|
40
|
+
"acorn": "^8.18.0",
|
|
25
41
|
"cac": "^6.7.14",
|
|
26
42
|
"esbuild": "^0.28.0",
|
|
27
43
|
"open": "^10.0.0"
|