@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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import {
|
|
2
|
+
box,
|
|
3
|
+
captureVoid,
|
|
4
|
+
deployCloudflare,
|
|
5
|
+
distCommand,
|
|
6
|
+
distHelp,
|
|
7
|
+
ensureGenerated,
|
|
8
|
+
envSummary,
|
|
9
|
+
findProject,
|
|
10
|
+
finishStrict,
|
|
11
|
+
fullHelp,
|
|
12
|
+
generate,
|
|
13
|
+
importCommand,
|
|
14
|
+
importHelp,
|
|
15
|
+
initHelp,
|
|
16
|
+
isVoidApp,
|
|
17
|
+
line,
|
|
18
|
+
preflight,
|
|
19
|
+
printPreflight,
|
|
20
|
+
row,
|
|
21
|
+
runInherit,
|
|
22
|
+
runVoid,
|
|
23
|
+
version,
|
|
24
|
+
voidAppsIn
|
|
25
|
+
} from "./index-wzy1xtr1.js";
|
|
26
|
+
import {
|
|
27
|
+
LAYOUTS,
|
|
28
|
+
readManifest,
|
|
29
|
+
validate,
|
|
30
|
+
zoneOf
|
|
31
|
+
} from "./index-ssv3a6wc.js";
|
|
32
|
+
import {
|
|
33
|
+
GROUPS
|
|
34
|
+
} from "./index-s7sq41qs.js";
|
|
35
|
+
|
|
36
|
+
// src/deploy/index.ts
|
|
37
|
+
import color from "picocolors";
|
|
38
|
+
async function deployCommand(args) {
|
|
39
|
+
const own = new Set(["--cloudflare", "--provision", "--force"]);
|
|
40
|
+
const cloudflare = args.includes("--cloudflare");
|
|
41
|
+
const voids = args.filter((arg) => !own.has(arg));
|
|
42
|
+
const project = await findProject();
|
|
43
|
+
if (!project) {
|
|
44
|
+
if (cloudflare) {
|
|
45
|
+
console.error("vc: no voidcommerce.json here — the Cloudflare deploy needs the manifest to know what the shop requires.");
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
return runVoid(["deploy", ...voids]);
|
|
49
|
+
}
|
|
50
|
+
if (cloudflare) {
|
|
51
|
+
if (voids.length > 0) {
|
|
52
|
+
console.error(`vc: --cloudflare takes only --provision and --force; ${voids.join(" ")} is void's and does not apply.`);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
return deployCloudflare(project, { provision: args.includes("--provision"), force: args.includes("--force") });
|
|
56
|
+
}
|
|
57
|
+
const check = await preflight(project, "void");
|
|
58
|
+
printPreflight(project, check, "void");
|
|
59
|
+
if (!check.ready && check.remote !== null && !args.includes("--force")) {
|
|
60
|
+
console.error(`${color.red("✗")} refusing to deploy a shop that is not ready for customers. Fix the above, or pass --force.
|
|
61
|
+
`);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
if (!check.ready && check.remote === null) {
|
|
65
|
+
console.log(color.dim(`Secrets could not be verified here; void's own deploy gate is the next check.
|
|
66
|
+
`));
|
|
67
|
+
}
|
|
68
|
+
return runVoid(["deploy", ...voids], project.appDir);
|
|
69
|
+
}
|
|
70
|
+
async function preflightCommand(args) {
|
|
71
|
+
const project = await findProject();
|
|
72
|
+
if (!project) {
|
|
73
|
+
console.error("vc: no voidcommerce.json here.");
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
const source = args.includes("--cloudflare") ? "wrangler" : "void";
|
|
77
|
+
const result = await preflight(project, source);
|
|
78
|
+
printPreflight(project, result, source);
|
|
79
|
+
return result.ready ? 0 : 1;
|
|
80
|
+
}
|
|
81
|
+
async function deployHelp() {
|
|
82
|
+
const captured = await captureVoid(["deploy", "--help"]);
|
|
83
|
+
const width = 80;
|
|
84
|
+
const ours = box("vc deploy", [
|
|
85
|
+
line("vc's preflight — every required key set, the hostnames right — then", width),
|
|
86
|
+
line("void deploy, untouched. Or, with --cloudflare, your own account via wrangler.", width),
|
|
87
|
+
line("", width),
|
|
88
|
+
line(color.bold("Usage"), width),
|
|
89
|
+
...row("vc deploy [void's flags]", "preflight, then void deploy — the Void platform, void's login", width, 2),
|
|
90
|
+
...row("vc deploy --cloudflare", "wrangler, your Cloudflare account, no Void login: preflight, build, scrub baked secrets, migrate D1, deploy", width, 2),
|
|
91
|
+
...row("vc deploy --cloudflare --provision", "first time: create the D1 database and the queue, record them", width, 2),
|
|
92
|
+
...row("vc deploy --cloudflare --force", "deploy a shop preflight says is not ready — deliberately", width, 2),
|
|
93
|
+
line("", width),
|
|
94
|
+
line(color.bold("Needs, for --cloudflare"), width),
|
|
95
|
+
...row("wrangler login", "or CLOUDFLARE_API_TOKEN; the account is pinned for you when there is one", width, 2),
|
|
96
|
+
...row("wrangler secret put <KEY>", "each secret, on the worker — preflight lists them", width, 2)
|
|
97
|
+
], width);
|
|
98
|
+
if (captured)
|
|
99
|
+
process.stdout.write(captured.out.replace(/\n*$/, `
|
|
100
|
+
`));
|
|
101
|
+
console.log(ours);
|
|
102
|
+
return captured?.code ?? 0;
|
|
103
|
+
}
|
|
104
|
+
async function preflightHelp() {
|
|
105
|
+
const width = 80;
|
|
106
|
+
console.log(box("vc preflight", [
|
|
107
|
+
line("Is this shop ready to advertise on? Every required key, and what breaks without it.", width),
|
|
108
|
+
line("", width),
|
|
109
|
+
...row("vc preflight", "secrets from the Void platform", width, 2),
|
|
110
|
+
...row("vc preflight --cloudflare", "secrets from the worker, via wrangler", width, 2),
|
|
111
|
+
line("", width),
|
|
112
|
+
line("Exit 0 when ready. `vc deploy` runs this first and refuses otherwise.", width)
|
|
113
|
+
], width));
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/init.ts
|
|
118
|
+
import * as p2 from "@clack/prompts";
|
|
119
|
+
import { mkdir } from "node:fs/promises";
|
|
120
|
+
import { basename, join } from "node:path";
|
|
121
|
+
import color3 from "picocolors";
|
|
122
|
+
|
|
123
|
+
// src/wizard.ts
|
|
124
|
+
import * as p from "@clack/prompts";
|
|
125
|
+
import color2 from "picocolors";
|
|
126
|
+
var bail = () => {
|
|
127
|
+
p.cancel("Nothing was written.");
|
|
128
|
+
process.exit(0);
|
|
129
|
+
};
|
|
130
|
+
var unwrap = (value) => {
|
|
131
|
+
if (p.isCancel(value))
|
|
132
|
+
bail();
|
|
133
|
+
return value;
|
|
134
|
+
};
|
|
135
|
+
async function askLayout() {
|
|
136
|
+
return unwrap(await p.select({
|
|
137
|
+
message: "Layout",
|
|
138
|
+
options: LAYOUTS.map((layout) => ({ value: layout.id, label: layout.label, hint: layout.hint })),
|
|
139
|
+
initialValue: "app"
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
async function askGroup(group2, previous, alsoRequired = []) {
|
|
143
|
+
const requiredIds = new Set([...group2.choices.filter((choice) => choice.required).map((c) => c.id), ...alsoRequired]);
|
|
144
|
+
const required = group2.choices.filter((choice) => requiredIds.has(choice.id));
|
|
145
|
+
const optional = group2.choices.filter((choice) => !requiredIds.has(choice.id));
|
|
146
|
+
const message = group2.title + (group2.intro ? `
|
|
147
|
+
${color2.dim(group2.intro)}` : "") + (required.length ? `
|
|
148
|
+
${color2.dim(`Included: ${required.map((choice) => choice.label).join(", ")}`)}` : "");
|
|
149
|
+
if (optional.length === 0) {
|
|
150
|
+
p.log.info(message);
|
|
151
|
+
return required.map((choice) => choice.id);
|
|
152
|
+
}
|
|
153
|
+
const options = optional.map((choice) => ({
|
|
154
|
+
value: choice.id,
|
|
155
|
+
label: choice.label,
|
|
156
|
+
hint: choice.hint
|
|
157
|
+
}));
|
|
158
|
+
if (group2.kind === "select") {
|
|
159
|
+
const initial2 = previous?.[0] ?? optional.find((choice) => choice.recommended)?.id ?? optional[0].id;
|
|
160
|
+
const picked2 = unwrap(await p.select({ message, options, initialValue: initial2 }));
|
|
161
|
+
return [...required.map((choice) => choice.id), picked2];
|
|
162
|
+
}
|
|
163
|
+
const initial = previous ?? optional.filter((choice) => choice.recommended).map((c) => c.id);
|
|
164
|
+
const picked = unwrap(await p.multiselect({ message, options, initialValues: initial, required: false }));
|
|
165
|
+
return [...required.map((choice) => choice.id), ...picked];
|
|
166
|
+
}
|
|
167
|
+
async function runWizard(existing, layout) {
|
|
168
|
+
const monorepo = layout === "monorepo";
|
|
169
|
+
const frontend = layout !== "app";
|
|
170
|
+
const shop = unwrap(await p.group({
|
|
171
|
+
name: () => p.text({
|
|
172
|
+
message: "Shop name",
|
|
173
|
+
placeholder: "Northwind",
|
|
174
|
+
initialValue: existing?.shop.name ?? "",
|
|
175
|
+
validate: (value) => value.trim() ? undefined : "A shop has a name."
|
|
176
|
+
}),
|
|
177
|
+
domain: () => p.text({
|
|
178
|
+
message: `Domain ${color2.dim(monorepo ? "— the storefront; api.<domain> is the worker; the CORS list and DNS records derive from it" : "— the shop answers here; the worker's custom domain")}`,
|
|
179
|
+
placeholder: "northwind.com",
|
|
180
|
+
initialValue: existing?.shop.domain ?? "",
|
|
181
|
+
validate: (value) => /^[a-z0-9-]+(\.[a-z0-9-]+)+\.[a-z]{2,}$|^[a-z0-9-]+\.[a-z]{2,}$/i.test(value.trim()) ? undefined : "A hostname, like northwind.com or shop.northwind.com."
|
|
182
|
+
}),
|
|
183
|
+
zone: ({ results }) => {
|
|
184
|
+
const domain = (results.domain ?? "").trim().toLowerCase();
|
|
185
|
+
const guess = zoneOf(domain);
|
|
186
|
+
if (!domain || domain === guess)
|
|
187
|
+
return Promise.resolve(undefined);
|
|
188
|
+
return p.text({
|
|
189
|
+
message: `DNS zone ${color2.dim(`— ${domain} lives inside it; records go here, and this zone must be on your DNS provider`)}`,
|
|
190
|
+
placeholder: guess,
|
|
191
|
+
initialValue: existing?.shop.zone ?? guess,
|
|
192
|
+
validate: (value) => {
|
|
193
|
+
const zone = value.trim().toLowerCase();
|
|
194
|
+
if (!zone)
|
|
195
|
+
return "The zone that contains the domain.";
|
|
196
|
+
return domain === zone || domain.endsWith(`.${zone}`) ? undefined : `${domain} is not inside ${zone}.`;
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
country: () => p.text({
|
|
201
|
+
message: `Country ${color2.dim("— sets the declared VAT rate and the default region")}`,
|
|
202
|
+
placeholder: "US",
|
|
203
|
+
initialValue: existing?.shop.country ?? "",
|
|
204
|
+
validate: (value) => /^[A-Za-z]{2}$/.test(value.trim()) ? undefined : "ISO-3166 alpha-2, like DK."
|
|
205
|
+
}),
|
|
206
|
+
taxRegistered: () => p.select({
|
|
207
|
+
message: `Registered to charge tax? ${color2.dim("— a business below its registration threshold must NOT add tax to a price, and one above it must")}`,
|
|
208
|
+
options: [
|
|
209
|
+
{ value: "no", label: "Not registered", hint: "prices are what the customer pays; no tax is added. The common case for a new shop" },
|
|
210
|
+
{ value: "yes", label: "Registered", hint: "the country's standard rate is declared and added" }
|
|
211
|
+
],
|
|
212
|
+
initialValue: existing?.shop.taxRegistered ? "yes" : "no"
|
|
213
|
+
}),
|
|
214
|
+
currency: () => p.text({
|
|
215
|
+
message: "Currency",
|
|
216
|
+
placeholder: "usd",
|
|
217
|
+
initialValue: existing?.shop.currency ?? "",
|
|
218
|
+
validate: (value) => /^[A-Za-z]{3}$/.test(value.trim()) ? undefined : "ISO-4217, like dkk."
|
|
219
|
+
}),
|
|
220
|
+
locale: () => p.text({
|
|
221
|
+
message: `Locale ${color2.dim("— the language of emails and launch content")}`,
|
|
222
|
+
placeholder: "en",
|
|
223
|
+
initialValue: existing?.shop.locale ?? "",
|
|
224
|
+
validate: (value) => /^[a-z]{2}(-[A-Z]{2})?$/.test(value.trim()) ? undefined : "like da, or en-GB."
|
|
225
|
+
}),
|
|
226
|
+
...frontend ? {
|
|
227
|
+
pagesHost: () => p.text({
|
|
228
|
+
message: `GitHub Pages host ${color2.dim("— where the storefront is published; leave blank if you serve the branch elsewhere")}`,
|
|
229
|
+
placeholder: "northwind.github.io",
|
|
230
|
+
initialValue: existing?.shop.pagesHost ?? "",
|
|
231
|
+
validate: (value) => !value.trim() || /^[a-z0-9-]+\.github\.io$/i.test(value.trim()) ? undefined : "<owner>.github.io, or blank"
|
|
232
|
+
})
|
|
233
|
+
} : {}
|
|
234
|
+
}, { onCancel: bail }));
|
|
235
|
+
const chosen = {};
|
|
236
|
+
for (const group2 of GROUPS) {
|
|
237
|
+
if (group2.id === "ui" && layout === "strict") {
|
|
238
|
+
p.log.info(`${group2.title}
|
|
239
|
+
${color2.dim("The generated storefront and panel. Strict has no custom code to host anything else.")}`);
|
|
240
|
+
chosen[group2.id] = ["admin"];
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const alsoRequired = monorepo && group2.id === "ui" ? ["custom"] : [];
|
|
244
|
+
chosen[group2.id] = await askGroup(group2, existing?.chosen[group2.id], alsoRequired);
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
version: 1,
|
|
248
|
+
layout,
|
|
249
|
+
shop: {
|
|
250
|
+
name: shop.name.trim(),
|
|
251
|
+
domain: shop.domain.trim().toLowerCase(),
|
|
252
|
+
country: shop.country.trim().toUpperCase(),
|
|
253
|
+
currency: shop.currency.trim().toLowerCase(),
|
|
254
|
+
locale: shop.locale.trim(),
|
|
255
|
+
taxRegistered: shop.taxRegistered === "yes",
|
|
256
|
+
zone: shop.zone?.trim().toLowerCase() || undefined,
|
|
257
|
+
pagesHost: frontend ? shop.pagesHost?.trim().toLowerCase() || undefined : undefined
|
|
258
|
+
},
|
|
259
|
+
chosen
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function summarise(manifest) {
|
|
263
|
+
const lines = [
|
|
264
|
+
`${color2.dim("Layout".padEnd(34))} ${LAYOUTS.find((l) => l.id === manifest.layout)?.label ?? manifest.layout}`
|
|
265
|
+
];
|
|
266
|
+
for (const group2 of GROUPS) {
|
|
267
|
+
const ids = manifest.chosen[group2.id] ?? [];
|
|
268
|
+
if (ids.length === 0)
|
|
269
|
+
continue;
|
|
270
|
+
const labels = ids.map((id) => group2.choices.find((choice) => choice.id === id)?.label ?? id).join(", ");
|
|
271
|
+
lines.push(`${color2.dim(group2.title.padEnd(34))} ${labels}`);
|
|
272
|
+
}
|
|
273
|
+
return lines.join(`
|
|
274
|
+
`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/init.ts
|
|
278
|
+
function parseFlags(args) {
|
|
279
|
+
const voids = [];
|
|
280
|
+
let layout;
|
|
281
|
+
let link;
|
|
282
|
+
for (let i = 0;i < args.length; i++) {
|
|
283
|
+
const arg = args[i];
|
|
284
|
+
const linked = arg.startsWith("--link=") ? arg.slice("--link=".length) : arg === "--link" ? args[++i] : undefined;
|
|
285
|
+
if (linked !== undefined) {
|
|
286
|
+
if (!linked)
|
|
287
|
+
return { voids, problem: "--link needs a directory holding the betterCommerce packages" };
|
|
288
|
+
link = linked;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const value = arg.startsWith("--layout=") ? arg.slice("--layout=".length) : arg === "--layout" ? args[++i] : undefined;
|
|
292
|
+
if (value === undefined) {
|
|
293
|
+
voids.push(arg);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (!LAYOUTS.some((l) => l.id === value)) {
|
|
297
|
+
return { voids, problem: `--layout must be one of ${LAYOUTS.map((l) => l.id).join(", ")}, not "${value ?? ""}"` };
|
|
298
|
+
}
|
|
299
|
+
layout = value;
|
|
300
|
+
}
|
|
301
|
+
return { layout, link, voids };
|
|
302
|
+
}
|
|
303
|
+
async function init(args) {
|
|
304
|
+
const flags = parseFlags(args);
|
|
305
|
+
if (flags.problem) {
|
|
306
|
+
console.error(`vc: ${flags.problem}`);
|
|
307
|
+
return 1;
|
|
308
|
+
}
|
|
309
|
+
if (flags.voids.length > 0)
|
|
310
|
+
return runVoid(["init", ...flags.voids]);
|
|
311
|
+
let root = process.cwd();
|
|
312
|
+
p2.intro(color3.bgCyan(color3.black(" voidcommerce ")));
|
|
313
|
+
const existing = await readManifest(root);
|
|
314
|
+
if (existing) {
|
|
315
|
+
p2.log.info(`Found ${color3.cyan("voidcommerce.json")} for ${color3.bold(existing.shop.name)} — answers are pre-filled; the layout (${existing.layout}) is fixed.`);
|
|
316
|
+
if (flags.layout && flags.layout !== existing.layout) {
|
|
317
|
+
p2.log.error(`This shop is laid out as "${existing.layout}". The layout cannot change after init — moving files is not a regeneration.`);
|
|
318
|
+
return 1;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const layout = existing?.layout ?? flags.layout ?? await askLayout();
|
|
322
|
+
if (layout === "strict") {
|
|
323
|
+
p2.log.step(`Strict: the app will be generated under ${color3.cyan(".vc/app")} — nothing there is yours to edit.`);
|
|
324
|
+
} else if (layout === "app") {
|
|
325
|
+
if (!isVoidApp(root)) {
|
|
326
|
+
p2.log.step(`No Void app here yet — ${color3.cyan("void init")} first, then the shop.`);
|
|
327
|
+
const before = new Set(voidAppsIn(root));
|
|
328
|
+
const code = await runVoid(["init"]);
|
|
329
|
+
if (code !== 0)
|
|
330
|
+
return code;
|
|
331
|
+
if (!isVoidApp(root)) {
|
|
332
|
+
const created = voidAppsIn(root).filter((dir) => !before.has(dir));
|
|
333
|
+
if (created.length !== 1) {
|
|
334
|
+
console.error(`vc: void init did not leave a Void app in ${root}, so there is nothing to build a shop on.`);
|
|
335
|
+
return 1;
|
|
336
|
+
}
|
|
337
|
+
root = created[0];
|
|
338
|
+
process.chdir(root);
|
|
339
|
+
p2.log.step(`Continuing in ${color3.cyan(`${basename(root)}/`)}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
const parts = [
|
|
344
|
+
["api", "the D1 starter — this is the worker"],
|
|
345
|
+
["frontend", "Static Pages — this is prerendered for GitHub Pages"]
|
|
346
|
+
];
|
|
347
|
+
for (const [dir, starter] of parts) {
|
|
348
|
+
const target = join(root, dir);
|
|
349
|
+
if (isVoidApp(target))
|
|
350
|
+
continue;
|
|
351
|
+
await mkdir(target, { recursive: true });
|
|
352
|
+
p2.log.step(`${color3.cyan("void init")} in ${color3.cyan(`${dir}/`)} — choose ${starter}.`);
|
|
353
|
+
const code = await runVoid(["init"], target);
|
|
354
|
+
if (code !== 0)
|
|
355
|
+
return code;
|
|
356
|
+
if (!isVoidApp(target)) {
|
|
357
|
+
console.error(`vc: void init did not leave a Void app in ${dir}/, so there is nothing to build on.`);
|
|
358
|
+
return 1;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const manifest = await runWizard(existing, layout);
|
|
363
|
+
const link = flags.link ?? existing?.link;
|
|
364
|
+
if (link)
|
|
365
|
+
manifest.link = link;
|
|
366
|
+
const problems = validate(manifest);
|
|
367
|
+
if (problems.length > 0) {
|
|
368
|
+
p2.log.error("These answers contradict each other:");
|
|
369
|
+
for (const problem of problems)
|
|
370
|
+
p2.log.message(` ${color3.red("✗")} ${problem}`);
|
|
371
|
+
p2.cancel("Nothing was written.");
|
|
372
|
+
return 1;
|
|
373
|
+
}
|
|
374
|
+
p2.note(summarise(manifest), `${manifest.shop.name} · ${manifest.shop.domain}`);
|
|
375
|
+
const go = await p2.confirm({ message: "Write the files?", initialValue: true });
|
|
376
|
+
if (p2.isCancel(go) || !go) {
|
|
377
|
+
p2.cancel("Nothing was written.");
|
|
378
|
+
return 0;
|
|
379
|
+
}
|
|
380
|
+
const spinner2 = p2.spinner();
|
|
381
|
+
spinner2.start("Generating");
|
|
382
|
+
const result = await generate(root, manifest);
|
|
383
|
+
spinner2.stop("Generated");
|
|
384
|
+
const { secrets, plaintext } = envSummary(manifest);
|
|
385
|
+
p2.note([
|
|
386
|
+
...result.written.map((file) => `${color3.green("+")} ${file}`),
|
|
387
|
+
...result.kept.map((file) => `${color3.dim("=")} ${file} ${color3.dim("(kept — yours)")}`)
|
|
388
|
+
].join(`
|
|
389
|
+
`), "Files");
|
|
390
|
+
p2.note([
|
|
391
|
+
`${color3.bold(String(secrets.length))} secrets ${color3.dim("→ wrangler secret put <NAME>")}`,
|
|
392
|
+
...secrets.map((key) => ` ${key}`),
|
|
393
|
+
"",
|
|
394
|
+
`${color3.bold(String(plaintext.length))} plaintext ${color3.dim("→ .env.production, already written")}`,
|
|
395
|
+
...plaintext.map((key) => ` ${key}`)
|
|
396
|
+
].join(`
|
|
397
|
+
`), "Before going live");
|
|
398
|
+
if (layout === "strict") {
|
|
399
|
+
const code = await finishStrict(root);
|
|
400
|
+
if (code !== 0)
|
|
401
|
+
return code;
|
|
402
|
+
}
|
|
403
|
+
const next = layout === "app" ? [
|
|
404
|
+
` ${color3.cyan("bun install")}`,
|
|
405
|
+
` ${color3.cyan("bun run maildev")} ${color3.dim("local inbox at http://localhost:1080")}`,
|
|
406
|
+
` ${color3.cyan("vc dev")}`,
|
|
407
|
+
` ${color3.cyan("vc preflight")} ${color3.dim("what is still missing, and why it matters")}`
|
|
408
|
+
] : layout === "strict" ? [
|
|
409
|
+
` ${color3.cyan("bun install")}`,
|
|
410
|
+
` ${color3.cyan("vc generate")} ${color3.dim("the app under .vc/app, void's artifacts, the migrations")}`,
|
|
411
|
+
` ${color3.cyan("bun run maildev")} ${color3.dim("local inbox at http://localhost:1080")}`,
|
|
412
|
+
` ${color3.cyan("vc dev")} ${color3.dim("runs in .vc/app")}`
|
|
413
|
+
] : [
|
|
414
|
+
` ${color3.cyan("bun install")} ${color3.dim("one install for both workspaces")}`,
|
|
415
|
+
` ${color3.cyan("bun run maildev")} ${color3.dim("local inbox at http://localhost:1080")}`,
|
|
416
|
+
` ${color3.cyan("bun run dev:api")} ${color3.dim("the worker, with the panel")}`,
|
|
417
|
+
` ${color3.cyan("bun run dev:frontend")} ${color3.dim("the storefront, against the local API")}`,
|
|
418
|
+
` ${color3.cyan("bun run preflight")} ${color3.dim("what is still missing, and why it matters")}`
|
|
419
|
+
];
|
|
420
|
+
p2.outro(["Next:", ...next].join(`
|
|
421
|
+
`));
|
|
422
|
+
return 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/regenerate.ts
|
|
426
|
+
import * as p3 from "@clack/prompts";
|
|
427
|
+
import color4 from "picocolors";
|
|
428
|
+
async function generateCommand() {
|
|
429
|
+
const project = await findProject();
|
|
430
|
+
if (!project) {
|
|
431
|
+
console.error("vc: no voidcommerce.json here. `vc init` writes one.");
|
|
432
|
+
return 1;
|
|
433
|
+
}
|
|
434
|
+
const problems = validate(project.manifest);
|
|
435
|
+
if (problems.length > 0) {
|
|
436
|
+
console.error("vc: voidcommerce.json contradicts itself:");
|
|
437
|
+
for (const problem of problems)
|
|
438
|
+
console.error(` ✗ ${problem}`);
|
|
439
|
+
return 1;
|
|
440
|
+
}
|
|
441
|
+
const result = await generate(project.root, project.manifest);
|
|
442
|
+
p3.log.step([
|
|
443
|
+
...result.written.map((file) => `${color4.green("+")} ${file}`),
|
|
444
|
+
...result.kept.map((file) => `${color4.dim("=")} ${file} ${color4.dim("(kept)")}`)
|
|
445
|
+
].join(`
|
|
446
|
+
`));
|
|
447
|
+
return project.manifest.layout === "strict" ? finishStrict(project.root) : 0;
|
|
448
|
+
}
|
|
449
|
+
async function generateHelp() {
|
|
450
|
+
const width = 80;
|
|
451
|
+
console.log(box("vc generate", [
|
|
452
|
+
line("The files from voidcommerce.json, without the form.", width),
|
|
453
|
+
line("", width),
|
|
454
|
+
...row("one app, monorepo", "rewrites the regenerated files; files you own are kept", width, 2),
|
|
455
|
+
...row("strict", "rewrites the whole app under .vc/app, then runs void prepare and void db generate", width, 2),
|
|
456
|
+
line("", width),
|
|
457
|
+
line("vc runs this itself when a strict root has no app yet.", width)
|
|
458
|
+
], width));
|
|
459
|
+
return 0;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/scripts.ts
|
|
463
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
464
|
+
import { join as join2, relative } from "node:path";
|
|
465
|
+
function appScript(name) {
|
|
466
|
+
return async (args) => {
|
|
467
|
+
const project = await findProject();
|
|
468
|
+
if (!project)
|
|
469
|
+
return runVoid([name, ...args]);
|
|
470
|
+
const code = await ensureGenerated(project);
|
|
471
|
+
if (code !== 0)
|
|
472
|
+
return code;
|
|
473
|
+
const pkgPath = join2(project.appDir, "package.json");
|
|
474
|
+
const scripts = existsSync(pkgPath) ? JSON.parse(readFileSync(pkgPath, "utf8")).scripts ?? {} : {};
|
|
475
|
+
if (!scripts[name]) {
|
|
476
|
+
console.error(`vc: ${relative(process.cwd(), pkgPath) || "package.json"} has no "${name}" script — void init writes one.`);
|
|
477
|
+
return 1;
|
|
478
|
+
}
|
|
479
|
+
return runInherit(process.execPath, ["run", name, ...args], project.appDir);
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function appScriptHelp(name) {
|
|
483
|
+
return async () => {
|
|
484
|
+
const width = 80;
|
|
485
|
+
console.log(box(`vc ${name}`, [
|
|
486
|
+
line(`The app's own "${name}" script, run where the app is.`, width),
|
|
487
|
+
line("", width),
|
|
488
|
+
...row("one app", "here", width, 2),
|
|
489
|
+
...row("monorepo", "api/, from the root or from inside api/", width, 2),
|
|
490
|
+
...row("strict", ".vc/app, generated first if it is not there", width, 2),
|
|
491
|
+
line("", width),
|
|
492
|
+
line("Arguments pass through to the script. void has no such command;", width),
|
|
493
|
+
line("void init writes this script into the app's package.json.", width)
|
|
494
|
+
], width));
|
|
495
|
+
return 0;
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/cli.ts
|
|
500
|
+
var EXTENDED = {
|
|
501
|
+
init: { run: init, help: initHelp },
|
|
502
|
+
generate: { run: generateCommand, help: generateHelp },
|
|
503
|
+
dist: { run: distCommand, help: distHelp },
|
|
504
|
+
import: { run: importCommand, help: importHelp },
|
|
505
|
+
deploy: { run: deployCommand, help: deployHelp },
|
|
506
|
+
preflight: { run: preflightCommand, help: preflightHelp },
|
|
507
|
+
dev: { run: appScript("dev"), help: appScriptHelp("dev") },
|
|
508
|
+
build: { run: appScript("build"), help: appScriptHelp("build") },
|
|
509
|
+
preview: { run: appScript("preview"), help: appScriptHelp("preview") }
|
|
510
|
+
};
|
|
511
|
+
var HELP = new Set(["help", "--help", "-h"]);
|
|
512
|
+
var VERSION = new Set(["--version", "-v", "-V"]);
|
|
513
|
+
async function passthrough(argv) {
|
|
514
|
+
const project = await findProject();
|
|
515
|
+
if (!project)
|
|
516
|
+
return runVoid(argv);
|
|
517
|
+
const code = await ensureGenerated(project);
|
|
518
|
+
if (code !== 0)
|
|
519
|
+
return code;
|
|
520
|
+
return runVoid(argv, project.appDir);
|
|
521
|
+
}
|
|
522
|
+
async function main(argv) {
|
|
523
|
+
const [command, ...rest] = argv;
|
|
524
|
+
if (command === undefined || HELP.has(command) && rest.length === 0)
|
|
525
|
+
return fullHelp();
|
|
526
|
+
if (VERSION.has(command) && rest.length === 0)
|
|
527
|
+
return version();
|
|
528
|
+
if (HELP.has(command)) {
|
|
529
|
+
const target = EXTENDED[rest[0] ?? ""];
|
|
530
|
+
return target ? target.help() : passthrough(argv);
|
|
531
|
+
}
|
|
532
|
+
const extended = EXTENDED[command];
|
|
533
|
+
if (extended) {
|
|
534
|
+
if (rest.some((arg) => arg === "--help" || arg === "-h"))
|
|
535
|
+
return extended.help();
|
|
536
|
+
return extended.run(rest);
|
|
537
|
+
}
|
|
538
|
+
return passthrough(argv);
|
|
539
|
+
}
|
|
540
|
+
process.exit(await main(process.argv.slice(2)));
|
|
541
|
+
export {
|
|
542
|
+
main,
|
|
543
|
+
EXTENDED
|
|
544
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Project } from "../project";
|
|
2
|
+
/**
|
|
3
|
+
* `vc deploy --cloudflare` — the shop to your own Cloudflare account, with
|
|
4
|
+
* wrangler, and nothing else. No Void account, no linked project.
|
|
5
|
+
*
|
|
6
|
+
* It is Void's documented manual path, automated:
|
|
7
|
+
*
|
|
8
|
+
* 1. wrangler is installed and logged in; the account is pinned
|
|
9
|
+
* 2. preflight — every required key is a worker secret or a committed
|
|
10
|
+
* plaintext value; the hostnames are the shop's
|
|
11
|
+
* 3. --provision: the D1 database and the queue exist and are recorded
|
|
12
|
+
* 4. build
|
|
13
|
+
* 5. SCRUB the emitted config. Void's build bakes every .env* value into
|
|
14
|
+
* the worker's vars as plaintext — a dev cron secret, `unset` for each
|
|
15
|
+
* credential — and a var shadows the secret of the same name. So every
|
|
16
|
+
* secret-class key and every `unset` is removed from dist/ssr/wrangler.json
|
|
17
|
+
* before it is uploaded; the worker reads those from `wrangler secret`.
|
|
18
|
+
* 6. apply the committed migrations to the remote D1
|
|
19
|
+
* 7. wrangler deploy, on exactly the config that was scrubbed
|
|
20
|
+
*/
|
|
21
|
+
export interface CloudflareOptions {
|
|
22
|
+
provision: boolean;
|
|
23
|
+
force: boolean;
|
|
24
|
+
}
|
|
25
|
+
export declare function deployCloudflare(project: Project, opts: CloudflareOptions): Promise<number>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vc deploy` extends `void deploy`: vc's preflight first, then void's
|
|
3
|
+
* deploy, verbatim. `--cloudflare` is the other road — wrangler, your own
|
|
4
|
+
* account, no Void login — and takes vc's own flags:
|
|
5
|
+
*
|
|
6
|
+
* --cloudflare deploy with wrangler to your Cloudflare account
|
|
7
|
+
* --provision create the D1 database and the queue on first deploy
|
|
8
|
+
* --force deploy even when preflight says the shop is not ready
|
|
9
|
+
*
|
|
10
|
+
* Everything else after `deploy` is void's, untouched.
|
|
11
|
+
*/
|
|
12
|
+
export declare function deployCommand(args: string[]): Promise<number>;
|
|
13
|
+
export declare function preflightCommand(args: string[]): Promise<number>;
|
|
14
|
+
/** void's deploy help, then what vc adds. */
|
|
15
|
+
export declare function deployHelp(): Promise<number>;
|
|
16
|
+
export declare function preflightHelp(): Promise<number>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enough JSONC to read and edit a `wrangler.jsonc` without losing its
|
|
3
|
+
* comments. Wrangler's own edits normalise the whole file; these do not.
|
|
4
|
+
*/
|
|
5
|
+
/** Parse JSON with `//` and `/* *\/` comments and trailing commas. */
|
|
6
|
+
export declare function parseJsonc<T = unknown>(text: string): T;
|
|
7
|
+
/** Set a top-level key, replacing its value or adding it last. Comments and the rest of the file are kept. */
|
|
8
|
+
export declare function upsertJsonc(text: string, key: string, value: unknown, indent?: string): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EnvKey } from "../catalog";
|
|
2
|
+
import type { Project } from "../project";
|
|
3
|
+
/**
|
|
4
|
+
* Is this deployment ready to advertise on?
|
|
5
|
+
*
|
|
6
|
+
* Every key in env.ts is required, which stops one being silently absent.
|
|
7
|
+
* It cannot stop one being PRESENT and meaningless: the `unset` sentinel
|
|
8
|
+
* lets a developer run the app without an Adyen account, and a deployment
|
|
9
|
+
* carrying it would start perfectly, take an order, and never charge
|
|
10
|
+
* anyone. This is the gate that says WHY each thing matters.
|
|
11
|
+
*/
|
|
12
|
+
export declare const UNSET = "unset";
|
|
13
|
+
export interface Preflight {
|
|
14
|
+
/** What is set, from .env.production and the worker's secrets. */
|
|
15
|
+
present: Map<string, string>;
|
|
16
|
+
/** Secret names on the worker; null when they could not be read. */
|
|
17
|
+
remote: Set<string> | null;
|
|
18
|
+
missing: EnvKey[];
|
|
19
|
+
routeProblem: string | null;
|
|
20
|
+
ready: boolean;
|
|
21
|
+
}
|
|
22
|
+
export type SecretSource = "wrangler" | "void";
|
|
23
|
+
/** Keys committed in .env.production — safe values only, by construction. */
|
|
24
|
+
export declare function productionEnv(appDir: string): Map<string, string>;
|
|
25
|
+
/** The worker's hostnames must be what the layout says. */
|
|
26
|
+
export declare function routeProblem(project: Project): string | null;
|
|
27
|
+
export declare function preflight(project: Project, source: SecretSource): Promise<Preflight>;
|
|
28
|
+
/** Print the report a person can act on. */
|
|
29
|
+
export declare function printPreflight(project: Project, result: Preflight, source: SecretSource): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wrangler, as vc uses it: found, asked who it is, and driven for the
|
|
3
|
+
* account operations a deploy needs. Nothing here needs a Void account.
|
|
4
|
+
*/
|
|
5
|
+
export declare function findWrangler(from?: string): string | null;
|
|
6
|
+
export interface Ran {
|
|
7
|
+
code: number;
|
|
8
|
+
out: string;
|
|
9
|
+
}
|
|
10
|
+
/** Run wrangler. `inherit` hands the terminal over (prompts, progress); otherwise output is captured. */
|
|
11
|
+
export declare function wrangler(bin: string, args: string[], cwd: string, inherit?: boolean): Promise<Ran>;
|
|
12
|
+
export interface WhoAmI {
|
|
13
|
+
state: "logged-in" | "logged-out" | "unknown";
|
|
14
|
+
accounts: Array<{
|
|
15
|
+
name: string;
|
|
16
|
+
id: string;
|
|
17
|
+
}>;
|
|
18
|
+
raw: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* `wrangler whoami` exits 0 whether or not you are logged in, so the text
|
|
22
|
+
* decides. Logged in, it prints a table of accounts; those are what pinning
|
|
23
|
+
* needs.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseWhoAmI(raw: string): WhoAmI;
|
|
26
|
+
export declare function whoami(bin: string, cwd: string): Promise<WhoAmI>;
|
|
27
|
+
/** Secret names on the worker, or null when they cannot be listed — a worker not deployed yet says so. */
|
|
28
|
+
export declare function secretNames(bin: string, cwd: string): Promise<Set<string> | null>;
|
|
29
|
+
export interface D1 {
|
|
30
|
+
name: string;
|
|
31
|
+
uuid: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function d1List(bin: string, cwd: string): Promise<D1[]>;
|
|
34
|
+
/** The database by name, created if absent. Idempotent. */
|
|
35
|
+
export declare function ensureD1(bin: string, cwd: string, name: string): Promise<D1>;
|
|
36
|
+
/** A queue by name. Already existing is fine. */
|
|
37
|
+
export declare function ensureQueue(bin: string, cwd: string, name: string): Promise<void>;
|
package/dist/dist.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vc dist` — the deployable app, as a self-contained tree.
|
|
3
|
+
*
|
|
4
|
+
* A strict repository has no app in it: `.vc/app` is generated and
|
|
5
|
+
* gitignored, and it reaches the root through symlinks. Nothing can build
|
|
6
|
+
* that except a machine that has run `vc generate` first.
|
|
7
|
+
*
|
|
8
|
+
* So this materialises it: the generated app with every symlink replaced by
|
|
9
|
+
* the real thing, a package.json carrying the versions the repository
|
|
10
|
+
* resolved, and the lockfile beside it. The result is an ordinary Void app
|
|
11
|
+
* that `bun install && vp build && wrangler deploy` can take from a cold
|
|
12
|
+
* checkout — which is exactly what a CI runner, or Cloudflare's own build,
|
|
13
|
+
* has.
|
|
14
|
+
*
|
|
15
|
+
* The generated workflow pushes this tree to a branch (`void-dist`) on every
|
|
16
|
+
* push to main, so the thing Cloudflare builds is a plain Void app and the
|
|
17
|
+
* thing a person edits stays a manifest.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DIST_DIR: string;
|
|
20
|
+
export declare const DIST_BRANCH = "void-dist";
|
|
21
|
+
export declare function distCommand(args: string[]): Promise<number>;
|
|
22
|
+
export declare function distHelp(): Promise<number>;
|