@saastemly/voidcommerce 0.3.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/dist/catalog.js +2 -1
- package/dist/cli.js +300 -58
- package/dist/deploy/github.d.ts +85 -0
- package/dist/deploy/index.d.ts +12 -0
- package/dist/deploy/keys.d.ts +77 -0
- package/dist/deploy/link.d.ts +2 -0
- package/dist/deploy/secrets.d.ts +59 -10
- package/dist/generate/ci.d.ts +33 -18
- package/dist/generate/index.d.ts +2 -0
- package/dist/index-0v6na3yp.js +35 -0
- package/dist/{index-84wq4qp8.js → index-5m3t0zfc.js} +244 -503
- package/dist/{index-z4qazajn.js → index-844b3qn9.js} +1 -4
- package/dist/index-b9b4dawy.js +797 -0
- package/dist/index-mpr7gm6k.js +4597 -0
- package/dist/{index-zh3tmj08.js → index-pz6m2hkm.js} +1 -1
- package/dist/index.js +12 -9
- package/dist/keys-b91c72zp.js +19 -0
- package/dist/manifest.js +3 -2
- package/package.json +2 -1
- package/src/cli.ts +17 -1
- package/src/deploy/github.ts +198 -0
- package/src/deploy/index.ts +109 -8
- package/src/deploy/keys.ts +276 -0
- package/src/deploy/link.ts +185 -0
- package/src/deploy/secrets.ts +230 -26
- package/src/dotenvx-primitives.d.ts +14 -0
- package/src/generate/ci.ts +168 -121
- package/src/generate/index.ts +43 -3
- package/src/generate/strict.ts +2 -0
- package/src/init.ts +1 -0
- package/src/regenerate.ts +1 -0
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PRIVATE_KEY_VAR,
|
|
3
|
+
SECRETS_FILE,
|
|
4
|
+
allEnvKeys,
|
|
5
|
+
declaredSecretNames,
|
|
6
|
+
decryptSecrets,
|
|
7
|
+
plaintextSecretNames,
|
|
8
|
+
renderEnvExample,
|
|
9
|
+
renderEnvLocal,
|
|
10
|
+
renderEnvProduction,
|
|
11
|
+
renderEnvTs
|
|
12
|
+
} from "./index-b9b4dawy.js";
|
|
1
13
|
import {
|
|
2
14
|
MANIFEST_FILE,
|
|
3
|
-
envKeysOf,
|
|
4
15
|
has,
|
|
5
16
|
hasFrontend,
|
|
6
|
-
isApex,
|
|
7
17
|
isSingleApp,
|
|
8
18
|
packagesOf,
|
|
9
19
|
readManifest,
|
|
@@ -11,11 +21,13 @@ import {
|
|
|
11
21
|
workerHosts,
|
|
12
22
|
writeManifest,
|
|
13
23
|
zone
|
|
14
|
-
} from "./index-
|
|
24
|
+
} from "./index-pz6m2hkm.js";
|
|
25
|
+
import {
|
|
26
|
+
CHOICES
|
|
27
|
+
} from "./index-844b3qn9.js";
|
|
15
28
|
import {
|
|
16
|
-
CHOICES,
|
|
17
29
|
__require
|
|
18
|
-
} from "./index-
|
|
30
|
+
} from "./index-0v6na3yp.js";
|
|
19
31
|
|
|
20
32
|
// src/generate/auth.ts
|
|
21
33
|
var VAT = {
|
|
@@ -347,146 +359,6 @@ ${render("commerce").replace(/^/gm, "\t")}
|
|
|
347
359
|
}
|
|
348
360
|
var camel = (id) => id.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
349
361
|
|
|
350
|
-
// src/generate/env.ts
|
|
351
|
-
var BASE = [
|
|
352
|
-
{
|
|
353
|
-
key: "SHOP_DOMAIN",
|
|
354
|
-
breaks: "everything derived from it is wrong at once — no public origin, the storefront rejected as untrusted, no DNS records",
|
|
355
|
-
plaintext: true
|
|
356
|
-
},
|
|
357
|
-
{
|
|
358
|
-
key: "COMMERCE_CRON_SECRET",
|
|
359
|
-
breaks: "the scheduler cannot authenticate, so every background job stops",
|
|
360
|
-
where: "openssl rand -base64 32"
|
|
361
|
-
},
|
|
362
|
-
{
|
|
363
|
-
key: "COMMERCE_WEBHOOK_SECRET",
|
|
364
|
-
breaks: "payment webhooks cannot be verified, so no order ever becomes paid",
|
|
365
|
-
where: "the payment provider's dashboard"
|
|
366
|
-
},
|
|
367
|
-
{
|
|
368
|
-
key: "SHIPPING_DOMESTIC_MINOR",
|
|
369
|
-
breaks: "delivery is quoted at nothing, and every order loses the carrier cost",
|
|
370
|
-
plaintext: true,
|
|
371
|
-
dev: "4900",
|
|
372
|
-
where: "your carrier agreement, in minor units ex VAT"
|
|
373
|
-
},
|
|
374
|
-
{
|
|
375
|
-
key: "SHIPPING_FREE_FROM_MINOR",
|
|
376
|
-
breaks: "the free-delivery threshold is undefined",
|
|
377
|
-
plaintext: true,
|
|
378
|
-
dev: "100000"
|
|
379
|
-
}
|
|
380
|
-
];
|
|
381
|
-
function allEnvKeys(manifest) {
|
|
382
|
-
const keys = [...BASE, ...envKeysOf(manifest)];
|
|
383
|
-
if (!isApex(manifest)) {
|
|
384
|
-
keys.splice(1, 0, {
|
|
385
|
-
key: "SHOP_ZONE",
|
|
386
|
-
breaks: "DNS records are written to the wrong zone, or to none",
|
|
387
|
-
plaintext: true
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
if (hasFrontend(manifest.layout) && manifest.shop.pagesHost) {
|
|
391
|
-
keys.splice(1, 0, {
|
|
392
|
-
key: "GITHUB_PAGES_HOST",
|
|
393
|
-
breaks: "the www record has nothing to point at",
|
|
394
|
-
plaintext: true
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
return keys;
|
|
398
|
-
}
|
|
399
|
-
var NUMERIC = new Set(["SHIPPING_DOMESTIC_MINOR", "SHIPPING_FREE_FROM_MINOR"]);
|
|
400
|
-
function renderEnvTs(manifest) {
|
|
401
|
-
const keys = allEnvKeys(manifest);
|
|
402
|
-
const lines = keys.map((key) => {
|
|
403
|
-
const helper = NUMERIC.has(key.key) ? "number()" : "string()";
|
|
404
|
-
const doc = [` /** ${key.breaks}${key.where ? ` — from: ${key.where}` : ""} */`];
|
|
405
|
-
return `${doc.join(`
|
|
406
|
-
`)}
|
|
407
|
-
${key.key}: ${helper},`;
|
|
408
|
-
});
|
|
409
|
-
return `import { defineEnv, number, string } from "void/env";
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Every env key the app reads. All of them are REQUIRED.
|
|
413
|
-
*
|
|
414
|
-
* Generated by \`vc init\` from voidcommerce.json — edit the manifest and
|
|
415
|
-
* regenerate rather than editing this by hand.
|
|
416
|
-
*
|
|
417
|
-
* Nothing is optional: an integration this shop supports is one the
|
|
418
|
-
* deployment sets up. No key has a default, because a default is compiled
|
|
419
|
-
* into the worker's vars and shadows the real secret. Locally, the literal
|
|
420
|
-
* value \`unset\` is the one documented way to say "I do not have this yet",
|
|
421
|
-
* and \`vc preflight\` refuses it.
|
|
422
|
-
*/
|
|
423
|
-
export default defineEnv({
|
|
424
|
-
${lines.join(`
|
|
425
|
-
`)}
|
|
426
|
-
});
|
|
427
|
-
`;
|
|
428
|
-
}
|
|
429
|
-
function renderEnvExample(manifest) {
|
|
430
|
-
const keys = allEnvKeys(manifest);
|
|
431
|
-
return [
|
|
432
|
-
"# Every key is required. Copy to .env for local development.",
|
|
433
|
-
"# `unset` is the one value that reads as absent — for a credential you do not have yet.",
|
|
434
|
-
"",
|
|
435
|
-
...keys.map((key) => `${key.key}=${key.dev ?? (key.plaintext ? "" : "unset")}`),
|
|
436
|
-
""
|
|
437
|
-
].join(`
|
|
438
|
-
`);
|
|
439
|
-
}
|
|
440
|
-
function renderEnvLocal(manifest) {
|
|
441
|
-
const keys = allEnvKeys(manifest);
|
|
442
|
-
const local = {
|
|
443
|
-
SHOP_DOMAIN: `${manifest.shop.domain.split(".")[0]}.test`,
|
|
444
|
-
GITHUB_PAGES_HOST: manifest.shop.pagesHost ?? "",
|
|
445
|
-
SHOP_ZONE: zone(manifest),
|
|
446
|
-
COMMERCE_CRON_SECRET: "dev-cron-secret-not-for-production",
|
|
447
|
-
COMMERCE_WEBHOOK_SECRET: "whsec_dev_secret",
|
|
448
|
-
SYSTEM_API_KEY: "dev-system-key-0123456789abcdefghijklmnopqrstuvwxyz",
|
|
449
|
-
EMAIL_FROM: `"${manifest.shop.name} <noreply@${manifest.shop.domain}>"`
|
|
450
|
-
};
|
|
451
|
-
return [
|
|
452
|
-
"# Local development. Gitignored; .env.example documents every key.",
|
|
453
|
-
"",
|
|
454
|
-
...keys.map((key) => `${key.key}=${local[key.key] ?? key.dev ?? "unset"}`),
|
|
455
|
-
...hasFrontend(manifest.layout) ? ["", "# The storefront, reaching the worker. Start the worker first.", "VITE_API_ORIGIN=http://localhost:5173"] : [],
|
|
456
|
-
""
|
|
457
|
-
].join(`
|
|
458
|
-
`);
|
|
459
|
-
}
|
|
460
|
-
function renderEnvProduction(manifest) {
|
|
461
|
-
const keys = allEnvKeys(manifest).filter((key) => key.plaintext);
|
|
462
|
-
const values = {
|
|
463
|
-
SHOP_DOMAIN: manifest.shop.domain,
|
|
464
|
-
GITHUB_PAGES_HOST: manifest.shop.pagesHost ?? "",
|
|
465
|
-
SHOP_ZONE: zone(manifest),
|
|
466
|
-
EMAIL_FROM: `"${manifest.shop.name} <noreply@${manifest.shop.domain}>"`,
|
|
467
|
-
ADYEN_ENVIRONMENT: "live",
|
|
468
|
-
BC_ENVIRONMENT: "production"
|
|
469
|
-
};
|
|
470
|
-
return [
|
|
471
|
-
"# Production values that are NOT secrets.",
|
|
472
|
-
"#",
|
|
473
|
-
"# On `void deploy --backend cloudflare` every .env* file is baked into the",
|
|
474
|
-
"# worker's vars as PLAINTEXT, so this holds only values safe to commit. Every",
|
|
475
|
-
"# credential is `wrangler secret put <NAME>` instead.",
|
|
476
|
-
"",
|
|
477
|
-
...keys.map((key) => `${key.key}=${values[key.key] ?? key.dev ?? ""}`),
|
|
478
|
-
""
|
|
479
|
-
].join(`
|
|
480
|
-
`);
|
|
481
|
-
}
|
|
482
|
-
function envSummary(manifest) {
|
|
483
|
-
const keys = allEnvKeys(manifest);
|
|
484
|
-
return {
|
|
485
|
-
secrets: keys.filter((key) => !key.plaintext).map((key) => key.key),
|
|
486
|
-
plaintext: keys.filter((key) => key.plaintext).map((key) => key.key)
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
|
|
490
362
|
// src/generate/frontend.ts
|
|
491
363
|
var FRONTEND_OUT = "dist/client";
|
|
492
364
|
var FRONTEND_BRANCH = "frontend-static";
|
|
@@ -841,6 +713,7 @@ function strictDependencies(manifest) {
|
|
|
841
713
|
for (const name of [
|
|
842
714
|
"@hono/node-server",
|
|
843
715
|
"@dotenvx/dotenvx",
|
|
716
|
+
"husky",
|
|
844
717
|
"@rolldown/plugin-babel",
|
|
845
718
|
"@tailwindcss/vite",
|
|
846
719
|
"@types/node",
|
|
@@ -1437,7 +1310,7 @@ import color from "picocolors";
|
|
|
1437
1310
|
// package.json
|
|
1438
1311
|
var package_default = {
|
|
1439
1312
|
name: "@saastemly/voidcommerce",
|
|
1440
|
-
version: "0.
|
|
1313
|
+
version: "0.5.0",
|
|
1441
1314
|
description: "Void, with a shop in it. `vc init` walks you through Better Auth, betterCommerce and every plugin; everything else passes through to `void`.",
|
|
1442
1315
|
type: "module",
|
|
1443
1316
|
license: "MIT",
|
|
@@ -1471,6 +1344,7 @@ var package_default = {
|
|
|
1471
1344
|
},
|
|
1472
1345
|
dependencies: {
|
|
1473
1346
|
"@clack/prompts": "^0.11.0",
|
|
1347
|
+
"@dotenvx/primitives": "^2.2.0",
|
|
1474
1348
|
picocolors: "^1.1.1"
|
|
1475
1349
|
},
|
|
1476
1350
|
devDependencies: {
|
|
@@ -1634,7 +1508,7 @@ import { existsSync as existsSync5 } from "node:fs";
|
|
|
1634
1508
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
1635
1509
|
|
|
1636
1510
|
// src/generate/index.ts
|
|
1637
|
-
import { cp as cp2, mkdir as mkdir2, readFile as readFile2, readdir as readdir2, symlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
1511
|
+
import { cp as cp2, mkdir as mkdir2, readFile as readFile2, readdir as readdir2, rm as rm2, symlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
1638
1512
|
import { existsSync as existsSync4, lstatSync } from "node:fs";
|
|
1639
1513
|
import { dirname as dirname2, join as join4 } from "node:path";
|
|
1640
1514
|
|
|
@@ -1765,36 +1639,38 @@ async function distHelp() {
|
|
|
1765
1639
|
|
|
1766
1640
|
// src/generate/ci.ts
|
|
1767
1641
|
function renderDistWorkflow(manifest) {
|
|
1768
|
-
|
|
1769
|
-
return `name: Build the deployable app
|
|
1642
|
+
return `name: Deploy the shop
|
|
1770
1643
|
|
|
1771
1644
|
# Generated by \`vc init\` from voidcommerce.json.
|
|
1772
1645
|
#
|
|
1773
|
-
#
|
|
1774
|
-
#
|
|
1775
|
-
#
|
|
1776
|
-
#
|
|
1646
|
+
# A push to main regenerates the Void app from the manifest, publishes it to
|
|
1647
|
+
# \`${DIST_BRANCH}\` as a standalone tree, and deploys it to Cloudflare.
|
|
1648
|
+
#
|
|
1649
|
+
# The credentials come from the repository, set once by \`vc link\`:
|
|
1777
1650
|
#
|
|
1778
|
-
#
|
|
1779
|
-
#
|
|
1780
|
-
#
|
|
1781
|
-
#
|
|
1651
|
+
# secrets.CLOUDFLARE_API_TOKEN deploys, and creates D1 + the queue
|
|
1652
|
+
# secrets.${PRIVATE_KEY_VAR} opens ${SECRETS_FILE}
|
|
1653
|
+
# vars.CLOUDFLARE_ACCOUNT_ID which account (an id, not a credential)
|
|
1654
|
+
#
|
|
1655
|
+
# Nothing here needs a Cloudflare dashboard visit and nothing needs wrangler
|
|
1656
|
+
# on your machine. See DEPLOY.md.
|
|
1782
1657
|
on:
|
|
1783
1658
|
push:
|
|
1784
1659
|
branches: [main, master]
|
|
1785
1660
|
workflow_dispatch:
|
|
1786
1661
|
|
|
1787
|
-
# A later push wins: an older tree must never overwrite a newer one
|
|
1662
|
+
# A later push wins: an older tree must never overwrite a newer one, and two
|
|
1663
|
+
# deploys must never race for the same worker.
|
|
1788
1664
|
concurrency:
|
|
1789
|
-
group:
|
|
1665
|
+
group: deploy-\${{ github.repository }}
|
|
1790
1666
|
cancel-in-progress: true
|
|
1791
1667
|
|
|
1792
|
-
# contents: write is what lets GITHUB_TOKEN force-push the branch. Nothing else.
|
|
1793
1668
|
permissions:
|
|
1794
1669
|
contents: write
|
|
1795
1670
|
|
|
1796
1671
|
jobs:
|
|
1797
|
-
|
|
1672
|
+
# ── Regenerate, and publish the standalone tree ────────────────────────
|
|
1673
|
+
dist:
|
|
1798
1674
|
runs-on: ubuntu-latest
|
|
1799
1675
|
steps:
|
|
1800
1676
|
- uses: actions/checkout@v6
|
|
@@ -1835,6 +1711,50 @@ jobs:
|
|
|
1835
1711
|
git add -A
|
|
1836
1712
|
git commit -q -m "\${{ github.sha }} — ${manifest.shop.domain}"
|
|
1837
1713
|
git push -f "https://x-access-token:\${GITHUB_TOKEN}@github.com/\${{ github.repository }}.git" ${DIST_BRANCH}
|
|
1714
|
+
|
|
1715
|
+
# ── Deploy it ──────────────────────────────────────────────────────────
|
|
1716
|
+
deploy:
|
|
1717
|
+
needs: dist
|
|
1718
|
+
runs-on: ubuntu-latest
|
|
1719
|
+
environment:
|
|
1720
|
+
name: production
|
|
1721
|
+
url: https://${manifest.shop.domain}
|
|
1722
|
+
env:
|
|
1723
|
+
CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
1724
|
+
CLOUDFLARE_ACCOUNT_ID: \${{ vars.CLOUDFLARE_ACCOUNT_ID }}
|
|
1725
|
+
${PRIVATE_KEY_VAR}: \${{ secrets.${PRIVATE_KEY_VAR} }}
|
|
1726
|
+
steps:
|
|
1727
|
+
- uses: actions/checkout@v6
|
|
1728
|
+
- uses: oven-sh/setup-bun@v2
|
|
1729
|
+
|
|
1730
|
+
# Checked before anything is built, so a repository that was never
|
|
1731
|
+
# linked says so in five seconds rather than four minutes.
|
|
1732
|
+
- name: Are the credentials here?
|
|
1733
|
+
run: |
|
|
1734
|
+
set -euo pipefail
|
|
1735
|
+
missing=""
|
|
1736
|
+
[ -n "\${CLOUDFLARE_API_TOKEN:-}" ] || missing="$missing CLOUDFLARE_API_TOKEN"
|
|
1737
|
+
[ -n "\${${PRIVATE_KEY_VAR}:-}" ] || missing="$missing ${PRIVATE_KEY_VAR}"
|
|
1738
|
+
if [ -n "$missing" ]; then
|
|
1739
|
+
echo "::error::this repository has no$missing. Run 'vc link' once, from a checkout."
|
|
1740
|
+
exit 1
|
|
1741
|
+
fi
|
|
1742
|
+
|
|
1743
|
+
- run: bun install --frozen-lockfile
|
|
1744
|
+
|
|
1745
|
+
# One command: preflight, provision D1 and the queue if they are not
|
|
1746
|
+
# there, build, strip the values Void bakes into the worker's plaintext
|
|
1747
|
+
# vars, apply the committed migrations, and deploy with the decrypted
|
|
1748
|
+
# secrets attached to the version. Provisioning is idempotent — it looks
|
|
1749
|
+
# a resource up by name before creating it — so this is safe every run.
|
|
1750
|
+
- name: vc deploy --cloudflare --provision
|
|
1751
|
+
run: bunx vc deploy --cloudflare --provision
|
|
1752
|
+
|
|
1753
|
+
# An upsert, so it is safe on every deploy and costs one pass when
|
|
1754
|
+
# nothing in data/ has changed. A failure here does not un-deploy a
|
|
1755
|
+
# working shop, so it warns rather than failing the run.
|
|
1756
|
+
- name: Push the catalogue
|
|
1757
|
+
run: bunx vc import || echo "::warning::the catalogue did not import; the shop is up but its products may be stale"
|
|
1838
1758
|
`;
|
|
1839
1759
|
}
|
|
1840
1760
|
function renderDeployReadme(manifest, zone2, hosts) {
|
|
@@ -1844,132 +1764,117 @@ function renderDeployReadme(manifest, zone2, hosts) {
|
|
|
1844
1764
|
\`${manifest.shop.domain}\` on Cloudflare Workers. Generated by \`vc init\`;
|
|
1845
1765
|
regenerate with \`vc generate\`.
|
|
1846
1766
|
|
|
1847
|
-
## The loop
|
|
1848
|
-
|
|
1849
|
-
Push to \`main\`. GitHub Actions regenerates the app from \`voidcommerce.json\`
|
|
1850
|
-
and force-pushes it to \`${DIST_BRANCH}\`; Cloudflare's Workers Builds builds that
|
|
1851
|
-
branch and deploys it. Nothing else runs.
|
|
1852
|
-
|
|
1853
|
-
Your secrets survive every deploy — \`wrangler deploy\` never deletes a secret.
|
|
1854
|
-
Plaintext \`vars\` are replaced from \`.env.production\` on each deploy, which is
|
|
1855
|
-
why no credential is allowed in that file.
|
|
1856
|
-
|
|
1857
|
-
## Once, before the first push
|
|
1858
|
-
|
|
1859
|
-
Each of these either creates something in your Cloudflare account or holds a
|
|
1860
|
-
credential, so none of them can live in a repository.
|
|
1861
|
-
|
|
1862
|
-
### 1. The zone
|
|
1863
|
-
|
|
1864
|
-
\`${zone2}\` must be on Cloudflare. A Worker custom domain is a record Cloudflare
|
|
1865
|
-
creates in its own zone, so a domain hosted anywhere else cannot have one. The
|
|
1866
|
-
worker answers on ${hosts.map((h) => `\`${h}\``).join(" and ")}.
|
|
1867
|
-
|
|
1868
|
-
### 2. The database, the queue, and the account id
|
|
1767
|
+
## The loop
|
|
1869
1768
|
|
|
1870
1769
|
\`\`\`sh
|
|
1871
|
-
|
|
1770
|
+
git push
|
|
1872
1771
|
\`\`\`
|
|
1873
1772
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
and
|
|
1877
|
-
|
|
1773
|
+
That is the deploy. GitHub Actions regenerates the app from
|
|
1774
|
+
\`voidcommerce.json\`, publishes a standalone copy to \`${DIST_BRANCH}\`, creates the
|
|
1775
|
+
database and queue if they are not there, applies the migrations, and deploys
|
|
1776
|
+
the worker with this repository's secrets attached.
|
|
1878
1777
|
|
|
1879
|
-
|
|
1778
|
+
You do not need wrangler on your machine, and you do not need to be logged
|
|
1779
|
+
into it. You do not need to open the Cloudflare dashboard after the one step
|
|
1780
|
+
below.
|
|
1880
1781
|
|
|
1881
|
-
|
|
1782
|
+
## Once, before the first push
|
|
1882
1783
|
|
|
1883
1784
|
\`\`\`sh
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
bunx dotenvx encrypt -f .env.secrets # ciphertext; commit this
|
|
1785
|
+
gh repo create --source=. --private --push # if there is no repo yet
|
|
1786
|
+
vc link
|
|
1887
1787
|
\`\`\`
|
|
1888
1788
|
|
|
1889
|
-
|
|
1890
|
-
hands the values to \`wrangler deploy --secrets-file\`, which stores them as
|
|
1891
|
-
real Worker secrets — not as plaintext \`vars\`, which anyone with dashboard
|
|
1892
|
-
access can read.
|
|
1789
|
+
\`vc link\` does three things and stores all of them on the GitHub repository:
|
|
1893
1790
|
|
|
1894
|
-
|
|
1895
|
-
|
|
1791
|
+
| what | where | why |
|
|
1792
|
+
|---|---|---|
|
|
1793
|
+
| \`${PRIVATE_KEY_VAR}\` | repository **secret** | opens \`${SECRETS_FILE}\`. Generated by \`vc link\`, never written to disk |
|
|
1794
|
+
| \`CLOUDFLARE_API_TOKEN\` | repository **secret** | deploys, and creates D1 and the queue |
|
|
1795
|
+
| \`CLOUDFLARE_ACCOUNT_ID\` | repository **variable** | which account. An identifier, not a credential |
|
|
1896
1796
|
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
\`wrangler secret put\` does not have that property, and stays available for
|
|
1900
|
-
anything you would rather never commit. \`vc preflight\` counts a secret the
|
|
1901
|
-
repository declares as present, and refuses any value committed in the clear.
|
|
1797
|
+
It will ask you to paste a Cloudflare API token. That is the only manual step
|
|
1798
|
+
in the whole setup, and it is worth saying exactly why it cannot be removed:
|
|
1902
1799
|
|
|
1903
|
-
|
|
1800
|
+
> **GitHub cannot mint a Cloudflare credential.** There is no OIDC or workload
|
|
1801
|
+
> identity federation between them — the feature request has been open since
|
|
1802
|
+
> 2025 with no commitment, and Cloudflare's own CI guidance still says to store
|
|
1803
|
+
> a token in your CI provider's secrets. The Cloudflare GitHub App does not
|
|
1804
|
+
> help either: it grants *Cloudflare* access to your *repository*, not the
|
|
1805
|
+
> reverse. Something has to authorise creating a database in your account, and
|
|
1806
|
+
> only Cloudflare can issue that authorisation.
|
|
1904
1807
|
|
|
1905
|
-
|
|
1906
|
-
Workers Builds automatically, and **that token has no D1 permission** — its
|
|
1907
|
-
scopes are Workers Scripts, KV, R2, Workers Routes and account/user reads. The
|
|
1908
|
-
deploy command below applies database migrations, so it needs more.
|
|
1808
|
+
Create the token at **My Profile → API Tokens → Create Token → Custom token**:
|
|
1909
1809
|
|
|
1910
|
-
|
|
1810
|
+
| scope | permission | for |
|
|
1811
|
+
|---|---|---|
|
|
1812
|
+
| Account | Workers Scripts: Edit | deploying the worker |
|
|
1813
|
+
| Account | D1: Edit | creating the database, applying migrations |
|
|
1814
|
+
| Account | Queues: Edit | the order queue |
|
|
1815
|
+
| Account | Workers KV Storage: Edit | sessions and caches |
|
|
1816
|
+
| Account | Workers R2 Storage: Edit | product images |
|
|
1817
|
+
| Account | Account Settings: Read | confirming which account |
|
|
1818
|
+
| Zone | Workers Routes: Edit (${zone2}) | answering on your domain |
|
|
1819
|
+
| User | User Details: Read, Memberships: Read | wrangler asks at startup |
|
|
1911
1820
|
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
| Account | Workers Scripts: Edit |
|
|
1915
|
-
| Account | D1: Edit |
|
|
1916
|
-
| Account | Workers KV Storage: Edit, Workers R2 Storage: Edit |
|
|
1917
|
-
| Account | Queues: Edit |
|
|
1918
|
-
| Account | Account Settings: Read |
|
|
1919
|
-
| Zone | Workers Routes: Edit (${zone2}) |
|
|
1920
|
-
| User | User Details: Read, Memberships: Read |
|
|
1821
|
+
The token Cloudflare generates for its own Workers Builds will **not** do: it
|
|
1822
|
+
has no D1 and no Queues permission, so it cannot create this shop's database.
|
|
1921
1823
|
|
|
1922
|
-
|
|
1923
|
-
deploy of this Worker; permissions are whatever that token has.
|
|
1824
|
+
### The zone
|
|
1924
1825
|
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
**Settings → Builds → Connect**. The Worker's name must equal the \`name\` in
|
|
1929
|
-
the \`wrangler.jsonc\` at the root directory, or the build fails.
|
|
1826
|
+
\`${zone2}\` must be on Cloudflare. A Worker custom domain is a record Cloudflare
|
|
1827
|
+
creates in its own zone, so a domain hosted anywhere else cannot have one. The
|
|
1828
|
+
worker answers on ${hosts.map((h) => `\`${h}\``).join(" and ")}.
|
|
1930
1829
|
|
|
1931
|
-
|
|
1932
|
-
|---|---|
|
|
1933
|
-
| branch | \`${DIST_BRANCH}\` |
|
|
1934
|
-
| root directory | \`/\` |
|
|
1935
|
-
| build command | \`bun install && bunx void prepare && bunx vp build\` |
|
|
1936
|
-
| deploy command | see below |
|
|
1937
|
-
| build variable | \`DOTENV_PRIVATE_KEY_SECRETS\`, marked as a secret — the one value that is not in the repository |
|
|
1938
|
-
| API token | the one from step 4 |
|
|
1830
|
+
## Secrets
|
|
1939
1831
|
|
|
1940
|
-
|
|
1832
|
+
They live in the repository, encrypted, in \`${SECRETS_FILE}\`:
|
|
1941
1833
|
|
|
1942
1834
|
\`\`\`sh
|
|
1943
|
-
|
|
1835
|
+
vc secrets set STRIPE_SECRET_KEY # prompts; never touches your shell history
|
|
1836
|
+
vc secrets # what is set, what is missing
|
|
1944
1837
|
\`\`\`
|
|
1945
1838
|
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
a secret
|
|
1839
|
+
**This needs no credential.** dotenvx is asymmetric: encryption uses the public
|
|
1840
|
+
key committed at the top of the file, so anyone with a clone can set or rotate
|
|
1841
|
+
a secret. Nobody with a clone can read one. Only the deploy decrypts, with the
|
|
1842
|
+
private key that lives in GitHub Actions.
|
|
1949
1843
|
|
|
1950
|
-
|
|
1951
|
-
|
|
1844
|
+
A pre-commit hook refuses a commit that would put a value in the clear, because
|
|
1845
|
+
that cannot be undone by a later commit — the value stays in the history.
|
|
1952
1846
|
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1847
|
+
**Know the tradeoff.** Ciphertext in git is permanent: if the private key ever
|
|
1848
|
+
leaks, every secret in the history is readable, including ones you rotated.
|
|
1849
|
+
\`wrangler secret put\` does not have that property, and stays available for
|
|
1850
|
+
anything you would rather never commit at all.
|
|
1956
1851
|
|
|
1957
|
-
|
|
1852
|
+
## The catalogue
|
|
1958
1853
|
|
|
1959
|
-
\`vc import\` pushes \`data/\` into the running shop
|
|
1960
|
-
|
|
1854
|
+
\`vc import\` pushes \`data/\` into the running shop, and the workflow runs it on
|
|
1855
|
+
every deploy. It is an upsert, so it costs one pass when nothing has changed.
|
|
1961
1856
|
|
|
1962
|
-
##
|
|
1857
|
+
## Doing it by hand
|
|
1963
1858
|
|
|
1964
|
-
The same thing
|
|
1859
|
+
The same thing, from a checkout, if CI is ever not the answer:
|
|
1965
1860
|
|
|
1966
1861
|
\`\`\`sh
|
|
1967
|
-
|
|
1862
|
+
export CLOUDFLARE_API_TOKEN=…
|
|
1863
|
+
export ${PRIVATE_KEY_VAR}=… # only if you kept a copy
|
|
1864
|
+
vc deploy --cloudflare --provision
|
|
1968
1865
|
\`\`\`
|
|
1969
1866
|
|
|
1970
|
-
which preflights, builds, strips the baked development values out
|
|
1971
|
-
worker's vars, applies the remote migrations, and deploys exactly what
|
|
1972
|
-
verified.
|
|
1867
|
+
which preflights, provisions, builds, strips the baked development values out
|
|
1868
|
+
of the worker's vars, applies the remote migrations, and deploys exactly what
|
|
1869
|
+
it verified.
|
|
1870
|
+
|
|
1871
|
+
## If you would rather use Cloudflare's own build
|
|
1872
|
+
|
|
1873
|
+
Point Workers Builds at the \`${DIST_BRANCH}\` branch — it is a plain Void app with
|
|
1874
|
+
a committed lockfile. The Worker must be named \`${worker}\`, matching the \`name\`
|
|
1875
|
+
in its \`wrangler.jsonc\`. You will need to set \`${PRIVATE_KEY_VAR}\` as a
|
|
1876
|
+
build secret in the dashboard, and replace the auto-generated API token with
|
|
1877
|
+
one that has D1 and Queues. That is the path \`vc link\` exists to avoid.
|
|
1973
1878
|
`;
|
|
1974
1879
|
}
|
|
1975
1880
|
|
|
@@ -2308,6 +2213,13 @@ async function put(root, file, content, result, mode) {
|
|
|
2308
2213
|
await writeFile2(path, content, "utf8");
|
|
2309
2214
|
result.written.push(file);
|
|
2310
2215
|
}
|
|
2216
|
+
async function retire(root, file, result) {
|
|
2217
|
+
const path = join4(root, file);
|
|
2218
|
+
if (!await exists(path))
|
|
2219
|
+
return;
|
|
2220
|
+
await rm2(path, { force: true });
|
|
2221
|
+
result.retired.push(file);
|
|
2222
|
+
}
|
|
2311
2223
|
async function mergeJson(root, file, fallback, mutate, result) {
|
|
2312
2224
|
const path = join4(root, file);
|
|
2313
2225
|
const json = await exists(path) ? JSON.parse(await readFile2(path, "utf8")) : { ...fallback };
|
|
@@ -2331,7 +2243,7 @@ async function link(root, file, target, result) {
|
|
|
2331
2243
|
}
|
|
2332
2244
|
var sorted = (record) => Object.fromEntries(Object.entries(record).sort());
|
|
2333
2245
|
async function generate(root, manifest) {
|
|
2334
|
-
const result = { written: [], kept: [], packages: packagesOf(manifest) };
|
|
2246
|
+
const result = { written: [], kept: [], retired: [], packages: packagesOf(manifest) };
|
|
2335
2247
|
await writeManifest(root, manifest);
|
|
2336
2248
|
result.written.push(MANIFEST_FILE);
|
|
2337
2249
|
switch (manifest.layout) {
|
|
@@ -2478,6 +2390,8 @@ async function generateStrictRoot(root, manifest, result) {
|
|
|
2478
2390
|
scripts["deploy"] ??= "vc deploy";
|
|
2479
2391
|
scripts["import:catalog"] ??= "vc import";
|
|
2480
2392
|
scripts["maildev"] ??= "maildev --smtp 1025 --web 1080";
|
|
2393
|
+
scripts["secrets"] ??= "vc secrets";
|
|
2394
|
+
scripts["prepare"] ??= "husky";
|
|
2481
2395
|
pkg["scripts"] = scripts;
|
|
2482
2396
|
}, result);
|
|
2483
2397
|
await put(root, ".gitignore", `node_modules
|
|
@@ -2492,7 +2406,14 @@ dist
|
|
|
2492
2406
|
.DS_Store
|
|
2493
2407
|
`, result, "own");
|
|
2494
2408
|
await put(root, "patches/void@0.10.13.patch", renderVoidPatch(), result, "regenerate");
|
|
2495
|
-
await put(root, ".
|
|
2409
|
+
await put(root, ".husky/pre-commit", `#!/usr/bin/env sh
|
|
2410
|
+
# Generated by \`vc init\`. Refuses a commit that would put a secret in the
|
|
2411
|
+
# clear in .env.secrets — which cannot be undone by a later commit,
|
|
2412
|
+
# because the value stays in the history.
|
|
2413
|
+
bunx vc guard
|
|
2414
|
+
`, result, "regenerate");
|
|
2415
|
+
await put(root, ".github/workflows/deploy.yml", renderDistWorkflow(manifest), result, "regenerate");
|
|
2416
|
+
await retire(root, ".github/workflows/void-dist.yml", result);
|
|
2496
2417
|
await put(root, "DEPLOY.md", renderDeployReadme(manifest, zone(manifest), workerHosts(manifest)), result, "regenerate");
|
|
2497
2418
|
await put(root, ".env", renderEnvLocal(manifest), result, "own");
|
|
2498
2419
|
await put(root, "data/README.md", DATA_README, result, "own");
|
|
@@ -2856,196 +2777,16 @@ ${ran.out.trim()}`);
|
|
|
2856
2777
|
}
|
|
2857
2778
|
|
|
2858
2779
|
// src/deploy/preflight.ts
|
|
2859
|
-
import { existsSync as
|
|
2860
|
-
import { join as
|
|
2861
|
-
import color4 from "picocolors";
|
|
2862
|
-
|
|
2863
|
-
// src/deploy/secrets.ts
|
|
2864
|
-
import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
2865
|
-
import { spawn as spawn3 } from "node:child_process";
|
|
2866
|
-
import { delimiter as delimiter3, dirname as dirname5, join as join7 } from "node:path";
|
|
2867
|
-
import { tmpdir } from "node:os";
|
|
2780
|
+
import { existsSync as existsSync7, readFileSync as readFileSync2 } from "node:fs";
|
|
2781
|
+
import { join as join7 } from "node:path";
|
|
2868
2782
|
import color3 from "picocolors";
|
|
2869
|
-
var SECRETS_FILE = ".env.secrets";
|
|
2870
|
-
var PRIVATE_KEY_VAR = "DOTENV_PRIVATE_KEY_SECRETS";
|
|
2871
|
-
function findDotenvx(from) {
|
|
2872
|
-
let dir = from;
|
|
2873
|
-
for (;; ) {
|
|
2874
|
-
const local = join7(dir, "node_modules", ".bin", "dotenvx");
|
|
2875
|
-
if (existsSync7(local))
|
|
2876
|
-
return local;
|
|
2877
|
-
const parent = dirname5(dir);
|
|
2878
|
-
if (parent === dir)
|
|
2879
|
-
break;
|
|
2880
|
-
dir = parent;
|
|
2881
|
-
}
|
|
2882
|
-
return (process.env["PATH"] ?? "").split(delimiter3).filter(Boolean).some((entry) => existsSync7(join7(entry, "dotenvx"))) ? "dotenvx" : null;
|
|
2883
|
-
}
|
|
2884
|
-
function run(cmd, args, cwd, env = {}) {
|
|
2885
|
-
return new Promise((resolve) => {
|
|
2886
|
-
const child = spawn3(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, ...env } });
|
|
2887
|
-
let out = "";
|
|
2888
|
-
child.stdout.on("data", (chunk) => {
|
|
2889
|
-
out += chunk;
|
|
2890
|
-
});
|
|
2891
|
-
child.stderr.on("data", (chunk) => {
|
|
2892
|
-
out += chunk;
|
|
2893
|
-
});
|
|
2894
|
-
child.on("error", (error) => resolve({ code: 1, out: String(error) }));
|
|
2895
|
-
child.on("exit", (code) => resolve({ code: code ?? 1, out }));
|
|
2896
|
-
});
|
|
2897
|
-
}
|
|
2898
|
-
function declaredSecretNames(root) {
|
|
2899
|
-
const path = join7(root, SECRETS_FILE);
|
|
2900
|
-
if (!existsSync7(path))
|
|
2901
|
-
return new Set;
|
|
2902
|
-
const names = new Set;
|
|
2903
|
-
for (const line2 of readFileSync2(path, "utf8").split(`
|
|
2904
|
-
`)) {
|
|
2905
|
-
const match = /^\s*([A-Z][A-Z0-9_]*)\s*=/.exec(line2);
|
|
2906
|
-
if (match && !match[1].startsWith("DOTENV_"))
|
|
2907
|
-
names.add(match[1]);
|
|
2908
|
-
}
|
|
2909
|
-
return names;
|
|
2910
|
-
}
|
|
2911
|
-
function plaintextSecretNames(root) {
|
|
2912
|
-
const path = join7(root, SECRETS_FILE);
|
|
2913
|
-
if (!existsSync7(path))
|
|
2914
|
-
return [];
|
|
2915
|
-
const bare = [];
|
|
2916
|
-
for (const line2 of readFileSync2(path, "utf8").split(`
|
|
2917
|
-
`)) {
|
|
2918
|
-
const match = /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)$/.exec(line2);
|
|
2919
|
-
if (!match || match[1].startsWith("DOTENV_"))
|
|
2920
|
-
continue;
|
|
2921
|
-
const value = match[2].trim().replace(/^['"]|['"]$/g, "");
|
|
2922
|
-
if (value && value !== "unset" && !value.startsWith("encrypted:"))
|
|
2923
|
-
bare.push(match[1]);
|
|
2924
|
-
}
|
|
2925
|
-
return bare;
|
|
2926
|
-
}
|
|
2927
|
-
async function decryptSecrets(project) {
|
|
2928
|
-
const root = project.root;
|
|
2929
|
-
if (!existsSync7(join7(root, SECRETS_FILE)))
|
|
2930
|
-
return { error: `${SECRETS_FILE} does not exist — \`vc secrets init\` writes one` };
|
|
2931
|
-
const dotenvx = findDotenvx(root);
|
|
2932
|
-
if (!dotenvx)
|
|
2933
|
-
return { error: "dotenvx is not installed. `bun add -d @dotenvx/dotenvx`" };
|
|
2934
|
-
if (!process.env[PRIVATE_KEY_VAR] && !existsSync7(join7(root, ".env.keys"))) {
|
|
2935
|
-
return {
|
|
2936
|
-
error: `${PRIVATE_KEY_VAR} is not set and there is no .env.keys here.
|
|
2937
|
-
It is the one value that stays out of the repository; set it where the deploy runs.`
|
|
2938
|
-
};
|
|
2939
|
-
}
|
|
2940
|
-
const result = await run(dotenvx, ["decrypt", "-f", SECRETS_FILE, "--stdout"], root);
|
|
2941
|
-
if (result.code !== 0)
|
|
2942
|
-
return { error: `dotenvx could not decrypt ${SECRETS_FILE}: ${result.out.trim().split(`
|
|
2943
|
-
`).slice(-2).join(" ")}` };
|
|
2944
|
-
const lines = result.out.split(`
|
|
2945
|
-
`).filter((line2) => /^\s*[A-Z][A-Z0-9_]*\s*=/.test(line2) && !line2.trimStart().startsWith("DOTENV_"));
|
|
2946
|
-
const names = lines.map((line2) => line2.slice(0, line2.indexOf("=")).trim());
|
|
2947
|
-
const path = join7(tmpdir(), `vc-secrets-${process.pid}-${Date.now()}.env`);
|
|
2948
|
-
writeFileSync(path, `${lines.join(`
|
|
2949
|
-
`)}
|
|
2950
|
-
`, { mode: 384 });
|
|
2951
|
-
return {
|
|
2952
|
-
path,
|
|
2953
|
-
names,
|
|
2954
|
-
cleanup: () => {
|
|
2955
|
-
try {
|
|
2956
|
-
writeFileSync(path, "", { mode: 384 });
|
|
2957
|
-
__require("node:fs").unlinkSync(path);
|
|
2958
|
-
} catch {}
|
|
2959
|
-
}
|
|
2960
|
-
};
|
|
2961
|
-
}
|
|
2962
|
-
async function secretsCommand(project, args) {
|
|
2963
|
-
const root = project.root;
|
|
2964
|
-
const required = allEnvKeys(project.manifest).filter((key) => !key.plaintext);
|
|
2965
|
-
const declared = declaredSecretNames(root);
|
|
2966
|
-
const bare = plaintextSecretNames(root);
|
|
2967
|
-
if (args.includes("--init"))
|
|
2968
|
-
return initSecrets(project);
|
|
2969
|
-
if (!existsSync7(join7(root, SECRETS_FILE))) {
|
|
2970
|
-
console.log(`
|
|
2971
|
-
${color3.yellow("No " + SECRETS_FILE + " yet.")} Secrets are set by hand with \`wrangler secret put\`,
|
|
2972
|
-
` + `which means the shop cannot be rebuilt from a checkout.
|
|
2973
|
-
|
|
2974
|
-
` + ` vc secrets --init write one, with every required key as \`unset\`
|
|
2975
|
-
`);
|
|
2976
|
-
return 1;
|
|
2977
|
-
}
|
|
2978
|
-
console.log(`
|
|
2979
|
-
${SECRETS_FILE} — committed, encrypted, and read by \`vc deploy --cloudflare\`
|
|
2980
|
-
`);
|
|
2981
|
-
for (const key of required) {
|
|
2982
|
-
const state = declared.has(key.key) ? color3.green("declared") : color3.red("MISSING ");
|
|
2983
|
-
console.log(` ${state} ${key.key}`);
|
|
2984
|
-
}
|
|
2985
|
-
const extra = [...declared].filter((name) => !required.some((key) => key.key === name));
|
|
2986
|
-
for (const name of extra)
|
|
2987
|
-
console.log(` ${color3.dim("extra ")} ${name} ${color3.dim("— not required by this shop")}`);
|
|
2988
|
-
if (bare.length > 0) {
|
|
2989
|
-
console.log(`
|
|
2990
|
-
${color3.red("✗")} ${bare.length} value${bare.length === 1 ? " is" : "s are"} committed IN THE CLEAR: ${bare.join(", ")}
|
|
2991
|
-
` + ` Run \`dotenvx encrypt -f ${SECRETS_FILE}\` before committing again.
|
|
2992
|
-
`);
|
|
2993
|
-
return 1;
|
|
2994
|
-
}
|
|
2995
|
-
const missing = required.filter((key) => !declared.has(key.key));
|
|
2996
|
-
if (missing.length > 0) {
|
|
2997
|
-
console.log(`
|
|
2998
|
-
${color3.red("✗")} ${missing.length} required secret${missing.length === 1 ? "" : "s"} not declared.
|
|
2999
|
-
` + ` dotenvx set ${missing[0].key} '…' -f ${SECRETS_FILE}
|
|
3000
|
-
`);
|
|
3001
|
-
return 1;
|
|
3002
|
-
}
|
|
3003
|
-
console.log(`
|
|
3004
|
-
${color3.green("✓")} every secret this shop needs is declared and encrypted.
|
|
3005
|
-
`);
|
|
3006
|
-
return 0;
|
|
3007
|
-
}
|
|
3008
|
-
async function initSecrets(project) {
|
|
3009
|
-
const path = join7(project.root, SECRETS_FILE);
|
|
3010
|
-
if (existsSync7(path)) {
|
|
3011
|
-
console.error(`vc: ${SECRETS_FILE} already exists; not overwriting it.`);
|
|
3012
|
-
return 1;
|
|
3013
|
-
}
|
|
3014
|
-
const required = allEnvKeys(project.manifest).filter((key) => !key.plaintext);
|
|
3015
|
-
const body = [
|
|
3016
|
-
"# Secrets, encrypted, and COMMITTED.",
|
|
3017
|
-
"#",
|
|
3018
|
-
"# Values are ciphertext; the key names are readable so a diff shows WHICH",
|
|
3019
|
-
"# secret changed without showing what it changed to. `unset` is the",
|
|
3020
|
-
"# documented placeholder and preflight refuses it.",
|
|
3021
|
-
"#",
|
|
3022
|
-
"# The private key stays out of the repository. It lives in .env.keys",
|
|
3023
|
-
"# locally (gitignored) and as one build variable where the deploy runs.",
|
|
3024
|
-
"",
|
|
3025
|
-
...required.flatMap((key) => [`# ${key.breaks}${key.where ? ` — from: ${key.where}` : ""}`, `${key.key}=unset`, ""])
|
|
3026
|
-
].join(`
|
|
3027
|
-
`);
|
|
3028
|
-
writeFileSync(path, body, { mode: 384 });
|
|
3029
|
-
console.log(`
|
|
3030
|
-
${color3.green("+")} ${SECRETS_FILE} — ${required.length} keys, all \`unset\`
|
|
3031
|
-
|
|
3032
|
-
` + `Next:
|
|
3033
|
-
` + ` 1. put the real values in, then
|
|
3034
|
-
` + ` 2. ${color3.cyan(`dotenvx encrypt -f ${SECRETS_FILE}`)}
|
|
3035
|
-
` + ` 3. commit ${SECRETS_FILE}; never commit .env.keys
|
|
3036
|
-
` + ` 4. set ${PRIVATE_KEY_VAR} where the deploy runs
|
|
3037
|
-
`);
|
|
3038
|
-
return 0;
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
|
-
// src/deploy/preflight.ts
|
|
3042
2783
|
var UNSET = "unset";
|
|
3043
2784
|
function productionEnv(appDir) {
|
|
3044
2785
|
const out = new Map;
|
|
3045
|
-
const path =
|
|
3046
|
-
if (!
|
|
2786
|
+
const path = join7(appDir, ".env.production");
|
|
2787
|
+
if (!existsSync7(path))
|
|
3047
2788
|
return out;
|
|
3048
|
-
for (const line2 of
|
|
2789
|
+
for (const line2 of readFileSync2(path, "utf8").split(`
|
|
3049
2790
|
`)) {
|
|
3050
2791
|
const match = /^\s*([A-Z0-9_]+)\s*=\s*(.*)$/.exec(line2);
|
|
3051
2792
|
if (!match)
|
|
@@ -3070,12 +2811,12 @@ async function voidSecretNames(appDir) {
|
|
|
3070
2811
|
return names;
|
|
3071
2812
|
}
|
|
3072
2813
|
function routeProblem(project) {
|
|
3073
|
-
const path =
|
|
3074
|
-
if (!
|
|
2814
|
+
const path = join7(project.appDir, "wrangler.jsonc");
|
|
2815
|
+
if (!existsSync7(path))
|
|
3075
2816
|
return "wrangler.jsonc is missing";
|
|
3076
2817
|
let routes = [];
|
|
3077
2818
|
try {
|
|
3078
|
-
routes = parseJsonc(
|
|
2819
|
+
routes = parseJsonc(readFileSync2(path, "utf8")).routes ?? [];
|
|
3079
2820
|
} catch {
|
|
3080
2821
|
return "wrangler.jsonc cannot be parsed";
|
|
3081
2822
|
}
|
|
@@ -3115,7 +2856,7 @@ async function preflight(project, source) {
|
|
|
3115
2856
|
function printPreflight(project, result, source) {
|
|
3116
2857
|
const keys = allEnvKeys(project.manifest);
|
|
3117
2858
|
if (result.remote === null) {
|
|
3118
|
-
console.log(
|
|
2859
|
+
console.log(color3.dim(source === "wrangler" ? `Could not read the worker's secrets — not deployed yet, or wrangler is not logged in.
|
|
3119
2860
|
Anything not in .env.production is reported as missing.
|
|
3120
2861
|
` : `Could not read the project's secrets from Void — not logged in, or no linked project.
|
|
3121
2862
|
Anything not in .env.production is reported as missing.
|
|
@@ -3125,37 +2866,37 @@ Anything not in .env.production is reported as missing.
|
|
|
3125
2866
|
for (const key of keys) {
|
|
3126
2867
|
const value = result.present.get(key.key);
|
|
3127
2868
|
const set = value !== undefined && value !== "" && value !== UNSET;
|
|
3128
|
-
console.log(` ${set ?
|
|
2869
|
+
console.log(` ${set ? color3.green("set ") : color3.dim("unset")} ${key.key}${key.plaintext ? color3.dim(" (plaintext)") : ""}`);
|
|
3129
2870
|
}
|
|
3130
2871
|
if (result.ready) {
|
|
3131
2872
|
console.log(`
|
|
3132
|
-
${
|
|
2873
|
+
${color3.green("Ready to go live.")}
|
|
3133
2874
|
`);
|
|
3134
2875
|
return;
|
|
3135
2876
|
}
|
|
3136
2877
|
console.log(`
|
|
3137
|
-
${
|
|
2878
|
+
${color3.red("NOT ready to go live.")}
|
|
3138
2879
|
`);
|
|
3139
2880
|
if (result.routeProblem) {
|
|
3140
|
-
console.log(`${
|
|
2881
|
+
console.log(`${color3.red("✗")} the worker's hostname
|
|
3141
2882
|
${result.routeProblem}
|
|
3142
2883
|
`);
|
|
3143
2884
|
}
|
|
3144
2885
|
if (result.bareSecrets.length > 0) {
|
|
3145
|
-
console.log(`${
|
|
2886
|
+
console.log(`${color3.red("✗")} committed in the clear in ${SECRETS_FILE}: ${result.bareSecrets.join(", ")}
|
|
3146
2887
|
` + ` Run \`dotenvx encrypt -f ${SECRETS_FILE}\` — and treat those values as burned.
|
|
3147
2888
|
`);
|
|
3148
2889
|
}
|
|
3149
2890
|
for (const key of result.missing) {
|
|
3150
|
-
console.log(`${
|
|
2891
|
+
console.log(`${color3.red("✗")} ${key.key}`);
|
|
3151
2892
|
console.log(` ${key.breaks}`);
|
|
3152
2893
|
if (key.where)
|
|
3153
|
-
console.log(` ${
|
|
2894
|
+
console.log(` ${color3.dim(`from: ${key.where}`)}`);
|
|
3154
2895
|
console.log("");
|
|
3155
2896
|
}
|
|
3156
2897
|
const secrets = result.missing.filter((key) => !key.plaintext);
|
|
3157
2898
|
if (secrets.length > 0) {
|
|
3158
|
-
const inRepo =
|
|
2899
|
+
const inRepo = existsSync7(join7(project.root, SECRETS_FILE));
|
|
3159
2900
|
console.log(inRepo ? `Put each in ${SECRETS_FILE}, which the deploy uploads:` : source === "wrangler" ? "Set each secret on the worker (this also creates the draft worker on a first deploy):" : "Set each secret on the project:");
|
|
3160
2901
|
for (const key of secrets) {
|
|
3161
2902
|
console.log(inRepo ? ` dotenvx set ${key.key} '…' -f ${SECRETS_FILE}` : ` ${source === "wrangler" ? "wrangler secret put" : "void secret put"} ${key.key}`);
|
|
@@ -3168,17 +2909,17 @@ ${color4.red("NOT ready to go live.")}
|
|
|
3168
2909
|
}
|
|
3169
2910
|
|
|
3170
2911
|
// src/deploy/cloudflare.ts
|
|
3171
|
-
import { existsSync as
|
|
3172
|
-
import { dirname as
|
|
3173
|
-
import
|
|
2912
|
+
import { existsSync as existsSync8, readFileSync as readFileSync3, writeFileSync } from "node:fs";
|
|
2913
|
+
import { dirname as dirname5, join as join8 } from "node:path";
|
|
2914
|
+
import color4 from "picocolors";
|
|
3174
2915
|
var fail = (message) => {
|
|
3175
2916
|
console.error(`
|
|
3176
|
-
${
|
|
2917
|
+
${color4.red("✗")} ${message}
|
|
3177
2918
|
`);
|
|
3178
2919
|
return 1;
|
|
3179
2920
|
};
|
|
3180
2921
|
function readConfig(path) {
|
|
3181
|
-
return parseJsonc(
|
|
2922
|
+
return parseJsonc(readFileSync3(path, "utf8"));
|
|
3182
2923
|
}
|
|
3183
2924
|
function findBuilder(from) {
|
|
3184
2925
|
let dir = from;
|
|
@@ -3187,11 +2928,11 @@ function findBuilder(from) {
|
|
|
3187
2928
|
["vp", "vp build"],
|
|
3188
2929
|
["vite", "vite build"]
|
|
3189
2930
|
]) {
|
|
3190
|
-
const path =
|
|
3191
|
-
if (
|
|
2931
|
+
const path = join8(dir, "node_modules", ".bin", bin);
|
|
2932
|
+
if (existsSync8(path))
|
|
3192
2933
|
return { cmd: path, label };
|
|
3193
2934
|
}
|
|
3194
|
-
const parent =
|
|
2935
|
+
const parent = dirname5(dir);
|
|
3195
2936
|
if (parent === dir)
|
|
3196
2937
|
return null;
|
|
3197
2938
|
dir = parent;
|
|
@@ -3199,8 +2940,8 @@ function findBuilder(from) {
|
|
|
3199
2940
|
}
|
|
3200
2941
|
async function deployCloudflare(project, opts) {
|
|
3201
2942
|
const app = project.appDir;
|
|
3202
|
-
const configPath =
|
|
3203
|
-
if (!
|
|
2943
|
+
const configPath = join8(app, "wrangler.jsonc");
|
|
2944
|
+
if (!existsSync8(configPath))
|
|
3204
2945
|
return fail("wrangler.jsonc is missing — `vc generate` writes it.");
|
|
3205
2946
|
const bin = findWrangler(app);
|
|
3206
2947
|
if (!bin)
|
|
@@ -3213,16 +2954,16 @@ async function deployCloudflare(project, opts) {
|
|
|
3213
2954
|
${who.raw.trim().split(`
|
|
3214
2955
|
`).slice(-4).join(`
|
|
3215
2956
|
`)}`);
|
|
3216
|
-
console.log(`${
|
|
2957
|
+
console.log(`${color4.green("✓")} wrangler is logged in`);
|
|
3217
2958
|
let config = readConfig(configPath);
|
|
3218
2959
|
let accountId = config.account_id || process.env["CLOUDFLARE_ACCOUNT_ID"] || "";
|
|
3219
2960
|
if (!accountId) {
|
|
3220
2961
|
if (who.accounts.length === 1) {
|
|
3221
2962
|
accountId = who.accounts[0].id;
|
|
3222
|
-
|
|
2963
|
+
writeFileSync(configPath, upsertJsonc(readFileSync3(configPath, "utf8"), "account_id", accountId));
|
|
3223
2964
|
project.manifest.cloudflare = { ...project.manifest.cloudflare, accountId };
|
|
3224
2965
|
await writeManifest(project.root, project.manifest);
|
|
3225
|
-
console.log(`${
|
|
2966
|
+
console.log(`${color4.green("✓")} pinned the account "${who.accounts[0].name}" in wrangler.jsonc`);
|
|
3226
2967
|
} else {
|
|
3227
2968
|
return fail(`the account is not pinned and wrangler sees ${who.accounts.length}. Set account_id in wrangler.jsonc to one of:
|
|
3228
2969
|
${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
|
|
@@ -3234,7 +2975,7 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
|
|
|
3234
2975
|
if (!check.ready) {
|
|
3235
2976
|
if (!opts.force)
|
|
3236
2977
|
return fail("refusing to deploy a shop that is not ready for customers. Fix the above, or pass --force for a deliberate partial deploy.");
|
|
3237
|
-
console.error(
|
|
2978
|
+
console.error(color4.yellow(`--force: deploying a shop that is NOT ready for customers.
|
|
3238
2979
|
`));
|
|
3239
2980
|
}
|
|
3240
2981
|
const worker = config.name || project.manifest.shop.domain.split(".")[0];
|
|
@@ -3245,15 +2986,15 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
|
|
|
3245
2986
|
return fail("the D1 database is not provisioned. Run once with --provision to create it and record its id.");
|
|
3246
2987
|
const db = await ensureD1(bin, app, `${worker}-db`);
|
|
3247
2988
|
const entry = { binding: "DB", database_name: db.name, database_id: db.uuid, migrations_dir: "./db/migrations" };
|
|
3248
|
-
|
|
2989
|
+
writeFileSync(configPath, upsertJsonc(readFileSync3(configPath, "utf8"), "d1_databases", [entry]));
|
|
3249
2990
|
project.manifest.cloudflare = { ...project.manifest.cloudflare, accountId, d1: { name: db.name, id: db.uuid } };
|
|
3250
2991
|
await writeManifest(project.root, project.manifest);
|
|
3251
|
-
console.log(`${
|
|
2992
|
+
console.log(`${color4.green("✓")} D1 "${db.name}" recorded in wrangler.jsonc and voidcommerce.json`);
|
|
3252
2993
|
config = readConfig(configPath);
|
|
3253
2994
|
}
|
|
3254
2995
|
if (opts.provision) {
|
|
3255
2996
|
await ensureQueue(bin, app, "commerce");
|
|
3256
|
-
console.log(`${
|
|
2997
|
+
console.log(`${color4.green("✓")} queue "commerce"`);
|
|
3257
2998
|
}
|
|
3258
2999
|
const builder = findBuilder(app);
|
|
3259
3000
|
if (!builder)
|
|
@@ -3263,10 +3004,10 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
|
|
|
3263
3004
|
const built = await wrangler(builder.cmd, ["build"], app, true);
|
|
3264
3005
|
if (built.code !== 0)
|
|
3265
3006
|
return fail(`the build failed (exit ${built.code}).`);
|
|
3266
|
-
const emittedPath =
|
|
3267
|
-
if (!
|
|
3007
|
+
const emittedPath = join8(app, "dist", "ssr", "wrangler.json");
|
|
3008
|
+
if (!existsSync8(emittedPath))
|
|
3268
3009
|
return fail(`the build emitted no ${emittedPath} — is this a Void app on the Cloudflare target?`);
|
|
3269
|
-
const emitted = JSON.parse(
|
|
3010
|
+
const emitted = JSON.parse(readFileSync3(emittedPath, "utf8"));
|
|
3270
3011
|
const secretKeys = new Set(allEnvKeys(project.manifest).filter((key) => !key.plaintext).map((key) => key.key));
|
|
3271
3012
|
const scrubbed = [];
|
|
3272
3013
|
for (const [key, value] of Object.entries(emitted.vars ?? {})) {
|
|
@@ -3278,8 +3019,8 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
|
|
|
3278
3019
|
const emittedD1 = emitted.d1_databases?.find((db) => db.binding === "DB");
|
|
3279
3020
|
if (!emittedD1 || emittedD1.database_id === "local")
|
|
3280
3021
|
return fail("the emitted config still carries a placeholder D1 id; wrangler.jsonc's DB binding was not picked up by the build.");
|
|
3281
|
-
|
|
3282
|
-
console.log(`${
|
|
3022
|
+
writeFileSync(emittedPath, JSON.stringify(emitted, null, 2));
|
|
3023
|
+
console.log(`${color4.green("✓")} scrubbed ${scrubbed.length} baked value${scrubbed.length === 1 ? "" : "s"} from the worker's vars${scrubbed.length ? `: ${scrubbed.join(", ")}` : ""}`);
|
|
3283
3024
|
console.log(`
|
|
3284
3025
|
▸ wrangler d1 migrations apply ${emittedD1.database_name} --remote`);
|
|
3285
3026
|
const migrated = await wrangler(bin, ["d1", "migrations", "apply", emittedD1.database_name, "--remote"], app, true);
|
|
@@ -3287,34 +3028,34 @@ ${who.accounts.map((a) => ` ${a.id} ${a.name}`).join(`
|
|
|
3287
3028
|
return fail(`applying migrations failed (exit ${migrated.code}); nothing was deployed.`);
|
|
3288
3029
|
const secretArgs = [];
|
|
3289
3030
|
let cleanupSecrets;
|
|
3290
|
-
if (
|
|
3031
|
+
if (existsSync8(join8(project.root, SECRETS_FILE))) {
|
|
3291
3032
|
const decrypted = await decryptSecrets(project);
|
|
3292
3033
|
if ("error" in decrypted)
|
|
3293
3034
|
return fail(`the repository's secrets could not be read: ${decrypted.error}`);
|
|
3294
3035
|
secretArgs.push("--secrets-file", decrypted.path);
|
|
3295
3036
|
cleanupSecrets = decrypted.cleanup;
|
|
3296
|
-
console.log(`${
|
|
3037
|
+
console.log(`${color4.green("✓")} ${decrypted.names.length} secrets from ${SECRETS_FILE}, uploaded with this version`);
|
|
3297
3038
|
}
|
|
3298
3039
|
console.log(`
|
|
3299
3040
|
▸ wrangler deploy -c dist/ssr/wrangler.json`);
|
|
3300
|
-
const deployed = await wrangler(bin, ["deploy", "-c",
|
|
3041
|
+
const deployed = await wrangler(bin, ["deploy", "-c", join8("dist", "ssr", "wrangler.json"), ...secretArgs], app, true);
|
|
3301
3042
|
cleanupSecrets?.();
|
|
3302
3043
|
if (deployed.code !== 0)
|
|
3303
3044
|
return fail(`wrangler deploy failed (exit ${deployed.code}).`);
|
|
3304
3045
|
const domain = project.manifest.shop.domain;
|
|
3305
3046
|
const url = `https://${workerHosts(project.manifest)[0]}`;
|
|
3306
3047
|
console.log(`
|
|
3307
|
-
${
|
|
3048
|
+
${color4.green("Live:")} ${url}`);
|
|
3308
3049
|
if (hasFrontend(project.manifest.layout))
|
|
3309
|
-
console.log(
|
|
3050
|
+
console.log(color4.dim("The storefront deploys itself from GitHub Actions on push."));
|
|
3310
3051
|
return 0;
|
|
3311
3052
|
}
|
|
3312
3053
|
|
|
3313
3054
|
// src/import.ts
|
|
3314
3055
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
3315
|
-
import { existsSync as
|
|
3316
|
-
import { join as
|
|
3317
|
-
import
|
|
3056
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
3057
|
+
import { join as join9 } from "node:path";
|
|
3058
|
+
import color5 from "picocolors";
|
|
3318
3059
|
function describe(counts) {
|
|
3319
3060
|
if (!counts)
|
|
3320
3061
|
return "";
|
|
@@ -3353,7 +3094,7 @@ async function post(url, key, body) {
|
|
|
3353
3094
|
return { ok: response.ok, status: response.status, body: { ...parsed, error: parsed.error ?? parsed.message } };
|
|
3354
3095
|
}
|
|
3355
3096
|
async function readJson(path) {
|
|
3356
|
-
if (!
|
|
3097
|
+
if (!existsSync9(path))
|
|
3357
3098
|
return null;
|
|
3358
3099
|
try {
|
|
3359
3100
|
return JSON.parse(await readFile3(path, "utf8"));
|
|
@@ -3381,41 +3122,41 @@ async function importCommand(args) {
|
|
|
3381
3122
|
}
|
|
3382
3123
|
const { url, how } = resolveTarget(project, args);
|
|
3383
3124
|
if (!dry)
|
|
3384
|
-
console.log(`${
|
|
3125
|
+
console.log(`${color5.dim("→")} ${url} ${color5.dim(`(${how})`)}
|
|
3385
3126
|
`);
|
|
3386
3127
|
if (!dry) {
|
|
3387
3128
|
let whoami2;
|
|
3388
3129
|
try {
|
|
3389
3130
|
whoami2 = await fetch(`${url}/api/auth/system/whoami`, { headers: { "x-system-key": key } });
|
|
3390
3131
|
} catch (error) {
|
|
3391
|
-
console.error(`${
|
|
3132
|
+
console.error(`${color5.red("✗")} could not reach ${url}: ${error instanceof Error ? error.message : String(error)}
|
|
3392
3133
|
` + ` Is the shop running? \`vc dev\` serves it locally; --local points here.
|
|
3393
3134
|
`);
|
|
3394
3135
|
return 1;
|
|
3395
3136
|
}
|
|
3396
3137
|
if (!whoami2.ok) {
|
|
3397
|
-
console.error(`${
|
|
3138
|
+
console.error(`${color5.red("✗")} the shop refused the system key (HTTP ${whoami2.status}).
|
|
3398
3139
|
` + ` Is SYSTEM_API_KEY the one this deployment was given?
|
|
3399
3140
|
`);
|
|
3400
3141
|
return 1;
|
|
3401
3142
|
}
|
|
3402
|
-
console.log(`${
|
|
3143
|
+
console.log(`${color5.green("✓")} authenticated as the system identity`);
|
|
3403
3144
|
}
|
|
3404
3145
|
const root = project.root;
|
|
3405
|
-
const products = await readJson(
|
|
3406
|
-
const categories = await readJson(
|
|
3407
|
-
const faqs = has(project.manifest, "faqs") ? await readJson(
|
|
3408
|
-
const posts = has(project.manifest, "blogs") ? await readJson(
|
|
3146
|
+
const products = await readJson(join9(root, "data", "catalog.json"));
|
|
3147
|
+
const categories = await readJson(join9(root, "data", "categories.json"));
|
|
3148
|
+
const faqs = has(project.manifest, "faqs") ? await readJson(join9(root, "content", "faqs.json")) : null;
|
|
3149
|
+
const posts = has(project.manifest, "blogs") ? await readJson(join9(root, "content", "posts.json")) : null;
|
|
3409
3150
|
if (!products && !faqs?.length && !posts?.length) {
|
|
3410
3151
|
console.log(`
|
|
3411
|
-
${
|
|
3152
|
+
${color5.yellow("Nothing to import.")} data/catalog.json is absent and the content files are empty.
|
|
3412
3153
|
` + `Products go in data/catalog.json as a JSON array; \`vc import --dry-run\` checks it without pushing.
|
|
3413
3154
|
`);
|
|
3414
3155
|
return 0;
|
|
3415
3156
|
}
|
|
3416
3157
|
if (dry) {
|
|
3417
3158
|
console.log(`
|
|
3418
|
-
${
|
|
3159
|
+
${color5.dim("--dry-run: nothing was pushed.")}`);
|
|
3419
3160
|
console.log(` ${products?.length ?? 0} products, ${categories?.length ?? 0} categories`);
|
|
3420
3161
|
console.log(` ${faqs?.length ?? 0} FAQ entries, ${posts?.length ?? 0} posts
|
|
3421
3162
|
`);
|
|
@@ -3431,15 +3172,15 @@ ${color6.dim("--dry-run: nothing was pushed.")}`);
|
|
|
3431
3172
|
});
|
|
3432
3173
|
if (result.ok) {
|
|
3433
3174
|
const report = result.body.report ?? {};
|
|
3434
|
-
console.log(`${
|
|
3175
|
+
console.log(`${color5.green("✓")} products: ${describe(report.products)}`);
|
|
3435
3176
|
if (report.categories)
|
|
3436
|
-
console.log(`${
|
|
3177
|
+
console.log(`${color5.green("✓")} categories: ${describe(report.categories)}`);
|
|
3437
3178
|
if (report.prices)
|
|
3438
|
-
console.log(`${
|
|
3179
|
+
console.log(`${color5.green("✓")} prices: ${describe(report.prices)}`);
|
|
3439
3180
|
if (report.addons)
|
|
3440
|
-
console.log(`${
|
|
3181
|
+
console.log(`${color5.green("✓")} addons: ${describe(report.addons)}`);
|
|
3441
3182
|
} else {
|
|
3442
|
-
console.error(`${
|
|
3183
|
+
console.error(`${color5.red("✗")} catalogue: ${result.body.error ?? `HTTP ${result.status}`}`);
|
|
3443
3184
|
failed = true;
|
|
3444
3185
|
}
|
|
3445
3186
|
}
|
|
@@ -3451,20 +3192,20 @@ ${color6.dim("--dry-run: nothing was pushed.")}`);
|
|
|
3451
3192
|
continue;
|
|
3452
3193
|
const result = await post(`${url}${path}`, key, { entries: rows, posts: rows });
|
|
3453
3194
|
if (result.ok) {
|
|
3454
|
-
console.log(`${
|
|
3195
|
+
console.log(`${color5.green("✓")} ${label}: ${describe(result.body)}`);
|
|
3455
3196
|
} else {
|
|
3456
|
-
console.error(`${
|
|
3197
|
+
console.error(`${color5.red("✗")} ${label}: ${result.body.error ?? `HTTP ${result.status}`}`);
|
|
3457
3198
|
failed = true;
|
|
3458
3199
|
}
|
|
3459
3200
|
}
|
|
3460
3201
|
if (failed) {
|
|
3461
3202
|
console.error(`
|
|
3462
|
-
${
|
|
3203
|
+
${color5.red("Some of it did not land.")} The import is an upsert, so fixing the cause and running it again is safe.
|
|
3463
3204
|
`);
|
|
3464
3205
|
return 1;
|
|
3465
3206
|
}
|
|
3466
3207
|
console.log(`
|
|
3467
|
-
${
|
|
3208
|
+
${color5.green("Imported.")} It is an upsert, so running it again costs one pass and changes nothing.
|
|
3468
3209
|
`);
|
|
3469
3210
|
return 0;
|
|
3470
3211
|
}
|
|
@@ -3485,4 +3226,4 @@ async function importHelp() {
|
|
|
3485
3226
|
return 0;
|
|
3486
3227
|
}
|
|
3487
3228
|
|
|
3488
|
-
export { renderAuthTs,
|
|
3229
|
+
export { renderAuthTs, FRONTEND_BRANCH, renderFrontendApiTs, renderFrontendEnvProduction, renderStorefrontPage, renderFrontendWorkflow, runVoid, runInherit, captureVoid, isVoidApp, voidAppsIn, STRICT_APP, VOID_VERSION, strictDependencies, renderVoidPatch, renderViteConfig, renderStrictTsconfig, renderVoidJson, renderDbSchema, renderDbSeed, renderLayoutTsx, renderAppCss, renderIndexServer, INDEX_PAGE, renderDashboardPage, CLIENT_ONLY, renderSignInPage, renderAuthClient, renderContentTs, renderCron, renderQueue, renderLiveStream, renderLiveRoute, renderStrictAppPackageJson, DATA_README, BRANDING_README, MIGRATIONS_README, finishStrict, line, row, box, fullHelp, initHelp, version, findProject, ensureGenerated, DIST_DIR, DIST_BRANCH, distCommand, distHelp, renderDistWorkflow, renderDeployReadme, renderRequirementsTs, renderDomainTs, generate, parseJsonc, upsertJsonc, findWrangler, parseWhoAmI, productionEnv, routeProblem, preflight, printPreflight, deployCloudflare, importCommand, importHelp };
|