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.
Files changed (3) hide show
  1. package/README.md +30 -0
  2. package/dist/index.mjs +102 -9
  3. 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.17.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 has `/index.html`
2907
- * appended, so the Document at `about/index.html` is the address `about`.
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wawesome",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
5
5
  "type": "module",
6
6
  "bin": {