wawesome 0.17.0 → 0.18.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 +30 -0
- package/dist/index.mjs +102 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -648,6 +648,36 @@ Function, so a page your root Function carries at `about/index.html` is shadowed
|
|
|
648
648
|
named `about`. The deploy says so and names both addresses. It refuses nothing — the two Functions are
|
|
649
649
|
versioned independently, and either one may deploy first.
|
|
650
650
|
|
|
651
|
+
### A page for an address nothing claims
|
|
652
|
+
|
|
653
|
+
A site with no handler answers an address none of its pages claim with the platform's not-found. Add
|
|
654
|
+
`"fallback"` and it answers with a page of your own instead:
|
|
655
|
+
|
|
656
|
+
```json
|
|
657
|
+
{
|
|
658
|
+
"app": "my-app",
|
|
659
|
+
"function": "root",
|
|
660
|
+
"assets": "dist",
|
|
661
|
+
"fallback": { "path": "404.html", "status": 404 }
|
|
662
|
+
}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
Two values rather than a mode. `404.html` at `404` is your own not-found page; `index.html` at `200`
|
|
666
|
+
is a single-page application's shell, which is what puts a client-routed path in front of your router
|
|
667
|
+
instead of a 404. Anything between those two shapes is yours to choose — the platform serves the pair
|
|
668
|
+
it was given.
|
|
669
|
+
|
|
670
|
+
Both halves have to be there, or the deploy stops before it uploads anything. The path names a file
|
|
671
|
+
this deploy carries, relative to your assets directory; the status has to be one a document can be
|
|
672
|
+
sent with, so `204` and `304` are refused. A fallback naming a file the deploy does not carry is
|
|
673
|
+
refused too, before anything is compiled or stored.
|
|
674
|
+
|
|
675
|
+
It is part of what identifies the deploy, so changing which page answers a miss, or the number it
|
|
676
|
+
answers with, is a new version like any other change. Where the Function carries a handler, the
|
|
677
|
+
handler answers that address and the fallback is never reached — and a miss beneath `assets/` stays a
|
|
678
|
+
404 either way, since a page served where a browser asked for a build chunk is markup answering a
|
|
679
|
+
request for script.
|
|
680
|
+
|
|
651
681
|
### Static files
|
|
652
682
|
|
|
653
683
|
Add `"assets"` to deploy static files beside your code:
|
package/dist/index.mjs
CHANGED
|
@@ -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.18.0";
|
|
909
909
|
//#endregion
|
|
910
910
|
//#region src/prompt.ts
|
|
911
911
|
/**
|
|
@@ -2252,19 +2252,23 @@ function sha256(bytes) {
|
|
|
2252
2252
|
}
|
|
2253
2253
|
/**
|
|
2254
2254
|
* What this deploy *is*: the server bundle together with the sorted set of
|
|
2255
|
-
* asset path and content-hash pairs.
|
|
2255
|
+
* asset path and content-hash pairs, and the **Fallback document** it declares.
|
|
2256
2256
|
*
|
|
2257
2257
|
* Computed here, before anything is uploaded, so a deploy that changed nothing
|
|
2258
2258
|
* is refused before a byte moves. Mirrors `deploy_digest` in
|
|
2259
2259
|
* `server/crates/core/src/features/assets/identity.rs` — the gateway recomputes
|
|
2260
2260
|
* it from what actually arrives, so the two have to agree exactly.
|
|
2261
|
+
*
|
|
2262
|
+
* A deploy declaring no fallback contributes nothing here rather than a stated
|
|
2263
|
+
* absence, so every digest computed before fallbacks existed is unmoved.
|
|
2261
2264
|
*/
|
|
2262
2265
|
const DEPLOY_DIGEST_DOMAIN = "wawesome-deploy-v1";
|
|
2263
|
-
function deployDigest(bundle, assets) {
|
|
2266
|
+
function deployDigest(bundle, assets, fallback) {
|
|
2264
2267
|
const digest = crypto.createHash("sha256");
|
|
2265
2268
|
digest.update(`${DEPLOY_DIGEST_DOMAIN}\n`);
|
|
2266
2269
|
digest.update(`${sha256(bundle)}\n`);
|
|
2267
2270
|
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`);
|
|
2271
|
+
if (fallback) digest.update(`fallback\0${fallback.path}\0${fallback.status}\n`);
|
|
2268
2272
|
return digest.digest("hex");
|
|
2269
2273
|
}
|
|
2270
2274
|
/** The manifest as the gateway reads it — the bytes on disk are the CLI's business. */
|
|
@@ -2903,11 +2907,12 @@ async function cronCommand(action, target, subtarget, options = {}) {
|
|
|
2903
2907
|
//#region src/mount-shadow.ts
|
|
2904
2908
|
/**
|
|
2905
2909
|
* The address a **Document** answers at, relative to its Function's mount: a
|
|
2906
|
-
* request path whose last segment carries no extension
|
|
2907
|
-
* appended
|
|
2910
|
+
* request path whose last segment carries no extension resolves to that path,
|
|
2911
|
+
* to it with `.html` appended and to it with `/index.html` appended, so both
|
|
2912
|
+
* `about.html` and `about/index.html` are the address `about`.
|
|
2908
2913
|
*/
|
|
2909
2914
|
function documentAddressPath(assetPath) {
|
|
2910
|
-
return assetPath.replace(/(^|\/)index\.html$/, "");
|
|
2915
|
+
return assetPath.replace(/(^|\/)index\.html$/, "").replace(/\.html$/, "");
|
|
2911
2916
|
}
|
|
2912
2917
|
/** See **Mount** in `CONTEXT.md` for why only the root Function's can be. */
|
|
2913
2918
|
function mountsCanShadow(functionSlug) {
|
|
@@ -2975,6 +2980,53 @@ function shadowingLines(shadowed) {
|
|
|
2975
2980
|
];
|
|
2976
2981
|
}
|
|
2977
2982
|
//#endregion
|
|
2983
|
+
//#region src/document-collision.ts
|
|
2984
|
+
/**
|
|
2985
|
+
* The addresses this deploy carries more than one file for.
|
|
2986
|
+
*
|
|
2987
|
+
* An address resolves to an ordered set of keys and the first one the version
|
|
2988
|
+
* carries answers, so `about`, `about.html` and `about/index.html` all sit at
|
|
2989
|
+
* `/about` and the first of them wins. Nothing is refused: a build that emitted
|
|
2990
|
+
* two is a worse day when the deploy fails than when it says which one answers.
|
|
2991
|
+
*/
|
|
2992
|
+
function collidingDocuments(assetPaths) {
|
|
2993
|
+
const byAddress = /* @__PURE__ */ new Map();
|
|
2994
|
+
for (const assetPath of assetPaths) {
|
|
2995
|
+
const address = documentAddressPath(assetPath);
|
|
2996
|
+
byAddress.set(address, [...byAddress.get(address) ?? [], assetPath]);
|
|
2997
|
+
}
|
|
2998
|
+
return [...byAddress].filter(([, carried]) => carried.length > 1).map(([address, carried]) => {
|
|
2999
|
+
const [answers, ...shadowed] = [...carried].sort((one, other) => preference(one) - preference(other));
|
|
3000
|
+
return {
|
|
3001
|
+
address,
|
|
3002
|
+
answers,
|
|
3003
|
+
shadowed
|
|
3004
|
+
};
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
/** Where a file sits in the candidate order the platform resolves an address through. */
|
|
3008
|
+
function preference(assetPath) {
|
|
3009
|
+
if (assetPath === documentAddressPath(assetPath)) return 1;
|
|
3010
|
+
return /(^|\/)index\.html$/.test(assetPath) ? 3 : 2;
|
|
3011
|
+
}
|
|
3012
|
+
/** What the deploy says about an address it carries two pages for. */
|
|
3013
|
+
function collidingLines(colliding) {
|
|
3014
|
+
if (colliding.length === 0) return [];
|
|
3015
|
+
const lines = [];
|
|
3016
|
+
for (const [index, collision] of colliding.entries()) {
|
|
3017
|
+
const at = collision.address === "" ? "/" : `/${collision.address}`;
|
|
3018
|
+
lines.push("", ` ${index === 0 ? "Collision: " : " "} \x1b[33m${collision.answers}\x1b[0m`, ...indented(`This page and ${collision.shadowed.join(", ")} both answer ${at}. ${collision.answers} is the one served there, and ${listed(collision.shadowed)} still answer${collision.shadowed.length === 1 ? "s" : ""} at its own name.`));
|
|
3019
|
+
}
|
|
3020
|
+
return [
|
|
3021
|
+
...lines,
|
|
3022
|
+
`${VALUE_COLUMN}Nothing was refused — every file this deploy declared is`,
|
|
3023
|
+
`${VALUE_COLUMN}stored and served.`
|
|
3024
|
+
];
|
|
3025
|
+
}
|
|
3026
|
+
function listed(paths) {
|
|
3027
|
+
return paths.length === 1 ? paths[0] : `${paths.slice(0, -1).join(", ")} and ${paths[paths.length - 1]}`;
|
|
3028
|
+
}
|
|
3029
|
+
//#endregion
|
|
2978
3030
|
//#region src/deploy.ts
|
|
2979
3031
|
function declaredFields(declared) {
|
|
2980
3032
|
return {
|
|
@@ -2984,6 +3036,28 @@ function declaredFields(declared) {
|
|
|
2984
3036
|
};
|
|
2985
3037
|
}
|
|
2986
3038
|
/**
|
|
3039
|
+
* The fallback this project declares.
|
|
3040
|
+
*
|
|
3041
|
+
* A declaration missing either half is refused here rather than at the gateway,
|
|
3042
|
+
* because the pair is part of what the deploy identifies itself by and that is
|
|
3043
|
+
* computed before anything is sent. Which statuses a document may be sent with,
|
|
3044
|
+
* and whether the path names a file the deploy carries, are the platform's to
|
|
3045
|
+
* judge: duplicating either rule here is two places for it to drift.
|
|
3046
|
+
*/
|
|
3047
|
+
function declaredFallback(value) {
|
|
3048
|
+
if (value === void 0 || value === null) return void 0;
|
|
3049
|
+
const declared = value;
|
|
3050
|
+
if (typeof declared.path === "string" && declared.path.trim() && Number.isInteger(declared.status)) return {
|
|
3051
|
+
path: declared.path.trim(),
|
|
3052
|
+
status: declared.status
|
|
3053
|
+
};
|
|
3054
|
+
console.error("[wawesome] Error: 'fallback' in wawesome-function.json must name a document and the");
|
|
3055
|
+
console.error("[wawesome] status it answers at.");
|
|
3056
|
+
console.error("[wawesome] A not-found page is \x1B[36m{ \"path\": \"404.html\", \"status\": 404 }\x1B[0m and a");
|
|
3057
|
+
console.error("[wawesome] single-page application's shell is \x1B[36m{ \"path\": \"index.html\", \"status\": 200 }\x1B[0m.");
|
|
3058
|
+
process.exit(1);
|
|
3059
|
+
}
|
|
3060
|
+
/**
|
|
2987
3061
|
* The visibility this project declares, refused here rather than at the gateway
|
|
2988
3062
|
* so a typo is a message at the keyboard.
|
|
2989
3063
|
*/
|
|
@@ -3031,7 +3105,8 @@ async function deploy(entryInput, options) {
|
|
|
3031
3105
|
const declared = {
|
|
3032
3106
|
visibility: declaredVisibility(config.visibility),
|
|
3033
3107
|
confirmPublish: Boolean(options.publish),
|
|
3034
|
-
confirmAssetDrop: Boolean(options.dropAssets)
|
|
3108
|
+
confirmAssetDrop: Boolean(options.dropAssets),
|
|
3109
|
+
fallback: declaredFallback(config.fallback)
|
|
3035
3110
|
};
|
|
3036
3111
|
const domainName = declaredDomain(config.domain);
|
|
3037
3112
|
if (isVerbose) {
|
|
@@ -3076,6 +3151,7 @@ async function deploy(entryInput, options) {
|
|
|
3076
3151
|
}
|
|
3077
3152
|
}
|
|
3078
3153
|
const declaresSchedules = config.schedules !== void 0;
|
|
3154
|
+
const declaresFallback = declared.fallback !== void 0;
|
|
3079
3155
|
const assets = config.assets ? collectAssets(path.resolve(config.assets)) : [];
|
|
3080
3156
|
const documents = assets.filter((asset) => isDocument(asset.path));
|
|
3081
3157
|
if (jsCode === null && documents.length === 0) {
|
|
@@ -3090,7 +3166,7 @@ async function deploy(entryInput, options) {
|
|
|
3090
3166
|
const uploadUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/code`;
|
|
3091
3167
|
if (isVerbose) console.log(`[wawesome:verbose] POST ${uploadUrl}`);
|
|
3092
3168
|
const declaredOnTheWire = declaredFields(declared);
|
|
3093
|
-
const uploadRes = assets.length > 0 || declaresSchedules || Object.keys(declaredOnTheWire).length > 0 || jsCode === null ? await authorizedFetch(uploadUrl, {
|
|
3169
|
+
const uploadRes = assets.length > 0 || declaresSchedules || declaresFallback || Object.keys(declaredOnTheWire).length > 0 || jsCode === null ? await authorizedFetch(uploadUrl, {
|
|
3094
3170
|
method: "POST",
|
|
3095
3171
|
headers: {
|
|
3096
3172
|
Authorization: `Bearer ${creds.tenant_jwt}`,
|
|
@@ -3100,6 +3176,7 @@ async function deploy(entryInput, options) {
|
|
|
3100
3176
|
...jsCode !== null ? { code: jsCode } : {},
|
|
3101
3177
|
...assets.length > 0 ? { assets: manifestOf(assets) } : {},
|
|
3102
3178
|
...declaresSchedules ? { schedules: config.schedules } : {},
|
|
3179
|
+
...declaresFallback ? { fallback: declared.fallback } : {},
|
|
3103
3180
|
...declaredOnTheWire
|
|
3104
3181
|
})
|
|
3105
3182
|
}) : await authorizedFetch(uploadUrl, {
|
|
@@ -3223,6 +3300,7 @@ async function deploy(entryInput, options) {
|
|
|
3223
3300
|
}
|
|
3224
3301
|
if (domain) for (const line of domainLines(domain)) console.log(line);
|
|
3225
3302
|
for (const line of shadowingLines(shadowed)) console.log(line);
|
|
3303
|
+
for (const line of collidingLines(collidingDocuments(assets.map((asset) => asset.path)))) console.log(line);
|
|
3226
3304
|
if (headroom) {
|
|
3227
3305
|
console.log("");
|
|
3228
3306
|
for (const line of headroom) console.log(line);
|
|
@@ -3261,7 +3339,7 @@ async function uploadAssets(creds, app, funcName, bundle, assets, declared, isVe
|
|
|
3261
3339
|
"Content-Type": "application/json"
|
|
3262
3340
|
},
|
|
3263
3341
|
body: JSON.stringify({
|
|
3264
|
-
deploy_digest: deployDigest(Buffer.from(bundle, "utf-8"), assets),
|
|
3342
|
+
deploy_digest: deployDigest(Buffer.from(bundle, "utf-8"), assets, declared.fallback),
|
|
3265
3343
|
assets: manifestOf(assets),
|
|
3266
3344
|
...declaredFields(declared)
|
|
3267
3345
|
})
|
|
@@ -5623,6 +5701,16 @@ function isExpired(credential, now) {
|
|
|
5623
5701
|
const at = new Date(credential.expires_at).getTime();
|
|
5624
5702
|
return !Number.isNaN(at) && at <= now;
|
|
5625
5703
|
}
|
|
5704
|
+
/**
|
|
5705
|
+
* Which rows somebody here minted and which they authorized a product to hold.
|
|
5706
|
+
*
|
|
5707
|
+
* The product is named beside the word rather than left to the label the token
|
|
5708
|
+
* endpoint writes: the label is a convention and this is the row's own answer.
|
|
5709
|
+
*/
|
|
5710
|
+
function kindOf(credential) {
|
|
5711
|
+
if (credential.kind !== "oauth") return "minted";
|
|
5712
|
+
return credential.client ? `connector (${credential.client.name})` : "connector";
|
|
5713
|
+
}
|
|
5626
5714
|
function statusOf(credential, now) {
|
|
5627
5715
|
if (credential.revoked_at) return "revoked";
|
|
5628
5716
|
if (isExpired(credential, now)) return "expired";
|
|
@@ -5745,6 +5833,7 @@ function formatCredentials(credentials, now) {
|
|
|
5745
5833
|
const rows = credentials.map((credential) => ({
|
|
5746
5834
|
name: credential.name,
|
|
5747
5835
|
prefix: credential.prefix,
|
|
5836
|
+
kind: kindOf(credential),
|
|
5748
5837
|
capabilities: credential.capabilities.join(", "),
|
|
5749
5838
|
apps: namedApps(credential.apps) ?? "every App",
|
|
5750
5839
|
lastUsed: asUtc(credential.last_used_at),
|
|
@@ -5760,6 +5849,10 @@ function formatCredentials(credentials, now) {
|
|
|
5760
5849
|
header: "PREFIX",
|
|
5761
5850
|
of: (row) => row.prefix
|
|
5762
5851
|
},
|
|
5852
|
+
{
|
|
5853
|
+
header: "KIND",
|
|
5854
|
+
of: (row) => row.kind
|
|
5855
|
+
},
|
|
5763
5856
|
{
|
|
5764
5857
|
header: "CAPABILITIES",
|
|
5765
5858
|
of: (row) => row.capabilities
|