@saastemly/voidcommerce 0.8.0 → 0.10.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/cli.js +2 -2
- package/dist/generate/frontend.d.ts +1 -1
- package/dist/{index-y6qzjd9h.js → index-mb1p1rh0.js} +66 -7
- package/dist/{index-j5xjh6qp.js → index-vhsq8jpy.js} +34 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -2
- package/dist/{keys-3zjezaxk.js → keys-2q3qzntw.js} +1 -1
- package/package.json +1 -1
- package/src/deploy/secrets.ts +26 -4
- package/src/generate/env.ts +5 -2
- package/src/generate/frontend.ts +27 -1
- package/src/generate/index.ts +54 -3
- package/src/index.ts +1 -0
package/dist/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
runVoid,
|
|
22
22
|
version,
|
|
23
23
|
voidAppsIn
|
|
24
|
-
} from "./index-
|
|
24
|
+
} from "./index-mb1p1rh0.js";
|
|
25
25
|
import {
|
|
26
26
|
LOCAL_KEY_FILE,
|
|
27
27
|
PRIVATE_KEY_VAR,
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
setVariable,
|
|
51
51
|
variableNames,
|
|
52
52
|
verifyCloudflareToken
|
|
53
|
-
} from "./index-
|
|
53
|
+
} from "./index-vhsq8jpy.js";
|
|
54
54
|
import {
|
|
55
55
|
LAYOUTS,
|
|
56
56
|
oneOrigin,
|
|
@@ -23,7 +23,7 @@ export declare const FRONTEND_BRANCH = "frontend-static";
|
|
|
23
23
|
export declare function renderFrontendApiTs(manifest: Manifest): string;
|
|
24
24
|
export declare function renderFrontendEnvProduction(manifest: Manifest): string;
|
|
25
25
|
/** The Vite config for a static Void site. Deliberately small: this is yours. */
|
|
26
|
-
export declare function renderFrontendViteConfig(): string;
|
|
26
|
+
export declare function renderFrontendViteConfig(manifest: Manifest): string;
|
|
27
27
|
/**
|
|
28
28
|
* `target: node` because Void 0.10.13's Cloudflare prerender calls miniflare
|
|
29
29
|
* with the single-worker shorthand that the miniflare its own dependencies
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
renderEnvLocal,
|
|
10
10
|
renderEnvProduction,
|
|
11
11
|
renderEnvTs
|
|
12
|
-
} from "./index-
|
|
12
|
+
} from "./index-vhsq8jpy.js";
|
|
13
13
|
import {
|
|
14
14
|
MANIFEST_FILE,
|
|
15
15
|
has,
|
|
@@ -422,7 +422,33 @@ function renderFrontendEnvProduction(manifest) {
|
|
|
422
422
|
VITE_API_ORIGIN=https://api.${manifest.shop.domain}
|
|
423
423
|
`;
|
|
424
424
|
}
|
|
425
|
-
function renderFrontendViteConfig() {
|
|
425
|
+
function renderFrontendViteConfig(manifest) {
|
|
426
|
+
if (oneOrigin(manifest.layout)) {
|
|
427
|
+
return `import { defineConfig } from "vite";
|
|
428
|
+
import { voidPlugin } from "void";
|
|
429
|
+
import { voidReact } from "@void/react/plugin";
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The storefront.
|
|
433
|
+
*
|
|
434
|
+
* In production one worker serves this page and the API it calls, so the
|
|
435
|
+
* client uses relative URLs and there is nothing to configure. In DEV they
|
|
436
|
+
* are two processes on two ports, and a relative /api call would hit this
|
|
437
|
+
* dev server instead of the worker — so it is proxied. Without this, local
|
|
438
|
+
* development is the only place the shop is broken.
|
|
439
|
+
*
|
|
440
|
+
* Start the worker first: it takes 5173, and this lands on the next port.
|
|
441
|
+
*/
|
|
442
|
+
export default defineConfig({
|
|
443
|
+
plugins: [voidPlugin(), voidReact()],
|
|
444
|
+
server: {
|
|
445
|
+
proxy: {
|
|
446
|
+
"/api": { target: "http://127.0.0.1:5173", changeOrigin: false },
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
`;
|
|
451
|
+
}
|
|
426
452
|
return `import { defineConfig } from "vite";
|
|
427
453
|
import { voidPlugin } from "void";
|
|
428
454
|
import { voidReact } from "@void/react/plugin";
|
|
@@ -1344,7 +1370,7 @@ import color from "picocolors";
|
|
|
1344
1370
|
// package.json
|
|
1345
1371
|
var package_default = {
|
|
1346
1372
|
name: "@saastemly/voidcommerce",
|
|
1347
|
-
version: "0.
|
|
1373
|
+
version: "0.10.0",
|
|
1348
1374
|
description: "Void, with a shop in it. `vc init` walks you through Better Auth, betterCommerce and every plugin; everything else passes through to `void`.",
|
|
1349
1375
|
type: "module",
|
|
1350
1376
|
license: "MIT",
|
|
@@ -2293,6 +2319,39 @@ async function exists(path) {
|
|
|
2293
2319
|
return false;
|
|
2294
2320
|
}
|
|
2295
2321
|
}
|
|
2322
|
+
async function putEnv(root, file, rendered, result) {
|
|
2323
|
+
const path = join4(root, file);
|
|
2324
|
+
if (!await exists(path)) {
|
|
2325
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
2326
|
+
await writeFile2(path, rendered, "utf8");
|
|
2327
|
+
result.written.push(file);
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
const read = (text) => {
|
|
2331
|
+
const out = new Map;
|
|
2332
|
+
for (const line2 of text.split(`
|
|
2333
|
+
`)) {
|
|
2334
|
+
const match = /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)$/.exec(line2);
|
|
2335
|
+
if (match)
|
|
2336
|
+
out.set(match[1], match[2]);
|
|
2337
|
+
}
|
|
2338
|
+
return out;
|
|
2339
|
+
};
|
|
2340
|
+
const existing = read(await readFile2(path, "utf8"));
|
|
2341
|
+
const wanted = read(rendered);
|
|
2342
|
+
const merged = rendered.split(`
|
|
2343
|
+
`).map((line2) => {
|
|
2344
|
+
const match = /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)$/.exec(line2);
|
|
2345
|
+
if (!match)
|
|
2346
|
+
return line2;
|
|
2347
|
+
const had = existing.get(match[1]);
|
|
2348
|
+
return had !== undefined && had !== "" && had !== "unset" ? `${match[1]}=${had}` : line2;
|
|
2349
|
+
}).join(`
|
|
2350
|
+
`);
|
|
2351
|
+
const dropped = [...existing.keys()].filter((key) => !wanted.has(key) && !key.startsWith("VITE_"));
|
|
2352
|
+
await writeFile2(path, merged, "utf8");
|
|
2353
|
+
result.written.push(dropped.length > 0 ? `${file} ${`(dropped ${dropped.join(", ")})`}` : file);
|
|
2354
|
+
}
|
|
2296
2355
|
async function put(root, file, content, result, mode) {
|
|
2297
2356
|
const path = join4(root, file);
|
|
2298
2357
|
if (mode === "own" && await exists(path)) {
|
|
@@ -2364,7 +2423,7 @@ async function generateApi(root, dir, manifest, result, opts) {
|
|
|
2364
2423
|
await put(root, at(".env.production"), renderEnvProduction(manifest), result, "regenerate");
|
|
2365
2424
|
await put(root, at("lib/deploy/requirements.ts"), renderRequirementsTs(manifest), result, "regenerate");
|
|
2366
2425
|
if (opts.env)
|
|
2367
|
-
await
|
|
2426
|
+
await putEnv(root, at(".env"), renderEnvLocal(manifest), result);
|
|
2368
2427
|
await put(root, at("lib/domain.ts"), renderDomainTs(manifest.layout), result, own);
|
|
2369
2428
|
await put(root, at("lib/payment.ts"), renderPaymentTs(manifest), result, own);
|
|
2370
2429
|
await put(root, at("lib/notifications.ts"), renderNotificationsTs(), result, own);
|
|
@@ -2424,7 +2483,7 @@ async function generateFrontend(root, dir, manifest, result) {
|
|
|
2424
2483
|
const at = (file) => join4(dir, file);
|
|
2425
2484
|
await put(root, at("lib/api.ts"), renderFrontendApiTs(manifest), result, "own");
|
|
2426
2485
|
await put(root, at(".env.production"), renderFrontendEnvProduction(manifest), result, "regenerate");
|
|
2427
|
-
await put(root, at("vite.config.ts"), renderFrontendViteConfig(), result, "own");
|
|
2486
|
+
await put(root, at("vite.config.ts"), renderFrontendViteConfig(manifest), result, "own");
|
|
2428
2487
|
await put(root, at("void.json"), renderFrontendVoidJson(), result, "own");
|
|
2429
2488
|
await put(root, at("tsconfig.json"), renderFrontendTsconfig(), result, "own");
|
|
2430
2489
|
await put(root, at("pages/index.tsx"), renderStorefrontPage(manifest), result, "own");
|
|
@@ -2517,7 +2576,7 @@ bunx vc guard
|
|
|
2517
2576
|
await put(root, ".github/workflows/deploy.yml", renderDistWorkflow(manifest), result, "regenerate");
|
|
2518
2577
|
await retire(root, ".github/workflows/void-dist.yml", result);
|
|
2519
2578
|
await put(root, "DEPLOY.md", renderDeployReadme(manifest, zone(manifest), workerHosts(manifest)), result, "regenerate");
|
|
2520
|
-
await
|
|
2579
|
+
await putEnv(root, ".env", renderEnvLocal(manifest), result);
|
|
2521
2580
|
await put(root, "data/README.md", DATA_README, result, "own");
|
|
2522
2581
|
await put(root, "branding/README.md", BRANDING_README, result, "own");
|
|
2523
2582
|
await put(root, "migrations/README.md", MIGRATIONS_README, result, "own");
|
|
@@ -3372,4 +3431,4 @@ async function importHelp() {
|
|
|
3372
3431
|
return 0;
|
|
3373
3432
|
}
|
|
3374
3433
|
|
|
3375
|
-
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 };
|
|
3434
|
+
export { renderAuthTs, FRONTEND_BRANCH, renderFrontendApiTs, renderFrontendEnvProduction, renderFrontendViteConfig, 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 };
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
envKeysOf,
|
|
3
3
|
hasFrontend,
|
|
4
4
|
isApex,
|
|
5
|
+
oneOrigin,
|
|
5
6
|
zone
|
|
6
7
|
} from "./index-tjy6yygc.js";
|
|
7
8
|
import {
|
|
@@ -247,7 +248,7 @@ function renderEnvLocal(manifest) {
|
|
|
247
248
|
"# Local development. Gitignored; .env.example documents every key.",
|
|
248
249
|
"",
|
|
249
250
|
...keys.map((key) => `${key.key}=${local[key.key] ?? key.dev ?? "unset"}`),
|
|
250
|
-
...hasFrontend(manifest.layout) ? ["", "# The storefront, reaching the worker. Start the worker first.", "VITE_API_ORIGIN=http://localhost:5173"] : [],
|
|
251
|
+
...hasFrontend(manifest.layout) && !oneOrigin(manifest.layout) ? ["", "# The storefront, reaching the worker. Start the worker first.", "VITE_API_ORIGIN=http://localhost:5173"] : [],
|
|
251
252
|
""
|
|
252
253
|
].join(`
|
|
253
254
|
`);
|
|
@@ -384,7 +385,7 @@ async function decryptSecrets(project) {
|
|
|
384
385
|
}
|
|
385
386
|
const expected = committedPublicKey(root);
|
|
386
387
|
if (expected) {
|
|
387
|
-
const { publicKeyFor } = await import("./keys-
|
|
388
|
+
const { publicKeyFor } = await import("./keys-2q3qzntw.js");
|
|
388
389
|
const derived = await publicKeyFor(privateKey);
|
|
389
390
|
if (derived && derived.toLowerCase() !== expected.toLowerCase()) {
|
|
390
391
|
return {
|
|
@@ -432,7 +433,7 @@ async function secretsCommand(project, args) {
|
|
|
432
433
|
if (args[0] === "set")
|
|
433
434
|
return setSecretValue(project, args.slice(1));
|
|
434
435
|
if (args.includes("--sync"))
|
|
435
|
-
return syncSecrets(project);
|
|
436
|
+
return syncSecrets(project, args.includes("--prune"));
|
|
436
437
|
if (!existsSync2(join2(root, SECRETS_FILE))) {
|
|
437
438
|
console.log(`
|
|
438
439
|
${color.yellow("No " + SECRETS_FILE + " yet.")} Secrets are set by hand with \`wrangler secret put\`,
|
|
@@ -625,7 +626,7 @@ function upsertLine(path, name, value) {
|
|
|
625
626
|
`)}${name}=${value}
|
|
626
627
|
`);
|
|
627
628
|
}
|
|
628
|
-
async function syncSecrets(project) {
|
|
629
|
+
async function syncSecrets(project, prune) {
|
|
629
630
|
const root = project.root;
|
|
630
631
|
const path = join2(root, SECRETS_FILE);
|
|
631
632
|
if (!existsSync2(path)) {
|
|
@@ -656,13 +657,39 @@ ${color.green("+")} ${missing.length} key${missing.length === 1 ? "" : "s"} adde
|
|
|
656
657
|
${color.green("✓")} every secret this shop needs is already declared.
|
|
657
658
|
`);
|
|
658
659
|
}
|
|
659
|
-
if (extra.length > 0) {
|
|
660
|
+
if (extra.length > 0 && prune) {
|
|
661
|
+
const body = readFileSync(path, "utf8");
|
|
662
|
+
const kept = [];
|
|
663
|
+
const lines = body.split(`
|
|
664
|
+
`);
|
|
665
|
+
for (let i = 0;i < lines.length; i++) {
|
|
666
|
+
const match = /^\s*([A-Z][A-Z0-9_]*)\s*=/.exec(lines[i] ?? "");
|
|
667
|
+
if (match && extra.includes(match[1])) {
|
|
668
|
+
while (kept.length > 0 && (kept[kept.length - 1].startsWith("#") || kept[kept.length - 1].trim() === ""))
|
|
669
|
+
kept.pop();
|
|
670
|
+
if (lines[i + 1]?.trim() === "")
|
|
671
|
+
i++;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
kept.push(lines[i]);
|
|
675
|
+
}
|
|
676
|
+
writeFileSync(path, `${kept.join(`
|
|
677
|
+
`).replace(/\n{3,}/g, `
|
|
678
|
+
|
|
679
|
+
`).replace(/\n*$/, "")}
|
|
680
|
+
`);
|
|
681
|
+
console.log(`${color.red("-")} ${extra.length} key${extra.length === 1 ? "" : "s"} removed from ${SECRETS_FILE}: ${extra.join(", ")}
|
|
682
|
+
|
|
683
|
+
` + color.dim(` Their ciphertext is still in git history, so nothing is unrecoverable —
|
|
684
|
+
but nothing in this shop reads them any more.
|
|
685
|
+
`));
|
|
686
|
+
} else if (extra.length > 0) {
|
|
660
687
|
console.log(`${color.yellow("!")} ${extra.length} declared key${extra.length === 1 ? " is" : "s are"} no longer required by this shop:
|
|
661
688
|
${extra.join(`
|
|
662
689
|
`)}
|
|
663
690
|
|
|
664
|
-
` + color.dim(` Left alone
|
|
665
|
-
|
|
691
|
+
` + color.dim(` Left alone: deleting a value you may still want is not this command's
|
|
692
|
+
call to make by default. \`vc secrets --sync --prune\` makes it.
|
|
666
693
|
`));
|
|
667
694
|
}
|
|
668
695
|
return 0;
|
package/dist/index.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export { parseJsonc, upsertJsonc } from "./deploy/jsonc";
|
|
|
18
18
|
export { parseWhoAmI, findWrangler } from "./deploy/wrangler";
|
|
19
19
|
export { preflight, printPreflight, productionEnv, routeProblem } from "./deploy/preflight";
|
|
20
20
|
export { deployCloudflare } from "./deploy/cloudflare";
|
|
21
|
-
export { FRONTEND_BRANCH, renderFrontendApiTs, renderFrontendEnvProduction, renderFrontendWorkflow, renderStorefrontPage, } from "./generate/frontend";
|
|
21
|
+
export { FRONTEND_BRANCH, renderFrontendViteConfig, renderFrontendApiTs, renderFrontendEnvProduction, renderFrontendWorkflow, renderStorefrontPage, } from "./generate/frontend";
|
|
22
22
|
export { distCommand, DIST_BRANCH, DIST_DIR } from "./dist";
|
|
23
23
|
export { renderDistWorkflow, renderDeployReadme } from "./generate/ci";
|
|
24
24
|
export { importCommand } from "./import";
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
renderDomainTs,
|
|
35
35
|
renderFrontendApiTs,
|
|
36
36
|
renderFrontendEnvProduction,
|
|
37
|
+
renderFrontendViteConfig,
|
|
37
38
|
renderFrontendWorkflow,
|
|
38
39
|
renderIndexServer,
|
|
39
40
|
renderLayoutTsx,
|
|
@@ -51,7 +52,7 @@ import {
|
|
|
51
52
|
routeProblem,
|
|
52
53
|
strictDependencies,
|
|
53
54
|
upsertJsonc
|
|
54
|
-
} from "./index-
|
|
55
|
+
} from "./index-mb1p1rh0.js";
|
|
55
56
|
import {
|
|
56
57
|
allEnvKeys,
|
|
57
58
|
envSummary,
|
|
@@ -59,7 +60,7 @@ import {
|
|
|
59
60
|
renderEnvLocal,
|
|
60
61
|
renderEnvProduction,
|
|
61
62
|
renderEnvTs
|
|
62
|
-
} from "./index-
|
|
63
|
+
} from "./index-vhsq8jpy.js";
|
|
63
64
|
import {
|
|
64
65
|
LAYOUTS,
|
|
65
66
|
MANIFEST_FILE,
|
|
@@ -124,6 +125,7 @@ export {
|
|
|
124
125
|
renderLayoutTsx,
|
|
125
126
|
renderIndexServer,
|
|
126
127
|
renderFrontendWorkflow,
|
|
128
|
+
renderFrontendViteConfig,
|
|
127
129
|
renderFrontendEnvProduction,
|
|
128
130
|
renderFrontendApiTs,
|
|
129
131
|
renderEnvTs,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saastemly/voidcommerce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Void, with a shop in it. `vc init` walks you through Better Auth, betterCommerce and every plugin; everything else passes through to `void`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/deploy/secrets.ts
CHANGED
|
@@ -250,7 +250,7 @@ export async function secretsCommand(project: Project, args: string[]): Promise<
|
|
|
250
250
|
|
|
251
251
|
if (args.includes("--init")) return initSecrets(project);
|
|
252
252
|
if (args[0] === "set") return setSecretValue(project, args.slice(1));
|
|
253
|
-
if (args.includes("--sync")) return syncSecrets(project);
|
|
253
|
+
if (args.includes("--sync")) return syncSecrets(project, args.includes("--prune"));
|
|
254
254
|
|
|
255
255
|
if (!existsSync(join(root, SECRETS_FILE))) {
|
|
256
256
|
console.log(
|
|
@@ -482,7 +482,7 @@ function upsertLine(path: string, name: string, value: string): void {
|
|
|
482
482
|
* claims — the value may still be wanted, and it is not this command's to
|
|
483
483
|
* destroy. Extras are reported instead.
|
|
484
484
|
*/
|
|
485
|
-
async function syncSecrets(project: Project): Promise<number> {
|
|
485
|
+
async function syncSecrets(project: Project, prune: boolean): Promise<number> {
|
|
486
486
|
const root = project.root;
|
|
487
487
|
const path = join(root, SECRETS_FILE);
|
|
488
488
|
if (!existsSync(path)) {
|
|
@@ -505,11 +505,33 @@ async function syncSecrets(project: Project): Promise<number> {
|
|
|
505
505
|
console.log(`\n${color.green("✓")} every secret this shop needs is already declared.\n`);
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
-
if (extra.length > 0) {
|
|
508
|
+
if (extra.length > 0 && prune) {
|
|
509
|
+
// Deleting a value is the operator's call, and --prune is them making
|
|
510
|
+
// it. The whole entry goes — its comment line too — so the file reads
|
|
511
|
+
// as though the key was never required.
|
|
512
|
+
const body = readFileSync(path, "utf8");
|
|
513
|
+
const kept: string[] = [];
|
|
514
|
+
const lines = body.split("\n");
|
|
515
|
+
for (let i = 0; i < lines.length; i++) {
|
|
516
|
+
const match = /^\s*([A-Z][A-Z0-9_]*)\s*=/.exec(lines[i] ?? "");
|
|
517
|
+
if (match && extra.includes(match[1]!)) {
|
|
518
|
+
// Drop the comment that introduces it, and the blank line after.
|
|
519
|
+
while (kept.length > 0 && (kept[kept.length - 1]!.startsWith("#") || kept[kept.length - 1]!.trim() === "")) kept.pop();
|
|
520
|
+
if (lines[i + 1]?.trim() === "") i++;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
kept.push(lines[i]!);
|
|
524
|
+
}
|
|
525
|
+
writeFileSync(path, `${kept.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n*$/, "")}\n`);
|
|
526
|
+
console.log(
|
|
527
|
+
`${color.red("-")} ${extra.length} key${extra.length === 1 ? "" : "s"} removed from ${SECRETS_FILE}: ${extra.join(", ")}\n\n` +
|
|
528
|
+
color.dim(` Their ciphertext is still in git history, so nothing is unrecoverable —\n but nothing in this shop reads them any more.\n`),
|
|
529
|
+
);
|
|
530
|
+
} else if (extra.length > 0) {
|
|
509
531
|
console.log(
|
|
510
532
|
`${color.yellow("!")} ${extra.length} declared key${extra.length === 1 ? " is" : "s are"} no longer required by this shop:\n` +
|
|
511
533
|
` ${extra.join("\n ")}\n\n` +
|
|
512
|
-
color.dim(
|
|
534
|
+
color.dim(` Left alone: deleting a value you may still want is not this command's\n call to make by default. \`vc secrets --sync --prune\` makes it.\n`),
|
|
513
535
|
);
|
|
514
536
|
}
|
|
515
537
|
return 0;
|
package/src/generate/env.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type EnvKey } from "../catalog";
|
|
2
|
-
import { type Manifest, envKeysOf, has, hasFrontend, isApex, zone } from "../manifest";
|
|
2
|
+
import { type Manifest, envKeysOf, has, hasFrontend, isApex, zone , oneOrigin} from "../manifest";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* `env.ts`, `.env.example` and `.env.production` — from the manifest.
|
|
@@ -123,7 +123,10 @@ export function renderEnvLocal(manifest: Manifest): string {
|
|
|
123
123
|
// One file for both halves in a strict repo, where the storefront and
|
|
124
124
|
// the worker share a root. The worker takes 5173 because it is started
|
|
125
125
|
// first; the storefront lands on the next free port and calls back here.
|
|
126
|
-
|
|
126
|
+
// Only a monorepo bakes the API's address in. Where one worker serves
|
|
127
|
+
// both, the client uses relative URLs and dev proxies /api instead —
|
|
128
|
+
// so a VITE_API_ORIGIN here would be read by nothing.
|
|
129
|
+
...(hasFrontend(manifest.layout) && !oneOrigin(manifest.layout)
|
|
127
130
|
? ["", "# The storefront, reaching the worker. Start the worker first.", "VITE_API_ORIGIN=http://localhost:5173"]
|
|
128
131
|
: []),
|
|
129
132
|
"",
|
package/src/generate/frontend.ts
CHANGED
|
@@ -74,7 +74,33 @@ VITE_API_ORIGIN=https://api.${manifest.shop.domain}
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/** The Vite config for a static Void site. Deliberately small: this is yours. */
|
|
77
|
-
export function renderFrontendViteConfig(): string {
|
|
77
|
+
export function renderFrontendViteConfig(manifest: Manifest): string {
|
|
78
|
+
if (oneOrigin(manifest.layout)) {
|
|
79
|
+
return `import { defineConfig } from "vite";
|
|
80
|
+
import { voidPlugin } from "void";
|
|
81
|
+
import { voidReact } from "@void/react/plugin";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The storefront.
|
|
85
|
+
*
|
|
86
|
+
* In production one worker serves this page and the API it calls, so the
|
|
87
|
+
* client uses relative URLs and there is nothing to configure. In DEV they
|
|
88
|
+
* are two processes on two ports, and a relative /api call would hit this
|
|
89
|
+
* dev server instead of the worker — so it is proxied. Without this, local
|
|
90
|
+
* development is the only place the shop is broken.
|
|
91
|
+
*
|
|
92
|
+
* Start the worker first: it takes 5173, and this lands on the next port.
|
|
93
|
+
*/
|
|
94
|
+
export default defineConfig({
|
|
95
|
+
plugins: [voidPlugin(), voidReact()],
|
|
96
|
+
server: {
|
|
97
|
+
proxy: {
|
|
98
|
+
"/api": { target: "http://127.0.0.1:5173", changeOrigin: false },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
78
104
|
return `import { defineConfig } from "vite";
|
|
79
105
|
import { voidPlugin } from "void";
|
|
80
106
|
import { voidReact } from "@void/react/plugin";
|
package/src/generate/index.ts
CHANGED
|
@@ -89,6 +89,57 @@ async function exists(path: string): Promise<boolean> {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Write a dotenv file that RECONCILES rather than freezes.
|
|
94
|
+
*
|
|
95
|
+
* `.env` used to be written once and never touched, so a shop that swapped a
|
|
96
|
+
* carrier or a payment rail kept the old keys forever and never gained the
|
|
97
|
+
* new ones. devprints ended up carrying five Adyen keys and a Resend key for
|
|
98
|
+
* a shop that uses neither, while missing two it needed. That is the same
|
|
99
|
+
* owned-file drift that shipped a `.gitignore` without `.env.keys` and an
|
|
100
|
+
* api.ts pointing at another shop's domain.
|
|
101
|
+
*
|
|
102
|
+
* So: the template decides which keys EXIST, and the file on disk decides
|
|
103
|
+
* what they are SET TO. A value a person put there is always kept; a key the
|
|
104
|
+
* manifest no longer declares goes, and is reported rather than vanishing.
|
|
105
|
+
*/
|
|
106
|
+
async function putEnv(root: string, file: string, rendered: string, result: GenerateResult) {
|
|
107
|
+
const path = join(root, file);
|
|
108
|
+
if (!(await exists(path))) {
|
|
109
|
+
await mkdir(dirname(path), { recursive: true });
|
|
110
|
+
await writeFile(path, rendered, "utf8");
|
|
111
|
+
result.written.push(file);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const read = (text: string): Map<string, string> => {
|
|
116
|
+
const out = new Map<string, string>();
|
|
117
|
+
for (const line of text.split("\n")) {
|
|
118
|
+
const match = /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)$/.exec(line);
|
|
119
|
+
if (match) out.set(match[1]!, match[2]!);
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
};
|
|
123
|
+
const existing = read(await readFile(path, "utf8"));
|
|
124
|
+
const wanted = read(rendered);
|
|
125
|
+
|
|
126
|
+
const merged = rendered
|
|
127
|
+
.split("\n")
|
|
128
|
+
.map((line) => {
|
|
129
|
+
const match = /^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)$/.exec(line);
|
|
130
|
+
if (!match) return line;
|
|
131
|
+
const had = existing.get(match[1]!);
|
|
132
|
+
// A real value the operator set beats the template's placeholder.
|
|
133
|
+
// `unset` and empty are placeholders, not answers.
|
|
134
|
+
return had !== undefined && had !== "" && had !== "unset" ? `${match[1]}=${had}` : line;
|
|
135
|
+
})
|
|
136
|
+
.join("\n");
|
|
137
|
+
|
|
138
|
+
const dropped = [...existing.keys()].filter((key) => !wanted.has(key) && !key.startsWith("VITE_"));
|
|
139
|
+
await writeFile(path, merged, "utf8");
|
|
140
|
+
result.written.push(dropped.length > 0 ? `${file} ${`(dropped ${dropped.join(", ")})`}` : file);
|
|
141
|
+
}
|
|
142
|
+
|
|
92
143
|
async function put(root: string, file: string, content: string, result: GenerateResult, mode: "regenerate" | "own") {
|
|
93
144
|
const path = join(root, file);
|
|
94
145
|
if (mode === "own" && (await exists(path))) {
|
|
@@ -182,7 +233,7 @@ async function generateApi(root: string, dir: string, manifest: Manifest, result
|
|
|
182
233
|
await put(root, at(".env.production"), renderEnvProduction(manifest), result, "regenerate");
|
|
183
234
|
await put(root, at("lib/deploy/requirements.ts"), renderRequirementsTs(manifest), result, "regenerate");
|
|
184
235
|
|
|
185
|
-
if (opts.env) await
|
|
236
|
+
if (opts.env) await putEnv(root, at(".env"), renderEnvLocal(manifest), result);
|
|
186
237
|
await put(root, at("lib/domain.ts"), renderDomainTs(manifest.layout), result, own);
|
|
187
238
|
await put(root, at("lib/payment.ts"), renderPaymentTs(manifest), result, own);
|
|
188
239
|
await put(root, at("lib/notifications.ts"), renderNotificationsTs(), result, own);
|
|
@@ -263,7 +314,7 @@ async function generateFrontend(root: string, dir: string, manifest: Manifest, r
|
|
|
263
314
|
|
|
264
315
|
await put(root, at("lib/api.ts"), renderFrontendApiTs(manifest), result, "own");
|
|
265
316
|
await put(root, at(".env.production"), renderFrontendEnvProduction(manifest), result, "regenerate");
|
|
266
|
-
await put(root, at("vite.config.ts"), renderFrontendViteConfig(), result, "own");
|
|
317
|
+
await put(root, at("vite.config.ts"), renderFrontendViteConfig(manifest), result, "own");
|
|
267
318
|
await put(root, at("void.json"), renderFrontendVoidJson(), result, "own");
|
|
268
319
|
await put(root, at("tsconfig.json"), renderFrontendTsconfig(), result, "own");
|
|
269
320
|
await put(root, at("pages/index.tsx"), renderStorefrontPage(manifest), result, "own");
|
|
@@ -395,7 +446,7 @@ bunx vc guard
|
|
|
395
446
|
// build. Two workflows on the same push would deploy the shop twice.
|
|
396
447
|
await retire(root, ".github/workflows/void-dist.yml", result);
|
|
397
448
|
await put(root, "DEPLOY.md", renderDeployReadme(manifest, zone(manifest), workerHosts(manifest)), result, "regenerate");
|
|
398
|
-
await
|
|
449
|
+
await putEnv(root, ".env", renderEnvLocal(manifest), result);
|
|
399
450
|
await put(root, "data/README.md", DATA_README, result, "own");
|
|
400
451
|
await put(root, "branding/README.md", BRANDING_README, result, "own");
|
|
401
452
|
await put(root, "migrations/README.md", MIGRATIONS_README, result, "own");
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ export { preflight, printPreflight, productionEnv, routeProblem } from "./deploy
|
|
|
27
27
|
export { deployCloudflare } from "./deploy/cloudflare";
|
|
28
28
|
export {
|
|
29
29
|
FRONTEND_BRANCH,
|
|
30
|
+
renderFrontendViteConfig,
|
|
30
31
|
renderFrontendApiTs,
|
|
31
32
|
renderFrontendEnvProduction,
|
|
32
33
|
renderFrontendWorkflow,
|