@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.
Files changed (62) hide show
  1. package/README.md +271 -0
  2. package/bin/vc +2 -0
  3. package/dist/catalog.d.ts +69 -0
  4. package/dist/catalog.js +34 -0
  5. package/dist/cli.d.ts +24 -0
  6. package/dist/cli.js +544 -0
  7. package/dist/deploy/cloudflare.d.ts +25 -0
  8. package/dist/deploy/index.d.ts +16 -0
  9. package/dist/deploy/jsonc.d.ts +8 -0
  10. package/dist/deploy/preflight.d.ts +29 -0
  11. package/dist/deploy/wrangler.d.ts +37 -0
  12. package/dist/dist.d.ts +22 -0
  13. package/dist/generate/auth.d.ts +2 -0
  14. package/dist/generate/ci.d.ts +24 -0
  15. package/dist/generate/env.d.ts +13 -0
  16. package/dist/generate/frontend.d.ts +47 -0
  17. package/dist/generate/index.d.ts +28 -0
  18. package/dist/generate/requirements.d.ts +13 -0
  19. package/dist/generate/strict.d.ts +72 -0
  20. package/dist/generate/support.d.ts +23 -0
  21. package/dist/help.d.ts +31 -0
  22. package/dist/import.d.ts +2 -0
  23. package/dist/index-s7sq41qs.js +590 -0
  24. package/dist/index-ssv3a6wc.js +172 -0
  25. package/dist/index-wzy1xtr1.js +3155 -0
  26. package/dist/index.d.ts +24 -0
  27. package/dist/index.js +190 -0
  28. package/dist/init.d.ts +1 -0
  29. package/dist/manifest.d.ts +131 -0
  30. package/dist/manifest.js +41 -0
  31. package/dist/project.d.ts +20 -0
  32. package/dist/regenerate.d.ts +9 -0
  33. package/dist/scripts.d.ts +12 -0
  34. package/dist/void.d.ts +30 -0
  35. package/dist/wizard.d.ts +7 -0
  36. package/package.json +50 -0
  37. package/src/catalog.ts +673 -0
  38. package/src/cli.ts +78 -0
  39. package/src/deploy/cloudflare.ts +166 -0
  40. package/src/deploy/index.ts +101 -0
  41. package/src/deploy/jsonc.ts +148 -0
  42. package/src/deploy/preflight.ts +137 -0
  43. package/src/deploy/wrangler.ts +111 -0
  44. package/src/dist.ts +157 -0
  45. package/src/generate/auth.ts +386 -0
  46. package/src/generate/ci.ts +208 -0
  47. package/src/generate/env.ts +164 -0
  48. package/src/generate/frontend.ts +275 -0
  49. package/src/generate/index.ts +390 -0
  50. package/src/generate/requirements.ts +48 -0
  51. package/src/generate/strict.ts +692 -0
  52. package/src/generate/support.ts +252 -0
  53. package/src/help.ts +172 -0
  54. package/src/import.ts +237 -0
  55. package/src/index.ts +37 -0
  56. package/src/init.ts +187 -0
  57. package/src/manifest.ts +303 -0
  58. package/src/project.ts +63 -0
  59. package/src/regenerate.ts +51 -0
  60. package/src/scripts.ts +53 -0
  61. package/src/void.ts +115 -0
  62. package/src/wizard.ts +234 -0
