@saastemly/voidcommerce 0.1.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 +271 -0
- package/bin/vc +2 -0
- package/dist/catalog.d.ts +69 -0
- package/dist/catalog.js +34 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.js +544 -0
- package/dist/deploy/cloudflare.d.ts +25 -0
- package/dist/deploy/index.d.ts +16 -0
- package/dist/deploy/jsonc.d.ts +8 -0
- package/dist/deploy/preflight.d.ts +29 -0
- package/dist/deploy/wrangler.d.ts +37 -0
- package/dist/dist.d.ts +22 -0
- package/dist/generate/auth.d.ts +2 -0
- package/dist/generate/ci.d.ts +24 -0
- package/dist/generate/env.d.ts +13 -0
- package/dist/generate/frontend.d.ts +47 -0
- package/dist/generate/index.d.ts +28 -0
- package/dist/generate/requirements.d.ts +13 -0
- package/dist/generate/strict.d.ts +72 -0
- package/dist/generate/support.d.ts +23 -0
- package/dist/help.d.ts +31 -0
- package/dist/import.d.ts +2 -0
- package/dist/index-s7sq41qs.js +590 -0
- package/dist/index-ssv3a6wc.js +172 -0
- package/dist/index-wzy1xtr1.js +3155 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +190 -0
- package/dist/init.d.ts +1 -0
- package/dist/manifest.d.ts +131 -0
- package/dist/manifest.js +41 -0
- package/dist/project.d.ts +20 -0
- package/dist/regenerate.d.ts +9 -0
- package/dist/scripts.d.ts +12 -0
- package/dist/void.d.ts +30 -0
- package/dist/wizard.d.ts +7 -0
- package/package.json +50 -0
- package/src/catalog.ts +673 -0
- package/src/cli.ts +78 -0
- package/src/deploy/cloudflare.ts +166 -0
- package/src/deploy/index.ts +101 -0
- package/src/deploy/jsonc.ts +148 -0
- package/src/deploy/preflight.ts +137 -0
- package/src/deploy/wrangler.ts +111 -0
- package/src/dist.ts +157 -0
- package/src/generate/auth.ts +386 -0
- package/src/generate/ci.ts +208 -0
- package/src/generate/env.ts +164 -0
- package/src/generate/frontend.ts +275 -0
- package/src/generate/index.ts +390 -0
- package/src/generate/requirements.ts +48 -0
- package/src/generate/strict.ts +692 -0
- package/src/generate/support.ts +252 -0
- package/src/help.ts +172 -0
- package/src/import.ts +237 -0
- package/src/index.ts +37 -0
- package/src/init.ts +187 -0
- package/src/manifest.ts +303 -0
- package/src/project.ts +63 -0
- package/src/regenerate.ts +51 -0
- package/src/scripts.ts +53 -0
- package/src/void.ts +115 -0
- package/src/wizard.ts +234 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { cp, mkdir, readFile, readdir, symlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { CHOICES } from "../catalog";
|
|
5
|
+
import { MANIFEST_FILE, type Manifest, has, isSingleApp, packagesOf, specifier, workerHosts, writeManifest, zone } from "../manifest";
|
|
6
|
+
import { renderAuthTs } from "./auth";
|
|
7
|
+
import {
|
|
8
|
+
envSummary,
|
|
9
|
+
renderEnvExample,
|
|
10
|
+
renderEnvLocal,
|
|
11
|
+
renderEnvProduction,
|
|
12
|
+
renderEnvTs,
|
|
13
|
+
} from "./env";
|
|
14
|
+
import {
|
|
15
|
+
FRONTEND_BRANCH,
|
|
16
|
+
renderFrontendApiTs,
|
|
17
|
+
renderFrontendEnvProduction,
|
|
18
|
+
renderFrontendTsconfig,
|
|
19
|
+
renderFrontendViteConfig,
|
|
20
|
+
renderFrontendVoidJson,
|
|
21
|
+
renderFrontendWorkflow,
|
|
22
|
+
renderStorefrontPage,
|
|
23
|
+
} from "./frontend";
|
|
24
|
+
import { renderDeployReadme, renderDistWorkflow } from "./ci";
|
|
25
|
+
import { renderRequirementsTs } from "./requirements";
|
|
26
|
+
import {
|
|
27
|
+
BRANDING_README,
|
|
28
|
+
CLIENT_ONLY,
|
|
29
|
+
DATA_README,
|
|
30
|
+
INDEX_PAGE,
|
|
31
|
+
MIGRATIONS_README,
|
|
32
|
+
STRICT_APP,
|
|
33
|
+
renderAppCss,
|
|
34
|
+
renderAuthClient,
|
|
35
|
+
renderContentTs,
|
|
36
|
+
renderCron,
|
|
37
|
+
renderDashboardPage,
|
|
38
|
+
renderDbSchema,
|
|
39
|
+
renderDbSeed,
|
|
40
|
+
renderIndexServer,
|
|
41
|
+
renderLayoutTsx,
|
|
42
|
+
renderLiveRoute,
|
|
43
|
+
renderLiveStream,
|
|
44
|
+
renderQueue,
|
|
45
|
+
renderSignInPage,
|
|
46
|
+
renderStrictAppPackageJson,
|
|
47
|
+
renderStrictTsconfig,
|
|
48
|
+
renderViteConfig,
|
|
49
|
+
renderVoidJson,
|
|
50
|
+
renderVoidPatch,
|
|
51
|
+
strictDependencies,
|
|
52
|
+
} from "./strict";
|
|
53
|
+
import { renderDomainTs, renderErpTs, renderMintTs, renderNotificationsTs, renderPaymentTs } from "./support";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Turn a manifest into files.
|
|
57
|
+
*
|
|
58
|
+
* Two kinds of file, and the difference matters:
|
|
59
|
+
*
|
|
60
|
+
* REGENERATED — `auth.ts`, `env.ts`, `.env.example`, `.env.production`,
|
|
61
|
+
* `lib/deploy/requirements.ts`, the Pages workflow. Derived entirely from
|
|
62
|
+
* the manifest; edit the manifest, not the file.
|
|
63
|
+
*
|
|
64
|
+
* OWNED — `lib/domain.ts`, `lib/payment.ts`, `lib/notifications.ts`,
|
|
65
|
+
* `lib/erp.ts`, `.env`, the frontend's `lib/api.ts`. Written once if
|
|
66
|
+
* absent, then yours. They are what a shop edits when it grows past the
|
|
67
|
+
* wizard, and overwriting them would destroy that work.
|
|
68
|
+
*
|
|
69
|
+
* Where the files go follows the layout: one app writes at the root; a
|
|
70
|
+
* monorepo writes api/ and frontend/ and a root that holds both; strict
|
|
71
|
+
* writes the whole app under .vc/app, where NOTHING is owned — the point of
|
|
72
|
+
* strict is that the manifest is the only source.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
export interface GenerateResult {
|
|
76
|
+
written: string[];
|
|
77
|
+
kept: string[];
|
|
78
|
+
packages: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function exists(path: string): Promise<boolean> {
|
|
82
|
+
try {
|
|
83
|
+
await readFile(path);
|
|
84
|
+
return true;
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function put(root: string, file: string, content: string, result: GenerateResult, mode: "regenerate" | "own") {
|
|
91
|
+
const path = join(root, file);
|
|
92
|
+
if (mode === "own" && (await exists(path))) {
|
|
93
|
+
result.kept.push(file);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
await mkdir(dirname(path), { recursive: true });
|
|
97
|
+
await writeFile(path, content, "utf8");
|
|
98
|
+
result.written.push(file);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Read a JSON file if present, let the caller add to it, write it back. Everything else in it is kept. */
|
|
102
|
+
async function mergeJson(root: string, file: string, fallback: Record<string, unknown>, mutate: (json: Record<string, unknown>) => void, result: GenerateResult) {
|
|
103
|
+
const path = join(root, file);
|
|
104
|
+
const json = (await exists(path)) ? (JSON.parse(await readFile(path, "utf8")) as Record<string, unknown>) : { ...fallback };
|
|
105
|
+
mutate(json);
|
|
106
|
+
await mkdir(dirname(path), { recursive: true });
|
|
107
|
+
await writeFile(path, `${JSON.stringify(json, null, 2)}\n`, "utf8");
|
|
108
|
+
result.written.push(file);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A symlink, if nothing is there yet. Never replaces a real file. */
|
|
112
|
+
async function link(root: string, file: string, target: string, result: GenerateResult) {
|
|
113
|
+
const path = join(root, file);
|
|
114
|
+
try {
|
|
115
|
+
lstatSync(path);
|
|
116
|
+
result.kept.push(file);
|
|
117
|
+
return;
|
|
118
|
+
} catch {
|
|
119
|
+
await mkdir(dirname(path), { recursive: true });
|
|
120
|
+
await symlink(target, path);
|
|
121
|
+
result.written.push(`${file} → ${target}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const sorted = (record: Record<string, string>) => Object.fromEntries(Object.entries(record).sort());
|
|
126
|
+
|
|
127
|
+
export async function generate(root: string, manifest: Manifest): Promise<GenerateResult> {
|
|
128
|
+
const result: GenerateResult = { written: [], kept: [], packages: packagesOf(manifest) };
|
|
129
|
+
|
|
130
|
+
await writeManifest(root, manifest);
|
|
131
|
+
result.written.push(MANIFEST_FILE);
|
|
132
|
+
|
|
133
|
+
switch (manifest.layout) {
|
|
134
|
+
case "app":
|
|
135
|
+
await generateApi(root, "", manifest, result, { ownable: true, env: true });
|
|
136
|
+
break;
|
|
137
|
+
case "monorepo":
|
|
138
|
+
await generateApi(root, "api", manifest, result, { ownable: true, env: true });
|
|
139
|
+
await generateFrontend(root, "frontend", manifest, result);
|
|
140
|
+
await generateRoot(root, manifest, result);
|
|
141
|
+
break;
|
|
142
|
+
case "strict":
|
|
143
|
+
await generateStrictRoot(root, manifest, result);
|
|
144
|
+
// The storefront IS the repository root, sharing its package.json.
|
|
145
|
+
await generateFrontend(root, "", manifest, result);
|
|
146
|
+
await generateApi(root, STRICT_APP, manifest, result, { ownable: false, env: false });
|
|
147
|
+
await generateStrictApp(root, STRICT_APP, manifest, result);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The worker — the whole app in one-app layout, api/ in a monorepo, .vc/app
|
|
155
|
+
* in strict. `ownable` is false where nothing may be hand-edited; `env` is
|
|
156
|
+
* false where `.env` is reached by symlink instead.
|
|
157
|
+
*/
|
|
158
|
+
async function generateApi(root: string, dir: string, manifest: Manifest, result: GenerateResult, opts: { ownable: boolean; env: boolean }) {
|
|
159
|
+
const at = (file: string) => join(dir, file);
|
|
160
|
+
const own = opts.ownable ? "own" : "regenerate";
|
|
161
|
+
const single = isSingleApp(manifest.layout);
|
|
162
|
+
|
|
163
|
+
await put(root, at("auth.ts"), renderAuthTs(manifest), result, "regenerate");
|
|
164
|
+
await put(root, at("env.ts"), renderEnvTs(manifest), result, "regenerate");
|
|
165
|
+
await put(root, at(".env.example"), renderEnvExample(manifest), result, "regenerate");
|
|
166
|
+
await put(root, at(".env.production"), renderEnvProduction(manifest), result, "regenerate");
|
|
167
|
+
await put(root, at("lib/deploy/requirements.ts"), renderRequirementsTs(manifest), result, "regenerate");
|
|
168
|
+
|
|
169
|
+
if (opts.env) await put(root, at(".env"), renderEnvLocal(manifest), result, "own");
|
|
170
|
+
await put(root, at("lib/domain.ts"), renderDomainTs(manifest.layout), result, own);
|
|
171
|
+
await put(root, at("lib/payment.ts"), renderPaymentTs(manifest), result, own);
|
|
172
|
+
await put(root, at("lib/notifications.ts"), renderNotificationsTs(), result, own);
|
|
173
|
+
if (has(manifest, "nft")) {
|
|
174
|
+
await put(root, at("lib/mint.ts"), renderMintTs(), result, own);
|
|
175
|
+
}
|
|
176
|
+
if (has(manifest, "business-central")) {
|
|
177
|
+
await put(root, at("lib/erp.ts"), renderErpTs(), result, own);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const name = manifest.shop.domain.split(".")[0]!;
|
|
181
|
+
const hosts = workerHosts(manifest);
|
|
182
|
+
const routes = `[\n${hosts.map((host) => ` { "pattern": "${host}", "custom_domain": true }`).join(",\n")}\n ]`;
|
|
183
|
+
// Cloudflare Email Service is reached through a binding, not a key.
|
|
184
|
+
const sendEmail = has(manifest, "cloudflare-email")
|
|
185
|
+
? `,
|
|
186
|
+
// Cloudflare Email Service. The binding IS the credential; the sending
|
|
187
|
+
// domain is onboarded once in the dashboard, which writes its own SPF,
|
|
188
|
+
// DKIM, DMARC and bounce records.
|
|
189
|
+
"send_email": [{ "name": "EMAIL" }]`
|
|
190
|
+
: "";
|
|
191
|
+
const cf = manifest.cloudflare;
|
|
192
|
+
const d1 = cf?.d1
|
|
193
|
+
? `,
|
|
194
|
+
// Created by \`vc deploy --cloudflare --provision\`; the id is what the deploy binds.
|
|
195
|
+
"d1_databases": [
|
|
196
|
+
{ "binding": "DB", "database_name": "${cf.d1.name}", "database_id": "${cf.d1.id}", "migrations_dir": "./db/migrations" }
|
|
197
|
+
]`
|
|
198
|
+
: "";
|
|
199
|
+
await put(
|
|
200
|
+
root,
|
|
201
|
+
at("wrangler.jsonc"),
|
|
202
|
+
`{
|
|
203
|
+
"$schema": "./node_modules/wrangler/config-schema.json",
|
|
204
|
+
"name": "${single ? name : `${name}-api`}",
|
|
205
|
+
// Pin the account. A multi-account token makes wrangler prompt, which CI cannot answer.
|
|
206
|
+
"account_id": "${cf?.accountId ?? ""}",
|
|
207
|
+
// The worker's hostname${hosts.length > 1 ? "s" : ""}. Cloudflare custom domain${hosts.length > 1 ? "s" : ""}, not DNS records this
|
|
208
|
+
// app writes — Cloudflare creates the record and the certificate itself.
|
|
209
|
+
// The zone (${zone(manifest)}) must be on Cloudflare; \`vc preflight\` checks the names.
|
|
210
|
+
"routes": ${routes}${sendEmail}${d1}
|
|
211
|
+
}
|
|
212
|
+
`,
|
|
213
|
+
result,
|
|
214
|
+
own,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
if (manifest.layout === "strict") return; // its package.json is the artifact's, written by generateStrictApp
|
|
218
|
+
|
|
219
|
+
await mergeJson(
|
|
220
|
+
root,
|
|
221
|
+
at("package.json"),
|
|
222
|
+
{ name: single ? name : `${name}-api`, type: "module", private: true },
|
|
223
|
+
(pkg) => {
|
|
224
|
+
const deps = { ...((pkg["dependencies"] as Record<string, string>) ?? {}) };
|
|
225
|
+
for (const dep of result.packages) deps[dep] ??= specifier(manifest, dep);
|
|
226
|
+
pkg["dependencies"] = sorted(deps);
|
|
227
|
+
const scripts = { ...((pkg["scripts"] as Record<string, string>) ?? {}) };
|
|
228
|
+
scripts["preflight"] ??= "vc preflight";
|
|
229
|
+
scripts["deploy"] ??= "vc deploy";
|
|
230
|
+
scripts["import:catalog"] ??= "vc import";
|
|
231
|
+
scripts["maildev"] ??= "maildev --smtp 1025 --web 1080";
|
|
232
|
+
pkg["scripts"] = scripts;
|
|
233
|
+
},
|
|
234
|
+
result,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The static storefront on GitHub Pages. Only in a monorepo. */
|
|
239
|
+
async function generateFrontend(root: string, dir: string, manifest: Manifest, result: GenerateResult) {
|
|
240
|
+
const at = (file: string) => join(dir, file);
|
|
241
|
+
|
|
242
|
+
await put(root, at("lib/api.ts"), renderFrontendApiTs(manifest), result, "own");
|
|
243
|
+
await put(root, at(".env.production"), renderFrontendEnvProduction(manifest), result, "regenerate");
|
|
244
|
+
await put(root, at("vite.config.ts"), renderFrontendViteConfig(), result, "own");
|
|
245
|
+
await put(root, at("void.json"), renderFrontendVoidJson(), result, "own");
|
|
246
|
+
await put(root, at("tsconfig.json"), renderFrontendTsconfig(), result, "own");
|
|
247
|
+
await put(root, at("pages/index.tsx"), renderStorefrontPage(manifest), result, "own");
|
|
248
|
+
await put(root, ".github/workflows/frontend-static.yml", renderFrontendWorkflow(manifest, dir), result, "regenerate");
|
|
249
|
+
|
|
250
|
+
// A monorepo's frontend is its own workspace; strict's shares the root's
|
|
251
|
+
// one package.json, which generateStrictRoot has already written.
|
|
252
|
+
if (dir === "") return;
|
|
253
|
+
await mergeJson(
|
|
254
|
+
root,
|
|
255
|
+
at("package.json"),
|
|
256
|
+
{ name: `${manifest.shop.domain.split(".")[0]}-frontend`, type: "module", private: true },
|
|
257
|
+
(pkg) => {
|
|
258
|
+
const deps = { ...((pkg["dependencies"] as Record<string, string>) ?? {}) };
|
|
259
|
+
for (const dep of ["better-auth", "@saastemly/better-commerce", ...(CHOICES.get("custom")?.packages ?? [])]) deps[dep] ??= specifier(manifest, dep);
|
|
260
|
+
pkg["dependencies"] = sorted(deps);
|
|
261
|
+
const dev = { ...((pkg["devDependencies"] as Record<string, string>) ?? {}) };
|
|
262
|
+
for (const dep of ["@hono/node-server", "@void/react", "vite", "vite-plus", "void"]) dev[dep] ??= "latest";
|
|
263
|
+
pkg["devDependencies"] = sorted(dev);
|
|
264
|
+
},
|
|
265
|
+
result,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The monorepo root: one install, and scripts that reach into both. */
|
|
270
|
+
async function generateRoot(root: string, manifest: Manifest, result: GenerateResult) {
|
|
271
|
+
await mergeJson(
|
|
272
|
+
root,
|
|
273
|
+
"package.json",
|
|
274
|
+
{ name: manifest.shop.domain.split(".")[0], type: "module", private: true },
|
|
275
|
+
(pkg) => {
|
|
276
|
+
pkg["workspaces"] = ["api", "frontend"];
|
|
277
|
+
const scripts = { ...((pkg["scripts"] as Record<string, string>) ?? {}) };
|
|
278
|
+
scripts["dev:api"] ??= "cd api && vc dev";
|
|
279
|
+
scripts["dev:frontend"] ??= "cd frontend && vc dev";
|
|
280
|
+
scripts["preflight"] ??= "cd api && vc preflight";
|
|
281
|
+
scripts["deploy"] ??= "cd api && vc deploy";
|
|
282
|
+
scripts["import:catalog"] ??= "cd api && vc import";
|
|
283
|
+
scripts["maildev"] ??= "cd api && bun run maildev";
|
|
284
|
+
pkg["scripts"] = scripts;
|
|
285
|
+
},
|
|
286
|
+
result,
|
|
287
|
+
);
|
|
288
|
+
await put(root, ".gitignore", "node_modules\ndist\n.void\n.wrangler\n*.tsbuildinfo\n.DS_Store\n", result, "own");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The strict repository: the manifest, one install, and what the manifest cannot express. */
|
|
292
|
+
async function generateStrictRoot(root: string, manifest: Manifest, result: GenerateResult) {
|
|
293
|
+
const deps = strictDependencies(manifest);
|
|
294
|
+
await mergeJson(
|
|
295
|
+
root,
|
|
296
|
+
"package.json",
|
|
297
|
+
{ name: manifest.shop.domain.split(".")[0], type: "module", private: true },
|
|
298
|
+
(pkg) => {
|
|
299
|
+
// What is there wins: a pinned version or a `file:` link to a local
|
|
300
|
+
// package is the person's decision, and regenerating must not undo it.
|
|
301
|
+
// Only names the choices need and the file lacks are added.
|
|
302
|
+
pkg["dependencies"] = sorted({ ...deps.dependencies, ...((pkg["dependencies"] as Record<string, string>) ?? {}) });
|
|
303
|
+
pkg["devDependencies"] = sorted({ ...deps.devDependencies, ...((pkg["devDependencies"] as Record<string, string>) ?? {}) });
|
|
304
|
+
pkg["patchedDependencies"] = { ...deps.patchedDependencies, ...((pkg["patchedDependencies"] as Record<string, string>) ?? {}) };
|
|
305
|
+
const scripts = { ...((pkg["scripts"] as Record<string, string>) ?? {}) };
|
|
306
|
+
scripts["generate"] ??= "vc generate";
|
|
307
|
+
scripts["dist"] ??= "vc dist";
|
|
308
|
+
// `dev` is the shop — the worker with the panel. The storefront is
|
|
309
|
+
// the root's own Void app, so it is vp's to run.
|
|
310
|
+
scripts["dev"] ??= "vc dev";
|
|
311
|
+
scripts["build"] ??= "vc build";
|
|
312
|
+
scripts["dev:storefront"] ??= "vp dev";
|
|
313
|
+
scripts["build:storefront"] ??= "vp build";
|
|
314
|
+
scripts["typecheck"] ??= `vc generate && tsc -p ${STRICT_APP}`;
|
|
315
|
+
scripts["preflight"] ??= "vc preflight";
|
|
316
|
+
scripts["deploy"] ??= "vc deploy";
|
|
317
|
+
scripts["import:catalog"] ??= "vc import";
|
|
318
|
+
scripts["maildev"] ??= "maildev --smtp 1025 --web 1080";
|
|
319
|
+
pkg["scripts"] = scripts;
|
|
320
|
+
},
|
|
321
|
+
result,
|
|
322
|
+
);
|
|
323
|
+
// `.void` and `dist` are the STOREFRONT's build artifacts at the root now;
|
|
324
|
+
// `.vc` holds the generated worker and its own.
|
|
325
|
+
await put(root, ".gitignore", "node_modules\n.vc\n.void\n.wrangler\n.env\n.env.local\ndist\n*.tsbuildinfo\n.DS_Store\n", result, "own");
|
|
326
|
+
await put(root, "patches/void@0.10.13.patch", renderVoidPatch(), result, "regenerate");
|
|
327
|
+
await put(root, ".github/workflows/void-dist.yml", renderDistWorkflow(manifest), result, "regenerate");
|
|
328
|
+
await put(root, "DEPLOY.md", renderDeployReadme(manifest, zone(manifest), workerHosts(manifest)), result, "regenerate");
|
|
329
|
+
await put(root, ".env", renderEnvLocal(manifest), result, "own");
|
|
330
|
+
await put(root, "data/README.md", DATA_README, result, "own");
|
|
331
|
+
await put(root, "branding/README.md", BRANDING_README, result, "own");
|
|
332
|
+
await put(root, "migrations/README.md", MIGRATIONS_README, result, "own");
|
|
333
|
+
if (has(manifest, "faqs")) await put(root, "content/faqs.json", "[]\n", result, "own");
|
|
334
|
+
if (has(manifest, "blogs")) await put(root, "content/posts.json", "[]\n", result, "own");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** The strict artifact: a complete Void app, every file regenerated. */
|
|
338
|
+
async function generateStrictApp(root: string, dir: string, manifest: Manifest, result: GenerateResult) {
|
|
339
|
+
const at = (file: string) => join(dir, file);
|
|
340
|
+
const gen = "regenerate";
|
|
341
|
+
|
|
342
|
+
// Reach the root's install, env and migrations rather than carrying copies.
|
|
343
|
+
await link(root, at("node_modules"), join("..", "..", "node_modules"), result);
|
|
344
|
+
await link(root, at(".env"), join("..", "..", ".env"), result);
|
|
345
|
+
await link(root, at("db/migrations"), join("..", "..", "..", "migrations"), result);
|
|
346
|
+
|
|
347
|
+
await put(root, at("package.json"), renderStrictAppPackageJson(manifest), result, gen);
|
|
348
|
+
await put(root, at("vite.config.ts"), renderViteConfig(manifest), result, gen);
|
|
349
|
+
await put(root, at("tsconfig.json"), renderStrictTsconfig(), result, gen);
|
|
350
|
+
await put(root, at("void.json"), renderVoidJson(), result, gen);
|
|
351
|
+
await put(root, at("db/schema.ts"), renderDbSchema(), result, gen);
|
|
352
|
+
await put(root, at("db/seed.ts"), renderDbSeed(), result, gen);
|
|
353
|
+
await put(root, at("pages/layout.tsx"), renderLayoutTsx(), result, gen);
|
|
354
|
+
await put(root, at("pages/app.css"), renderAppCss(), result, gen);
|
|
355
|
+
await put(root, at("pages/index.server.ts"), renderIndexServer(), result, gen);
|
|
356
|
+
await put(root, at("pages/index.tsx"), INDEX_PAGE, result, gen);
|
|
357
|
+
await put(root, at("pages/api-dashboard/index.tsx"), renderDashboardPage(manifest), result, gen);
|
|
358
|
+
await put(root, at("pages/api-dashboard/index.server.ts"), CLIENT_ONLY, result, gen);
|
|
359
|
+
await put(root, at("pages/sign-in/index.tsx"), renderSignInPage(manifest), result, gen);
|
|
360
|
+
await put(root, at("pages/sign-in/index.server.ts"), CLIENT_ONLY, result, gen);
|
|
361
|
+
await put(root, at("lib/auth-client.ts"), renderAuthClient(manifest), result, gen);
|
|
362
|
+
await put(root, at("crons/commerce-ticks.ts"), renderCron(), result, gen);
|
|
363
|
+
await put(root, at("queues/commerce.ts"), renderQueue(), result, gen);
|
|
364
|
+
await put(root, at("src/live.ts"), renderLiveStream(), result, gen);
|
|
365
|
+
await put(root, at("routes/live.ts"), renderLiveRoute(), result, gen);
|
|
366
|
+
|
|
367
|
+
// Inputs the manifest cannot express: snapshot them into the artifact.
|
|
368
|
+
if (has(manifest, "faqs") || has(manifest, "blogs")) {
|
|
369
|
+
await mkdir(join(root, at("content")), { recursive: true });
|
|
370
|
+
for (const file of ["faqs.json", "posts.json"]) {
|
|
371
|
+
const source = join(root, "content", file);
|
|
372
|
+
if (existsSync(source)) {
|
|
373
|
+
await cp(source, join(root, at(`content/${file}`)));
|
|
374
|
+
result.written.push(at(`content/${file}`));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
await put(root, at("lib/content.ts"), renderContentTs(manifest), result, gen);
|
|
378
|
+
}
|
|
379
|
+
const branding = join(root, "branding");
|
|
380
|
+
if (existsSync(branding)) {
|
|
381
|
+
const files = (await readdir(branding)).filter((name) => name !== "README.md");
|
|
382
|
+
if (files.length > 0) {
|
|
383
|
+
await mkdir(join(root, at("public/branding")), { recursive: true });
|
|
384
|
+
for (const file of files) await cp(join(branding, file), join(root, at(`public/branding/${file}`)), { recursive: true });
|
|
385
|
+
result.written.push(`${at("public/branding/")} (${files.length} files)`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export { envSummary };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Manifest } from "../manifest";
|
|
2
|
+
import { allEnvKeys } from "./env";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `lib/deploy/requirements.ts` — the preflight manifest.
|
|
6
|
+
*
|
|
7
|
+
* Every key in `env.ts` is required, which stops one being silently absent.
|
|
8
|
+
* It cannot stop one being PRESENT and meaningless: the `unset` sentinel lets
|
|
9
|
+
* a developer run the app without an Adyen account, and a deployment carrying
|
|
10
|
+
* it would start perfectly, take an order, and never charge anyone.
|
|
11
|
+
*
|
|
12
|
+
* This is the second gate, and the one that says WHY — because a list of key
|
|
13
|
+
* names tells an operator nothing about which one is urgent.
|
|
14
|
+
*/
|
|
15
|
+
export function renderRequirementsTs(manifest: Manifest): string {
|
|
16
|
+
const keys = allEnvKeys(manifest);
|
|
17
|
+
const entries = keys.map(
|
|
18
|
+
(key) =>
|
|
19
|
+
` { key: ${JSON.stringify(key.key)}, breaks: ${JSON.stringify(key.breaks)}${key.where ? `, where: ${JSON.stringify(key.where)}` : ""}${key.plaintext ? ", plaintext: true" : ""} },`,
|
|
20
|
+
);
|
|
21
|
+
return `// GENERATED by \`vc init\` from voidcommerce.json.
|
|
22
|
+
/**
|
|
23
|
+
* What "live" requires, and what breaks without each thing.
|
|
24
|
+
*
|
|
25
|
+
* \`vc deploy\` refuses on any missing entry; \`vc preflight\` prints the same
|
|
26
|
+
* list with what is set. The \`unset\` sentinel counts as missing.
|
|
27
|
+
*/
|
|
28
|
+
export const UNSET = "unset";
|
|
29
|
+
|
|
30
|
+
export interface Requirement {
|
|
31
|
+
key: string;
|
|
32
|
+
breaks: string;
|
|
33
|
+
where?: string;
|
|
34
|
+
plaintext?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const REQUIREMENTS: Requirement[] = [
|
|
38
|
+
${entries.join("\n")}
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export function check(present: Map<string, string | undefined>): Requirement[] {
|
|
42
|
+
return REQUIREMENTS.filter((requirement) => {
|
|
43
|
+
const value = present.get(requirement.key);
|
|
44
|
+
return !present.has(requirement.key) || value === "" || value === UNSET;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
}
|