wawesome 0.4.0 → 0.5.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 +80 -1
- package/dist/index.mjs +152 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -258,6 +258,84 @@ Two rules to know about:
|
|
|
258
258
|
**HTML is refused at deploy time.** Your Function renders its own markup, and a document served from
|
|
259
259
|
your App's own origin is the sharpest same-origin vector a static file has.
|
|
260
260
|
|
|
261
|
+
**An SVG is served with script denied.** The rule behind the refusal above is that nothing you deploy
|
|
262
|
+
as a file runs script on your App's own origin, and an SVG opened directly in a browser would. It is
|
|
263
|
+
served rather than refused because denying it costs the file nothing: an `<img src="logo.svg">` never
|
|
264
|
+
ran that script, so your drawings render as they always did — every SVG and XML file carries
|
|
265
|
+
`Content-Security-Policy: script-src 'none'`, and only navigating straight to one loses anything.
|
|
266
|
+
|
|
267
|
+
### Schedules
|
|
268
|
+
|
|
269
|
+
Add `"schedules"` to run a Function on a recurring timer, with no caller:
|
|
270
|
+
|
|
271
|
+
```json
|
|
272
|
+
{
|
|
273
|
+
"app": "my-app",
|
|
274
|
+
"function": "nightly-reconcile",
|
|
275
|
+
"entry": "src/index.ts",
|
|
276
|
+
"schedules": [{ "name": "overnight", "expression": "0 3 * * *" }]
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
An expression is **five fields, read in UTC** — minute, hour, day of month, month, day of week. There
|
|
281
|
+
is no seconds field, and no timezone: a local zone would make one night a year fire a job twice and
|
|
282
|
+
another night not at all.
|
|
283
|
+
|
|
284
|
+
The name is yours to choose and is what the platform keys the schedule by, so editing an expression
|
|
285
|
+
is a change to the same schedule rather than the deletion of one and the creation of another — its
|
|
286
|
+
history and its paused state stay attached to it.
|
|
287
|
+
|
|
288
|
+
Your deploy applies them and prints each one with the time it will next run. It is refused, before
|
|
289
|
+
anything is built or uploaded, if an expression cannot be read, if it would run more often than every
|
|
290
|
+
five minutes, or if one Function declares more than five schedules.
|
|
291
|
+
|
|
292
|
+
Two rules worth knowing before you edit the file:
|
|
293
|
+
|
|
294
|
+
- **A schedule you delete from the file is disabled, not deleted.** Its history stays, and declaring
|
|
295
|
+
it again is what turns it back on — so a typo costs you a deploy rather than a job's record.
|
|
296
|
+
- **A deploy never resumes a schedule a person paused.** When it runs is code; whether it is running
|
|
297
|
+
is not, and an unrelated commit the next morning must not restart what you stopped at 3am.
|
|
298
|
+
|
|
299
|
+
Leaving `schedules` out of the file entirely says nothing about them and changes nothing. Writing
|
|
300
|
+
`"schedules": []` says this Function declares none, which disables the ones it used to have.
|
|
301
|
+
|
|
302
|
+
### Keeping a Function off the web
|
|
303
|
+
|
|
304
|
+
A job on a timer should not also be sitting at a guessable URL where a stranger can fire it. Add
|
|
305
|
+
`"visibility"` to make a Function unreachable from the internet:
|
|
306
|
+
|
|
307
|
+
```json
|
|
308
|
+
{
|
|
309
|
+
"app": "my-app",
|
|
310
|
+
"function": "nightly-reconcile",
|
|
311
|
+
"entry": "src/index.ts",
|
|
312
|
+
"visibility": "private"
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
A private Function has **no address at all** — not a hidden one, not one behind a credential. A
|
|
317
|
+
request for it is answered with the same 404 as a Function that was never deployed, on your App's
|
|
318
|
+
own hostname and on the development path form alike. This is how you deploy a job with side effects
|
|
319
|
+
without leaving it where a stranger who guesses the slug can fire it.
|
|
320
|
+
|
|
321
|
+
Leave the line out and your Function is public, which is what every Function without it has always
|
|
322
|
+
been.
|
|
323
|
+
|
|
324
|
+
Today that is all a private Function is: there is no way *in* to one yet. Schedules do not fire yet,
|
|
325
|
+
and the authenticated trigger that runs a Function by hand is not built — so make one private when
|
|
326
|
+
you want it off the web, and expect to publish it again to call it until those land.
|
|
327
|
+
|
|
328
|
+
Making a Function private takes nothing but the deploy. Making it public again does not: deleting
|
|
329
|
+
the line is refused, and the deploy tells you so having written nothing.
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
npx wawesome deploy --publish
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
That is deliberate — the line that keeps a job off the internet is one line, and a deploy that
|
|
336
|
+
quietly honoured its deletion would put the job back on the open internet with nothing said. Every
|
|
337
|
+
deploy prints the visibility it landed, beside the URL or in place of it.
|
|
338
|
+
|
|
261
339
|
### Reserved headers
|
|
262
340
|
|
|
263
341
|
`x-wawesome-*` belongs to the platform in both directions. It is stripped off the request before your
|
|
@@ -265,11 +343,12 @@ handler sees it, and off your response before the caller does — so **do not na
|
|
|
265
343
|
on that prefix**: it is dropped silently rather than rejected, and you will not get an error telling
|
|
266
344
|
you why it vanished.
|
|
267
345
|
|
|
268
|
-
|
|
346
|
+
Four headers arrive or leave on it, and the stripping is what makes them worth trusting:
|
|
269
347
|
|
|
270
348
|
| Header | Direction | What it means |
|
|
271
349
|
| --- | --- | --- |
|
|
272
350
|
| `x-wawesome-forwarded-prefix` | inbound | The mount that was stripped from the path. Join it to the path you observe to rebuild the caller's URL. |
|
|
351
|
+
| `x-wawesome-trigger` | inbound | How this run started: `caller` when someone called your address. A caller cannot forge it, so you may skip your own authorization on anything else and open nothing. |
|
|
273
352
|
| `x-wawesome-invocation-id` | outbound | The id of this run — the key to fetch its logs with `npx wawesome logs --invocation <id>`. |
|
|
274
353
|
| `x-wawesome-error` | outbound | Present only when the platform failed, never when your Function did. Its *absence* means the status on the wire is yours — up to the moment your response is committed, and no further. |
|
|
275
354
|
|
package/dist/index.mjs
CHANGED
|
@@ -759,7 +759,7 @@ async function buildJs(entryInput, options) {
|
|
|
759
759
|
* that has to name this version — `--version`, the dependency a scaffolded
|
|
760
760
|
* project pins — reads it here, so a release bumps one file.
|
|
761
761
|
*/
|
|
762
|
-
const CLI_VERSION = "0.
|
|
762
|
+
const CLI_VERSION = "0.5.0";
|
|
763
763
|
//#endregion
|
|
764
764
|
//#region src/prompt.ts
|
|
765
765
|
/**
|
|
@@ -1155,6 +1155,42 @@ async function whoami() {
|
|
|
1155
1155
|
console.log(` Gateway: ${creds.gateway_url}\n`);
|
|
1156
1156
|
}
|
|
1157
1157
|
//#endregion
|
|
1158
|
+
//#region src/schedules.ts
|
|
1159
|
+
/**
|
|
1160
|
+
* When a schedule next runs, or why it does not run at all.
|
|
1161
|
+
*
|
|
1162
|
+
* The three ways of being off never render alike: someone who cannot tell a
|
|
1163
|
+
* pause from a schedule they deleted from their config file will press resume
|
|
1164
|
+
* and watch nothing happen.
|
|
1165
|
+
*/
|
|
1166
|
+
function whenItRuns(schedule) {
|
|
1167
|
+
switch (schedule.state) {
|
|
1168
|
+
case "active": return schedule.next_fire_at ? `next run ${asUtc(schedule.next_fire_at)}` : "next run unknown";
|
|
1169
|
+
case "paused": return "paused — resume it to run again";
|
|
1170
|
+
case "undeclared": return "not in this config file — disabled, kept";
|
|
1171
|
+
case "suspended": return "suspended — the workspace owes payment";
|
|
1172
|
+
default: return schedule.state;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
/** `2026-08-23 03:00 UTC`, which is the zone every expression is read in. */
|
|
1176
|
+
function asUtc(iso) {
|
|
1177
|
+
const at = new Date(iso);
|
|
1178
|
+
if (Number.isNaN(at.getTime())) return iso;
|
|
1179
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1180
|
+
return `${at.getUTCFullYear()}-${pad(at.getUTCMonth() + 1)}-${pad(at.getUTCDate())} ${pad(at.getUTCHours())}:${pad(at.getUTCMinutes())} UTC`;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* The block a deploy prints for the schedules it just applied. Every schedule
|
|
1184
|
+
* appears, including the ones that are off, so putting recurring compute on the
|
|
1185
|
+
* bill — or taking it off — is never silent.
|
|
1186
|
+
*/
|
|
1187
|
+
function scheduleLines(schedules) {
|
|
1188
|
+
if (schedules.length === 0) return [];
|
|
1189
|
+
const nameWidth = Math.max(...schedules.map((s) => s.name.length));
|
|
1190
|
+
const expressionWidth = Math.max(...schedules.map((s) => s.expression.length));
|
|
1191
|
+
return [" Schedules:", ...[...schedules].sort((a, b) => a.name.localeCompare(b.name)).map((schedule) => ` ${schedule.name.padEnd(nameWidth)} ${schedule.expression.padEnd(expressionWidth)} ${whenItRuns(schedule)}`)];
|
|
1192
|
+
}
|
|
1193
|
+
//#endregion
|
|
1158
1194
|
//#region src/usage.ts
|
|
1159
1195
|
const USAGE_TIMEOUT_MS = 2e3;
|
|
1160
1196
|
/**
|
|
@@ -1323,9 +1359,15 @@ function billingPageUrl() {
|
|
|
1323
1359
|
return `${base}/billing`;
|
|
1324
1360
|
}
|
|
1325
1361
|
}
|
|
1362
|
+
/** Refusals whose remedy is on the billing page and nowhere else. */
|
|
1363
|
+
const PLAN_LIMITS = [
|
|
1364
|
+
"app-slots-exhausted",
|
|
1365
|
+
"storage-exhausted",
|
|
1366
|
+
"paid-plan-required"
|
|
1367
|
+
];
|
|
1326
1368
|
/** The rule itself is the gateway's prose, and is deliberately not restated here. */
|
|
1327
1369
|
function planLimitAdvice(reason) {
|
|
1328
|
-
if (reason
|
|
1370
|
+
if (!reason || !PLAN_LIMITS.includes(reason)) return "";
|
|
1329
1371
|
return `Where to resolve it: ${billingPageUrl()}`;
|
|
1330
1372
|
}
|
|
1331
1373
|
//#endregion
|
|
@@ -1472,6 +1514,31 @@ function trimTrailingSlashes(origin) {
|
|
|
1472
1514
|
}
|
|
1473
1515
|
//#endregion
|
|
1474
1516
|
//#region src/deploy.ts
|
|
1517
|
+
function declaredFields(declared) {
|
|
1518
|
+
return {
|
|
1519
|
+
...declared.visibility ? { visibility: declared.visibility } : {},
|
|
1520
|
+
...declared.confirmPublish ? { confirm_publish: true } : {}
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* The visibility this project declares, refused here rather than at the gateway
|
|
1525
|
+
* so a typo is a message at the keyboard.
|
|
1526
|
+
*/
|
|
1527
|
+
function declaredVisibility(value) {
|
|
1528
|
+
if (value === void 0 || value === null) return void 0;
|
|
1529
|
+
if (value === "public" || value === "private") return value;
|
|
1530
|
+
console.error(`[wawesome] Error: 'visibility' in wawesome-function.json is '${String(value)}'.`);
|
|
1531
|
+
console.error("[wawesome] It must be \"public\" or \"private\".");
|
|
1532
|
+
process.exit(1);
|
|
1533
|
+
}
|
|
1534
|
+
function reportPublishRefusal(message) {
|
|
1535
|
+
console.error(`\n[wawesome] \x1b[31mError: ${message}\x1b[0m`);
|
|
1536
|
+
console.error("[wawesome] Nothing was deployed. This Function is private, so its address");
|
|
1537
|
+
console.error("[wawesome] resolves for nobody — publishing it puts it back on the open");
|
|
1538
|
+
console.error("[wawesome] internet.");
|
|
1539
|
+
console.error("[wawesome] Re-run with \x1B[36mwawesome deploy --publish\x1B[0m to publish it, or put");
|
|
1540
|
+
console.error("[wawesome] \x1B[36m\"visibility\": \"private\"\x1B[0m back in wawesome-function.json.\n");
|
|
1541
|
+
}
|
|
1475
1542
|
/**
|
|
1476
1543
|
* Deploy a function: build → upload JS to gateway → promote.
|
|
1477
1544
|
*/
|
|
@@ -1489,6 +1556,10 @@ async function deploy(entryInput, options) {
|
|
|
1489
1556
|
process.exit(1);
|
|
1490
1557
|
}
|
|
1491
1558
|
const { app, function: funcName } = config;
|
|
1559
|
+
const declared = {
|
|
1560
|
+
visibility: declaredVisibility(config.visibility),
|
|
1561
|
+
confirmPublish: Boolean(options.publish)
|
|
1562
|
+
};
|
|
1492
1563
|
if (isVerbose) {
|
|
1493
1564
|
console.log(`[wawesome:verbose] Deploying to app=${app}, function=${funcName}`);
|
|
1494
1565
|
console.log(`[wawesome:verbose] Gateway: ${creds.gateway_url}`);
|
|
@@ -1520,12 +1591,14 @@ async function deploy(entryInput, options) {
|
|
|
1520
1591
|
for (const line of surfaceReportLines(findings)) (refused ? console.error : console.warn)(line);
|
|
1521
1592
|
if (refused) process.exit(1);
|
|
1522
1593
|
}
|
|
1594
|
+
const declaresSchedules = config.schedules !== void 0;
|
|
1523
1595
|
const assets = config.assets ? collectAssets(path.resolve(config.assets)) : [];
|
|
1524
|
-
if (assets.length > 0) await uploadAssets(creds, app, funcName, jsCode, assets, isVerbose);
|
|
1596
|
+
if (assets.length > 0) await uploadAssets(creds, app, funcName, jsCode, assets, declared, isVerbose, declaresSchedules);
|
|
1525
1597
|
console.log(`[wawesome] Uploading code for ${app}/${funcName}...`);
|
|
1526
1598
|
const uploadUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/code`;
|
|
1527
1599
|
if (isVerbose) console.log(`[wawesome:verbose] POST ${uploadUrl}`);
|
|
1528
|
-
const
|
|
1600
|
+
const declaredOnTheWire = declaredFields(declared);
|
|
1601
|
+
const uploadRes = assets.length > 0 || declaresSchedules || Object.keys(declaredOnTheWire).length > 0 ? await fetch(uploadUrl, {
|
|
1529
1602
|
method: "POST",
|
|
1530
1603
|
headers: {
|
|
1531
1604
|
Authorization: `Bearer ${creds.tenant_jwt}`,
|
|
@@ -1533,7 +1606,9 @@ async function deploy(entryInput, options) {
|
|
|
1533
1606
|
},
|
|
1534
1607
|
body: JSON.stringify({
|
|
1535
1608
|
code: jsCode,
|
|
1536
|
-
assets: manifestOf(assets)
|
|
1609
|
+
...assets.length > 0 ? { assets: manifestOf(assets) } : {},
|
|
1610
|
+
...declaresSchedules ? { schedules: config.schedules } : {},
|
|
1611
|
+
...declaredOnTheWire
|
|
1537
1612
|
})
|
|
1538
1613
|
}) : await fetch(uploadUrl, {
|
|
1539
1614
|
method: "POST",
|
|
@@ -1545,9 +1620,16 @@ async function deploy(entryInput, options) {
|
|
|
1545
1620
|
});
|
|
1546
1621
|
if (!uploadRes.ok) {
|
|
1547
1622
|
const errorBody = await uploadRes.text();
|
|
1623
|
+
const refusal = rejectionOf(errorBody, uploadRes.status, "Version with this code bundle already exists.");
|
|
1548
1624
|
if (uploadRes.status === 401) console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
|
|
1549
|
-
else if (
|
|
1550
|
-
|
|
1625
|
+
else if (refusal.reason === "paid-plan-required") {
|
|
1626
|
+
console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m`);
|
|
1627
|
+
console.error(`[wawesome] ${planLimitAdvice(refusal.reason)}\n`);
|
|
1628
|
+
} else if (uploadRes.status === 409) {
|
|
1629
|
+
if (refusal.reason === "publish-needs-confirmation") {
|
|
1630
|
+
reportPublishRefusal(refusal.message);
|
|
1631
|
+
process.exit(1);
|
|
1632
|
+
}
|
|
1551
1633
|
console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m`);
|
|
1552
1634
|
console.error("[wawesome] Code versions are immutable and cannot be overwritten.");
|
|
1553
1635
|
console.error("[wawesome] To switch active version, run: \x1B[36mwawesome version switch\x1B[0m\n");
|
|
@@ -1557,33 +1639,40 @@ async function deploy(entryInput, options) {
|
|
|
1557
1639
|
}
|
|
1558
1640
|
process.exit(1);
|
|
1559
1641
|
}
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1642
|
+
const uploadData = await uploadRes.json();
|
|
1643
|
+
const version = uploadData.version_number;
|
|
1644
|
+
const schedules = uploadData.schedules ?? [];
|
|
1645
|
+
const visibility = uploadData.visibility ?? declared.visibility ?? "public";
|
|
1646
|
+
const nothingBuilt = uploadData.status === "declarations-updated";
|
|
1647
|
+
if (nothingBuilt) console.log(`[wawesome] ✅ Code and files unchanged (version ${version ?? "unknown"}); what the config file declares applied.`);
|
|
1648
|
+
else console.log(`[wawesome] ✅ Code uploaded (version ${version ?? "unknown"}).`);
|
|
1649
|
+
if (!nothingBuilt) {
|
|
1650
|
+
console.log(`[wawesome] Promoting to production...`);
|
|
1651
|
+
const deployUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/deploy`;
|
|
1652
|
+
if (isVerbose) console.log(`[wawesome:verbose] POST ${deployUrl}`);
|
|
1653
|
+
const deployBody = {};
|
|
1654
|
+
if (version !== void 0) deployBody.version_number = version;
|
|
1655
|
+
const deployRes = await fetch(deployUrl, {
|
|
1656
|
+
method: "POST",
|
|
1657
|
+
headers: {
|
|
1658
|
+
Authorization: `Bearer ${creds.tenant_jwt}`,
|
|
1659
|
+
"Content-Type": "application/json"
|
|
1660
|
+
},
|
|
1661
|
+
body: JSON.stringify(deployBody)
|
|
1662
|
+
});
|
|
1663
|
+
if (!deployRes.ok) {
|
|
1664
|
+
const errorBody = await deployRes.text();
|
|
1665
|
+
if (deployRes.status === 401) console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
|
|
1666
|
+
else {
|
|
1667
|
+
console.error(`[wawesome] Error: Deployment failed (HTTP ${deployRes.status}).`);
|
|
1668
|
+
if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
|
|
1669
|
+
}
|
|
1670
|
+
process.exit(1);
|
|
1581
1671
|
}
|
|
1582
|
-
process.exit(1);
|
|
1583
1672
|
}
|
|
1584
1673
|
let address = null;
|
|
1585
1674
|
let surface = null;
|
|
1586
|
-
try {
|
|
1675
|
+
if (visibility === "public") try {
|
|
1587
1676
|
const { slug } = await resolveWorkspace(creds);
|
|
1588
1677
|
surface = await fetchInvocationSurface(creds.gateway_url);
|
|
1589
1678
|
address = publicAddress(surface, slug, app, funcName);
|
|
@@ -1597,19 +1686,27 @@ async function deploy(entryInput, options) {
|
|
|
1597
1686
|
if (isVerbose) console.log(`[wawesome:verbose] Could not read the plan's usage: ${errorText(err)}`);
|
|
1598
1687
|
}
|
|
1599
1688
|
console.log("\n======================================================");
|
|
1600
|
-
console.log("🚀 \x1B[32mDEPLOYED SUCCESSFULLY!\x1B[0m");
|
|
1689
|
+
console.log(nothingBuilt ? "🕒 \x1B[32mCONFIGURATION APPLIED\x1B[0m" : "🚀 \x1B[32mDEPLOYED SUCCESSFULLY!\x1B[0m");
|
|
1601
1690
|
console.log("======================================================");
|
|
1602
|
-
console.log(`\n App:
|
|
1603
|
-
console.log(` Function:
|
|
1604
|
-
if (version !== void 0) console.log(` Version:
|
|
1605
|
-
if (assets.length > 0) console.log(` Assets:
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
console.log(
|
|
1691
|
+
console.log(`\n App: ${app}`);
|
|
1692
|
+
console.log(` Function: ${funcName}`);
|
|
1693
|
+
if (version !== void 0) console.log(` Version: ${version}${nothingBuilt ? " (already live, nothing promoted)" : ""}`);
|
|
1694
|
+
if (assets.length > 0) console.log(` Assets: ${assets.length}`);
|
|
1695
|
+
console.log(` Visibility: ${visibility}`);
|
|
1696
|
+
if (schedules.length > 0) {
|
|
1697
|
+
console.log("");
|
|
1698
|
+
for (const line of scheduleLines(schedules)) console.log(line);
|
|
1699
|
+
}
|
|
1700
|
+
if (visibility === "private") {
|
|
1701
|
+
console.log("\n URL: none — this Function is private, so it is not reachable");
|
|
1702
|
+
console.log(" from the web at all.");
|
|
1703
|
+
} else if (address) {
|
|
1704
|
+
console.log(`\n URL: \x1b[36m${address}\x1b[0m`);
|
|
1705
|
+
console.log(` ${SUBTREE_NOTE}`);
|
|
1609
1706
|
} else if (surface) {
|
|
1610
|
-
console.log("\n URL:
|
|
1611
|
-
console.log("
|
|
1612
|
-
console.log("
|
|
1707
|
+
console.log("\n URL: none — this gateway serves no public address form.");
|
|
1708
|
+
console.log(" Set CONTENT_ORIGIN on it, or ALLOW_PATH_INVOCATION_FORM");
|
|
1709
|
+
console.log(" for local development.");
|
|
1613
1710
|
}
|
|
1614
1711
|
if (headroom) {
|
|
1615
1712
|
console.log("");
|
|
@@ -1620,7 +1717,8 @@ async function deploy(entryInput, options) {
|
|
|
1620
1717
|
app,
|
|
1621
1718
|
functionName: funcName,
|
|
1622
1719
|
version,
|
|
1623
|
-
address
|
|
1720
|
+
address,
|
|
1721
|
+
schedules
|
|
1624
1722
|
};
|
|
1625
1723
|
}
|
|
1626
1724
|
/**
|
|
@@ -1630,7 +1728,7 @@ async function deploy(entryInput, options) {
|
|
|
1630
1728
|
* The identity of the whole deploy goes with the question, so a redeploy that
|
|
1631
1729
|
* changed nothing at all is refused here — before a byte of it has moved.
|
|
1632
1730
|
*/
|
|
1633
|
-
async function uploadAssets(creds, app, funcName, bundle, assets, isVerbose) {
|
|
1731
|
+
async function uploadAssets(creds, app, funcName, bundle, assets, declared, isVerbose, continueWhenUnchanged) {
|
|
1634
1732
|
const manifestUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/assets/manifest`;
|
|
1635
1733
|
const manifestRes = await fetch(manifestUrl, {
|
|
1636
1734
|
method: "POST",
|
|
@@ -1640,14 +1738,23 @@ async function uploadAssets(creds, app, funcName, bundle, assets, isVerbose) {
|
|
|
1640
1738
|
},
|
|
1641
1739
|
body: JSON.stringify({
|
|
1642
1740
|
deploy_digest: deployDigest(Buffer.from(bundle, "utf-8"), assets),
|
|
1643
|
-
assets: manifestOf(assets)
|
|
1741
|
+
assets: manifestOf(assets),
|
|
1742
|
+
...declaredFields(declared)
|
|
1644
1743
|
})
|
|
1645
1744
|
});
|
|
1646
1745
|
if (!manifestRes.ok) {
|
|
1647
1746
|
const errorBody = await manifestRes.text();
|
|
1648
1747
|
if (manifestRes.status === 401) console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
|
|
1649
1748
|
else if (manifestRes.status === 409) {
|
|
1749
|
+
if (continueWhenUnchanged) {
|
|
1750
|
+
console.log("[wawesome] Code and files unchanged since the last deploy; applying schedules.");
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1650
1753
|
const refusal = rejectionOf(errorBody, manifestRes.status, "This deploy is already deployed.");
|
|
1754
|
+
if (refusal.reason === "publish-needs-confirmation") {
|
|
1755
|
+
reportPublishRefusal(refusal.message);
|
|
1756
|
+
process.exit(1);
|
|
1757
|
+
}
|
|
1651
1758
|
console.error(`\n[wawesome] \x1b[31mError: ${refusal.message}\x1b[0m`);
|
|
1652
1759
|
console.error("[wawesome] Nothing changed since the last deploy, so nothing was uploaded.\n");
|
|
1653
1760
|
} else {
|
|
@@ -3595,7 +3702,7 @@ async function workspaceCommand(action, target, options) {
|
|
|
3595
3702
|
//#region src/index.ts
|
|
3596
3703
|
const cli = cac("wawesome");
|
|
3597
3704
|
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));
|
|
3598
|
-
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").action((entry, options) => deploy(entry, options));
|
|
3705
|
+
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));
|
|
3599
3706
|
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) => {
|
|
3600
3707
|
if (!action || action === "list" || action === "ls") return listVersions(options);
|
|
3601
3708
|
if (action === "switch" || action === "use" || action === "select") return switchVersion(target, options);
|