package/README.md ADDED
@@ -0,0 +1,271 @@
1
+ # voidcommerce
2
+
3
+ Void, with a shop in it.
4
+
5
+ ```sh
6
+ vc init # void init, then a form for the shop
7
+ vc --help # void's help, with vc's commands merged in
8
+ vc dev # everything else is void's, verbatim
9
+ ```
10
+
11
+ ## vc extends void; it replaces nothing
12
+
13
+ Every command vc does not name goes to `void` untouched — same arguments,
14
+ same terminal, same exit code. The named ones run void's version and add vc's
15
+ part after it:
16
+
17
+ | | void's part | then vc's |
18
+ |---|---|---|
19
+ | `vc init` | `void init`, when there is no Void app here yet | the shop form |
20
+ | `vc --help` | `void --help` | a `shop` group merged into the Commands box |
21
+ | `vc init --help` | `void init --help` | a second box, `vc init` |
22
+ | `vc --version` | void's version | vc's |
23
+ | `vc dev`, `vc build`, `vc preview` | — void has no such commands | the app's own script, where the app is |
24
+
25
+ `vc init --github` and void's other partial modes are void's alone. `vc help
26
+ db execute` is void's. When void is not installed, vc says so and stops —
27
+ except for help, which it prints alone and says why. `dev`, `build` and
28
+ `preview` are the scripts `void init` writes into the app's package.json;
29
+ vc only decides where they run — `api/` from a monorepo root, `.vc/app` at
30
+ a strict root.
31
+
32
+ ## The first question: one app, or api + frontend
33
+
34
+ | | where it runs | what `vc init` does |
35
+ |---|---|---|
36
+ | **One app** | one Void app on Cloudflare Workers at `<domain>` and `www` — the API and the generated storefront and panel together | `void init` here, then the form |
37
+ | **Monorepo** | `api/` on Workers at `api.<domain>` with the panel; `frontend/` a static Void site on GitHub Pages at `<domain>` | `void init` in `api/` (the D1 starter), `void init` in `frontend/` (Static Pages), then the form |
38
+ | **Strict** *(experimental)* | `api.<domain>` on Workers; the storefront on `<domain>` | no `void init` at all: the repository root IS the storefront, and the worker is generated under `.vc/app` |
39
+
40
+ The layout decides what the domain means, so it is asked before anything
41
+ else, and it cannot change after init — moving files is not a regeneration.
42
+ `vc init --layout app|monorepo|strict` skips the question for scripts.
43
+
44
+ The domain need not be a registrable apex. `shop.example.com` is a shop inside
45
+ the zone `example.com`, and the two are different questions: the domain is
46
+ where the shop answers, the zone is where records go and which zone Cloudflare
47
+ needs for the worker's custom domain. The wizard asks for the zone only when
48
+ the domain is not one. A subdomain has no `www`, so it does not get a route
49
+ nobody would type.
50
+
51
+ Every layout puts the worker on a Cloudflare custom domain, which Cloudflare
52
+ creates in its own zone — so the zone must be on Cloudflare, and DNS at
53
+ Simply is refused with that reason rather than generating a shop with no
54
+ hostname. Where Cloudflare also does the mail, there are no records left for
55
+ the DNS plugin to write and the shop needs no DNS token at all.
56
+
57
+ In a monorepo the Pages host (`<owner>.github.io`) is required, the frontend
58
+ gets the custom storefront kit, and a workflow publishes it: every push builds
59
+ `frontend/`, prerenders it, and force-pushes the tree to a `static` branch with
60
+ the shop's domain in `CNAME`. Pages serves that branch from `/`.
61
+
62
+ ### Strict: a monorepo turned inside out
63
+
64
+ A monorepo has `api/` and `frontend/` side by side. Strict has the same two
65
+ halves and the same hosting, but only one of them is a directory you can see:
66
+
67
+ ```
68
+ voidcommerce.json the answers — the worker is generated from this
69
+ package.json ONE package.json, shared by the storefront and the worker
70
+ pages/ lib/ the STOREFRONT. Yours, and what the repository looks like
71
+ vite.config.ts its Vite config; void.json sets output: "static"
72
+ data/ the catalogue, pushed by vc import
73
+ content/ faqs.json and posts.json — launch content, snapshotted in
74
+ branding/ static assets, copied to public/branding/
75
+ migrations/ what production has already applied — see below
76
+ patches/ a void fix that has not shipped upstream
77
+ .env local values, shared by both halves
78
+ .vc/app/ the WORKER. Generated, gitignored, nobody's to edit
79
+ ```
80
+
81
+ So the repository is, in technicality, a frontend app that builds a backend
82
+ app — and in practice a monorepo without the ceremony of being one. What you
83
+ edit is the shop front; the API, the panel, the cron, the queue and the live
84
+ stream are generated underneath it from the manifest and rewritten by
85
+ `vc generate`. A hand edit to a generated app drifts from the thing it exists
86
+ to describe, and strict is the mode that refuses to let it. If you need a file
87
+ the manifest cannot produce, that is the signal to use the one-app layout.
88
+
89
+ Two branches come out of one push:
90
+
91
+ | branch | what is on it | who serves it |
92
+ |---|---|---|
93
+ | `void-dist` | the worker, as a self-contained Void app | Cloudflare Workers Builds |
94
+ | `frontend-static` | the prerendered storefront | GitHub Pages, or Cloudflare Pages |
95
+
96
+ Locally, `bun run dev` is the shop — the worker with its panel — and
97
+ `bun run dev:storefront` is the shop front, which calls it at
98
+ `http://localhost:5173`. Start the worker first; the storefront takes the next
99
+ free port.
100
+
101
+ At a strict root every Void command runs in the artifact: `vc dev` is
102
+ `void dev` in `.vc/app`, generated first if it is not there. `vc generate`
103
+ also runs `void prepare` and `void db generate`, so the app typechecks and
104
+ carries its migrations. Migrations are the one artifact that must be
105
+ committed: regenerated on a fresh clone they would get new names, and a deploy
106
+ would try to apply them twice — so they live in `migrations/` at the root and
107
+ the app reaches them through a symlink.
108
+
109
+ Strict pins `void` to the version its vendored patch applies to. A strict
110
+ project has been generated, typechecked and production-built against real
111
+ packages; it has not yet been served or deployed end to end.
112
+
113
+ ## Push, and it is live
114
+
115
+ With everything on Cloudflare, a shop can reach production without a single
116
+ credential in CI:
117
+
118
+ ```
119
+ main ──push──▶ GitHub Actions: vc dist ──▶ void-dist branch ──▶ Cloudflare Workers Builds ──▶ live
120
+ ```
121
+
122
+ `main` is the manifest and the data. The workflow regenerates the app and
123
+ force-pushes a self-contained Void app to `void-dist`; Cloudflare builds *that*
124
+ branch and deploys it, so it never needs to know voidcommerce exists. The
125
+ generated `DEPLOY.md` lists the one-time setup, including the one genuinely
126
+ non-obvious part: the API token Cloudflare generates for a build has no D1
127
+ permission, so a build that applies migrations needs a token you make.
128
+
129
+ Email is the same story. Cloudflare Email Service is reached through a Worker
130
+ binding rather than an API key, and onboarding the sending domain writes its
131
+ own SPF, DKIM and DMARC records — so mail costs no secret and no DNS work.
132
+ Sending to real customers needs a Workers Paid plan.
133
+
134
+ ## Deploying
135
+
136
+ ```sh
137
+ vc preflight # is the shop ready to advertise on?
138
+ vc deploy # preflight, then void deploy — the Void platform
139
+ vc deploy --cloudflare --provision # first time: your own Cloudflare account, with wrangler
140
+ vc deploy --cloudflare # every time after
141
+ ```
142
+
143
+ `vc deploy` extends `void deploy`: vc's preflight runs first, then void's
144
+ deploy exactly as you typed it, with void's login. The Void platform is in
145
+ private beta; the pass-through is there for when it is not.
146
+
147
+ `--cloudflare` is the other road, and needs no Void account at all. It is
148
+ Void's documented manual path, automated with wrangler:
149
+
150
+ 1. **wrangler is installed and logged in** (`wrangler login`, or
151
+ `CLOUDFLARE_API_TOKEN`). When wrangler sees exactly one account, vc pins it
152
+ in `wrangler.jsonc` for you; with several, it asks you to choose.
153
+ 2. **Preflight.** Every required key is either a worker secret
154
+ (`wrangler secret put`) or a committed plaintext value; the worker's
155
+ hostnames are the shop's. Not ready means no deploy, unless `--force`.
156
+ On a first deploy the worker does not exist yet, so its secrets cannot be
157
+ listed: preflight prints the `wrangler secret put` lines, and running them
158
+ creates the draft worker.
159
+ 3. **`--provision`** creates the D1 database and the queue, and records them
160
+ in `wrangler.jsonc` and `voidcommerce.json` — so a regenerated strict
161
+ artifact carries the real ids.
162
+ 4. **Build**, then **scrub**. Void's build bakes every `.env*` value into the
163
+ worker's `vars` as plaintext — a dev cron secret, `unset` for each
164
+ credential — and a var shadows the secret of the same name. Every
165
+ secret-class key and every `unset` is removed from the emitted config
166
+ before it is uploaded; the worker reads those from its secrets.
167
+ 5. **Migrate** the remote D1 from the committed migrations, then
168
+ **`wrangler deploy`** exactly the config that was scrubbed.
169
+
170
+ At a monorepo root `vc deploy` deploys `api/`; the frontend deploys itself
171
+ from GitHub Actions on push. At a strict root it deploys the artifact.
172
+
173
+ The Cloudflare road has been exercised against a fake wrangler that answers
174
+ like the real one, and its `wrangler deploy` invocation dry-run against a real
175
+ build; it has not yet put a shop on a real account.
176
+
177
+ ## What `vc init` asks
178
+
179
+ `void init` asks three questions, which is right for Void — a Void app can be
180
+ anything. A shop cannot be anything: it has a payment rail or it does not
181
+ trade, it has email or nobody can sign in, it has a tax rate or it sells
182
+ unlawfully. So `vc init` asks about all of it, in the order a person thinks
183
+ about it:
184
+
185
+ | | you choose |
186
+ |---|---|
187
+ | **Who signs in** | magic link, email code, passkeys, Google/Apple/GitHub, password, username, phone, guest |
188
+ | **Account features** | every official Better Auth plugin — 2FA, organizations, API keys, JWT, multi-session, … |
189
+ | **Catalogue** | custom fields, personalization, inventory, bundles, price lists, translations, search |
190
+ | **Selling** | channels, discounts, gift cards, loyalty, subscriptions, draft orders, returns, wishlists, reviews, customer groups, agentic commerce |
191
+ | **Operations** | webhooks, workflows, files, marketplace |
192
+ | **Payment rail** | Adyen *or* Stripe — one, never both |
193
+ | **Tax** | declared rates, TaxJar, Avalara |
194
+ | **Carriers** | Shippo, EasyPost, UPS, FedEx, DHL, USPS, Royal Mail, Aramex, DoorDash, Stuart |
195
+ | **ERP** | Business Central |
196
+ | **Email** | Cloudflare Email Service (a Worker binding, no API key) or Resend |
197
+ | **DNS, content** | Cloudflare or Simply; blog and FAQ |
198
+ | **Interfaces** | the generated storefront and panel (better-admin-ui, standard theming, one line to turn off), or the custom storefront kit (better-auth-ui + better-commerce-ui, hosted in your own app) |
199
+
200
+ Every choice carries a one-line hint written for the person choosing — *"teams
201
+ that share a shop account — a club ordering trophies as one buyer"* — not a
202
+ plugin name. Required choices are shown so you know what you are getting;
203
+ recommended ones are ticked, because a shop that starts with reviews off has
204
+ to remember to turn them on and never does.
205
+
206
+ ## The manifest
207
+
208
+ Answers are written to **`voidcommerce.json`**, and everything else is
209
+ generated from it. Run `vc init` again to change your mind: the answers are
210
+ pre-filled, regenerated files are rewritten, and files you own are kept.
211
+
212
+ That is the difference between a scaffolder and a generator. A wizard that
213
+ writes `auth.ts` once and forgets what it asked is useless on day thirty when
214
+ you want subscriptions and cannot remember which of forty checkboxes you
215
+ ticked.
216
+
217
+ ## What gets generated
218
+
219
+ ```
220
+ voidcommerce.json the answers
221
+ auth.ts the plugin ring — every choice, with the hint as a comment
222
+ env.ts every key REQUIRED, none optional, none defaulted
223
+ .env.example with `unset` for each secret
224
+ .env.production plaintext values only
225
+ lib/deploy/requirements.ts what breaks without each key — read by `vc preflight`
226
+ wrangler.jsonc the worker's custom domain, api.<domain>
227
+ ```
228
+
229
+ And, written once then **yours**: `.env`, `lib/domain.ts`, `lib/payment.ts`,
230
+ `lib/notifications.ts`, `lib/erp.ts`. They are what a shop edits when it
231
+ grows past the wizard, so they are never overwritten.
232
+
233
+ A monorepo puts all of that under `api/`, and adds:
234
+
235
+ ```
236
+ package.json workspaces: api, frontend; dev:api, dev:frontend, deploy …
237
+ .github/workflows/frontend-pages.yml build frontend/, publish to the static branch, CNAME
238
+ frontend/lib/api.ts the one client: better-auth/react + commerceClient (yours)
239
+ frontend/.env VITE_API_ORIGIN=http://localhost:5173 (yours)
240
+ frontend/.env.production VITE_API_ORIGIN=https://api.<domain>
241
+ frontend/void.json target: node, output: static — merged into void's
242
+ ```
243
+
244
+ ## Why the files are plain
245
+
246
+ Void's `gen` has no extensibility and its directory set is closed — it will
247
+ not scan a `plugins/` folder. So `vc` writes ordinary Void files a reader can
248
+ see, the way `rails generate` writes into files you own, rather than hiding
249
+ registrations in a virtual module. A `vc` project is a valid Void project with
250
+ nothing magic in it, which is what would let Void absorb these generators one
251
+ day as a copy rather than a negotiation.
252
+
253
+ ## The rules the wizard enforces
254
+
255
+ - **Nothing is optional.** If a shop supports an integration, the deployment
256
+ sets it up. `env.ts` has no `.optional()` and no defaults — a default is
257
+ compiled into the worker's vars and shadows the real secret.
258
+ - **One integration per job.** Adyen *or* Stripe, Cloudflare *or* Simply.
259
+ Carrying both means every reader works out which half is live.
260
+ - **DNS at Simply is refused when the API is on `api.<domain>`.** A Worker
261
+ custom domain needs the zone on Cloudflare; the wizard says so rather than
262
+ generating a configuration that resolves and then errors for every visitor.
263
+ - **The country's VAT is declared** so a fresh deploy is never tax-free. For a
264
+ country the wizard does not know, it writes `TODO VERIFY` rather than a
265
+ guess.
266
+ - **The worker carries no catalogue.** `catalogImport` is registered with no
267
+ source; `vc import` pushes from `data/catalog.json`.
268
+ - **Passwords are explicitly off** unless chosen, because Better Auth's
269
+ default is off and a version bump could flip it.
270
+
271
+ The generated shop typechecks clean against the real packages.
package/bin/vc ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ import "../src/cli.ts";
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Everything `vc init` can offer, and what choosing it means.
3
+ *
4
+ * ── Why a registry ───────────────────────────────────────────────────────
5
+ *
6
+ * The wizard is only as good as its descriptions. A checkbox that says
7
+ * "organization" tells nobody anything; one that says "teams that share a
8
+ * shop — a club ordering trophies as one account" is a decision somebody can
9
+ * make. So every entry carries a one-line `hint` written for the person
10
+ * choosing, not for the person who wrote the plugin.
11
+ *
12
+ * Each entry also carries what choosing it COSTS: the packages to install,
13
+ * the env keys it needs, and whether those keys are secrets. That is how the
14
+ * generator knows what to write into `env.ts`, `.env.example` and the
15
+ * deploy requirements without a second hand-kept list.
16
+ */
17
+ export interface EnvKey {
18
+ key: string;
19
+ /** What breaks without it — the sentence an operator reads at preflight. */
20
+ breaks: string;
21
+ /** Safe to commit in `.env.production`. Everything else is a secret. */
22
+ plaintext?: boolean | undefined;
23
+ /** A working value for local development. */
24
+ dev?: string | undefined;
25
+ /** Where the real value comes from. */
26
+ where?: string | undefined;
27
+ }
28
+ export interface Choice {
29
+ id: string;
30
+ label: string;
31
+ hint: string;
32
+ /** npm packages the choice pulls in. */
33
+ packages?: string[] | undefined;
34
+ env?: EnvKey[] | undefined;
35
+ /** Chosen unless the person unticks it. */
36
+ recommended?: boolean | undefined;
37
+ /** Cannot be unticked; shown so the person knows it is there. */
38
+ required?: boolean | undefined;
39
+ /** Ids this choice needs; the wizard adds them. */
40
+ requires?: string[] | undefined;
41
+ /** Ids this choice cannot coexist with. */
42
+ conflicts?: string[] | undefined;
43
+ }
44
+ export interface Group {
45
+ id: string;
46
+ title: string;
47
+ /** One line under the title. */
48
+ intro?: string | undefined;
49
+ choices: Choice[];
50
+ /** `select` picks exactly one; `multiselect` any number. */
51
+ kind: "select" | "multiselect";
52
+ }
53
+ export declare const SIGN_IN: Group;
54
+ export declare const AUTH_PLUGINS: Group;
55
+ export declare const COMMERCE_CATALOG: Group;
56
+ export declare const COMMERCE_SALES: Group;
57
+ export declare const COMMERCE_OPERATIONS: Group;
58
+ export declare const PAYMENT: Group;
59
+ export declare const TAX: Group;
60
+ export declare const CARRIERS: Group;
61
+ export declare const ERP: Group;
62
+ export declare const EMAIL: Group;
63
+ export declare const DNS: Group;
64
+ export declare const CONTENT: Group;
65
+ export declare const UI: Group;
66
+ /** The order the wizard walks them in. */
67
+ export declare const GROUPS: Group[];
68
+ /** Every choice, by id, for lookups the generator makes. */
69
+ export declare const CHOICES: Map<string, Choice>;
@@ -0,0 +1,34 @@
1
+ import {
2
+ AUTH_PLUGINS,
3
+ CARRIERS,
4
+ CHOICES,
5
+ COMMERCE_CATALOG,
6
+ COMMERCE_OPERATIONS,
7
+ COMMERCE_SALES,
8
+ CONTENT,
9
+ DNS,
10
+ EMAIL,
11
+ ERP,
12
+ GROUPS,
13
+ PAYMENT,
14
+ SIGN_IN,
15
+ TAX,
16
+ UI
17
+ } from "./index-s7sq41qs.js";
18
+ export {
19
+ UI,
20
+ TAX,
21
+ SIGN_IN,
22
+ PAYMENT,
23
+ GROUPS,
24
+ ERP,
25
+ EMAIL,
26
+ DNS,
27
+ CONTENT,
28
+ COMMERCE_SALES,
29
+ COMMERCE_OPERATIONS,
30
+ COMMERCE_CATALOG,
31
+ CHOICES,
32
+ CARRIERS,
33
+ AUTH_PLUGINS
34
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `vc` — Void, with a shop in it.
3
+ *
4
+ * vc extends parts of void; it replaces none of it. Every command not named
5
+ * below goes to `void` verbatim — same arguments, same terminal, same exit
6
+ * code — so a person who knows Void already knows vc. The named ones run
7
+ * void's version first and add vc's part after: `vc init` is void init then
8
+ * the shop form; `vc --help` is void's help with a `shop` group merged in.
9
+ * `dev`, `build` and `preview` are not void's at all — they are the app's
10
+ * own scripts — so vc only decides where they run.
11
+ *
12
+ * The one thing vc changes about a pass-through is WHERE it runs: at a
13
+ * strict root the Void app is the artifact under .vc/app, so void's commands
14
+ * run there, after generating it if it is not there yet; at a monorepo root
15
+ * they run in api/.
16
+ */
17
+ interface Extended {
18
+ run: (args: string[]) => Promise<number>;
19
+ help: () => Promise<number>;
20
+ }
21
+ /** The commands vc extends or adds. Add one here and it is dispatched, helped, and tested. */
22
+ export declare const EXTENDED: Record<string, Extended>;
23
+ export declare function main(argv: string[]): Promise<number>;
24
+ export {};