wawesome 0.16.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 +65 -12
- package/dist/index.mjs +155 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -478,9 +478,9 @@ concluding the platform cannot do it.
|
|
|
478
478
|
|
|
479
479
|
| Tool | What it does | Needs |
|
|
480
480
|
|:---------------------------|:------------------------------------------------------------------------|:--------------|
|
|
481
|
-
| `whoami` | The workspace, where it is administered, the words this credential carries, the Apps it reaches | nothing |
|
|
481
|
+
| `whoami` | The workspace, where it is administered, the words this credential carries and which of them a tool here asks for, the Apps it reaches | nothing |
|
|
482
482
|
| `list_templates` | The template catalogue, with what each one is for | nothing |
|
|
483
|
-
| `get_template` | One template's
|
|
483
|
+
| `get_template` | One template's source, environment contract and outbound providers; its tests and build configuration are named, and read on request | nothing |
|
|
484
484
|
| `list_apps` | The Apps the credential reaches, each with its address and its dashboard page | `read:apps` |
|
|
485
485
|
| `list_functions` | The Functions in one App, and what their live version carries | `read:functions` |
|
|
486
486
|
| `list_versions` | A Function's Versions, and which one is live | `read:functions` |
|
|
@@ -493,6 +493,8 @@ concluding the platform cannot do it.
|
|
|
493
493
|
| `deploy_function` | Deploys code and the static files it carries | `write:functions` |
|
|
494
494
|
| `deploy_status` | How a deploy went, by the handle it answered with | `read:functions` |
|
|
495
495
|
| `rollback_function` | Puts a Version the Function already has back on its public address | `write:functions` |
|
|
496
|
+
| `request_upload` | Asks the person you are working with for files the conversation cannot produce, and answers a link to give them | `write:functions` |
|
|
497
|
+
| `list_uploads` | What the person sent against one upload session, ready to declare on the next deploy | `read:functions` |
|
|
496
498
|
| `set_env_var` | Sets an App's environment variable, read at the next invocation | `write:env` |
|
|
497
499
|
| `attach_domain` | Attaches a domain to an App and answers the DNS records that make it work | `write:domains` |
|
|
498
500
|
| `cancel_domain_claim` | Gives up a domain claim nobody has proved control of, freeing the App's place | `write:domains` |
|
|
@@ -505,8 +507,11 @@ beside it, and the presets below are named in exactly these words.
|
|
|
505
507
|
|
|
506
508
|
`whoami` is the one to call first. It answers the workspace, every word the credential carries and
|
|
507
509
|
the Apps it reaches, and it asks for nothing — so your agent can say what it is able to do before it
|
|
508
|
-
offers, rather than finding the edge by being refused in front of you.
|
|
509
|
-
|
|
510
|
+
offers, rather than finding the edge by being refused in front of you. Each word also says whether a
|
|
511
|
+
tool here asks for it. Your credential can carry `write:schedules` with no schedule tool on this
|
|
512
|
+
endpoint, and the mark is what stops an agent offering to schedule a job: a word with no tool behind
|
|
513
|
+
it is done on the dashboard rather than being withheld from you. A member's session calling the endpoint gets
|
|
514
|
+
the same answer for the role they hold.
|
|
510
515
|
|
|
511
516
|
A deploy is two calls. The first says which of your files the platform does not already hold, by
|
|
512
517
|
content hash; your agent uploads those bytes to `PUT /v1/assets/{content_hash}` with the same
|
|
@@ -643,6 +648,36 @@ Function, so a page your root Function carries at `about/index.html` is shadowed
|
|
|
643
648
|
named `about`. The deploy says so and names both addresses. It refuses nothing — the two Functions are
|
|
644
649
|
versioned independently, and either one may deploy first.
|
|
645
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
|
+
|
|
646
681
|
### Static files
|
|
647
682
|
|
|
648
683
|
Add `"assets"` to deploy static files beside your code:
|
|
@@ -681,6 +716,18 @@ Two rules to know about:
|
|
|
681
716
|
`favicon.ico`, `robots.txt` and a `.well-known/` directory are what that is for; hashed build
|
|
682
717
|
output belongs under `assets/`.
|
|
683
718
|
|
|
719
|
+
**A version carries exactly the files its deploy declared.** Nothing is inherited from the version
|
|
720
|
+
before it, so taking `"assets"` out of `wawesome-function.json` — or pointing it at a directory your
|
|
721
|
+
build no longer writes to — is not an edit but every file on the site going at once. A deploy that
|
|
722
|
+
would do that against a Function serving files today is refused, and tells you how many would go:
|
|
723
|
+
|
|
724
|
+
```bash
|
|
725
|
+
npx wawesome deploy --drop-assets
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
Removing *some* of the files is ordinary editing and needs nothing: only the wholesale drop is
|
|
729
|
+
refused, because that is the shape of a mistake rather than of a decision.
|
|
730
|
+
|
|
684
731
|
**A page is a file like any other.** An `index.html` your build wrote is deployed, hashed, retained
|
|
685
732
|
and billed exactly as your other files are, and it is what a directory-style address resolves to: a
|
|
686
733
|
path whose last segment carries no extension is served the `index.html` beneath it, so `/`, `/about`,
|
|
@@ -790,10 +837,16 @@ Add `"domain"` and your App answers at a name you own, beside the address it alr
|
|
|
790
837
|
}
|
|
791
838
|
```
|
|
792
839
|
|
|
793
|
-
The deploy attaches it and prints
|
|
794
|
-
own the name
|
|
795
|
-
|
|
796
|
-
|
|
840
|
+
The deploy attaches it and prints three DNS records to add at your registrar. The TXT record proves
|
|
841
|
+
you own the name. The `_acme-challenge` CNAME beside it issues the certificate and keeps renewing it
|
|
842
|
+
for as long as it stays there, so nobody comes back to your registrar at renewal time — if an
|
|
843
|
+
`_acme-challenge` TXT record is already at that name, delete it, because a TXT and this CNAME cannot
|
|
844
|
+
both sit there. The last CNAME points traffic here, and you add that one once the certificate is
|
|
845
|
+
issued, so a site that is already live never spends a minute answering on a certificate that is not
|
|
846
|
+
there yet.
|
|
847
|
+
|
|
848
|
+
Nothing you are asked to add ever changes. Copy a value into your registrar, come back an hour later,
|
|
849
|
+
and it is the same value.
|
|
797
850
|
|
|
798
851
|
Your deploy waits for neither. It attaches the domain, prints the records and finishes, and every
|
|
799
852
|
later deploy states where the domain got to: not verified, ownership verified, certificate issued, or
|
|
@@ -807,10 +860,10 @@ npx wawesome domains
|
|
|
807
860
|
|
|
808
861
|
It prints the domain, where it got to, when the platform last checked, and the records to add. It
|
|
809
862
|
changes nothing, and a deploy credential carrying `read:domains` can run it, so a pipeline reads the
|
|
810
|
-
same thing you do.
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
863
|
+
same thing you do. Where the platform does not hold a record yet it prints a row saying so rather
|
|
864
|
+
than dropping the record from the list. That row is what this command fills in, and it is where the
|
|
865
|
+
deploy sends you: deploying again would change nothing, and a deploy that changes nothing is
|
|
866
|
+
refused.
|
|
814
867
|
|
|
815
868
|
A custom domain is granted from the Solo plan upwards, one per App. On a plan that grants none, the
|
|
816
869
|
deploy prints the refusal and lands everything else it was doing.
|
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
|
/**
|
|
@@ -1873,10 +1873,9 @@ function domainsUrl(creds, app) {
|
|
|
1873
1873
|
/**
|
|
1874
1874
|
* Where a domain got to, said beside the name itself.
|
|
1875
1875
|
*
|
|
1876
|
-
* Each state names the one record that is outstanding
|
|
1877
|
-
*
|
|
1878
|
-
*
|
|
1879
|
-
* certificate that is not there yet.
|
|
1876
|
+
* Each state names the one record that is outstanding. The two that prove the
|
|
1877
|
+
* name go in first and the traffic CNAME a day later, so a site that is already
|
|
1878
|
+
* live never answers on a certificate that is not there yet.
|
|
1880
1879
|
*
|
|
1881
1880
|
* Read off the records too, never the states alone: naming a record the payload
|
|
1882
1881
|
* does not carry left a reader holding the CNAME alone (#610).
|
|
@@ -1885,22 +1884,26 @@ function whereItGotTo(domain) {
|
|
|
1885
1884
|
const { state } = domain;
|
|
1886
1885
|
if (state.serving) return "serving";
|
|
1887
1886
|
if (state.certificate_active) return "certificate issued, so point the CNAME here to cut over";
|
|
1888
|
-
if (state.ownership_verified) return "
|
|
1889
|
-
if (domain.
|
|
1890
|
-
return "not verified yet, so add the
|
|
1887
|
+
if (state.ownership_verified) return domain.certificate_record_pending ? "name verified, and the certificate record is not ready" : "name verified, so add the certificate CNAME below";
|
|
1888
|
+
if (domain.ownership_record_pending) return "not verified yet, and the TXT record is not ready";
|
|
1889
|
+
return "not verified yet, so add the records below";
|
|
1891
1890
|
}
|
|
1892
1891
|
/**
|
|
1893
1892
|
* What a record is for, and when to add it.
|
|
1894
1893
|
*
|
|
1895
|
-
* The order is stated rather than implied. The TXT proves
|
|
1896
|
-
*
|
|
1897
|
-
* serving a live site never answers on a
|
|
1898
|
-
* That is why the platform validates before
|
|
1899
|
-
* the
|
|
1900
|
-
* instruction to wait (#617).
|
|
1894
|
+
* The order is stated rather than implied. The TXT proves the name is theirs
|
|
1895
|
+
* and the CNAME beside it issues the certificate; the traffic record is pointed
|
|
1896
|
+
* here afterwards, so a name already serving a live site never answers on a
|
|
1897
|
+
* certificate that is not there yet. That is why the platform validates before
|
|
1898
|
+
* the cutover, and a reader holding the traffic record alone could read the old
|
|
1899
|
+
* caption as a label rather than as an instruction to wait (#617).
|
|
1901
1900
|
*/
|
|
1902
1901
|
function whatItIsFor(purpose) {
|
|
1903
|
-
|
|
1902
|
+
switch (purpose) {
|
|
1903
|
+
case "ownership": return "Add this first. It proves you own the name";
|
|
1904
|
+
case "certificate": return "Add this first too. It issues the certificate and keeps it renewed";
|
|
1905
|
+
default: return "Add this once the certificate is active. It points traffic here";
|
|
1906
|
+
}
|
|
1904
1907
|
}
|
|
1905
1908
|
/** What the dashboard's empty state shows. Kept the same by hand, not by anything. */
|
|
1906
1909
|
const DOMAIN_CONFIG_LINE = "\"domain\": \"shop.client.com\"";
|
|
@@ -1948,7 +1951,7 @@ function domainLines(outcome) {
|
|
|
1948
1951
|
* the whole of #610 and #617.
|
|
1949
1952
|
*/
|
|
1950
1953
|
function attachedDomainLines(domain, printedBy = "deploy") {
|
|
1951
|
-
const { hostname, records, state, last_checked_at, detaching, certificate_record_pending } = domain;
|
|
1954
|
+
const { hostname, records, state, last_checked_at, detaching, ownership_record_pending, certificate_record_pending } = domain;
|
|
1952
1955
|
if (detaching) return [
|
|
1953
1956
|
"",
|
|
1954
1957
|
` Domain: \x1b[33m${hostname}: being detached\x1b[0m`,
|
|
@@ -1964,9 +1967,13 @@ function attachedDomainLines(domain, printedBy = "deploy") {
|
|
|
1964
1967
|
lines.push(`${VALUE_COLUMN}This app's own address answers too, so nothing pointed at it breaks.`);
|
|
1965
1968
|
return lines;
|
|
1966
1969
|
}
|
|
1970
|
+
const pending = Boolean(ownership_record_pending || certificate_record_pending);
|
|
1967
1971
|
lines.push("", " DNS records to add at your registrar:");
|
|
1968
|
-
lines.push(...recordLines(records,
|
|
1969
|
-
|
|
1972
|
+
lines.push(...recordLines(records, {
|
|
1973
|
+
ownership_record_pending,
|
|
1974
|
+
certificate_record_pending
|
|
1975
|
+
}, " "));
|
|
1976
|
+
lines.push("", ...closingLines(pending, printedBy));
|
|
1970
1977
|
return lines;
|
|
1971
1978
|
}
|
|
1972
1979
|
/**
|
|
@@ -1993,9 +2000,9 @@ function pairLines(domain, legs, printedBy) {
|
|
|
1993
2000
|
lines.push("", " DNS records to add at your registrar:");
|
|
1994
2001
|
for (const leg of outstanding) {
|
|
1995
2002
|
lines.push("", ` ${leg.hostname}:`);
|
|
1996
|
-
lines.push(...recordLines(leg.records ?? [], leg
|
|
2003
|
+
lines.push(...recordLines(leg.records ?? [], leg, " "));
|
|
1997
2004
|
}
|
|
1998
|
-
const pending = outstanding.some((leg) => leg.certificate_record_pending);
|
|
2005
|
+
const pending = outstanding.some((leg) => leg.ownership_record_pending || leg.certificate_record_pending);
|
|
1999
2006
|
lines.push("", ...closingLines(pending, printedBy));
|
|
2000
2007
|
return lines;
|
|
2001
2008
|
}
|
|
@@ -2009,12 +2016,16 @@ function whereTheLegGotTo(leg) {
|
|
|
2009
2016
|
* 40-odd characters a person has to copy exactly, and a fourth column puts the
|
|
2010
2017
|
* row past the width of the terminal it is being copied out of.
|
|
2011
2018
|
*/
|
|
2012
|
-
function recordLines(records,
|
|
2019
|
+
function recordLines(records, pending, indent) {
|
|
2013
2020
|
const nameWidth = Math.max(0, ...records.map((record) => record.name.length));
|
|
2014
2021
|
const row = (type, name, value = "") => `${indent} ${type.padEnd(5)} ${name.padEnd(nameWidth)} ${value}`.trimEnd();
|
|
2015
2022
|
const lines = [];
|
|
2016
|
-
if (
|
|
2017
|
-
|
|
2023
|
+
if (pending.ownership_record_pending) lines.push("", `${indent}${whatItIsFor("ownership")}:`, row("TXT", "not ready yet"));
|
|
2024
|
+
if (pending.certificate_record_pending) lines.push("", `${indent}${whatItIsFor("certificate")}:`, row("CNAME", "not ready yet"));
|
|
2025
|
+
for (const record of records) {
|
|
2026
|
+
lines.push("", `${indent}${whatItIsFor(record.purpose)}:`, row(record.type, record.name, record.value));
|
|
2027
|
+
if (record.purpose === "certificate") lines.push(`${indent} Delete any _acme-challenge TXT record already at that name.`);
|
|
2028
|
+
}
|
|
2018
2029
|
return lines;
|
|
2019
2030
|
}
|
|
2020
2031
|
/** Every hostname a claim answers at, for a block naming the claim in one line. */
|
|
@@ -2029,9 +2040,9 @@ function claimedHostnames(domain) {
|
|
|
2029
2040
|
* nothing is refused, so the one piece of advice the reader was given was the
|
|
2030
2041
|
* one they could not take.
|
|
2031
2042
|
*/
|
|
2032
|
-
function closingLines(
|
|
2033
|
-
if (!
|
|
2034
|
-
const asking = "
|
|
2043
|
+
function closingLines(aRecordIsPending, printedBy) {
|
|
2044
|
+
if (!aRecordIsPending) return indented(`${printedBy === "deploy" ? "The deploy is finished. " : ""}This domain comes up on its own once the records resolve.`);
|
|
2045
|
+
const asking = "A record is not ready yet. The platform asks the certificate vendor again on its own.";
|
|
2035
2046
|
if (printedBy === "domains") return indented(`${asking} Run this again in a minute.`);
|
|
2036
2047
|
return [...indented(`${asking} The deploy is finished either way. To read the record when it lands, run`), `${VALUE_COLUMN} \x1b[36mwawesome domains\x1b[0m`];
|
|
2037
2048
|
}
|
|
@@ -2241,19 +2252,23 @@ function sha256(bytes) {
|
|
|
2241
2252
|
}
|
|
2242
2253
|
/**
|
|
2243
2254
|
* What this deploy *is*: the server bundle together with the sorted set of
|
|
2244
|
-
* asset path and content-hash pairs.
|
|
2255
|
+
* asset path and content-hash pairs, and the **Fallback document** it declares.
|
|
2245
2256
|
*
|
|
2246
2257
|
* Computed here, before anything is uploaded, so a deploy that changed nothing
|
|
2247
2258
|
* is refused before a byte moves. Mirrors `deploy_digest` in
|
|
2248
2259
|
* `server/crates/core/src/features/assets/identity.rs` — the gateway recomputes
|
|
2249
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.
|
|
2250
2264
|
*/
|
|
2251
2265
|
const DEPLOY_DIGEST_DOMAIN = "wawesome-deploy-v1";
|
|
2252
|
-
function deployDigest(bundle, assets) {
|
|
2266
|
+
function deployDigest(bundle, assets, fallback) {
|
|
2253
2267
|
const digest = crypto.createHash("sha256");
|
|
2254
2268
|
digest.update(`${DEPLOY_DIGEST_DOMAIN}\n`);
|
|
2255
2269
|
digest.update(`${sha256(bundle)}\n`);
|
|
2256
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`);
|
|
2257
2272
|
return digest.digest("hex");
|
|
2258
2273
|
}
|
|
2259
2274
|
/** The manifest as the gateway reads it — the bytes on disk are the CLI's business. */
|
|
@@ -2892,11 +2907,12 @@ async function cronCommand(action, target, subtarget, options = {}) {
|
|
|
2892
2907
|
//#region src/mount-shadow.ts
|
|
2893
2908
|
/**
|
|
2894
2909
|
* The address a **Document** answers at, relative to its Function's mount: a
|
|
2895
|
-
* request path whose last segment carries no extension
|
|
2896
|
-
* 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`.
|
|
2897
2913
|
*/
|
|
2898
2914
|
function documentAddressPath(assetPath) {
|
|
2899
|
-
return assetPath.replace(/(^|\/)index\.html$/, "");
|
|
2915
|
+
return assetPath.replace(/(^|\/)index\.html$/, "").replace(/\.html$/, "");
|
|
2900
2916
|
}
|
|
2901
2917
|
/** See **Mount** in `CONTEXT.md` for why only the root Function's can be. */
|
|
2902
2918
|
function mountsCanShadow(functionSlug) {
|
|
@@ -2964,14 +2980,84 @@ function shadowingLines(shadowed) {
|
|
|
2964
2980
|
];
|
|
2965
2981
|
}
|
|
2966
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
|
|
2967
3030
|
//#region src/deploy.ts
|
|
2968
3031
|
function declaredFields(declared) {
|
|
2969
3032
|
return {
|
|
2970
3033
|
...declared.visibility ? { visibility: declared.visibility } : {},
|
|
2971
|
-
...declared.confirmPublish ? { confirm_publish: true } : {}
|
|
3034
|
+
...declared.confirmPublish ? { confirm_publish: true } : {},
|
|
3035
|
+
...declared.confirmAssetDrop ? { confirm_asset_drop: true } : {}
|
|
2972
3036
|
};
|
|
2973
3037
|
}
|
|
2974
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
|
+
/**
|
|
2975
3061
|
* The visibility this project declares, refused here rather than at the gateway
|
|
2976
3062
|
* so a typo is a message at the keyboard.
|
|
2977
3063
|
*/
|
|
@@ -2982,6 +3068,15 @@ function declaredVisibility(value) {
|
|
|
2982
3068
|
console.error("[wawesome] It must be \"public\" or \"private\".");
|
|
2983
3069
|
process.exit(1);
|
|
2984
3070
|
}
|
|
3071
|
+
function reportAssetDropRefusal(message) {
|
|
3072
|
+
console.error(`\n[wawesome] \x1b[31mError: ${message}\x1b[0m`);
|
|
3073
|
+
console.error("[wawesome] Nothing was deployed. A version carries exactly the files its");
|
|
3074
|
+
console.error("[wawesome] deploy declared, so this one would take every file the live");
|
|
3075
|
+
console.error("[wawesome] version serves off the site at once.");
|
|
3076
|
+
console.error("[wawesome] Point \x1B[36m\"assets\"\x1B[0m in wawesome-function.json at the directory");
|
|
3077
|
+
console.error("[wawesome] holding them, or re-run with \x1B[36mwawesome deploy --drop-assets\x1B[0m to");
|
|
3078
|
+
console.error("[wawesome] remove them.\n");
|
|
3079
|
+
}
|
|
2985
3080
|
function reportPublishRefusal(message) {
|
|
2986
3081
|
console.error(`\n[wawesome] \x1b[31mError: ${message}\x1b[0m`);
|
|
2987
3082
|
console.error("[wawesome] Nothing was deployed. This Function is private, so its address");
|
|
@@ -3009,7 +3104,9 @@ async function deploy(entryInput, options) {
|
|
|
3009
3104
|
const { app, function: funcName } = config;
|
|
3010
3105
|
const declared = {
|
|
3011
3106
|
visibility: declaredVisibility(config.visibility),
|
|
3012
|
-
confirmPublish: Boolean(options.publish)
|
|
3107
|
+
confirmPublish: Boolean(options.publish),
|
|
3108
|
+
confirmAssetDrop: Boolean(options.dropAssets),
|
|
3109
|
+
fallback: declaredFallback(config.fallback)
|
|
3013
3110
|
};
|
|
3014
3111
|
const domainName = declaredDomain(config.domain);
|
|
3015
3112
|
if (isVerbose) {
|
|
@@ -3054,6 +3151,7 @@ async function deploy(entryInput, options) {
|
|
|
3054
3151
|
}
|
|
3055
3152
|
}
|
|
3056
3153
|
const declaresSchedules = config.schedules !== void 0;
|
|
3154
|
+
const declaresFallback = declared.fallback !== void 0;
|
|
3057
3155
|
const assets = config.assets ? collectAssets(path.resolve(config.assets)) : [];
|
|
3058
3156
|
const documents = assets.filter((asset) => isDocument(asset.path));
|
|
3059
3157
|
if (jsCode === null && documents.length === 0) {
|
|
@@ -3068,7 +3166,7 @@ async function deploy(entryInput, options) {
|
|
|
3068
3166
|
const uploadUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/code`;
|
|
3069
3167
|
if (isVerbose) console.log(`[wawesome:verbose] POST ${uploadUrl}`);
|
|
3070
3168
|
const declaredOnTheWire = declaredFields(declared);
|
|
3071
|
-
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, {
|
|
3072
3170
|
method: "POST",
|
|
3073
3171
|
headers: {
|
|
3074
3172
|
Authorization: `Bearer ${creds.tenant_jwt}`,
|
|
@@ -3078,6 +3176,7 @@ async function deploy(entryInput, options) {
|
|
|
3078
3176
|
...jsCode !== null ? { code: jsCode } : {},
|
|
3079
3177
|
...assets.length > 0 ? { assets: manifestOf(assets) } : {},
|
|
3080
3178
|
...declaresSchedules ? { schedules: config.schedules } : {},
|
|
3179
|
+
...declaresFallback ? { fallback: declared.fallback } : {},
|
|
3081
3180
|
...declaredOnTheWire
|
|
3082
3181
|
})
|
|
3083
3182
|
}) : await authorizedFetch(uploadUrl, {
|
|
@@ -3100,6 +3199,10 @@ async function deploy(entryInput, options) {
|
|
|
3100
3199
|
reportPublishRefusal(refusal.message);
|
|
3101
3200
|
process.exit(1);
|
|
3102
3201
|
}
|
|
3202
|
+
if (refusal.reason === "asset-drop-needs-confirmation") {
|
|
3203
|
+
reportAssetDropRefusal(refusal.message);
|
|
3204
|
+
process.exit(1);
|
|
3205
|
+
}
|
|
3103
3206
|
console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m`);
|
|
3104
3207
|
const advice = planLimitAdvice(refusal.reason);
|
|
3105
3208
|
if (advice) console.error(`[wawesome] ${advice}\n`);
|
|
@@ -3197,6 +3300,7 @@ async function deploy(entryInput, options) {
|
|
|
3197
3300
|
}
|
|
3198
3301
|
if (domain) for (const line of domainLines(domain)) console.log(line);
|
|
3199
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);
|
|
3200
3304
|
if (headroom) {
|
|
3201
3305
|
console.log("");
|
|
3202
3306
|
for (const line of headroom) console.log(line);
|
|
@@ -3235,7 +3339,7 @@ async function uploadAssets(creds, app, funcName, bundle, assets, declared, isVe
|
|
|
3235
3339
|
"Content-Type": "application/json"
|
|
3236
3340
|
},
|
|
3237
3341
|
body: JSON.stringify({
|
|
3238
|
-
deploy_digest: deployDigest(Buffer.from(bundle, "utf-8"), assets),
|
|
3342
|
+
deploy_digest: deployDigest(Buffer.from(bundle, "utf-8"), assets, declared.fallback),
|
|
3239
3343
|
assets: manifestOf(assets),
|
|
3240
3344
|
...declaredFields(declared)
|
|
3241
3345
|
})
|
|
@@ -5597,6 +5701,16 @@ function isExpired(credential, now) {
|
|
|
5597
5701
|
const at = new Date(credential.expires_at).getTime();
|
|
5598
5702
|
return !Number.isNaN(at) && at <= now;
|
|
5599
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
|
+
}
|
|
5600
5714
|
function statusOf(credential, now) {
|
|
5601
5715
|
if (credential.revoked_at) return "revoked";
|
|
5602
5716
|
if (isExpired(credential, now)) return "expired";
|
|
@@ -5719,6 +5833,7 @@ function formatCredentials(credentials, now) {
|
|
|
5719
5833
|
const rows = credentials.map((credential) => ({
|
|
5720
5834
|
name: credential.name,
|
|
5721
5835
|
prefix: credential.prefix,
|
|
5836
|
+
kind: kindOf(credential),
|
|
5722
5837
|
capabilities: credential.capabilities.join(", "),
|
|
5723
5838
|
apps: namedApps(credential.apps) ?? "every App",
|
|
5724
5839
|
lastUsed: asUtc(credential.last_used_at),
|
|
@@ -5734,6 +5849,10 @@ function formatCredentials(credentials, now) {
|
|
|
5734
5849
|
header: "PREFIX",
|
|
5735
5850
|
of: (row) => row.prefix
|
|
5736
5851
|
},
|
|
5852
|
+
{
|
|
5853
|
+
header: "KIND",
|
|
5854
|
+
of: (row) => row.kind
|
|
5855
|
+
},
|
|
5737
5856
|
{
|
|
5738
5857
|
header: "CAPABILITIES",
|
|
5739
5858
|
of: (row) => row.capabilities
|
|
@@ -5920,7 +6039,7 @@ async function credentialsCommand(action, target, options) {
|
|
|
5920
6039
|
//#region src/index.ts
|
|
5921
6040
|
const cli = cac("wawesome");
|
|
5922
6041
|
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));
|
|
5923
|
-
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").action((entry, options) => deploy(entry, options));
|
|
6042
|
+
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));
|
|
5924
6043
|
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) => {
|
|
5925
6044
|
if (!action || action === "list" || action === "ls") return listVersions(options);
|
|
5926
6045
|
if (action === "switch" || action === "use" || action === "select") return switchVersion(target, options);
|