wawesome 0.0.13 → 0.0.15
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 +27 -0
- package/dist/guest-parity-CWuYJPbS.mjs +272 -0
- package/dist/guest-parity.d.mts +16 -0
- package/dist/guest-parity.mjs +2 -0
- package/dist/index.mjs +75 -29
- package/dist/vitest-setup.d.mts +1 -0
- package/dist/vitest-setup.mjs +5 -0
- package/package.json +16 -1
package/README.md
CHANGED
|
@@ -215,6 +215,33 @@ Three headers arrive or leave on it, and the stripping is what makes them worth
|
|
|
215
215
|
| `x-wawesome-invocation-id` | outbound | The id of this run — the key to fetch its logs with `npx wawesome logs --invocation <id>`. |
|
|
216
216
|
| `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
217
|
|
|
218
|
+
### Testing against the guest's JavaScript surface
|
|
219
|
+
|
|
220
|
+
Your Function does not run on Node. The engine has no `Intl`, and its `toLocaleString` ignores the
|
|
221
|
+
locale you pass — `(1234.5).toLocaleString('en-US')` comes back as `"1234.5"`, not `"1,234.50"`. On
|
|
222
|
+
Node both work, which is how a green suite ships a Function that throws in production, or renders
|
|
223
|
+
markup the browser then refuses to hydrate.
|
|
224
|
+
|
|
225
|
+
Point your test suite at the guest's surface instead:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
// vitest.config.ts
|
|
229
|
+
import { defineConfig } from "vitest/config";
|
|
230
|
+
|
|
231
|
+
export default defineConfig({
|
|
232
|
+
test: { setupFiles: ["wawesome/vitest-setup"] },
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Templates scaffolded with `wawesome init --template` ship this already. With it in place `Intl` is
|
|
237
|
+
gone, `MessageChannel` is the platform's own implementation rather than Node's, and the
|
|
238
|
+
locale-sensitive methods throw with a message naming the remedy — they throw rather than return the
|
|
239
|
+
engine's unlocalised answer because the platform declares them unsupported, and a wrong string that
|
|
240
|
+
fails nowhere is the thing this is here to stop you shipping.
|
|
241
|
+
|
|
242
|
+
If you bundle an `Intl` polyfill, declare it in your `package.json` as you normally would — a
|
|
243
|
+
dependency that provides `Intl` is left in place rather than stripped out from under you.
|
|
244
|
+
|
|
218
245
|
### Local Development / Gateway Overrides
|
|
219
246
|
|
|
220
247
|
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,272 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region ../../server/guest-surface/message-channel.js
|
|
4
|
+
const ENTANGLED = Symbol("entangled");
|
|
5
|
+
const QUEUE = Symbol("queue");
|
|
6
|
+
const STARTED = Symbol("started");
|
|
7
|
+
const CLOSED = Symbol("closed");
|
|
8
|
+
const LISTENERS = Symbol("listeners");
|
|
9
|
+
const ONMESSAGE = Symbol("onmessage");
|
|
10
|
+
var MessagePort = class {
|
|
11
|
+
constructor() {
|
|
12
|
+
this[ENTANGLED] = null;
|
|
13
|
+
this[QUEUE] = [];
|
|
14
|
+
this[STARTED] = false;
|
|
15
|
+
this[CLOSED] = false;
|
|
16
|
+
this[LISTENERS] = [];
|
|
17
|
+
this[ONMESSAGE] = null;
|
|
18
|
+
this.onmessageerror = null;
|
|
19
|
+
}
|
|
20
|
+
get onmessage() {
|
|
21
|
+
return this[ONMESSAGE];
|
|
22
|
+
}
|
|
23
|
+
set onmessage(handler) {
|
|
24
|
+
this[ONMESSAGE] = handler;
|
|
25
|
+
if (handler) this.start();
|
|
26
|
+
}
|
|
27
|
+
start() {
|
|
28
|
+
if (this[STARTED]) return;
|
|
29
|
+
this[STARTED] = true;
|
|
30
|
+
drain(this);
|
|
31
|
+
}
|
|
32
|
+
close() {
|
|
33
|
+
this[CLOSED] = true;
|
|
34
|
+
const peer = this[ENTANGLED];
|
|
35
|
+
this[ENTANGLED] = null;
|
|
36
|
+
if (peer) peer[ENTANGLED] = null;
|
|
37
|
+
}
|
|
38
|
+
postMessage(data) {
|
|
39
|
+
const peer = this[ENTANGLED];
|
|
40
|
+
if (!peer || peer[CLOSED]) return;
|
|
41
|
+
peer[QUEUE].push(data);
|
|
42
|
+
if (peer[STARTED]) drain(peer);
|
|
43
|
+
}
|
|
44
|
+
addEventListener(type, listener) {
|
|
45
|
+
if (type !== "message" || !listener) return;
|
|
46
|
+
this[LISTENERS].push(listener);
|
|
47
|
+
}
|
|
48
|
+
removeEventListener(type, listener) {
|
|
49
|
+
if (type !== "message") return;
|
|
50
|
+
const at = this[LISTENERS].indexOf(listener);
|
|
51
|
+
if (at !== -1) this[LISTENERS].splice(at, 1);
|
|
52
|
+
}
|
|
53
|
+
dispatchEvent(event) {
|
|
54
|
+
deliver(this, event);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function drain(port) {
|
|
59
|
+
if (port[QUEUE].length === 0) return;
|
|
60
|
+
const data = port[QUEUE].shift();
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
if (port[CLOSED]) return;
|
|
63
|
+
deliver(port, {
|
|
64
|
+
type: "message",
|
|
65
|
+
data,
|
|
66
|
+
target: port,
|
|
67
|
+
ports: []
|
|
68
|
+
});
|
|
69
|
+
drain(port);
|
|
70
|
+
}, 0);
|
|
71
|
+
}
|
|
72
|
+
function deliver(port, event) {
|
|
73
|
+
if (typeof port.onmessage === "function") port.onmessage(event);
|
|
74
|
+
for (const listener of port[LISTENERS].slice()) if (typeof listener === "function") listener.call(port, event);
|
|
75
|
+
else if (listener && typeof listener.handleEvent === "function") listener.handleEvent(event);
|
|
76
|
+
}
|
|
77
|
+
var MessageChannel = class {
|
|
78
|
+
constructor() {
|
|
79
|
+
this.port1 = new MessagePort();
|
|
80
|
+
this.port2 = new MessagePort();
|
|
81
|
+
this.port1[ENTANGLED] = this.port2;
|
|
82
|
+
this.port2[ENTANGLED] = this.port1;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region ../../server/guest-surface/locale-methods.js
|
|
87
|
+
function installUnsupportedLocaleMethods(scope, methods, remedy) {
|
|
88
|
+
const replaced = [];
|
|
89
|
+
for (const { target, name } of methods) {
|
|
90
|
+
const owner = resolve(scope, target);
|
|
91
|
+
if (!owner || typeof owner[name] !== "function") continue;
|
|
92
|
+
const message = `wawesome: ${target}.${name} is not supported — ${remedy}.`;
|
|
93
|
+
const thrower = function() {
|
|
94
|
+
throw new TypeError(message);
|
|
95
|
+
};
|
|
96
|
+
Object.defineProperty(thrower, "name", {
|
|
97
|
+
value: name,
|
|
98
|
+
configurable: true
|
|
99
|
+
});
|
|
100
|
+
replaced.push({
|
|
101
|
+
target,
|
|
102
|
+
name,
|
|
103
|
+
owner,
|
|
104
|
+
previous: Object.getOwnPropertyDescriptor(owner, name)
|
|
105
|
+
});
|
|
106
|
+
Object.defineProperty(owner, name, {
|
|
107
|
+
value: thrower,
|
|
108
|
+
writable: true,
|
|
109
|
+
enumerable: false,
|
|
110
|
+
configurable: true
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return replaced;
|
|
114
|
+
}
|
|
115
|
+
function resolve(scope, target) {
|
|
116
|
+
return target.split(".").reduce((current, part) => current == null ? current : current[part], scope);
|
|
117
|
+
}
|
|
118
|
+
//#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
|
+
//#region src/guest-parity.ts
|
|
194
|
+
const SHIMS = {
|
|
195
|
+
MessageChannel,
|
|
196
|
+
MessagePort
|
|
197
|
+
};
|
|
198
|
+
function applyGuestParity(options = {}) {
|
|
199
|
+
const scope = globalThis;
|
|
200
|
+
const declared = declaredPackages(options.projectDir ?? process.cwd());
|
|
201
|
+
const undo = [];
|
|
202
|
+
const removed = [];
|
|
203
|
+
const exempted = [];
|
|
204
|
+
for (const entry of unsupportedGlobals()) {
|
|
205
|
+
if (isPolyfilled(entry, declared)) {
|
|
206
|
+
exempted.push(entry.name);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const descriptor = Object.getOwnPropertyDescriptor(scope, entry.name);
|
|
210
|
+
if (!descriptor) continue;
|
|
211
|
+
undo.push(() => Object.defineProperty(scope, entry.name, descriptor));
|
|
212
|
+
delete scope[entry.name];
|
|
213
|
+
removed.push(entry.name);
|
|
214
|
+
}
|
|
215
|
+
const shimmed = [];
|
|
216
|
+
for (const entry of shimmedGlobals()) {
|
|
217
|
+
const replacement = SHIMS[entry.name];
|
|
218
|
+
if (!replacement) throw new Error(`wawesome: the guest surface declares ${entry.name} as a platform shim, and no implementation is registered for it.`);
|
|
219
|
+
const descriptor = Object.getOwnPropertyDescriptor(scope, entry.name);
|
|
220
|
+
undo.push(() => {
|
|
221
|
+
if (descriptor) Object.defineProperty(scope, entry.name, descriptor);
|
|
222
|
+
else delete scope[entry.name];
|
|
223
|
+
});
|
|
224
|
+
scope[entry.name] = replacement;
|
|
225
|
+
shimmed.push(entry.name);
|
|
226
|
+
}
|
|
227
|
+
const replaced = installUnsupportedLocaleMethods(scope, declaredMethods(), methodRemedy());
|
|
228
|
+
undo.push(() => {
|
|
229
|
+
for (const { owner, name, previous } of replaced) if (previous) Object.defineProperty(owner, name, previous);
|
|
230
|
+
});
|
|
231
|
+
return {
|
|
232
|
+
removed,
|
|
233
|
+
shimmed,
|
|
234
|
+
replacedMethods: replaced.map(({ target, name }) => `${target}.${name}`),
|
|
235
|
+
exempted,
|
|
236
|
+
restore() {
|
|
237
|
+
for (const step of undo.reverse()) step();
|
|
238
|
+
undo.length = 0;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
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
|
+
//#endregion
|
|
272
|
+
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 };
|
package/dist/index.mjs
CHANGED
|
@@ -31,25 +31,40 @@ function readSettings() {
|
|
|
31
31
|
return {};
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
const GATEWAY_ENV_NAMES = ["GATEWAY_URL", "WAWESOME_GATEWAY_URL"];
|
|
35
|
+
const DASHBOARD_ENV_NAMES = ["DASHBOARD_URL", "WAWESOME_DASHBOARD_URL"];
|
|
36
|
+
const GATEWAY_URL_FALLBACK = "https://api.wawesome.io";
|
|
37
|
+
const DASHBOARD_URL_FALLBACK = "https://dashboard.wawesome.io";
|
|
38
|
+
function resolveAddress(overrideUrl, envNames, settingsKey, stored, fallback) {
|
|
39
|
+
if (overrideUrl) return overrideUrl;
|
|
40
|
+
for (const name of envNames) {
|
|
41
|
+
const fromEnv = process.env[name];
|
|
42
|
+
if (fromEnv) return fromEnv;
|
|
43
|
+
}
|
|
44
|
+
const configured = readSettings()[settingsKey];
|
|
45
|
+
if (configured && typeof configured === "string") return configured;
|
|
46
|
+
return stored || fallback;
|
|
47
|
+
}
|
|
48
|
+
function getGatewayUrl(overrideUrl) {
|
|
49
|
+
return resolveAddress(overrideUrl, GATEWAY_ENV_NAMES, "gateway_url", readCredentials()?.gateway_url, GATEWAY_URL_FALLBACK);
|
|
50
|
+
}
|
|
51
|
+
getGatewayUrl();
|
|
52
|
+
function getDashboardUrl(overrideUrl) {
|
|
53
|
+
return resolveAddress(overrideUrl, DASHBOARD_ENV_NAMES, "dashboard_url", readCredentials()?.dashboard_url, DASHBOARD_URL_FALLBACK);
|
|
54
|
+
}
|
|
34
55
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
56
|
+
* The dashboard for the environment this login is going to.
|
|
57
|
+
*
|
|
58
|
+
* The two addresses describe one environment, so the stored rung is read only
|
|
59
|
+
* where the gateway being logged into is the one the credentials already name.
|
|
60
|
+
* Carrying a dashboard across a change of gateway is the wrong workspace, and
|
|
61
|
+
* so is dropping one where the gateway did not change at all.
|
|
41
62
|
*/
|
|
42
|
-
function
|
|
43
|
-
if (overrideUrl) return overrideUrl;
|
|
44
|
-
if (process.env.GATEWAY_URL) return process.env.GATEWAY_URL;
|
|
45
|
-
if (process.env.WAWESOME_GATEWAY_URL) return process.env.WAWESOME_GATEWAY_URL;
|
|
46
|
-
const settings = readSettings();
|
|
47
|
-
if (settings.gateway_url && typeof settings.gateway_url === "string") return settings.gateway_url;
|
|
63
|
+
function resolveDashboardUrlForLogin(overrideUrl, gatewayUrl) {
|
|
48
64
|
const creds = readCredentials();
|
|
49
|
-
|
|
50
|
-
return "
|
|
65
|
+
const sameEnvironment = creds?.gateway_url === gatewayUrl;
|
|
66
|
+
return resolveAddress(overrideUrl, DASHBOARD_ENV_NAMES, "dashboard_url", sameEnvironment ? creds?.dashboard_url : void 0, DASHBOARD_URL_FALLBACK);
|
|
51
67
|
}
|
|
52
|
-
getGatewayUrl();
|
|
53
68
|
/** Localhost port used during OAuth callback */
|
|
54
69
|
const OAUTH_CALLBACK_PORT = 9999;
|
|
55
70
|
/** Path to the user-level credentials file */
|
|
@@ -166,7 +181,7 @@ async function buildJs(entryInput, options) {
|
|
|
166
181
|
* that has to name this version — `--version`, the dependency a scaffolded
|
|
167
182
|
* project pins — reads it here, so a release bumps one file.
|
|
168
183
|
*/
|
|
169
|
-
const CLI_VERSION = "0.0.
|
|
184
|
+
const CLI_VERSION = "0.0.15";
|
|
170
185
|
//#endregion
|
|
171
186
|
//#region src/prompt.ts
|
|
172
187
|
/**
|
|
@@ -247,14 +262,21 @@ var GatewayError = class extends Error {
|
|
|
247
262
|
* was doing, so callers handle it before reaching this.
|
|
248
263
|
*/
|
|
249
264
|
async function asGatewayError(res, fallback) {
|
|
250
|
-
|
|
265
|
+
return rejectionOf(await res.text().catch(() => ""), res.status, fallback);
|
|
266
|
+
}
|
|
267
|
+
/** The same reading, for a caller that has already taken the body off the wire. */
|
|
268
|
+
function rejectionOf(body, status, fallback) {
|
|
251
269
|
try {
|
|
252
270
|
const parsed = JSON.parse(body);
|
|
253
|
-
return new GatewayError(parsed.error || fallback,
|
|
271
|
+
return new GatewayError(parsed.error || fallback, status, parsed.reason, body);
|
|
254
272
|
} catch {
|
|
255
|
-
return new GatewayError(fallback,
|
|
273
|
+
return new GatewayError(fallback, status, void 0, body);
|
|
256
274
|
}
|
|
257
275
|
}
|
|
276
|
+
/** Why the gateway refused, where what was thrown might not be a refusal at all. */
|
|
277
|
+
function reasonOf(err) {
|
|
278
|
+
return err instanceof GatewayError ? err.reason : void 0;
|
|
279
|
+
}
|
|
258
280
|
/** What went wrong, as text, whatever was thrown. */
|
|
259
281
|
function errorText(err) {
|
|
260
282
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -306,6 +328,10 @@ function renameAdvice(reason) {
|
|
|
306
328
|
text: "Use lowercase letters, numbers and single hyphens.",
|
|
307
329
|
retryable: true
|
|
308
330
|
};
|
|
331
|
+
case "too-short": return {
|
|
332
|
+
text: "Use at least three characters.",
|
|
333
|
+
retryable: true
|
|
334
|
+
};
|
|
309
335
|
case "locked": return {
|
|
310
336
|
text: "A deploy landed while this was running, which fixed the address for good.",
|
|
311
337
|
retryable: false
|
|
@@ -377,6 +403,7 @@ async function promptForWorkspaceName(options) {
|
|
|
377
403
|
*/
|
|
378
404
|
async function login(options) {
|
|
379
405
|
const gatewayUrl = getGatewayUrl(options.gateway || options.api);
|
|
406
|
+
const dashboardUrl = resolveDashboardUrlForLogin(options.dashboard, gatewayUrl);
|
|
380
407
|
const provider = options.provider || "github";
|
|
381
408
|
const isVerbose = Boolean(options.verbose);
|
|
382
409
|
const authUrl = `${SUPABASE_URL}/auth/v1/authorize?provider=${provider}&redirect_to=${encodeURIComponent(`http://localhost:${OAUTH_CALLBACK_PORT}/callback`)}`;
|
|
@@ -476,6 +503,7 @@ async function login(options) {
|
|
|
476
503
|
} catch {}
|
|
477
504
|
writeCredentials({
|
|
478
505
|
gateway_url: gatewayUrl,
|
|
506
|
+
dashboard_url: dashboardUrl,
|
|
479
507
|
tenant_jwt: exchangeData.tenant_jwt,
|
|
480
508
|
tenant_id: primaryTenantId,
|
|
481
509
|
user_email: userEmail,
|
|
@@ -638,8 +666,15 @@ function headroomLines(usage) {
|
|
|
638
666
|
const line = ` ${allowanceLabel(key).padEnd(labelWidth)} ${amount(key, allowance.used, allowance.limit).padEnd(amountWidth)}${percent}`;
|
|
639
667
|
lines.push(line.trimEnd());
|
|
640
668
|
}
|
|
669
|
+
const refused = refusedAtShare(usage);
|
|
670
|
+
if (refused !== null) lines.push(` Refused: ${formatCount(refused)} request${refused === 1 ? "" : "s"} at your share`);
|
|
641
671
|
return lines;
|
|
642
672
|
}
|
|
673
|
+
function refusedAtShare(usage) {
|
|
674
|
+
const refused = usage.refusals?.at_granted_share;
|
|
675
|
+
if (typeof refused !== "number" || !Number.isFinite(refused) || refused <= 0) return null;
|
|
676
|
+
return refused;
|
|
677
|
+
}
|
|
643
678
|
function allowanceLabel(key) {
|
|
644
679
|
if (KNOWN_LABELS[key]) return KNOWN_LABELS[key];
|
|
645
680
|
const words = key.replace(/[_-]+/g, " ").trim();
|
|
@@ -765,12 +800,8 @@ async function deploy(entryInput, options) {
|
|
|
765
800
|
const errorBody = await uploadRes.text();
|
|
766
801
|
if (uploadRes.status === 401) console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
|
|
767
802
|
else if (uploadRes.status === 409) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const parsed = JSON.parse(errorBody);
|
|
771
|
-
if (parsed.error) msg = parsed.error;
|
|
772
|
-
} catch {}
|
|
773
|
-
console.error(`\n[wawesome] \x1b[31mError: ${msg}\x1b[0m`);
|
|
803
|
+
const refusal = rejectionOf(errorBody, uploadRes.status, "Version with this code bundle already exists.");
|
|
804
|
+
console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m`);
|
|
774
805
|
console.error("[wawesome] Code versions are immutable and cannot be overwritten.");
|
|
775
806
|
console.error("[wawesome] To switch active version, run: \x1B[36mwawesome version switch\x1B[0m\n");
|
|
776
807
|
} else {
|
|
@@ -839,6 +870,20 @@ async function deploy(entryInput, options) {
|
|
|
839
870
|
};
|
|
840
871
|
}
|
|
841
872
|
//#endregion
|
|
873
|
+
//#region src/billing.ts
|
|
874
|
+
function billingPageUrl() {
|
|
875
|
+
const base = getDashboardUrl().replace(/\/+$/, "");
|
|
876
|
+
try {
|
|
877
|
+
return new URL("billing", `${base}/`).toString();
|
|
878
|
+
} catch {
|
|
879
|
+
return `${base}/billing`;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function appSlotAdvice(reason) {
|
|
883
|
+
if (reason !== "app-slots-exhausted") return "";
|
|
884
|
+
return `Where to resolve it: ${billingPageUrl()}`;
|
|
885
|
+
}
|
|
886
|
+
//#endregion
|
|
842
887
|
//#region src/env.ts
|
|
843
888
|
const STANDARD_SECRET_MESSAGES = [
|
|
844
889
|
"Encrypted at rest using AES-256",
|
|
@@ -1675,6 +1720,7 @@ async function ensureSession(options) {
|
|
|
1675
1720
|
try {
|
|
1676
1721
|
await login({
|
|
1677
1722
|
api: options.api,
|
|
1723
|
+
dashboard: options.dashboard,
|
|
1678
1724
|
verbose: options.verbose
|
|
1679
1725
|
});
|
|
1680
1726
|
} catch (err) {
|
|
@@ -1737,7 +1783,7 @@ async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionNa
|
|
|
1737
1783
|
return;
|
|
1738
1784
|
} catch (err) {
|
|
1739
1785
|
console.log(`[wawesome] ${errorText(err)}`);
|
|
1740
|
-
refusal = err
|
|
1786
|
+
refusal = reasonOf(err);
|
|
1741
1787
|
const { text, retryable } = renameAdvice(refusal);
|
|
1742
1788
|
if (text) console.log(`[wawesome] ${text}`);
|
|
1743
1789
|
if (!retryable) break;
|
|
@@ -1757,7 +1803,7 @@ async function wireUp(creds, appSlug, manifest, answers) {
|
|
|
1757
1803
|
try {
|
|
1758
1804
|
await ensureApp(creds, appSlug);
|
|
1759
1805
|
} catch (err) {
|
|
1760
|
-
fail(errorText(err), scaffolded);
|
|
1806
|
+
fail(errorText(err), ...[appSlotAdvice(reasonOf(err)), scaffolded].filter(Boolean));
|
|
1761
1807
|
}
|
|
1762
1808
|
for (const { declared, value } of answers) {
|
|
1763
1809
|
if (!value) {
|
|
@@ -2747,12 +2793,12 @@ cli.command("env [action] [key] [value]", "Manage environment variables (set, li
|
|
|
2747
2793
|
cli.command("env set <key> <value>", "Set or overwrite an environment variable on the current app").option("-s, --secret", "Flag variable as secret (write-only)").option("-v, --verbose", "Enable verbose debug output").action((key, value, options) => setEnvVar(key, value, options));
|
|
2748
2794
|
cli.command("env list", "List environment variables for the current app").alias("env ls").option("-v, --verbose", "Enable verbose debug output").action((options) => listEnvVars(options));
|
|
2749
2795
|
cli.command("env rm <key>", "Delete an environment variable from the current app").alias("env remove").alias("env delete").alias("env unset").option("-v, --verbose", "Enable verbose debug output").action((key, options) => removeEnvVar(key, options));
|
|
2750
|
-
cli.command("login", "Authenticate with the wawesome.io platform").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("--gateway <url>", "Alias for --api <url>").option("--provider <name>", "OAuth provider (default: github)").option("--workspace <name>", "Name for the workspace, when signing up without a terminal to prompt").option("-v, --verbose", "Enable verbose debug output").action((options) => login(options));
|
|
2796
|
+
cli.command("login", "Authenticate with the wawesome.io platform").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("--gateway <url>", "Alias for --api <url>").option("--dashboard <url>", "Dashboard URL (default: https://dashboard.wawesome.io)").option("--provider <name>", "OAuth provider (default: github)").option("--workspace <name>", "Name for the workspace, when signing up without a terminal to prompt").option("-v, --verbose", "Enable verbose debug output").action((options) => login(options));
|
|
2751
2797
|
cli.command("logout", "Clear stored authentication credentials").action(() => logout());
|
|
2752
2798
|
cli.command("whoami", "Show current login session info").action(() => whoami());
|
|
2753
2799
|
cli.command("workspace [action] [name]", "Show the workspace, or rename its public address").usage("workspace <action> [name]\n\nActions:\n show Show the workspace name, address, and whether it can still change\n rename <name> Change the public address, while nothing live depends on it").example("wawesome workspace").example("wawesome workspace rename northwind").option("-v, --verbose", "Enable verbose debug output").action((action, name, options) => workspaceCommand(action, name, options));
|
|
2754
2800
|
cli.command("templates [action]", "Browse the template catalog").usage("templates [action]\n\nActions:\n list (ls) Show every available template (default)").example("wawesome templates").example("wawesome templates list").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("-v, --verbose", "Enable verbose debug output").action((action, options) => templatesCommand(action, options));
|
|
2755
|
-
cli.command("init", "Scaffold a new function project in the current directory").usage("init [options]\n\nWith --template, the project is fetched from the template catalog, wired up\nfrom what the template declares it needs, and deployed. Run 'wawesome templates'\nto see what is available.").example("wawesome init").example("wawesome init --template stripe-webhook").option("-t, --template <name>", "Scaffold from a catalog template and deploy it").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("--no-install", "Skip installing dependencies after scaffolding").option("--root", "Generate a root function router template").option("-v, --verbose", "Enable verbose debug output").action((options) => init(options));
|
|
2801
|
+
cli.command("init", "Scaffold a new function project in the current directory").usage("init [options]\n\nWith --template, the project is fetched from the template catalog, wired up\nfrom what the template declares it needs, and deployed. Run 'wawesome templates'\nto see what is available.").example("wawesome init").example("wawesome init --template stripe-webhook").option("-t, --template <name>", "Scaffold from a catalog template and deploy it").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("--dashboard <url>", "Dashboard URL (default: https://dashboard.wawesome.io)").option("--no-install", "Skip installing dependencies after scaffolding").option("--root", "Generate a root function router template").option("-v, --verbose", "Enable verbose debug output").action((options) => init(options));
|
|
2756
2802
|
cli.command("logs [function-name-or-invocation-id]", "View invocation history, fetch log output, or follow live").usage(`logs [target] [options]
|
|
2757
2803
|
|
|
2758
2804
|
The target argument determines what the command does:
|
|
@@ -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.0.15",
|
|
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"
|