@pithy-sh/cli 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/LICENSE +21 -0
- package/package.json +72 -0
- package/scripts/templateManifest.ts +49 -0
- package/scripts/tsconfig.json +26 -0
- package/scripts/vendorTemplate.ts +84 -0
- package/scripts/verifyPack.ts +88 -0
- package/src/audit/cliAudit.ts +406 -0
- package/src/bin.ts +111 -0
- package/src/capabilities/add.ts +288 -0
- package/src/capabilities/addBootstrap.ts +275 -0
- package/src/capabilities/catalog.ts +175 -0
- package/src/capabilities/compose.ts +39 -0
- package/src/capabilities/configConstants.ts +74 -0
- package/src/capabilities/configImports.ts +397 -0
- package/src/capabilities/eject.ts +331 -0
- package/src/capabilities/emailProvisioner.ts +346 -0
- package/src/capabilities/entitlementGap.ts +70 -0
- package/src/capabilities/entryExports.ts +162 -0
- package/src/capabilities/flow.ts +550 -0
- package/src/capabilities/hostRegistry.ts +368 -0
- package/src/capabilities/loadFailure.ts +208 -0
- package/src/capabilities/manifests.ts +238 -0
- package/src/capabilities/mediaProvisioner.ts +471 -0
- package/src/capabilities/mintSecrets.ts +306 -0
- package/src/capabilities/paymentsProvisioner.ts +207 -0
- package/src/capabilities/prerequisites.ts +168 -0
- package/src/capabilities/r2Bucket.ts +113 -0
- package/src/capabilities/reconcile.ts +1483 -0
- package/src/capabilities/remove.ts +597 -0
- package/src/capabilities/requiredOptions.ts +92 -0
- package/src/capabilities/rotateSecrets.ts +305 -0
- package/src/capabilities/secrets.ts +178 -0
- package/src/capabilities/secretsDispatcher.ts +29 -0
- package/src/capabilities/secretsProvisioner.ts +389 -0
- package/src/capabilities/storageProvisioner.ts +414 -0
- package/src/capabilities/supportProvisioner.ts +515 -0
- package/src/capabilities/testersLoader.ts +52 -0
- package/src/capabilities/testersProvisioner.ts +236 -0
- package/src/capabilities/turnstileProvisioner.ts +347 -0
- package/src/capabilities/vectorProvisioner.ts +260 -0
- package/src/ci/fileModes.ts +223 -0
- package/src/ci/sourceFiles.ts +200 -0
- package/src/ci/workflowDrivers.ts +524 -0
- package/src/cloudflare/accountAnswer.ts +110 -0
- package/src/cloudflare/config.ts +685 -0
- package/src/cloudflare/storeId.ts +129 -0
- package/src/commands/add.ts +372 -0
- package/src/commands/alias.ts +205 -0
- package/src/commands/dashboard.ts +651 -0
- package/src/commands/deploy.ts +150 -0
- package/src/commands/dev.ts +37 -0
- package/src/commands/doctor.ts +2059 -0
- package/src/commands/email.ts +425 -0
- package/src/commands/env.ts +155 -0
- package/src/commands/feature.ts +359 -0
- package/src/commands/init.ts +538 -0
- package/src/commands/media.ts +303 -0
- package/src/commands/migrate.ts +129 -0
- package/src/commands/payments.ts +336 -0
- package/src/commands/provision.ts +368 -0
- package/src/commands/remove.ts +151 -0
- package/src/commands/secrets.ts +652 -0
- package/src/commands/seed.ts +229 -0
- package/src/commands/storage.ts +309 -0
- package/src/commands/support.ts +331 -0
- package/src/commands/testers.ts +1020 -0
- package/src/commands/token.ts +364 -0
- package/src/commands/turnstile.ts +271 -0
- package/src/commands/ui.ts +222 -0
- package/src/commands/upgrade.ts +517 -0
- package/src/commands/vector.ts +390 -0
- package/src/commands/worker.ts +295 -0
- package/src/dashboard/api.ts +323 -0
- package/src/dashboard/connect.ts +758 -0
- package/src/dashboard/contract.ts +289 -0
- package/src/dashboard/grant.ts +124 -0
- package/src/dashboard/registry.ts +519 -0
- package/src/dashboard/resolveTarget.ts +119 -0
- package/src/dev/delivery.ts +174 -0
- package/src/dev/devLogin.ts +155 -0
- package/src/dev/devLoginTargets.ts +91 -0
- package/src/dev/env.ts +206 -0
- package/src/dev/hostWorkers.ts +290 -0
- package/src/dev/keys.ts +111 -0
- package/src/dev/logging.ts +87 -0
- package/src/dev/openUrl.ts +75 -0
- package/src/dev/orchestrator.ts +1014 -0
- package/src/dev/ports.ts +220 -0
- package/src/dev/readyWatch.ts +142 -0
- package/src/dev/state.ts +90 -0
- package/src/devSecrets/bootstrapVars.ts +265 -0
- package/src/devSecrets/devVars.ts +240 -0
- package/src/devSecrets/edit.ts +256 -0
- package/src/devSecrets/file.ts +277 -0
- package/src/devSecrets/generate.ts +428 -0
- package/src/devSecrets/location.ts +80 -0
- package/src/devSecrets/mode.ts +71 -0
- package/src/devSecrets/records.ts +30 -0
- package/src/devSecrets/report.ts +99 -0
- package/src/devSecrets/seed.ts +344 -0
- package/src/devSecrets/store.ts +262 -0
- package/src/devSecrets/targets.ts +204 -0
- package/src/dispatch.ts +147 -0
- package/src/docs/catalog.ts +246 -0
- package/src/docs/writeCatalog.ts +45 -0
- package/src/doctor/cloudflare.ts +287 -0
- package/src/doctor/devPreferences.ts +155 -0
- package/src/doctor/devSecrets.ts +464 -0
- package/src/doctor/devVars.ts +414 -0
- package/src/doctor/devVarsLocal.ts +138 -0
- package/src/doctor/environments.ts +155 -0
- package/src/doctor/health.ts +354 -0
- package/src/doctor/localDelivery.ts +91 -0
- package/src/doctor/portsRegistry.ts +252 -0
- package/src/doctor/projectName.ts +584 -0
- package/src/doctor/secretBindings.ts +166 -0
- package/src/doctor/settings.ts +274 -0
- package/src/doctor/settingsSources.ts +202 -0
- package/src/doctor/workerName.ts +174 -0
- package/src/doctor/wranglerVars.ts +33 -0
- package/src/feature/bindings.ts +93 -0
- package/src/feature/create.ts +179 -0
- package/src/feature/destroy.ts +160 -0
- package/src/feature/devConfig.ts +201 -0
- package/src/feature/identity.ts +100 -0
- package/src/feature/manifest.ts +132 -0
- package/src/feature/ports.ts +615 -0
- package/src/feature/provision.ts +362 -0
- package/src/feature/sync.ts +148 -0
- package/src/feature/worktree.ts +282 -0
- package/src/help/groups.ts +47 -0
- package/src/help/rootUsage.ts +135 -0
- package/src/main.ts +73 -0
- package/src/migrations/ledger.ts +129 -0
- package/src/migrations/registry.ts +47 -0
- package/src/migrations/run.ts +1066 -0
- package/src/notifier/check.ts +129 -0
- package/src/notifier/installer.ts +48 -0
- package/src/notifier/notify.ts +152 -0
- package/src/notifier/state.ts +248 -0
- package/src/notifier/version.ts +59 -0
- package/src/platform/editor.ts +333 -0
- package/src/platform/rc.ts +118 -0
- package/src/platform/shell.ts +83 -0
- package/src/project/appBindings.ts +184 -0
- package/src/project/appWorkflows.ts +266 -0
- package/src/project/applyDomains.ts +166 -0
- package/src/project/askDomains.ts +220 -0
- package/src/project/atomic.ts +466 -0
- package/src/project/bindingEntries.ts +425 -0
- package/src/project/config.ts +701 -0
- package/src/project/dashboard.ts +118 -0
- package/src/project/deploy.ts +364 -0
- package/src/project/devVars.ts +113 -0
- package/src/project/domainPrompt.ts +191 -0
- package/src/project/domains.ts +386 -0
- package/src/project/envInventory.ts +356 -0
- package/src/project/environment.ts +125 -0
- package/src/project/extensions.ts +69 -0
- package/src/project/jsonc.ts +289 -0
- package/src/project/packageManager.ts +238 -0
- package/src/project/readOptionalFile.ts +342 -0
- package/src/project/rollback.ts +145 -0
- package/src/project/scaffold.ts +1088 -0
- package/src/project/templateFiles.ts +53 -0
- package/src/project/verifyDeploy.ts +230 -0
- package/src/project/versionMetadata.ts +77 -0
- package/src/project/workerAddress.ts +176 -0
- package/src/project/workerCommand.ts +564 -0
- package/src/project/workerIdentity.ts +50 -0
- package/src/project/workerManifest.ts +135 -0
- package/src/project/workerScaffold.ts +289 -0
- package/src/project/workerScope.ts +394 -0
- package/src/project/workers.ts +86 -0
- package/src/project/workflows.ts +281 -0
- package/src/project/wrangler.ts +168 -0
- package/src/provision/confirm.ts +86 -0
- package/src/provision/environment.ts +407 -0
- package/src/provision/featureConfig.ts +98 -0
- package/src/provision/mode.ts +62 -0
- package/src/provision/pendingSecrets.ts +96 -0
- package/src/provision/resources.ts +126 -0
- package/src/provision/secretBindings.ts +149 -0
- package/src/provision/store.ts +33 -0
- package/src/provision/unprovisioned.ts +114 -0
- package/src/provision/wranglerEnv.ts +220 -0
- package/src/rootFlags.ts +48 -0
- package/src/seed/drivers.ts +423 -0
- package/src/seed/media.ts +187 -0
- package/src/seed/plan.ts +137 -0
- package/src/seed/prepare.ts +224 -0
- package/src/seed/registry.ts +25 -0
- package/src/seed/run.ts +793 -0
- package/src/seed/safety.ts +206 -0
- package/src/terminal/logger.ts +42 -0
- package/src/terminal/output.ts +64 -0
- package/src/terminal/style.ts +132 -0
- package/src/test-utils/doctorHarness.ts +190 -0
- package/src/test-utils/migrateHarness.ts +126 -0
- package/src/test-utils/seedHarness.ts +173 -0
- package/src/test-utils/tempRepo.ts +45 -0
- package/src/tokens/config.ts +16 -0
- package/src/tokens/engine.ts +345 -0
- package/src/tokens/mintedTokens.ts +233 -0
- package/src/tokens/sinks.ts +84 -0
- package/src/ui/flow.ts +451 -0
- package/src/ui/react.ts +112 -0
- package/src/ui/routeAllowlist.ts +208 -0
- package/src/ui/scaffold.ts +113 -0
- package/src/ui/screenStyles.ts +127 -0
- package/src/ui/stubs.ts +135 -0
- package/src/ui/templates.ts +52 -0
- package/src/ui/wire.ts +311 -0
- package/src/ui/workerUi.ts +172 -0
- package/templates/starter/.dev.secrets.example.jsonc +43 -0
- package/templates/starter/.dev.vars.example +30 -0
- package/templates/starter/apps/api/package.json +22 -0
- package/templates/starter/apps/api/pithy.config.ts +65 -0
- package/templates/starter/apps/api/pithy.worker.jsonc +11 -0
- package/templates/starter/apps/api/src/bindings.workers.test.ts +18 -0
- package/templates/starter/apps/api/src/cloudflare-test.d.ts +11 -0
- package/templates/starter/apps/api/src/index.ts +8 -0
- package/templates/starter/apps/api/tsconfig.json +26 -0
- package/templates/starter/apps/api/wrangler.jsonc +68 -0
- package/templates/starter/biome.template.jsonc +75 -0
- package/templates/starter/gitignore +37 -0
- package/templates/starter/package.json +28 -0
- package/templates/starter/pithy.config.ts +67 -0
- package/templates/starter/plugins/no-console.grit +25 -0
- package/templates/starter/plugins/no-process-io.grit +25 -0
- package/templates/starter/tsconfig.json +14 -0
- package/templates/starter/tsconfig.tools.json +30 -0
- package/templates/starter/vitest.config.ts +124 -0
- package/templates/starter/vitest.workers.config.ts +26 -0
package/src/ui/flow.ts
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { readdir } from "node:fs/promises";
|
|
5
|
+
import { basename, join, sep } from "node:path";
|
|
6
|
+
import { ConflictError, NotFoundError, ValidationError } from "@pithy-sh/core/src/error/pithyError";
|
|
7
|
+
import { allCapabilities, projectEnvironments, type WorkerConfig } from "../project/config";
|
|
8
|
+
import { detectPackageManager, type PackageManager } from "../project/packageManager";
|
|
9
|
+
import { readOptionalFile } from "../project/readOptionalFile";
|
|
10
|
+
import { withRollback } from "../project/rollback";
|
|
11
|
+
import { pathExists } from "../project/scaffold";
|
|
12
|
+
import { deriveWorkerFirst, uncoveredRoutes } from "./routeAllowlist";
|
|
13
|
+
import { scaffoldFiles } from "./scaffold";
|
|
14
|
+
import { PITHY_SCREEN_DIR, unstyledScreenClasses } from "./screenStyles";
|
|
15
|
+
import { resolveStub, UI_STUBS, type UiStub } from "./stubs";
|
|
16
|
+
import { loadStubFiles } from "./templates";
|
|
17
|
+
import { readAssets, wireAssets, wireManifest, wirePackage, wireSolution } from "./wire";
|
|
18
|
+
import { readWorkerUi } from "./workerUi";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `pithy ui add` and `pithy ui sync`, as flows — the command file stays argument parsing and output,
|
|
22
|
+
* the way `capabilities/flow.ts` sits behind `pithy add`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** The capability the auth template's screens are written against. */
|
|
26
|
+
const AUTH_CAPABILITY = "auth";
|
|
27
|
+
|
|
28
|
+
/** The capability the paywall and subscription screens are written against. */
|
|
29
|
+
const PAYMENTS_CAPABILITY = "payments";
|
|
30
|
+
|
|
31
|
+
/** Which capability-gated screen set a decision is about. */
|
|
32
|
+
export type UiScreenSet = typeof AUTH_CAPABILITY | typeof PAYMENTS_CAPABILITY;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Ask whether to scaffold one capability's screens.
|
|
36
|
+
*
|
|
37
|
+
* One seam for every screen set rather than one per set: a third capability's screens must be a new member
|
|
38
|
+
* of {@link UiScreenSet} and nothing else, and a prompt-per-capability is exactly the shape that stops
|
|
39
|
+
* being true.
|
|
40
|
+
*/
|
|
41
|
+
export type UiScreenPrompt = (request: { screens: UiScreenSet; suggestion: boolean }) => Promise<boolean>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The one name this file uses for a Worker: the directory it lives in, `apps/<name>`.
|
|
45
|
+
*
|
|
46
|
+
* Derived here rather than accepted as an argument, because a Worker has **two** names and the wrong one
|
|
47
|
+
* is what a caller has to hand. `ResolvedWorker.name` is the *deployed* name — `<project>-<worker>`, the
|
|
48
|
+
* string in `wrangler.jsonc` — and taking it wrote `apps/<project>-<worker>/tsconfig.client.json` into the
|
|
49
|
+
* root solution file, a reference to a directory that has never existed. `tsc -b` stops on it with TS6053
|
|
50
|
+
* and Vite refuses to load the Worker's `vite.config.ts`, so a freshly scaffolded project could neither
|
|
51
|
+
* typecheck nor start.
|
|
52
|
+
*
|
|
53
|
+
* Everything below is either a path or a `--worker` value, and both are the directory. The deployed name
|
|
54
|
+
* belongs in `wrangler.jsonc` and nowhere else.
|
|
55
|
+
*/
|
|
56
|
+
function workerName(workerDir: string): string {
|
|
57
|
+
return basename(workerDir);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** One entry of `pithy ui list`. */
|
|
61
|
+
export interface UiStubListing {
|
|
62
|
+
/** The `<framework>` positional. */
|
|
63
|
+
id: string;
|
|
64
|
+
/** One line describing what it scaffolds. */
|
|
65
|
+
description: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Every framework `pithy ui add` can scaffold, sorted. */
|
|
69
|
+
export function listStubs(): UiStubListing[] {
|
|
70
|
+
return Object.values(UI_STUBS)
|
|
71
|
+
.map((stub) => ({ id: stub.id, description: stub.description }))
|
|
72
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Options for {@link runUiAdd} — the target worker, already resolved, plus the choices. */
|
|
76
|
+
export interface UiAddOptions {
|
|
77
|
+
/** The project root — where the lockfile that names the package manager lives. */
|
|
78
|
+
projectDir: string;
|
|
79
|
+
/** The target worker's directory, `apps/<name>` — which is also where its name comes from. */
|
|
80
|
+
workerDir: string;
|
|
81
|
+
/** The target worker's loaded `pithy.config.ts` — the route table the allowlist is derived from. */
|
|
82
|
+
config: WorkerConfig;
|
|
83
|
+
/** The `<framework>` positional. */
|
|
84
|
+
framework: string;
|
|
85
|
+
/** `--auth` / `--no-auth`. Undefined means "decide", by prompt when one is attached. */
|
|
86
|
+
auth?: boolean;
|
|
87
|
+
/** `--payments` / `--no-payments`. Undefined means "decide", the same way. */
|
|
88
|
+
payments?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Ask whether to scaffold a capability's screens. Supplied only when a human is attached; without it
|
|
91
|
+
* every decision falls to whether that capability is composed, so no invocation can ever block.
|
|
92
|
+
*/
|
|
93
|
+
prompt?: UiScreenPrompt;
|
|
94
|
+
/** Package-manager override. Tests set it; otherwise it is detected from the project's lockfile. */
|
|
95
|
+
packageManager?: PackageManager;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** What `pithy ui add` did. */
|
|
99
|
+
export interface UiAddReport {
|
|
100
|
+
/** The worker the front end landed in. */
|
|
101
|
+
worker: string;
|
|
102
|
+
/** The stub that wrote it. */
|
|
103
|
+
framework: string;
|
|
104
|
+
/** Whether the auth template was included. */
|
|
105
|
+
auth: boolean;
|
|
106
|
+
/** Whether the payments screens were included. */
|
|
107
|
+
payments: boolean;
|
|
108
|
+
/** Worker-relative paths created by this run, sorted. */
|
|
109
|
+
created: string[];
|
|
110
|
+
/** Worker-relative paths that already existed and were left byte-identical, sorted. */
|
|
111
|
+
skipped: string[];
|
|
112
|
+
/**
|
|
113
|
+
* Class names Pithy's screens render that nothing in this Worker's stylesheets defines — empty on
|
|
114
|
+
* every ordinary run, and the exact list to fix when it is not.
|
|
115
|
+
*
|
|
116
|
+
* Separate from {@link created} because "wrote the screens" and "the screens are styled" are two
|
|
117
|
+
* claims and only one of them was ever reported. A backfill writes `routes/pithy/sign-in.tsx` and
|
|
118
|
+
* correctly skips the adopter's `src/styles.css`; if the rules those screens need are not on disk,
|
|
119
|
+
* the run said `created` and the adopter met an unstyled login page with nothing pointing at why.
|
|
120
|
+
*
|
|
121
|
+
* Measured against the files as they stand **after** the write, not against the template, because
|
|
122
|
+
* the template is not what renders: a stylesheet the adopter has since edited, or deleted, is the
|
|
123
|
+
* case this exists for.
|
|
124
|
+
*/
|
|
125
|
+
unstyled: string[];
|
|
126
|
+
/** The derived `assets.run_worker_first` allowlist now in `wrangler.jsonc`. */
|
|
127
|
+
runWorkerFirst: string[];
|
|
128
|
+
/** The project's package manager. */
|
|
129
|
+
packageManager: PackageManager;
|
|
130
|
+
/** Dependency names added to the worker's `package.json`. */
|
|
131
|
+
dependencies: string[];
|
|
132
|
+
/** Dev-dependency names added to the worker's `package.json`. */
|
|
133
|
+
devDependencies: string[];
|
|
134
|
+
/** Script names added (or superseded, for a `dev` that still held `wrangler dev`). */
|
|
135
|
+
scripts: string[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Whether to write one capability's screens. `--auth`/`--no-auth` and `--payments`/`--no-payments` decide
|
|
140
|
+
* outright. With neither, a human is asked (defaulting to yes when the capability is composed on this
|
|
141
|
+
* worker) and anyone else — `--json`, an agent, CI — gets that same default with no prompt, because no
|
|
142
|
+
* invocation may block.
|
|
143
|
+
*
|
|
144
|
+
* Asking for a capability's screens on a worker that does not compose it is an error, not a scaffold of
|
|
145
|
+
* broken imports.
|
|
146
|
+
*/
|
|
147
|
+
async function resolveScreens(
|
|
148
|
+
options: UiAddOptions,
|
|
149
|
+
screens: UiScreenSet,
|
|
150
|
+
requested: boolean | undefined,
|
|
151
|
+
composed: boolean,
|
|
152
|
+
): Promise<boolean> {
|
|
153
|
+
let wanted = requested;
|
|
154
|
+
if (wanted === undefined)
|
|
155
|
+
wanted = options.prompt ? await options.prompt({ screens, suggestion: composed }) : composed;
|
|
156
|
+
if (wanted && !composed) {
|
|
157
|
+
const worker = workerName(options.workerDir);
|
|
158
|
+
throw new ValidationError({
|
|
159
|
+
message: `The ${screens} screens need the ${screens} capability, and ${worker} doesn't compose it.`,
|
|
160
|
+
action: `Run pithy add ${screens} --worker ${worker} first, or leave them out with --no-${screens}.`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return wanted;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Whether a worker composes a capability by name. */
|
|
167
|
+
function composes(config: WorkerConfig, capability: string): boolean {
|
|
168
|
+
return allCapabilities(config).some((composed) => composed.name === capability);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The files this run may write: the whole template on a first scaffold, and only what does not exist
|
|
173
|
+
* yet on a backfill (`--auth` after `--no-auth`).
|
|
174
|
+
*
|
|
175
|
+
* A worker that already carries a front end and gains nothing new is the "adding a UI twice" error —
|
|
176
|
+
* clean and actionable, never a partial overwrite. A worker carrying a *different* stub is refused
|
|
177
|
+
* outright: two frameworks in one worker is not a merge anyone should attempt for you.
|
|
178
|
+
*/
|
|
179
|
+
async function planFiles(
|
|
180
|
+
options: UiAddOptions,
|
|
181
|
+
stub: UiStub,
|
|
182
|
+
screens: { auth: boolean; payments: boolean },
|
|
183
|
+
): Promise<{ files: Record<string, string>; strict: boolean }> {
|
|
184
|
+
const current = await readWorkerUi(options.workerDir);
|
|
185
|
+
const worker = workerName(options.workerDir);
|
|
186
|
+
const files = await loadStubFiles(stub, {
|
|
187
|
+
worker,
|
|
188
|
+
...screens,
|
|
189
|
+
packageManager: options.packageManager ?? "npm",
|
|
190
|
+
});
|
|
191
|
+
if (!current) return { files, strict: true };
|
|
192
|
+
|
|
193
|
+
if (current.stub !== stub.id) {
|
|
194
|
+
throw new ConflictError({
|
|
195
|
+
message: `${worker} already has a ${current.stub} front end.`,
|
|
196
|
+
action: "One worker, one front end. Remove it, or add the new one to a different worker with --worker.",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// `pathExists`, the shared `lstat`, and not the local `access` this used to roll. `access` follows a
|
|
201
|
+
// link, so a dangling one planted at a template path answered "nothing there" — the file counted as
|
|
202
|
+
// fresh, and the plan handed it to `scaffoldFiles` to write. That is the same predicate five other
|
|
203
|
+
// modules got wrong, in the one module the tripwire could not see because it imports no writer of its
|
|
204
|
+
// own. One question, one implementation.
|
|
205
|
+
let fresh = 0;
|
|
206
|
+
for (const rel of Object.keys(files)) {
|
|
207
|
+
if (!(await pathExists(join(options.workerDir, rel)))) fresh += 1;
|
|
208
|
+
}
|
|
209
|
+
if (fresh === 0) {
|
|
210
|
+
throw new ConflictError({
|
|
211
|
+
message: `${worker} already has a ${current.stub} front end.`,
|
|
212
|
+
action: "Add screens by dropping files into src/routes/app/. Run pithy ui sync to re-derive its route allowlist.",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
// The whole record, non-strict: the writer skips what exists and reports it, so the run says what
|
|
216
|
+
// it added AND what it left alone.
|
|
217
|
+
return { files, strict: false };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Whether Pithy's screens in this worker render styled, read off the worker exactly as it stands.
|
|
222
|
+
*
|
|
223
|
+
* The template's own files are not the answer, and neither is the plan. On a backfill the plan still
|
|
224
|
+
* carries Pithy's `src/styles.css` while the file on disk is the adopter's — so checking the plan would
|
|
225
|
+
* report a screen as styled by rules that exist in this process and nowhere else. Only the disk renders.
|
|
226
|
+
*
|
|
227
|
+
* **The whole of `src/` is swept, not the paths this run happened to plan (#401).** Two reasons, and the
|
|
228
|
+
* second is the one that matters now the finding can fail a build:
|
|
229
|
+
*
|
|
230
|
+
* - `pithy ui sync` plans nothing. A check that could only run over a plan could only run at scaffold,
|
|
231
|
+
* which is the defect: the report ran once, printed, and nothing ever asked again.
|
|
232
|
+
* - A class defined in a stylesheet Pithy did not write is defined. An adopter with `src/brand.css` was
|
|
233
|
+
* previously told their screens were unstyled, because that file was in no plan. A false finding is
|
|
234
|
+
* affordable in a warning and not in a gate.
|
|
235
|
+
*/
|
|
236
|
+
async function unstyledOnDisk(workerDir: string): Promise<string[]> {
|
|
237
|
+
const src = join(workerDir, "src");
|
|
238
|
+
// `recursive` gives worker-relative paths under `src/`, in whatever separator the platform uses;
|
|
239
|
+
// normalized to `/` because `PITHY_SCREEN_DIR` is a path *within a scaffolded worker*, and those are
|
|
240
|
+
// written the way the templates write them. A worker with no `src/` at all answers the empty set.
|
|
241
|
+
const entries = await readdir(src, { recursive: true }).catch(() => [] as string[]);
|
|
242
|
+
const onDisk: Record<string, string> = {};
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
const path = `src/${entry.split(sep).join("/")}`;
|
|
245
|
+
const stylesheet = path.endsWith(".css");
|
|
246
|
+
const screen = path.startsWith(PITHY_SCREEN_DIR) && path.endsWith(".tsx");
|
|
247
|
+
if (!stylesheet && !screen) continue;
|
|
248
|
+
const contents = await readOptionalFile(join(workerDir, path));
|
|
249
|
+
if (contents !== null) onDisk[path] = contents;
|
|
250
|
+
}
|
|
251
|
+
return unstyledScreenClasses(onDisk);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Every file this run may create or edit, absolute — what a failure has to put back.
|
|
256
|
+
*
|
|
257
|
+
* The template's own paths, plus the four documents `ui/wire.ts` edits rather than creates. Stated here
|
|
258
|
+
* rather than discovered afterwards, because a rollback that guesses is one that misses a file.
|
|
259
|
+
*/
|
|
260
|
+
function touchedPaths(options: UiAddOptions, planned: Record<string, string>): string[] {
|
|
261
|
+
return [
|
|
262
|
+
...Object.keys(planned).map((rel) => join(options.workerDir, rel)),
|
|
263
|
+
join(options.workerDir, "wrangler.jsonc"),
|
|
264
|
+
join(options.workerDir, "pithy.worker.jsonc"),
|
|
265
|
+
join(options.workerDir, "package.json"),
|
|
266
|
+
join(options.projectDir, "tsconfig.json"),
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Scaffold a front end into one worker and wire it end to end: the client files, the `assets` stanza
|
|
272
|
+
* that serves them, the dev command `pithy dev` runs, the build command `pithy deploy` runs, and the
|
|
273
|
+
* packages the build needs. The SPA and the API stay one deploy on one origin.
|
|
274
|
+
*
|
|
275
|
+
* **A run that fails leaves the project as it found it**, and that is two things rather than one.
|
|
276
|
+
*
|
|
277
|
+
* The allowlist is derived *before* anything is written: composing the app is the step most likely to
|
|
278
|
+
* throw — a capability misconfigured, a required binding missing (#258), a syntax error in a file the
|
|
279
|
+
* adopter has edited — and it used to run after the whole template had landed. So a failure left the
|
|
280
|
+
* files written, the wiring absent, and the retry refused by this command's own "already has a front
|
|
281
|
+
* end" guard, which cannot tell a finished front end from one abandoned a minute ago (#259).
|
|
282
|
+
*
|
|
283
|
+
* And every write after that runs under {@link withRollback}, because ordering alone only removes the
|
|
284
|
+
* failure anyone has already seen: the next step added goes back on the end of the list, and the
|
|
285
|
+
* property quietly stops being true. Stated over the outcome, it survives the next step.
|
|
286
|
+
*/
|
|
287
|
+
export async function runUiAdd(options: UiAddOptions): Promise<UiAddReport> {
|
|
288
|
+
const stub = resolveStub(options.framework);
|
|
289
|
+
const packageManager = options.packageManager ?? (await detectPackageManager(options.projectDir));
|
|
290
|
+
const auth = await resolveScreens(options, AUTH_CAPABILITY, options.auth, composes(options.config, AUTH_CAPABILITY));
|
|
291
|
+
const payments = await resolveScreens(
|
|
292
|
+
options,
|
|
293
|
+
PAYMENTS_CAPABILITY,
|
|
294
|
+
options.payments,
|
|
295
|
+
composes(options.config, PAYMENTS_CAPABILITY),
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const plan = await planFiles({ ...options, packageManager }, stub, { auth, payments });
|
|
299
|
+
// Compose first. `deriveWorkerFirst` assembles every capability's routes, once per environment, and
|
|
300
|
+
// reads nothing this run is about to write — which is what makes moving it ahead of the write legal.
|
|
301
|
+
const patterns = deriveWorkerFirst(options.config, await projectEnvironments(options.projectDir));
|
|
302
|
+
|
|
303
|
+
const scope = { root: options.projectDir, paths: touchedPaths(options, plan.files) };
|
|
304
|
+
const result = await withRollback(scope, async () => {
|
|
305
|
+
const written = await scaffoldFiles({ workerDir: options.workerDir, files: plan.files, strict: plan.strict });
|
|
306
|
+
const assets = await wireAssets(options.workerDir, patterns);
|
|
307
|
+
await wireManifest(options.workerDir, stub, packageManager);
|
|
308
|
+
const pkg = await wirePackage(options.projectDir, options.workerDir, stub);
|
|
309
|
+
// Last, and project-wide rather than per-worker: the client's programs join the root solution file so
|
|
310
|
+
// `bun run typecheck` builds them. Two tsconfigs nothing references are two tsconfigs nothing checks.
|
|
311
|
+
await wireSolution(options.projectDir, workerName(options.workerDir));
|
|
312
|
+
// Read inside the scope, because it reads the files this block just wrote — outside, a rollback
|
|
313
|
+
// would already have taken them away.
|
|
314
|
+
const unstyled = await unstyledOnDisk(options.workerDir);
|
|
315
|
+
return { written, assets, pkg, unstyled };
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
worker: workerName(options.workerDir),
|
|
320
|
+
framework: stub.id,
|
|
321
|
+
auth,
|
|
322
|
+
payments,
|
|
323
|
+
created: result.written.written,
|
|
324
|
+
skipped: result.written.skipped,
|
|
325
|
+
unstyled: result.unstyled,
|
|
326
|
+
runWorkerFirst: result.assets.after,
|
|
327
|
+
packageManager,
|
|
328
|
+
dependencies: result.pkg.dependencies,
|
|
329
|
+
devDependencies: result.pkg.devDependencies,
|
|
330
|
+
scripts: result.pkg.scripts,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** What `pithy ui sync` did. */
|
|
335
|
+
export interface UiSyncReport {
|
|
336
|
+
/** The worker whose allowlist was re-derived. */
|
|
337
|
+
worker: string;
|
|
338
|
+
/** The allowlist before this run. */
|
|
339
|
+
before: string[];
|
|
340
|
+
/** The allowlist after it. */
|
|
341
|
+
after: string[];
|
|
342
|
+
/** Whether anything actually moved — false on a re-run, which is the point. */
|
|
343
|
+
changed: boolean;
|
|
344
|
+
/**
|
|
345
|
+
* Routes the allowlist **in the file** does not cover: the ones the SPA shell is answering with a 200
|
|
346
|
+
* and the wrong body. Always empty after a write — the run just re-derived the list — so this is the
|
|
347
|
+
* finding `--check` exists to produce, and the one thing that fails the exit.
|
|
348
|
+
*/
|
|
349
|
+
uncovered: string[];
|
|
350
|
+
/**
|
|
351
|
+
* Class names Pithy's screens render that nothing in this worker's stylesheets defines — read off the
|
|
352
|
+
* worker as it stands now, not as it stood when it was scaffolded.
|
|
353
|
+
*
|
|
354
|
+
* **This is the check that used to run once and never again (#401).** It was written at `pithy ui add`
|
|
355
|
+
* because that is where the defect it caught happened: a backfill wrote the sign-in screens and
|
|
356
|
+
* correctly skipped the adopter's `src/styles.css`, and the run reported `created` over a login page
|
|
357
|
+
* whose `stack`, `divider` and `secondary` nothing defined. But `styles.css` is theirs, and the
|
|
358
|
+
* ordinary case is an edit a week later — deleting a rule, replacing the file, dropping
|
|
359
|
+
* `pithy-screens.css` in a tidy-up. None of that errors. The screen renders, unstyled, with a 200.
|
|
360
|
+
*
|
|
361
|
+
* So it belongs here too, on the one command that re-checks a front end after scaffold, and under
|
|
362
|
+
* `--check` it is one of the two findings that fail the exit. The counter-argument is real and worth
|
|
363
|
+
* stating: a class with no rule is a cosmetic defect, and a build that fails over one teaches people
|
|
364
|
+
* to write `.stack {}` and move on. What settles it is that the fix anybody actually reaches for is
|
|
365
|
+
* restoring `pithy-screens.css` or defining the rule they meant to define — and that deleting the
|
|
366
|
+
* class from the screen is also a fix, because the screen is theirs. A stub rule is available and is
|
|
367
|
+
* not what the message asks for.
|
|
368
|
+
*/
|
|
369
|
+
unstyled: string[];
|
|
370
|
+
/**
|
|
371
|
+
* `assets.not_found_handling` as it stands. Reported because SPA routing depends on it: an adopter
|
|
372
|
+
* who set it to something else has deep links 404ing in Hono rather than serving the app shell, and
|
|
373
|
+
* `ui sync` does not overwrite a value they chose.
|
|
374
|
+
*/
|
|
375
|
+
notFoundHandling: string | undefined;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Re-derive one worker's `assets.run_worker_first` from its current route table, or — with `check` —
|
|
380
|
+
* only report how far the file has drifted from it.
|
|
381
|
+
*
|
|
382
|
+
* The allowlist is written once, at `pithy ui add`, and every route mounted afterwards is a route the
|
|
383
|
+
* asset router answers before the worker runs. `pithy add <capability>` is one way that happens; the
|
|
384
|
+
* adopter writing a route into their own app capability is the other, and that one runs no command at
|
|
385
|
+
* all. So the allowlist has to be re-derivable on demand *and* checkable in CI, because a list that
|
|
386
|
+
* has gone stale does not fail — it returns 200 with the SPA shell.
|
|
387
|
+
*
|
|
388
|
+
* **It re-runs the unstyled check too (#401), which is the other thing that goes stale in silence.**
|
|
389
|
+
* Both findings have the same shape: a 200, the wrong output, and no error anywhere. One is the shell
|
|
390
|
+
* answering a route; the other is a screen rendering with no rules. This is the command that re-checks a
|
|
391
|
+
* front end, so it checks both, and `--check` fails on either.
|
|
392
|
+
*
|
|
393
|
+
* Creates no files either way, and re-running changes nothing.
|
|
394
|
+
*/
|
|
395
|
+
export async function runUiSync(options: {
|
|
396
|
+
/** The project root — where the `environments` declaration the derivation needs lives. */
|
|
397
|
+
projectDir: string;
|
|
398
|
+
/** The target worker's directory, `apps/<name>` — which is also where its name comes from. */
|
|
399
|
+
workerDir: string;
|
|
400
|
+
/** The target worker's loaded `pithy.config.ts`. */
|
|
401
|
+
config: WorkerConfig;
|
|
402
|
+
/** Report the drift and write nothing — the CI gate. */
|
|
403
|
+
check?: boolean;
|
|
404
|
+
}): Promise<UiSyncReport> {
|
|
405
|
+
const worker = workerName(options.workerDir);
|
|
406
|
+
const current = await readWorkerUi(options.workerDir);
|
|
407
|
+
if (!current) {
|
|
408
|
+
throw new NotFoundError({
|
|
409
|
+
message: `${worker} has no front end.`,
|
|
410
|
+
action: `Run pithy ui add react --worker ${worker} to scaffold one.`,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const environments = await projectEnvironments(options.projectDir);
|
|
415
|
+
const after = deriveWorkerFirst(options.config, environments);
|
|
416
|
+
// Read on both paths, and before the write on the one that writes. `wireAssets` touches
|
|
417
|
+
// `wrangler.jsonc` and no stylesheet, so the answer is the same either side of it — taking it first
|
|
418
|
+
// means the report is assembled from one reading of the worker rather than two.
|
|
419
|
+
const unstyled = await unstyledOnDisk(options.workerDir);
|
|
420
|
+
if (options.check) {
|
|
421
|
+
const assets = await readAssets(options.workerDir);
|
|
422
|
+
return {
|
|
423
|
+
worker,
|
|
424
|
+
before: assets.runWorkerFirst,
|
|
425
|
+
after,
|
|
426
|
+
changed: assets.runWorkerFirst.join("\n") !== after.join("\n") || assets.notFoundHandling === undefined,
|
|
427
|
+
uncovered: uncoveredRoutes(options.config, assets.runWorkerFirst, environments),
|
|
428
|
+
unstyled,
|
|
429
|
+
notFoundHandling: assets.notFoundHandling,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// `after` above is the same derivation the write needs, so it is computed once and handed over —
|
|
434
|
+
// and, incidentally, before anything is written here too.
|
|
435
|
+
const change = await wireAssets(options.workerDir, after);
|
|
436
|
+
const before = change.before ?? [];
|
|
437
|
+
return {
|
|
438
|
+
worker,
|
|
439
|
+
before,
|
|
440
|
+
after: change.after,
|
|
441
|
+
// Every way this call can have moved the file, not just the allowlist — a report that says
|
|
442
|
+
// nothing changed while the file did is worse than no report, because CI keys off it.
|
|
443
|
+
changed: before.join("\n") !== change.after.join("\n") || change.wroteNotFoundHandling,
|
|
444
|
+
// The list was just re-derived from this same route table, so nothing is left outside it.
|
|
445
|
+
uncovered: [],
|
|
446
|
+
// Unlike `uncovered`, this one is not emptied by the write. Nothing here touches a stylesheet, so a
|
|
447
|
+
// missing rule is still missing — and saying so is the point of re-running the check at all.
|
|
448
|
+
unstyled,
|
|
449
|
+
notFoundHandling: change.notFoundHandling,
|
|
450
|
+
};
|
|
451
|
+
}
|
package/src/ui/react.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { PACKAGE_VERSION } from "@pithy-sh/core/src/version.generated";
|
|
5
|
+
import { HOME_SCREEN, TEMPLATE_DIR, TEMPLATE_GROUPS, WORKER_TOKEN } from "@pithy-sh/ui-react/src/templates";
|
|
6
|
+
import { kitRange } from "../project/scaffold";
|
|
7
|
+
import { WRANGLER_RANGE } from "../project/workerScaffold";
|
|
8
|
+
import type { UiStub, UiStubContext, UiStubFile } from "./stubs";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The React 19 stub — the first framework `pithy ui add` scaffolds, registered through the same
|
|
12
|
+
* {@link UiStub} declaration any later framework would use.
|
|
13
|
+
*
|
|
14
|
+
* Two templates. **bare** is a SPA with one typed `fetch("/health")`: no auth imports, no dead files.
|
|
15
|
+
* **auth** is that plus the passwordless screens, a session hook, and a route guard.
|
|
16
|
+
*
|
|
17
|
+
* The screens live in `@pithy-sh/ui-react`, a template library with its own React toolchain, so they
|
|
18
|
+
* are typechecked and linted like any other source. This module is the part that belongs to the CLI:
|
|
19
|
+
* which of them a given invocation writes, and how the result is run and built. A second framework
|
|
20
|
+
* brings its own library and its own module here; neither needs to touch the other's toolchain.
|
|
21
|
+
*
|
|
22
|
+
* Every client file is `.tsx`. That is load-bearing: the Worker's own `tsconfig.json` includes
|
|
23
|
+
* `src/**\/*.ts`, which does **not** match `.tsx`, so the client never enters the Worker's program and
|
|
24
|
+
* the Worker's config needs no edit. The ambient declarations live at the Worker root as
|
|
25
|
+
* `client-env.d.ts` rather than under `src/`, because `.d.ts` *would* match that glob.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** The same path in and out — true of every template except the home screen. */
|
|
29
|
+
function inPlace(paths: readonly string[]): UiStubFile[] {
|
|
30
|
+
return paths.map((path) => ({ source: path, target: path }));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function manifest(context: UiStubContext): UiStubFile[] {
|
|
34
|
+
const files = inPlace(TEMPLATE_GROUPS.base);
|
|
35
|
+
files.push({
|
|
36
|
+
source: context.auth ? HOME_SCREEN.auth : HOME_SCREEN.bare,
|
|
37
|
+
target: HOME_SCREEN.target,
|
|
38
|
+
});
|
|
39
|
+
// The capability groups stack rather than choose: they name disjoint files over one layout, so a Worker
|
|
40
|
+
// composing both auth and payments gets both screen sets and no path moves.
|
|
41
|
+
if (context.auth) files.push(...inPlace(TEMPLATE_GROUPS.auth));
|
|
42
|
+
if (context.payments) files.push(...inPlace(TEMPLATE_GROUPS.payments));
|
|
43
|
+
return files;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** React 19, through Vite 8 and `@cloudflare/vite-plugin`. */
|
|
47
|
+
export const reactStub: UiStub = {
|
|
48
|
+
id: "react",
|
|
49
|
+
description: "React 19 SPA on Vite, served by the worker as static assets",
|
|
50
|
+
templateDir: TEMPLATE_DIR,
|
|
51
|
+
manifest,
|
|
52
|
+
substitutions: (context) => ({ [WORKER_TOKEN]: context.worker }),
|
|
53
|
+
dependencies: {
|
|
54
|
+
react: "^19.2.8",
|
|
55
|
+
"react-dom": "^19.2.8",
|
|
56
|
+
// **Every screen renders through the translator seam, so the front end depends on it the way it
|
|
57
|
+
// depends on React.** This is not the auth/payments pattern and could not be: those screens are
|
|
58
|
+
// written only when their capability is composed, and their package arrives with `pithy add auth`.
|
|
59
|
+
// `src/pithy-locale.tsx`, `src/router.tsx` and the home screen all reach it — all `base`, all
|
|
60
|
+
// written on every scaffold — so a project that never composes `i18n` still imports the package, and
|
|
61
|
+
// before this line `vite build` could not resolve it. The alternative was to un-translate the base
|
|
62
|
+
// screens, which is the feature.
|
|
63
|
+
//
|
|
64
|
+
// A dependency rather than a devDependency, for the reason `react` is one: it is bundled into what
|
|
65
|
+
// the browser downloads. And core's `PACKAGE_VERSION` for the reason `@pithy-sh/vite` uses it —
|
|
66
|
+
// the release train below is what makes that honest, and `react.test.ts` holds both halves.
|
|
67
|
+
"@pithy-sh/i18n": kitRange(PACKAGE_VERSION),
|
|
68
|
+
},
|
|
69
|
+
devDependencies: {
|
|
70
|
+
"@cloudflare/vite-plugin": "^1.48.0",
|
|
71
|
+
// Derived, never a literal: `"^0.0.0"` 404'd the adopter's next install, and no release would ever
|
|
72
|
+
// have moved it. `kitRange` answers `null` while nothing under the scope is published, which drops
|
|
73
|
+
// the line entirely, and writes a real range the day one exists.
|
|
74
|
+
//
|
|
75
|
+
// **`PACKAGE_VERSION` is core's, and this is a sibling** — which `stampWorkerManifest` forbids
|
|
76
|
+
// outright, because two packages that version independently share no honest range. What makes it
|
|
77
|
+
// honest here is `.changeset/config.json`: `@pithy-sh/core` and `@pithy-sh/vite` are a `fixed` group,
|
|
78
|
+
// always released together at one version, so core's number IS vite's number. `linked` would not do
|
|
79
|
+
// it — a release touching only core leaves a linked sibling on its old version, and the range would
|
|
80
|
+
// then name something vite never published. `react.test.ts` holds the group and the equality; break
|
|
81
|
+
// either and this line goes back to inventing a version.
|
|
82
|
+
"@pithy-sh/vite": kitRange(PACKAGE_VERSION),
|
|
83
|
+
"@types/react": "^19.2.18",
|
|
84
|
+
"@types/react-dom": "^19.2.4",
|
|
85
|
+
"@vitejs/plugin-react": "^6.0.4",
|
|
86
|
+
// A DOM for the seeded gate, and for every client test the adopter writes after it. The starter's
|
|
87
|
+
// node project already collects `.tsx` co-located tests (#245) but runs them in `environment:
|
|
88
|
+
// "node"`, where `document` does not exist — so `src/turnstile.test.tsx` names its own environment
|
|
89
|
+
// in a docblock, and this is the package that has to be installed for that name to resolve (#383).
|
|
90
|
+
"happy-dom": "^20.11.2",
|
|
91
|
+
vite: "^8.2.1",
|
|
92
|
+
// The Worker's own pin, imported from the producer that writes it — this was a third copy of a
|
|
93
|
+
// literal that `scaffoldParity.test.ts` held between only the other two.
|
|
94
|
+
wrangler: WRANGLER_RANGE,
|
|
95
|
+
},
|
|
96
|
+
// `--configLoader runner` is load-bearing, not a preference. Vite's default config loader bundles
|
|
97
|
+
// vite.config.ts and leaves every bare import external, so Node itself has to import
|
|
98
|
+
// `@pithy-sh/vite` — which, like every package here, ships raw TypeScript with extensionless
|
|
99
|
+
// relative imports. Node cannot resolve those, and refuses to strip types under node_modules at
|
|
100
|
+
// all. The runner loads the config through Vite's own resolver instead, where both are ordinary.
|
|
101
|
+
devCommand: (port) => ["vite", "dev", "--configLoader", "runner", "--strictPort", "--port", port],
|
|
102
|
+
readySignal: "ready in \\d+",
|
|
103
|
+
buildCommand: ["vite", "build", "--configLoader", "runner"],
|
|
104
|
+
// The adopter's own entry points. Every one loads vite.config.ts, so every one needs the runner —
|
|
105
|
+
// and `dev` deliberately omits `--port`/`--strictPort`: a bare `vite dev` is someone working on the
|
|
106
|
+
// front end alone, where Vite picking its own port is right. `pithy dev` is what pins ports.
|
|
107
|
+
scripts: {
|
|
108
|
+
dev: "vite dev --configLoader runner",
|
|
109
|
+
build: "vite build --configLoader runner",
|
|
110
|
+
preview: "vite preview --configLoader runner",
|
|
111
|
+
},
|
|
112
|
+
};
|