@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/src/scripts.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { box, line, row } from "./help";
|
|
4
|
+
import { ensureGenerated, findProject } from "./project";
|
|
5
|
+
import { runInherit, runVoid } from "./void";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `vc dev`, `vc build`, `vc preview` — the app's own scripts, where the app is.
|
|
9
|
+
*
|
|
10
|
+
* void has no `dev` command: `void init` writes `dev`, `build` and `preview`
|
|
11
|
+
* into the app's package.json (`vp dev`, or `vite dev`), and that is what
|
|
12
|
+
* runs. vc adds nothing to them but the WHERE — a monorepo's api/ from the
|
|
13
|
+
* root, a strict artifact under .vc/app, generated first if it is not there.
|
|
14
|
+
* Outside a project they go to void, which says what it thinks of them.
|
|
15
|
+
*/
|
|
16
|
+
export type AppScript = "dev" | "build" | "preview";
|
|
17
|
+
|
|
18
|
+
export function appScript(name: AppScript) {
|
|
19
|
+
return async (args: string[]): Promise<number> => {
|
|
20
|
+
const project = await findProject();
|
|
21
|
+
if (!project) return runVoid([name, ...args]);
|
|
22
|
+
const code = await ensureGenerated(project);
|
|
23
|
+
if (code !== 0) return code;
|
|
24
|
+
const pkgPath = join(project.appDir, "package.json");
|
|
25
|
+
const scripts = existsSync(pkgPath)
|
|
26
|
+
? ((JSON.parse(readFileSync(pkgPath, "utf8")) as { scripts?: Record<string, string> }).scripts ?? {})
|
|
27
|
+
: {};
|
|
28
|
+
if (!scripts[name]) {
|
|
29
|
+
console.error(`vc: ${relative(process.cwd(), pkgPath) || "package.json"} has no "${name}" script — void init writes one.`);
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
return runInherit(process.execPath, ["run", name, ...args], project.appDir);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function appScriptHelp(name: AppScript) {
|
|
37
|
+
return async (): Promise<number> => {
|
|
38
|
+
const width = 80;
|
|
39
|
+
console.log(
|
|
40
|
+
box(`vc ${name}`, [
|
|
41
|
+
line(`The app's own "${name}" script, run where the app is.`, width),
|
|
42
|
+
line("", width),
|
|
43
|
+
...row("one app", "here", width, 2),
|
|
44
|
+
...row("monorepo", "api/, from the root or from inside api/", width, 2),
|
|
45
|
+
...row("strict", ".vc/app, generated first if it is not there", width, 2),
|
|
46
|
+
line("", width),
|
|
47
|
+
line("Arguments pass through to the script. void has no such command;", width),
|
|
48
|
+
line("void init writes this script into the app's package.json.", width),
|
|
49
|
+
], width),
|
|
50
|
+
);
|
|
51
|
+
return 0;
|
|
52
|
+
};
|
|
53
|
+
}
|
package/src/void.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { constants as os } from "node:os";
|
|
4
|
+
import { delimiter, dirname, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The `void` binary, and the two ways vc uses it.
|
|
8
|
+
*
|
|
9
|
+
* `runVoid` hands the terminal over — for everything vc passes through, and
|
|
10
|
+
* for `void init` when vc extends it. `captureVoid` reads what void would
|
|
11
|
+
* have printed, so vc can merge its own lines into void's help rather than
|
|
12
|
+
* replace it.
|
|
13
|
+
*
|
|
14
|
+
* Void is the app's dependency, not vc's, so the nearest node_modules wins,
|
|
15
|
+
* then PATH, then `bunx void` — the way Void's own docs say to invoke it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface VoidBin {
|
|
19
|
+
cmd: string;
|
|
20
|
+
prefix: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function findVoid(from = process.cwd()): VoidBin | null {
|
|
24
|
+
let dir = from;
|
|
25
|
+
for (;;) {
|
|
26
|
+
const local = join(dir, "node_modules", ".bin", "void");
|
|
27
|
+
if (existsSync(local)) return { cmd: local, prefix: [] };
|
|
28
|
+
const parent = dirname(dir);
|
|
29
|
+
if (parent === dir) break;
|
|
30
|
+
dir = parent;
|
|
31
|
+
}
|
|
32
|
+
if (onPath("void")) return { cmd: "void", prefix: [] };
|
|
33
|
+
if (onPath("bunx")) return { cmd: "bunx", prefix: ["void"] };
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function onPath(bin: string): boolean {
|
|
38
|
+
return (process.env["PATH"] ?? "")
|
|
39
|
+
.split(delimiter)
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.some((dir) => existsSync(join(dir, bin)));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const VOID_MISSING = "`void` is not installed here. `bun add -d void` — Void recommends a local install — then try again.";
|
|
45
|
+
|
|
46
|
+
/** Hand the terminal to void. Resolves with the exit code void would have had. */
|
|
47
|
+
export function runVoid(args: string[], cwd = process.cwd()): Promise<number> {
|
|
48
|
+
const bin = findVoid(cwd);
|
|
49
|
+
if (!bin) {
|
|
50
|
+
console.error(VOID_MISSING);
|
|
51
|
+
return Promise.resolve(1);
|
|
52
|
+
}
|
|
53
|
+
return runInherit(bin.cmd, [...bin.prefix, ...args], cwd);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Hand the terminal to any command, forwarding signals, resolving with its exit code. */
|
|
57
|
+
export function runInherit(cmd: string, args: string[], cwd: string): Promise<number> {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const child = spawn(cmd, args, { cwd, stdio: "inherit" });
|
|
60
|
+
// A signal to vc is a signal to void: Ctrl-C must not leave a wizard running.
|
|
61
|
+
const signals: NodeJS.Signals[] = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
62
|
+
const forward = signals.map((signal) => [signal, () => child.kill(signal)] as const);
|
|
63
|
+
for (const [signal, handler] of forward) process.on(signal, handler);
|
|
64
|
+
child.on("error", () => {
|
|
65
|
+
console.error(VOID_MISSING);
|
|
66
|
+
resolve(1);
|
|
67
|
+
});
|
|
68
|
+
child.on("exit", (code, signal) => {
|
|
69
|
+
for (const [name, handler] of forward) process.off(name, handler);
|
|
70
|
+
resolve(code ?? (signal ? 128 + (os.signals[signal] ?? 0) : 1));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** What void prints for `args`, or null when void is not here. Stdout then stderr, as void ordered them. */
|
|
76
|
+
export function captureVoid(args: string[], cwd = process.cwd()): Promise<{ code: number; out: string } | null> {
|
|
77
|
+
const bin = findVoid(cwd);
|
|
78
|
+
if (!bin) return Promise.resolve(null);
|
|
79
|
+
const wantColor = Boolean(process.stdout.isTTY) && !process.env["NO_COLOR"];
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const child = spawn(bin.cmd, [...bin.prefix, ...args], {
|
|
82
|
+
cwd,
|
|
83
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
84
|
+
env: { ...process.env, ...(wantColor ? { FORCE_COLOR: "1" } : {}) },
|
|
85
|
+
});
|
|
86
|
+
let out = "";
|
|
87
|
+
child.stdout.on("data", (chunk) => {
|
|
88
|
+
out += chunk;
|
|
89
|
+
});
|
|
90
|
+
child.stderr.on("data", (chunk) => {
|
|
91
|
+
out += chunk;
|
|
92
|
+
});
|
|
93
|
+
child.on("error", () => resolve(null));
|
|
94
|
+
child.on("exit", (code) => resolve({ code: code ?? 1, out }));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A directory Void already knows: its Vite config imports void, or it carries a void.json. */
|
|
99
|
+
export function isVoidApp(dir: string): boolean {
|
|
100
|
+
if (existsSync(join(dir, "void.json"))) return true;
|
|
101
|
+
for (const name of ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"]) {
|
|
102
|
+
const path = join(dir, name);
|
|
103
|
+
if (!existsSync(path)) continue;
|
|
104
|
+
if (/from\s+["']void["']|voidPlugin\s*\(/.test(readFileSync(path, "utf8"))) return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The Void apps directly under `dir` — for when `void init` chose to make a subfolder. */
|
|
110
|
+
export function voidAppsIn(dir: string): string[] {
|
|
111
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
112
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules")
|
|
113
|
+
.map((entry) => join(dir, entry.name))
|
|
114
|
+
.filter(isVoidApp);
|
|
115
|
+
}
|
package/src/wizard.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import color from "picocolors";
|
|
3
|
+
import { GROUPS, type Group } from "./catalog";
|
|
4
|
+
import { LAYOUTS, type Layout, type Manifest, zoneOf } from "./manifest";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The forms.
|
|
8
|
+
*
|
|
9
|
+
* ── The shape of a good wizard ───────────────────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* `void init` asks three questions and stops, which is right for Void — a
|
|
12
|
+
* Void app can be anything. A shop cannot be anything: it has a payment rail
|
|
13
|
+
* or it does not trade, it has email or nobody can sign in, it has a tax rate
|
|
14
|
+
* or it sells unlawfully. So this asks about all of it, in the order a person
|
|
15
|
+
* would think about it — how it is laid out, who signs in, what you sell, how
|
|
16
|
+
* you get paid, how it ships, where the domain lives — and every choice says
|
|
17
|
+
* what it is FOR.
|
|
18
|
+
*
|
|
19
|
+
* The layout is asked FIRST and on its own, because it decides where
|
|
20
|
+
* `void init` has to run before the rest of the form makes sense.
|
|
21
|
+
*
|
|
22
|
+
* Required choices are shown ticked and cannot be unticked, so the person
|
|
23
|
+
* sees what they are getting rather than discovering it in `auth.ts`.
|
|
24
|
+
* Recommended choices are ticked by default, because a shop that starts with
|
|
25
|
+
* reviews and wishlists off has to remember to turn them on, and never does.
|
|
26
|
+
*
|
|
27
|
+
* Cancelling anywhere exits cleanly with nothing written. A half-written
|
|
28
|
+
* manifest is worse than none.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const bail = (): never => {
|
|
32
|
+
p.cancel("Nothing was written.");
|
|
33
|
+
process.exit(0);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const unwrap = <T>(value: T | symbol): T => {
|
|
37
|
+
if (p.isCancel(value)) bail();
|
|
38
|
+
return value as T;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** The first question. */
|
|
42
|
+
export async function askLayout(): Promise<Layout> {
|
|
43
|
+
return unwrap(
|
|
44
|
+
await p.select<Layout>({
|
|
45
|
+
message: "Layout",
|
|
46
|
+
options: LAYOUTS.map((layout) => ({ value: layout.id, label: layout.label, hint: layout.hint })),
|
|
47
|
+
initialValue: "app",
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* One group as a prompt.
|
|
54
|
+
*
|
|
55
|
+
* Multiselect prompts cannot mark an option as both selected and disabled,
|
|
56
|
+
* so a required choice is shown in the intro line and added afterwards
|
|
57
|
+
* rather than drawn as a tick the person cannot undo. `alsoRequired` is what
|
|
58
|
+
* the layout adds — the monorepo's frontend needs the storefront kit.
|
|
59
|
+
*/
|
|
60
|
+
async function askGroup(group: Group, previous: string[] | undefined, alsoRequired: string[] = []): Promise<string[]> {
|
|
61
|
+
const requiredIds = new Set([...group.choices.filter((choice) => choice.required).map((c) => c.id), ...alsoRequired]);
|
|
62
|
+
const required = group.choices.filter((choice) => requiredIds.has(choice.id));
|
|
63
|
+
const optional = group.choices.filter((choice) => !requiredIds.has(choice.id));
|
|
64
|
+
|
|
65
|
+
const message =
|
|
66
|
+
group.title +
|
|
67
|
+
(group.intro ? `\n${color.dim(group.intro)}` : "") +
|
|
68
|
+
(required.length
|
|
69
|
+
? `\n${color.dim(`Included: ${required.map((choice) => choice.label).join(", ")}`)}`
|
|
70
|
+
: "");
|
|
71
|
+
|
|
72
|
+
if (optional.length === 0) {
|
|
73
|
+
p.log.info(message);
|
|
74
|
+
return required.map((choice) => choice.id);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const options = optional.map((choice) => ({
|
|
78
|
+
value: choice.id,
|
|
79
|
+
label: choice.label,
|
|
80
|
+
hint: choice.hint,
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
if (group.kind === "select") {
|
|
84
|
+
const initial =
|
|
85
|
+
previous?.[0] ?? optional.find((choice) => choice.recommended)?.id ?? optional[0]!.id;
|
|
86
|
+
const picked = unwrap(await p.select({ message, options, initialValue: initial }));
|
|
87
|
+
return [...required.map((choice) => choice.id), picked];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const initial = previous ?? optional.filter((choice) => choice.recommended).map((c) => c.id);
|
|
91
|
+
const picked = unwrap(
|
|
92
|
+
await p.multiselect({ message, options, initialValues: initial, required: false }),
|
|
93
|
+
);
|
|
94
|
+
return [...required.map((choice) => choice.id), ...picked];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The rest of the form, once the layout is known and the Void app(s) exist. */
|
|
98
|
+
export async function runWizard(existing: Manifest | null, layout: Layout): Promise<Manifest> {
|
|
99
|
+
const monorepo = layout === "monorepo";
|
|
100
|
+
const frontend = layout !== "app";
|
|
101
|
+
|
|
102
|
+
const shop = unwrap(
|
|
103
|
+
await p.group(
|
|
104
|
+
{
|
|
105
|
+
name: () =>
|
|
106
|
+
p.text({
|
|
107
|
+
message: "Shop name",
|
|
108
|
+
placeholder: "Northwind",
|
|
109
|
+
initialValue: existing?.shop.name ?? "",
|
|
110
|
+
validate: (value) => (value.trim() ? undefined : "A shop has a name."),
|
|
111
|
+
}),
|
|
112
|
+
domain: () =>
|
|
113
|
+
p.text({
|
|
114
|
+
message: `Domain ${color.dim(
|
|
115
|
+
monorepo
|
|
116
|
+
? "— the storefront; api.<domain> is the worker; the CORS list and DNS records derive from it"
|
|
117
|
+
: "— the shop answers here; the worker's custom domain",
|
|
118
|
+
)}`,
|
|
119
|
+
placeholder: "northwind.com",
|
|
120
|
+
initialValue: existing?.shop.domain ?? "",
|
|
121
|
+
validate: (value) =>
|
|
122
|
+
/^[a-z0-9-]+(\.[a-z0-9-]+)+\.[a-z]{2,}$|^[a-z0-9-]+\.[a-z]{2,}$/i.test(value.trim())
|
|
123
|
+
? undefined
|
|
124
|
+
: "A hostname, like northwind.com or shop.northwind.com.",
|
|
125
|
+
}),
|
|
126
|
+
// Only a shop that is not its own registrable domain has a zone to
|
|
127
|
+
// name — and it must, because that is where records go and which
|
|
128
|
+
// zone Cloudflare needs for the worker's custom domain.
|
|
129
|
+
zone: ({ results }) => {
|
|
130
|
+
const domain = (results.domain ?? "").trim().toLowerCase();
|
|
131
|
+
const guess = zoneOf(domain);
|
|
132
|
+
if (!domain || domain === guess) return Promise.resolve(undefined);
|
|
133
|
+
return p.text({
|
|
134
|
+
message: `DNS zone ${color.dim(`— ${domain} lives inside it; records go here, and this zone must be on your DNS provider`)}`,
|
|
135
|
+
placeholder: guess,
|
|
136
|
+
initialValue: existing?.shop.zone ?? guess,
|
|
137
|
+
validate: (value) => {
|
|
138
|
+
const zone = value.trim().toLowerCase();
|
|
139
|
+
if (!zone) return "The zone that contains the domain.";
|
|
140
|
+
return domain === zone || domain.endsWith(`.${zone}`) ? undefined : `${domain} is not inside ${zone}.`;
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
country: () =>
|
|
145
|
+
p.text({
|
|
146
|
+
message: `Country ${color.dim("— sets the declared VAT rate and the default region")}`,
|
|
147
|
+
placeholder: "US",
|
|
148
|
+
initialValue: existing?.shop.country ?? "",
|
|
149
|
+
validate: (value) => (/^[A-Za-z]{2}$/.test(value.trim()) ? undefined : "ISO-3166 alpha-2, like DK."),
|
|
150
|
+
}),
|
|
151
|
+
taxRegistered: () =>
|
|
152
|
+
p.select({
|
|
153
|
+
message: `Registered to charge tax? ${color.dim("— a business below its registration threshold must NOT add tax to a price, and one above it must")}`,
|
|
154
|
+
options: [
|
|
155
|
+
{ value: "no", label: "Not registered", hint: "prices are what the customer pays; no tax is added. The common case for a new shop" },
|
|
156
|
+
{ value: "yes", label: "Registered", hint: "the country's standard rate is declared and added" },
|
|
157
|
+
],
|
|
158
|
+
initialValue: existing?.shop.taxRegistered ? "yes" : "no",
|
|
159
|
+
}),
|
|
160
|
+
currency: () =>
|
|
161
|
+
p.text({
|
|
162
|
+
message: "Currency",
|
|
163
|
+
placeholder: "usd",
|
|
164
|
+
initialValue: existing?.shop.currency ?? "",
|
|
165
|
+
validate: (value) => (/^[A-Za-z]{3}$/.test(value.trim()) ? undefined : "ISO-4217, like dkk."),
|
|
166
|
+
}),
|
|
167
|
+
locale: () =>
|
|
168
|
+
p.text({
|
|
169
|
+
message: `Locale ${color.dim("— the language of emails and launch content")}`,
|
|
170
|
+
placeholder: "en",
|
|
171
|
+
initialValue: existing?.shop.locale ?? "",
|
|
172
|
+
validate: (value) => (/^[a-z]{2}(-[A-Z]{2})?$/.test(value.trim()) ? undefined : "like da, or en-GB."),
|
|
173
|
+
}),
|
|
174
|
+
...(frontend
|
|
175
|
+
? {
|
|
176
|
+
pagesHost: () =>
|
|
177
|
+
p.text({
|
|
178
|
+
message: `GitHub Pages host ${color.dim("— where the storefront is published; leave blank if you serve the branch elsewhere")}`,
|
|
179
|
+
placeholder: "northwind.github.io",
|
|
180
|
+
initialValue: existing?.shop.pagesHost ?? "",
|
|
181
|
+
validate: (value) =>
|
|
182
|
+
!value.trim() || /^[a-z0-9-]+\.github\.io$/i.test(value.trim()) ? undefined : "<owner>.github.io, or blank",
|
|
183
|
+
}),
|
|
184
|
+
}
|
|
185
|
+
: {}),
|
|
186
|
+
},
|
|
187
|
+
{ onCancel: bail },
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const chosen: Record<string, string[]> = {};
|
|
192
|
+
for (const group of GROUPS) {
|
|
193
|
+
if (group.id === "ui" && layout === "strict") {
|
|
194
|
+
// Strict has no custom code, so the generated interface is the only one.
|
|
195
|
+
p.log.info(`${group.title}\n${color.dim("The generated storefront and panel. Strict has no custom code to host anything else.")}`);
|
|
196
|
+
chosen[group.id] = ["admin"];
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const alsoRequired = monorepo && group.id === "ui" ? ["custom"] : [];
|
|
200
|
+
chosen[group.id] = await askGroup(group, existing?.chosen[group.id], alsoRequired);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
version: 1,
|
|
205
|
+
layout,
|
|
206
|
+
shop: {
|
|
207
|
+
name: shop.name.trim(),
|
|
208
|
+
domain: shop.domain.trim().toLowerCase(),
|
|
209
|
+
country: shop.country.trim().toUpperCase(),
|
|
210
|
+
currency: shop.currency.trim().toLowerCase(),
|
|
211
|
+
locale: shop.locale.trim(),
|
|
212
|
+
taxRegistered: (shop as { taxRegistered?: string }).taxRegistered === "yes",
|
|
213
|
+
zone: (shop as { zone?: string }).zone?.trim().toLowerCase() || undefined,
|
|
214
|
+
pagesHost: frontend ? (shop as { pagesHost?: string }).pagesHost?.trim().toLowerCase() || undefined : undefined,
|
|
215
|
+
},
|
|
216
|
+
chosen,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** A closing summary the person can read before anything is written. */
|
|
221
|
+
export function summarise(manifest: Manifest): string {
|
|
222
|
+
const lines: string[] = [
|
|
223
|
+
`${color.dim("Layout".padEnd(34))} ${LAYOUTS.find((l) => l.id === manifest.layout)?.label ?? manifest.layout}`,
|
|
224
|
+
];
|
|
225
|
+
for (const group of GROUPS) {
|
|
226
|
+
const ids = manifest.chosen[group.id] ?? [];
|
|
227
|
+
if (ids.length === 0) continue;
|
|
228
|
+
const labels = ids
|
|
229
|
+
.map((id) => group.choices.find((choice) => choice.id === id)?.label ?? id)
|
|
230
|
+
.join(", ");
|
|
231
|
+
lines.push(`${color.dim(group.title.padEnd(34))} ${labels}`);
|
|
232
|
+
}
|
|
233
|
+
return lines.join("\n");
|
|
234
|
+
}
|