wawesome 0.18.1 → 0.19.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/dist/{guest-parity-Df4rlLDW.mjs → guest-parity-B5GLO6LA.mjs} +1 -1
- package/dist/guest-parity.mjs +1 -1
- package/dist/{guest-surface-CXON0L5V.mjs → guest-surface-4bHyTMIy.mjs} +11 -0
- package/dist/index.mjs +228 -156
- package/dist/vite.mjs +1 -1
- package/dist/vitest-setup.mjs +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as unsupportedGlobals, i as shimmedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-
|
|
1
|
+
import { a as unsupportedGlobals, i as shimmedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-4bHyTMIy.mjs";
|
|
2
2
|
//#region ../../server/guest-surface/message-channel.js
|
|
3
3
|
const ENTANGLED = Symbol("entangled");
|
|
4
4
|
const QUEUE = Symbol("queue");
|
package/dist/guest-parity.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as applyGuestParity } from "./guest-parity-
|
|
1
|
+
import { t as applyGuestParity } from "./guest-parity-B5GLO6LA.mjs";
|
|
2
2
|
export { applyGuestParity };
|
|
@@ -27,6 +27,17 @@ var globals = [
|
|
|
27
27
|
"intl-locales-supported",
|
|
28
28
|
"intl-segmenter-polyfill"
|
|
29
29
|
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "URLPattern",
|
|
33
|
+
"status": "unsupported",
|
|
34
|
+
"remedy": "bundle a URLPattern polyfill, for example urlpattern-polyfill",
|
|
35
|
+
"providedBy": ["urlpattern-polyfill"]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "WebSocket",
|
|
39
|
+
"status": "unsupported",
|
|
40
|
+
"remedy": "the sandbox has no sockets, so nothing can supply this one; open the connection from the browser instead, or reach a realtime service over HTTPS"
|
|
30
41
|
}
|
|
31
42
|
];
|
|
32
43
|
var methods = [
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as unsupportedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-
|
|
1
|
+
import { a as unsupportedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-4bHyTMIy.mjs";
|
|
2
2
|
import cac from "cac";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
@@ -905,7 +905,7 @@ async function buildHandler(entryInput, config, options) {
|
|
|
905
905
|
* that has to name this version — `--version`, the dependency a scaffolded
|
|
906
906
|
* project pins — reads it here, so a release bumps one file.
|
|
907
907
|
*/
|
|
908
|
-
const CLI_VERSION = "0.
|
|
908
|
+
const CLI_VERSION = "0.19.0";
|
|
909
909
|
//#endregion
|
|
910
910
|
//#region src/prompt.ts
|
|
911
911
|
/**
|
|
@@ -962,6 +962,144 @@ function isInteractive() {
|
|
|
962
962
|
return Boolean(process.stdin.isTTY);
|
|
963
963
|
}
|
|
964
964
|
//#endregion
|
|
965
|
+
//#region ../shared/public-address.ts
|
|
966
|
+
const INVOCATION_PREFIX = "/x";
|
|
967
|
+
const SLUG_SEPARATOR = "--";
|
|
968
|
+
const SURFACE_ROUTE = "/v1/invocation-surface";
|
|
969
|
+
const SUBTREE_NOTE = "Every path beneath this address reaches the Function.";
|
|
970
|
+
const ROOT_FUNCTION_SLUG = "root";
|
|
971
|
+
/**
|
|
972
|
+
* A gateway that serves no form of the address — and the value a client uses for
|
|
973
|
+
* one that did not answer at all, deliberately the same: both name no address,
|
|
974
|
+
* and there is nothing else either could honestly do.
|
|
975
|
+
*/
|
|
976
|
+
const NO_INVOCATION_SURFACE = {
|
|
977
|
+
contentOrigin: null,
|
|
978
|
+
pathFormOrigin: null
|
|
979
|
+
};
|
|
980
|
+
async function fetchInvocationSurface(gatewayUrl) {
|
|
981
|
+
const origin = trimTrailingSlashes(gatewayUrl);
|
|
982
|
+
const res = await fetch(`${origin}${SURFACE_ROUTE}`);
|
|
983
|
+
if (!res.ok) throw new Error(`Failed to read the gateway's address forms (HTTP ${res.status}).`);
|
|
984
|
+
const body = await res.json();
|
|
985
|
+
return {
|
|
986
|
+
contentOrigin: body.content_origin ?? null,
|
|
987
|
+
pathFormOrigin: body.path_form ? origin : null
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
/** The one address the Function answers at, `null` where it has none. */
|
|
991
|
+
function publicAddress(surface, tenantSlug, appSlug, functionSlug) {
|
|
992
|
+
const base = appBase(surface, tenantSlug, appSlug);
|
|
993
|
+
if (base === null) return null;
|
|
994
|
+
if (functionSlug === "root") return base;
|
|
995
|
+
return `${base}/${encodeURIComponent(functionSlug)}`;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Where one of a Function's static files answers, `null` where none of them do.
|
|
999
|
+
*
|
|
1000
|
+
* A **Static asset** is served on the App's own hostname and on no other
|
|
1001
|
+
* address, so a gateway serving only the path form has no address to give for
|
|
1002
|
+
* one — see **Static route** in `CONTEXT.md`.
|
|
1003
|
+
*/
|
|
1004
|
+
function assetAddress(surface, tenantSlug, appSlug, functionSlug, assetPath) {
|
|
1005
|
+
if (!surface.contentOrigin) return null;
|
|
1006
|
+
const mount = publicAddress(surface, tenantSlug, appSlug, functionSlug);
|
|
1007
|
+
if (mount === null) return null;
|
|
1008
|
+
return `${mount}/${assetPath.split("/").map(encodeURIComponent).join("/")}`;
|
|
1009
|
+
}
|
|
1010
|
+
/** The same address with the app and function still to be chosen. */
|
|
1011
|
+
function publicAddressTemplate(surface, tenantSlug) {
|
|
1012
|
+
if (surface.contentOrigin) {
|
|
1013
|
+
if (!isLegalDnsLabel(tenantSlug)) return null;
|
|
1014
|
+
const hostname = appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}<app>`);
|
|
1015
|
+
return hostname === null ? null : `${hostname}/<function>`;
|
|
1016
|
+
}
|
|
1017
|
+
if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/<app>/<function>`;
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
function appBase(surface, tenantSlug, appSlug) {
|
|
1021
|
+
if (surface.contentOrigin) {
|
|
1022
|
+
if (!isLegalDnsLabel(tenantSlug) || !isLegalDnsLabel(appSlug)) return null;
|
|
1023
|
+
return appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}${appSlug}`);
|
|
1024
|
+
}
|
|
1025
|
+
if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/${encodeURIComponent(appSlug)}`;
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* The App's own hostname, built the way the gateway rebuilds it from its own
|
|
1030
|
+
* configuration: the label one level beneath the content domain. Callers check
|
|
1031
|
+
* the slugs the label is made of, since a slug the gateway would refuse to read
|
|
1032
|
+
* a `Host` for names no App there.
|
|
1033
|
+
*/
|
|
1034
|
+
function appHostname(contentOrigin, label) {
|
|
1035
|
+
let origin;
|
|
1036
|
+
try {
|
|
1037
|
+
origin = new URL(contentOrigin);
|
|
1038
|
+
} catch {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
return `${origin.protocol}//${label}.${origin.host}`;
|
|
1042
|
+
}
|
|
1043
|
+
/** The surface, or none where the gateway did not answer. */
|
|
1044
|
+
async function invocationSurfaceOrNone(gatewayUrl) {
|
|
1045
|
+
try {
|
|
1046
|
+
return await fetchInvocationSurface(gatewayUrl);
|
|
1047
|
+
} catch {
|
|
1048
|
+
return NO_INVOCATION_SURFACE;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
function pathFormBase(origin, tenantSlug) {
|
|
1052
|
+
return `${trimTrailingSlashes(origin)}${INVOCATION_PREFIX}/${encodeURIComponent(tenantSlug)}`;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* The legal DNS label closest to what somebody typed, empty where nothing legal
|
|
1056
|
+
* survives. What a workspace name suggests as an address, and what the CLI
|
|
1057
|
+
* offers as a default — never applied to a typed address, which is refused
|
|
1058
|
+
* rather than rewritten into a name nobody chose.
|
|
1059
|
+
*/
|
|
1060
|
+
function slugifyLabel(raw) {
|
|
1061
|
+
let label = "";
|
|
1062
|
+
for (const c of raw.trim().toLowerCase()) if (c >= "a" && c <= "z" || c >= "0" && c <= "9") label += c;
|
|
1063
|
+
else if (label.length > 0 && !label.endsWith("-")) label += "-";
|
|
1064
|
+
label = label.slice(0, 63);
|
|
1065
|
+
while (label.endsWith("-")) label = label.slice(0, -1);
|
|
1066
|
+
return label;
|
|
1067
|
+
}
|
|
1068
|
+
/** Must match `dns_label::is_legal` in `server/crates/core/src/value_objects.rs`. */
|
|
1069
|
+
function isLegalDnsLabel(candidate) {
|
|
1070
|
+
return candidate.length > 0 && candidate.length <= 63 && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
|
|
1071
|
+
}
|
|
1072
|
+
function trimTrailingSlashes(origin) {
|
|
1073
|
+
return origin.replace(/\/+$/, "");
|
|
1074
|
+
}
|
|
1075
|
+
//#endregion
|
|
1076
|
+
//#region src/slug.ts
|
|
1077
|
+
/**
|
|
1078
|
+
* The character rules every slug on the platform obeys, mirrored from the
|
|
1079
|
+
* `dns_label` module in `server/crates/core/src/value_objects.rs`.
|
|
1080
|
+
*
|
|
1081
|
+
* A typed name is checked while the user is still standing there, rather than
|
|
1082
|
+
* accepted, sent, and rewritten by the gateway into something else that ends up
|
|
1083
|
+
* in a client's dashboard. The rules live beside the address they build, so the
|
|
1084
|
+
* CLI and the dashboard cannot come to disagree about what a legal name is.
|
|
1085
|
+
*/
|
|
1086
|
+
/** Maximum length of a single DNS label (RFC 1035 §2.3.4). */
|
|
1087
|
+
const MAX_SLUG_LENGTH = 63;
|
|
1088
|
+
/**
|
|
1089
|
+
* Normalise arbitrary input into a legal DNS label, returning an empty string
|
|
1090
|
+
* when nothing legal survives — callers decide whether that is an error.
|
|
1091
|
+
*/
|
|
1092
|
+
function sanitizeSlug(raw) {
|
|
1093
|
+
return slugifyLabel(raw);
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Whether `candidate` is already a legal DNS label — i.e. whether
|
|
1097
|
+
* {@link sanitizeSlug} would leave it untouched.
|
|
1098
|
+
*/
|
|
1099
|
+
function isLegalSlug(candidate) {
|
|
1100
|
+
return candidate.length > 0 && candidate.length <= MAX_SLUG_LENGTH && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
|
|
1101
|
+
}
|
|
1102
|
+
//#endregion
|
|
965
1103
|
//#region src/session.ts
|
|
966
1104
|
/**
|
|
967
1105
|
* A gateway request, retried once against a renewed session if it comes back 401.
|
|
@@ -1170,24 +1308,36 @@ async function renameTenantSlug(creds, slug) {
|
|
|
1170
1308
|
//#endregion
|
|
1171
1309
|
//#region src/auth.ts
|
|
1172
1310
|
/**
|
|
1173
|
-
* Ask what to call the workspace being created.
|
|
1311
|
+
* Ask what to call the workspace being created, and what it should answer at.
|
|
1174
1312
|
*
|
|
1175
|
-
* There is no default worth offering: a name nobody typed is exactly what
|
|
1176
|
-
* replaced.
|
|
1177
|
-
*
|
|
1178
|
-
|
|
1179
|
-
|
|
1313
|
+
* There is no default name worth offering: a name nobody typed is exactly what
|
|
1314
|
+
* this replaced. The address has one — the name, slugified — because it is a
|
|
1315
|
+
* DNS label and typing it a second time is work the name already did. A
|
|
1316
|
+
* non-interactive login has nobody to ask, so it says what flag to pass rather
|
|
1317
|
+
* than inventing a name and minting a workspace under it; an address it is not
|
|
1318
|
+
* given is still minted, which is what a pipeline that says nothing gets.
|
|
1319
|
+
*/
|
|
1320
|
+
async function promptForWorkspace(options) {
|
|
1180
1321
|
const supplied = options.workspace?.trim();
|
|
1181
|
-
if (supplied) return
|
|
1322
|
+
if (supplied) return {
|
|
1323
|
+
name: supplied,
|
|
1324
|
+
address: options.address?.trim() || ""
|
|
1325
|
+
};
|
|
1182
1326
|
if (!isInteractive()) throw new Error("No workspace found, and no name to create one with. Re-run with --workspace <name>.");
|
|
1183
1327
|
console.log("\n[wawesome] You don't have a workspace yet. Let's create one.");
|
|
1184
|
-
console.log(" (
|
|
1328
|
+
console.log(" (the name is what you'll see in the dashboard; the address is the first half of every URL you deploy)");
|
|
1185
1329
|
const session = openPromptSession();
|
|
1186
1330
|
try {
|
|
1187
1331
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1188
|
-
const
|
|
1189
|
-
if (
|
|
1190
|
-
|
|
1332
|
+
const name = (await session.ask(" Workspace name?", "")).trim();
|
|
1333
|
+
if (!name) {
|
|
1334
|
+
console.log("[wawesome] A workspace needs a name.");
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
return {
|
|
1338
|
+
name,
|
|
1339
|
+
address: (await session.ask(" Public address?", options.address?.trim() || sanitizeSlug(name))).trim()
|
|
1340
|
+
};
|
|
1191
1341
|
}
|
|
1192
1342
|
} finally {
|
|
1193
1343
|
session.close();
|
|
@@ -1235,9 +1385,9 @@ async function completeLogin({ gatewayUrl, dashboardUrl, accessToken, refreshTok
|
|
|
1235
1385
|
let workspaceName;
|
|
1236
1386
|
let workspaceSlug;
|
|
1237
1387
|
if (tenants.length === 0) {
|
|
1238
|
-
const
|
|
1388
|
+
const chosen = await promptForWorkspace(options);
|
|
1239
1389
|
await confirmTermsAccepted(options);
|
|
1240
|
-
console.log(`[wawesome] Creating workspace "${
|
|
1390
|
+
console.log(`[wawesome] Creating workspace "${chosen.name}"...`);
|
|
1241
1391
|
const initRes = await fetch(`${gatewayUrl}/api/v1/onboarding/init`, {
|
|
1242
1392
|
method: "POST",
|
|
1243
1393
|
headers: {
|
|
@@ -1245,7 +1395,8 @@ async function completeLogin({ gatewayUrl, dashboardUrl, accessToken, refreshTok
|
|
|
1245
1395
|
"Content-Type": "application/json"
|
|
1246
1396
|
},
|
|
1247
1397
|
body: JSON.stringify({
|
|
1248
|
-
tenant_name:
|
|
1398
|
+
tenant_name: chosen.name,
|
|
1399
|
+
tenant_slug: chosen.address || void 0,
|
|
1249
1400
|
accepted_terms: true
|
|
1250
1401
|
})
|
|
1251
1402
|
});
|
|
@@ -1253,7 +1404,7 @@ async function completeLogin({ gatewayUrl, dashboardUrl, accessToken, refreshTok
|
|
|
1253
1404
|
const initData = await initRes.json();
|
|
1254
1405
|
primaryTenantId = initData.tenant_id;
|
|
1255
1406
|
workspaceSlug = initData.tenant_slug;
|
|
1256
|
-
workspaceName = initData.tenant_name ||
|
|
1407
|
+
workspaceName = initData.tenant_name || chosen.name;
|
|
1257
1408
|
} else primaryTenantId = tenants[0].tenant_id;
|
|
1258
1409
|
if (isVerbose) console.log(`[wawesome:verbose] Using tenant: ${primaryTenantId}`);
|
|
1259
1410
|
console.log("[wawesome] Exchanging for tenant-scoped credentials...");
|
|
@@ -1544,6 +1695,17 @@ function wrapped(said) {
|
|
|
1544
1695
|
function indented(said) {
|
|
1545
1696
|
return wrapped(said).map((line) => `${VALUE_COLUMN}${line}`);
|
|
1546
1697
|
}
|
|
1698
|
+
/**
|
|
1699
|
+
* What fits in `room` characters, with an ellipsis where it does not. Cut
|
|
1700
|
+
* rather than wrapped: a row that took two lines would push the row above it
|
|
1701
|
+
* out of sight. A room too small for even that shows nothing, so nothing ever
|
|
1702
|
+
* runs past the width it was given.
|
|
1703
|
+
*/
|
|
1704
|
+
function truncate(said, room) {
|
|
1705
|
+
if (said.length <= room) return said;
|
|
1706
|
+
if (room <= 1) return "";
|
|
1707
|
+
return `${said.slice(0, room - 1)}…`;
|
|
1708
|
+
}
|
|
1547
1709
|
//#endregion
|
|
1548
1710
|
//#region src/domains.ts
|
|
1549
1711
|
/**
|
|
@@ -2281,103 +2443,6 @@ function manifestOf(assets) {
|
|
|
2281
2443
|
}));
|
|
2282
2444
|
}
|
|
2283
2445
|
//#endregion
|
|
2284
|
-
//#region ../shared/public-address.ts
|
|
2285
|
-
const INVOCATION_PREFIX = "/x";
|
|
2286
|
-
const SLUG_SEPARATOR = "--";
|
|
2287
|
-
const SURFACE_ROUTE = "/v1/invocation-surface";
|
|
2288
|
-
const SUBTREE_NOTE = "Every path beneath this address reaches the Function.";
|
|
2289
|
-
const ROOT_FUNCTION_SLUG = "root";
|
|
2290
|
-
/**
|
|
2291
|
-
* A gateway that serves no form of the address — and the value a client uses for
|
|
2292
|
-
* one that did not answer at all, deliberately the same: both name no address,
|
|
2293
|
-
* and there is nothing else either could honestly do.
|
|
2294
|
-
*/
|
|
2295
|
-
const NO_INVOCATION_SURFACE = {
|
|
2296
|
-
contentOrigin: null,
|
|
2297
|
-
pathFormOrigin: null
|
|
2298
|
-
};
|
|
2299
|
-
async function fetchInvocationSurface(gatewayUrl) {
|
|
2300
|
-
const origin = trimTrailingSlashes(gatewayUrl);
|
|
2301
|
-
const res = await fetch(`${origin}${SURFACE_ROUTE}`);
|
|
2302
|
-
if (!res.ok) throw new Error(`Failed to read the gateway's address forms (HTTP ${res.status}).`);
|
|
2303
|
-
const body = await res.json();
|
|
2304
|
-
return {
|
|
2305
|
-
contentOrigin: body.content_origin ?? null,
|
|
2306
|
-
pathFormOrigin: body.path_form ? origin : null
|
|
2307
|
-
};
|
|
2308
|
-
}
|
|
2309
|
-
/** The one address the Function answers at, `null` where it has none. */
|
|
2310
|
-
function publicAddress(surface, tenantSlug, appSlug, functionSlug) {
|
|
2311
|
-
const base = appBase(surface, tenantSlug, appSlug);
|
|
2312
|
-
if (base === null) return null;
|
|
2313
|
-
if (functionSlug === "root") return base;
|
|
2314
|
-
return `${base}/${encodeURIComponent(functionSlug)}`;
|
|
2315
|
-
}
|
|
2316
|
-
/**
|
|
2317
|
-
* Where one of a Function's static files answers, `null` where none of them do.
|
|
2318
|
-
*
|
|
2319
|
-
* A **Static asset** is served on the App's own hostname and on no other
|
|
2320
|
-
* address, so a gateway serving only the path form has no address to give for
|
|
2321
|
-
* one — see **Static route** in `CONTEXT.md`.
|
|
2322
|
-
*/
|
|
2323
|
-
function assetAddress(surface, tenantSlug, appSlug, functionSlug, assetPath) {
|
|
2324
|
-
if (!surface.contentOrigin) return null;
|
|
2325
|
-
const mount = publicAddress(surface, tenantSlug, appSlug, functionSlug);
|
|
2326
|
-
if (mount === null) return null;
|
|
2327
|
-
return `${mount}/${assetPath.split("/").map(encodeURIComponent).join("/")}`;
|
|
2328
|
-
}
|
|
2329
|
-
/** The same address with the app and function still to be chosen. */
|
|
2330
|
-
function publicAddressTemplate(surface, tenantSlug) {
|
|
2331
|
-
if (surface.contentOrigin) {
|
|
2332
|
-
if (!isLegalDnsLabel(tenantSlug)) return null;
|
|
2333
|
-
const hostname = appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}<app>`);
|
|
2334
|
-
return hostname === null ? null : `${hostname}/<function>`;
|
|
2335
|
-
}
|
|
2336
|
-
if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/<app>/<function>`;
|
|
2337
|
-
return null;
|
|
2338
|
-
}
|
|
2339
|
-
function appBase(surface, tenantSlug, appSlug) {
|
|
2340
|
-
if (surface.contentOrigin) {
|
|
2341
|
-
if (!isLegalDnsLabel(tenantSlug) || !isLegalDnsLabel(appSlug)) return null;
|
|
2342
|
-
return appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}${appSlug}`);
|
|
2343
|
-
}
|
|
2344
|
-
if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/${encodeURIComponent(appSlug)}`;
|
|
2345
|
-
return null;
|
|
2346
|
-
}
|
|
2347
|
-
/**
|
|
2348
|
-
* The App's own hostname, built the way the gateway rebuilds it from its own
|
|
2349
|
-
* configuration: the label one level beneath the content domain. Callers check
|
|
2350
|
-
* the slugs the label is made of, since a slug the gateway would refuse to read
|
|
2351
|
-
* a `Host` for names no App there.
|
|
2352
|
-
*/
|
|
2353
|
-
function appHostname(contentOrigin, label) {
|
|
2354
|
-
let origin;
|
|
2355
|
-
try {
|
|
2356
|
-
origin = new URL(contentOrigin);
|
|
2357
|
-
} catch {
|
|
2358
|
-
return null;
|
|
2359
|
-
}
|
|
2360
|
-
return `${origin.protocol}//${label}.${origin.host}`;
|
|
2361
|
-
}
|
|
2362
|
-
/** The surface, or none where the gateway did not answer. */
|
|
2363
|
-
async function invocationSurfaceOrNone(gatewayUrl) {
|
|
2364
|
-
try {
|
|
2365
|
-
return await fetchInvocationSurface(gatewayUrl);
|
|
2366
|
-
} catch {
|
|
2367
|
-
return NO_INVOCATION_SURFACE;
|
|
2368
|
-
}
|
|
2369
|
-
}
|
|
2370
|
-
function pathFormBase(origin, tenantSlug) {
|
|
2371
|
-
return `${trimTrailingSlashes(origin)}${INVOCATION_PREFIX}/${encodeURIComponent(tenantSlug)}`;
|
|
2372
|
-
}
|
|
2373
|
-
/** Must match `dns_label::is_legal` in `server/crates/core/src/value_objects.rs`. */
|
|
2374
|
-
function isLegalDnsLabel(candidate) {
|
|
2375
|
-
return candidate.length > 0 && candidate.length <= 63 && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
|
|
2376
|
-
}
|
|
2377
|
-
function trimTrailingSlashes(origin) {
|
|
2378
|
-
return origin.replace(/\/+$/, "");
|
|
2379
|
-
}
|
|
2380
|
-
//#endregion
|
|
2381
2446
|
//#region src/deploy-target.ts
|
|
2382
2447
|
const RECORD_PATH = path.join(".wawesome", "last-deploy.json");
|
|
2383
2448
|
function readLastDeployTarget(projectDir = process.cwd()) {
|
|
@@ -2910,7 +2975,8 @@ async function cronCommand(action, target, subtarget, options = {}) {
|
|
|
2910
2975
|
* The address a **Document** answers at, relative to its Function's mount: a
|
|
2911
2976
|
* request path whose last segment carries no extension resolves to that path,
|
|
2912
2977
|
* to it with `.html` appended and to it with `/index.html` appended, so both
|
|
2913
|
-
* `about.html` and `about/index.html` are the address `about`.
|
|
2978
|
+
* `about.html` and `about/index.html` are the address `about`. A last segment
|
|
2979
|
+
* spelled `index` skips the middle one, which is what this strips first.
|
|
2914
2980
|
*/
|
|
2915
2981
|
function documentAddressPath(assetPath) {
|
|
2916
2982
|
return assetPath.replace(/(^|\/)index\.html$/, "").replace(/\.html$/, "");
|
|
@@ -3037,6 +3103,17 @@ function declaredFields(declared) {
|
|
|
3037
3103
|
};
|
|
3038
3104
|
}
|
|
3039
3105
|
/**
|
|
3106
|
+
* What this deploy says it carries.
|
|
3107
|
+
*
|
|
3108
|
+
* Trimmed here so that a flag whose value came out of a shell variable nobody
|
|
3109
|
+
* set is a deploy that said nothing rather than one the gateway turns down. How
|
|
3110
|
+
* long a message may be and whether it is one line are the platform's rule, and
|
|
3111
|
+
* holding them here as well is two places for them to drift.
|
|
3112
|
+
*/
|
|
3113
|
+
function declaredMessage(stated) {
|
|
3114
|
+
return typeof stated === "string" ? stated.trim() || void 0 : void 0;
|
|
3115
|
+
}
|
|
3116
|
+
/**
|
|
3040
3117
|
* The fallback this project declares.
|
|
3041
3118
|
*
|
|
3042
3119
|
* A declaration missing either half is refused here rather than at the gateway,
|
|
@@ -3107,7 +3184,8 @@ async function deploy(entryInput, options) {
|
|
|
3107
3184
|
visibility: declaredVisibility(config.visibility),
|
|
3108
3185
|
confirmPublish: Boolean(options.publish),
|
|
3109
3186
|
confirmAssetDrop: Boolean(options.dropAssets),
|
|
3110
|
-
fallback: declaredFallback(config.fallback)
|
|
3187
|
+
fallback: declaredFallback(config.fallback),
|
|
3188
|
+
message: declaredMessage(options.message)
|
|
3111
3189
|
};
|
|
3112
3190
|
const domainName = declaredDomain(config.domain);
|
|
3113
3191
|
if (isVerbose) {
|
|
@@ -3166,7 +3244,10 @@ async function deploy(entryInput, options) {
|
|
|
3166
3244
|
console.log(`[wawesome] Uploading code for ${app}/${funcName}...`);
|
|
3167
3245
|
const uploadUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/code`;
|
|
3168
3246
|
if (isVerbose) console.log(`[wawesome:verbose] POST ${uploadUrl}`);
|
|
3169
|
-
const declaredOnTheWire =
|
|
3247
|
+
const declaredOnTheWire = {
|
|
3248
|
+
...declaredFields(declared),
|
|
3249
|
+
...declared.message ? { message: declared.message } : {}
|
|
3250
|
+
};
|
|
3170
3251
|
const uploadRes = assets.length > 0 || declaresSchedules || declaresFallback || Object.keys(declaredOnTheWire).length > 0 || jsCode === null ? await authorizedFetch(uploadUrl, {
|
|
3171
3252
|
method: "POST",
|
|
3172
3253
|
headers: {
|
|
@@ -3190,7 +3271,7 @@ async function deploy(entryInput, options) {
|
|
|
3190
3271
|
});
|
|
3191
3272
|
if (!uploadRes.ok) {
|
|
3192
3273
|
const errorBody = await uploadRes.text();
|
|
3193
|
-
const refusal = rejectionOf(errorBody, uploadRes.status, "Version with this code bundle already exists.");
|
|
3274
|
+
const refusal = rejectionOf(errorBody, uploadRes.status, uploadRes.status === 409 ? "Version with this code bundle already exists." : `Code upload failed (HTTP ${uploadRes.status}).`);
|
|
3194
3275
|
if (uploadRes.status === 401 || uploadRes.status === 403) console.error(`[wawesome] Error: ${unauthorizedMessage(uploadRes.status, errorBody)}`);
|
|
3195
3276
|
else if (refusal.reason === "paid-plan-required") {
|
|
3196
3277
|
console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m`);
|
|
@@ -3212,7 +3293,7 @@ async function deploy(entryInput, options) {
|
|
|
3212
3293
|
console.error("[wawesome] To switch active version, run: \x1B[36mwawesome version switch\x1B[0m\n");
|
|
3213
3294
|
}
|
|
3214
3295
|
} else {
|
|
3215
|
-
console.error(
|
|
3296
|
+
console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m\n`);
|
|
3216
3297
|
if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
|
|
3217
3298
|
}
|
|
3218
3299
|
process.exit(1);
|
|
@@ -3483,11 +3564,6 @@ async function setEnvVar(key, value, options) {
|
|
|
3483
3564
|
console.log("");
|
|
3484
3565
|
} else console.log(`[wawesome] ✅ Set variable '${key}' on app '${app}'.`);
|
|
3485
3566
|
}
|
|
3486
|
-
function truncate(str, maxLen) {
|
|
3487
|
-
if (str.length <= maxLen) return str;
|
|
3488
|
-
if (maxLen <= 3) return str.slice(0, maxLen);
|
|
3489
|
-
return str.slice(0, maxLen - 1) + "…";
|
|
3490
|
-
}
|
|
3491
3567
|
/**
|
|
3492
3568
|
* List environment variables on the current app.
|
|
3493
3569
|
*/
|
|
@@ -3808,26 +3884,6 @@ function renderPostDeploy(postDeploy, address) {
|
|
|
3808
3884
|
if (!address) return postDeploy;
|
|
3809
3885
|
return postDeploy.split("{{url}}").join(address);
|
|
3810
3886
|
}
|
|
3811
|
-
/**
|
|
3812
|
-
* Normalise arbitrary input into a legal DNS label, returning an empty string
|
|
3813
|
-
* when nothing legal survives — callers decide whether that is an error.
|
|
3814
|
-
*/
|
|
3815
|
-
function sanitizeSlug(raw) {
|
|
3816
|
-
let label = "";
|
|
3817
|
-
for (const c of raw.trim().toLowerCase()) if (c >= "a" && c <= "z") label += c;
|
|
3818
|
-
else if (c >= "0" && c <= "9") label += c;
|
|
3819
|
-
else if (label.length > 0 && !label.endsWith("-")) label += "-";
|
|
3820
|
-
label = label.slice(0, 63);
|
|
3821
|
-
while (label.endsWith("-")) label = label.slice(0, -1);
|
|
3822
|
-
return label;
|
|
3823
|
-
}
|
|
3824
|
-
/**
|
|
3825
|
-
* Whether `candidate` is already a legal DNS label — i.e. whether
|
|
3826
|
-
* {@link sanitizeSlug} would leave it untouched.
|
|
3827
|
-
*/
|
|
3828
|
-
function isLegalSlug(candidate) {
|
|
3829
|
-
return candidate.length > 0 && candidate.length <= 63 && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
|
|
3830
|
-
}
|
|
3831
3887
|
//#endregion
|
|
3832
3888
|
//#region src/tarball.ts
|
|
3833
3889
|
/**
|
|
@@ -4577,6 +4633,24 @@ export default {
|
|
|
4577
4633
|
//#region src/version.ts
|
|
4578
4634
|
/** How both surfaces name a version nobody can switch to. */
|
|
4579
4635
|
const RECLAIMED = "⚠ [assets reclaimed]";
|
|
4636
|
+
/** Where the message sits, under the row rather than in a column of its own. */
|
|
4637
|
+
const MESSAGE_INDENT = " ".repeat(10);
|
|
4638
|
+
const ASSUMED_WIDTH = 80;
|
|
4639
|
+
function terminalWidth() {
|
|
4640
|
+
const columns = process.stdout.columns;
|
|
4641
|
+
return columns && columns > 40 ? columns : ASSUMED_WIDTH;
|
|
4642
|
+
}
|
|
4643
|
+
/**
|
|
4644
|
+
* The lines one version takes in the listing: its row, and the words its deploy
|
|
4645
|
+
* stated beneath it where it stated any.
|
|
4646
|
+
*/
|
|
4647
|
+
function versionLines(v, activeNumber, width) {
|
|
4648
|
+
const isActive = v.version_number === activeNumber;
|
|
4649
|
+
const row = `${`#${v.version_number}`.padEnd(7)} | ${v.cwasm_hash.slice(0, 8).padEnd(8)} | ${new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19).padEnd(20)} | ${v.status.padEnd(9)} | ${isActive ? "\x1B[32m★ [active]\x1B[0m" : v.rollbackable === false ? `\x1b[33m${RECLAIMED}\x1b[0m` : ""}`;
|
|
4650
|
+
const message = v.message?.trim();
|
|
4651
|
+
if (!message) return [row];
|
|
4652
|
+
return [row, `${MESSAGE_INDENT}\x1b[2m${truncate(message, width - MESSAGE_INDENT.length)}\x1b[0m`];
|
|
4653
|
+
}
|
|
4580
4654
|
async function fetchVersions(gatewayUrl, tenantJwt, app, funcName, env, isVerbose) {
|
|
4581
4655
|
const url = `${gatewayUrl}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/versions?environment=${encodeURIComponent(env)}`;
|
|
4582
4656
|
if (isVerbose) console.log(`[wawesome:verbose] GET ${url}`);
|
|
@@ -4620,15 +4694,8 @@ async function listVersions(options) {
|
|
|
4620
4694
|
console.log(`\n📦 \x1b[1mVersions for '${app}/${funcName}' (environment: ${env})\x1b[0m\n`);
|
|
4621
4695
|
console.log("VERSION | HASH | CREATED AT | STATUS | ACTIVE");
|
|
4622
4696
|
console.log("--------|----------|----------------------|-----------|-------");
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
const verStr = `#${v.version_number}`.padEnd(7);
|
|
4626
|
-
const hashStr = v.cwasm_hash.slice(0, 8).padEnd(8);
|
|
4627
|
-
const dateStr = new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19).padEnd(20);
|
|
4628
|
-
const statusStr = v.status.padEnd(9);
|
|
4629
|
-
const badge = isActive ? "\x1B[32m★ [active]\x1B[0m" : v.rollbackable === false ? `\x1b[33m${RECLAIMED}\x1b[0m` : "";
|
|
4630
|
-
console.log(`${verStr} | ${hashStr} | ${dateStr} | ${statusStr} | ${badge}`);
|
|
4631
|
-
}
|
|
4697
|
+
const width = terminalWidth();
|
|
4698
|
+
for (const v of data.versions) for (const line of versionLines(v, data.active_version_number, width)) console.log(line);
|
|
4632
4699
|
console.log("");
|
|
4633
4700
|
}
|
|
4634
4701
|
/**
|
|
@@ -4676,8 +4743,11 @@ async function switchVersion(targetInput, options) {
|
|
|
4676
4743
|
const hashShort = v.cwasm_hash.slice(0, 8);
|
|
4677
4744
|
const dateStr = new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19);
|
|
4678
4745
|
const badge = isActive ? " ★ [active]" : v.rollbackable === false ? ` ${RECLAIMED}` : "";
|
|
4746
|
+
const said = v.message?.trim();
|
|
4747
|
+
const row = `v${v.version_number} (${hashShort}) • ${dateStr}${badge}`;
|
|
4748
|
+
const room = terminalWidth() - row.length - 3 - 2;
|
|
4679
4749
|
return {
|
|
4680
|
-
name:
|
|
4750
|
+
name: said ? `${row} • ${truncate(said, room)}` : row,
|
|
4681
4751
|
value: v.version_number
|
|
4682
4752
|
};
|
|
4683
4753
|
});
|
|
@@ -4717,6 +4787,8 @@ async function switchVersion(targetInput, options) {
|
|
|
4717
4787
|
console.log(` Function: ${funcName}`);
|
|
4718
4788
|
console.log(` Environment: ${env}`);
|
|
4719
4789
|
console.log(` Active Ver: v${selectedVersionNum}`);
|
|
4790
|
+
const restored = data.versions.find((v) => v.version_number === selectedVersionNum)?.message?.trim();
|
|
4791
|
+
if (restored) console.log(` Message: ${truncate(restored, terminalWidth() - 15)}`);
|
|
4720
4792
|
console.log(`======================================================\n`);
|
|
4721
4793
|
}
|
|
4722
4794
|
//#endregion
|
|
@@ -6040,7 +6112,7 @@ async function credentialsCommand(action, target, options) {
|
|
|
6040
6112
|
//#region src/index.ts
|
|
6041
6113
|
const cli = cac("wawesome");
|
|
6042
6114
|
cli.command("build [entry]", "Bundle a serverless function to an optimized JS file").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").action((entry, options) => buildJs(entry, options));
|
|
6043
|
-
cli.command("deploy [entry]", "Build, upload, and promote a serverless function").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").option("--skip-build", "Skip the build step, deploy an already-built bundle").option("--publish", "Confirm putting a private function back on its public address").option("--drop-assets", "Confirm a deploy carrying no static files where the live version serves some").action((entry, options) => deploy(entry, options));
|
|
6115
|
+
cli.command("deploy [entry]", "Build, upload, and promote a serverless function").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").option("--skip-build", "Skip the build step, deploy an already-built bundle").option("--publish", "Confirm putting a private function back on its public address").option("--drop-assets", "Confirm a deploy carrying no static files where the live version serves some").option("-m, --message <text>", "One line about what this deploy carries, kept on the version it makes").action((entry, options) => deploy(entry, options));
|
|
6044
6116
|
cli.command("version [action] [target]", "Manage versions (e.g. 'version list', 'version switch [version]')").option("-e, --env <environment>", "Target environment (default: production)").option("-v, --verbose", "Enable verbose debug output").action((action, target, options) => {
|
|
6045
6117
|
if (!action || action === "list" || action === "ls") return listVersions(options);
|
|
6046
6118
|
if (action === "switch" || action === "use" || action === "select") return switchVersion(target, options);
|
|
@@ -6055,7 +6127,7 @@ cli.command("env rm <key>", "Delete an environment variable from the current app
|
|
|
6055
6127
|
cli.command("domains [action] [hostname]", "Show the custom domain on the current app, its state and its DNS records").usage("domains [action] [hostname]\n\nWith no action, prints the app's domain, every half of it, where each got to, when the platform last checked, and the records to add.\n\nActions:\n adopt Write the attached domain into wawesome-function.json\n detach <hostname> Stop serving the app at a domain you attached\n\nA domain is attached by declaring it in wawesome-function.json and deploying, from the dashboard, or by an agent.").example("wawesome domains").example("wawesome domains adopt").example("wawesome domains detach shop.client.com").option("-a, --app <slug>", "App to act on (default: the app in wawesome-function.json)").action((action, hostname, options) => domainsCommand(action, hostname, options));
|
|
6056
6128
|
cli.command("domains adopt", "Write the domain the current app answers at into wawesome-function.json").option("-a, --app <slug>", "App to act on (default: the app in wawesome-function.json)").action((options) => adoptDomainCommand(void 0, options));
|
|
6057
6129
|
cli.command("domains detach <hostname>", "Stop serving the current app at a domain you attached").option("-a, --app <slug>", "App to act on (default: the app in wawesome-function.json)").action((hostname, options) => detachDomainCommand(hostname, options));
|
|
6058
|
-
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("--accept-terms", "Accept the terms of service and privacy policy, when signing up without a terminal to prompt").option("-v, --verbose", "Enable verbose debug output").action((options) => login(options));
|
|
6130
|
+
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("--address <name>", "Public address for the workspace being created (default: one is minted)").option("--accept-terms", "Accept the terms of service and privacy policy, when signing up without a terminal to prompt").option("-v, --verbose", "Enable verbose debug output").action((options) => login(options));
|
|
6059
6131
|
cli.command("logout", "Clear stored authentication credentials").action(() => logout());
|
|
6060
6132
|
cli.command("whoami", "Show current login session info").action(() => whoami());
|
|
6061
6133
|
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));
|
package/dist/vite.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as unsupportedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-
|
|
1
|
+
import { a as unsupportedGlobals, n as methodRemedy, r as polyfilledGlobals, t as declaredMethods } from "./guest-surface-4bHyTMIy.mjs";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
//#region src/vite.ts
|
package/dist/vitest-setup.mjs
CHANGED