@pithy-sh/vite 0.1.1 → 0.1.3

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.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * **`client-env.d.ts` is generated from the four declared client projections (#398).**
3
+ *
4
+ * `@pithy-sh/ui-react`'s `templates/client-env.d.ts` is the ambient declaration for the four
5
+ * `virtual:pithy/*` modules, copied into an adopter's Worker by `pithy ui add react`. It used to be
6
+ * hand-written, three packages away from the `client:` projections that produce the values it
7
+ * describes, and #392 held the two together with a spawned compiler because there was no faithful
8
+ * source to generate from: `Capability.client` is typed `(context) => ClientProjection`, so each
9
+ * capability's real shape existed only as an inferred literal inside a closure.
10
+ *
11
+ * #395 removed that. The four capabilities now declare their projection types, and the declaration is
12
+ * the source of truth rather than a restatement of the literal. So there is exactly one statement of
13
+ * each shape, and this module copies it — **a gate that watches two things agree is strictly worse
14
+ * than one thing.**
15
+ *
16
+ * ## Why the declared type is copied as text rather than re-printed from a type
17
+ *
18
+ * Because the doc comments are half of what the file is worth. A declaration emitted from a resolved
19
+ * type carries the shape and loses every sentence attached to it, and those sentences are what tell a
20
+ * screen that `action` must be rendered and never retyped, or that a Paddle client token is
21
+ * publishable by design. Copying the type node's own source text keeps the unions, the `| null` arms
22
+ * and the prose exactly as the capability wrote them.
23
+ *
24
+ * The one thing that is *not* copied is what is not a shape at all — see {@link PREAMBLE} and
25
+ * {@link ENABLED_EXPORT}, both of which are policy and are written as fixed text.
26
+ *
27
+ * ## Nothing an adopter runs
28
+ *
29
+ * The kit generates at its own build time and commits the artifact. `pithy ui add react` still copies
30
+ * a plain static `.d.ts` into a Worker, and a scaffolded project typechecks with no new step.
31
+ */
32
+ /** One `virtual:pithy/*` module, and the declared type that is its whole shape. */
33
+ export interface DeclaredModule {
34
+ /** The capability name — the segment after `virtual:pithy/`. */
35
+ readonly module: string;
36
+ /** The specifier the projection is read from. Resolved through the package's own exports map. */
37
+ readonly specifier: string;
38
+ /** The exported type alias that declares what a browser receives. */
39
+ readonly type: string;
40
+ }
41
+ /**
42
+ * The modules the declaration covers, in the order they are emitted.
43
+ *
44
+ * This list is the whole reach of the generated file: a further capability projecting to a browser is
45
+ * a further entry here, and until it is one, nothing declares it. The specifiers resolve because each
46
+ * capability is a devDependency of `@pithy-sh/vite` — a build-time dependency, like the compiler. An
47
+ * adopter installs none of them to use the plugin, and never runs this module.
48
+ */
49
+ export declare const DECLARED_MODULES: readonly DeclaredModule[];
50
+ /**
51
+ * The source text of every top-level `export type X = …;` in a module, keyed by name.
52
+ *
53
+ * The value is the type node's own text, verbatim: every doc comment, every union arm, every `| null`
54
+ * exactly as written. It is found by scanning for the `;` that closes the alias at bracket depth zero,
55
+ * over code characters only.
56
+ */
57
+ export declare function parseTypeAliases(source: string): Map<string, string>;
58
+ /**
59
+ * Replace every reference to a sibling type alias with that alias's own text, re-indented to sit where
60
+ * the reference did.
61
+ *
62
+ * `PaymentsClientProduct` is the only one today, and it is named rather than inlined at the source for
63
+ * a reason #395 recorded — the annotation goes on the `.map` callback, where a fresh literal is still
64
+ * checked in both directions. The declaration an adopter reads has no such seam to hang a name on, so
65
+ * the shape is written where it is used.
66
+ */
67
+ export declare function inlineAliases(text: string, aliases: ReadonlyMap<string, string>, seen?: readonly string[]): string;
68
+ /** One `declare module "virtual:pithy/<module>"` block, fixed text and declared shape together. */
69
+ export declare function renderModule(module: string, declaredType: string): string;
70
+ /**
71
+ * The whole file, from each module's projection source.
72
+ *
73
+ * Pure, and the module list is an argument, so the test can hand it a projection this repository does
74
+ * not contain and read what came out. A generator asserted only against the real four would pass just
75
+ * as well if it ignored them and printed a constant.
76
+ */
77
+ export declare function renderClientEnv(sources: ReadonlyMap<string, string>, modules?: readonly DeclaredModule[]): string;
78
+ /**
79
+ * Read the four projections' sources through their own packages' exports maps.
80
+ *
81
+ * **Resolved, then mirrored back to `src` — Jim, #476.** Until every package shipped a build,
82
+ * `@pithy-sh/auth/src/client/projection` resolved to the TypeScript itself. It resolves to
83
+ * `dist/client/projection.js` now, where the types are stripped and `AuthClientProjection` does not
84
+ * exist, so the resolved path is walked back to the module it was emitted from.
85
+ *
86
+ * The declaration beside it, `dist/client/projection.d.ts`, is the other candidate and was tried first.
87
+ * It is arguably the more truthful input — it is the type an adopter can actually see — but it makes
88
+ * this generator's build wait on five capability packages' builds, and `@pithy-sh/vite` depends on none
89
+ * of them. That is five cross-package `dependsOn` edges in `turbo.jsonc` that a sixth capability's
90
+ * projection would silently need and silently not have: the generator would read a stale declaration,
91
+ * emit a stale template, and say nothing. Source has no such ordering, `turbo.jsonc` already names
92
+ * every `packages/<name>/src/client/projection.ts` as this build's input, and the authored text is
93
+ * what the file's own header promises an adopter is emitted verbatim.
94
+ *
95
+ * `dist/x.js` → `src/x.ts` is not a guess about the layout: `tsdown` and `tsc -p tsconfig.build.json`
96
+ * both mirror `src/` into `dist/`, so the two are the same path under a different root, and
97
+ * `packaging.test.ts` fails on any published module missing either half.
98
+ */
99
+ export declare function readProjectionSources(): Promise<Map<string, string>>;
100
+ /** The declaration as it should be on disk. What `bun run generate` writes and the gate compares. */
101
+ export declare function generateClientEnv(): Promise<string>;
102
+ //# sourceMappingURL=clientEnvDeclaration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clientEnvDeclaration.d.ts","sourceRoot":"","sources":["../src/clientEnvDeclaration.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC7B,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,iGAAiG;IACjG,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,cAAc,EAMrD,CAAC;AAuGF;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAyBpE;AAQD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,IAAI,GAAE,SAAS,MAAM,EAAO,GAC3B,MAAM,CAiBR;AA2CD,mGAAmG;AACnG,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAUzE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACpC,OAAO,GAAE,SAAS,cAAc,EAAqB,GACpD,MAAM,CAYR;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAS1E;AAED,qGAAqG;AACrG,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAEzD"}
@@ -0,0 +1,299 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ //#region src/clientEnvDeclaration.ts
4
+ /**
5
+ * The modules the declaration covers, in the order they are emitted.
6
+ *
7
+ * This list is the whole reach of the generated file: a further capability projecting to a browser is
8
+ * a further entry here, and until it is one, nothing declares it. The specifiers resolve because each
9
+ * capability is a devDependency of `@pithy-sh/vite` — a build-time dependency, like the compiler. An
10
+ * adopter installs none of them to use the plugin, and never runs this module.
11
+ */
12
+ const DECLARED_MODULES = [
13
+ {
14
+ module: "auth",
15
+ specifier: "@pithy-sh/auth/src/client/projection",
16
+ type: "AuthClientProjection"
17
+ },
18
+ {
19
+ module: "i18n",
20
+ specifier: "@pithy-sh/i18n/src/client/projection",
21
+ type: "I18nClientProjection"
22
+ },
23
+ {
24
+ module: "payments",
25
+ specifier: "@pithy-sh/payments/src/client/projection",
26
+ type: "PaymentsClientProjection"
27
+ },
28
+ {
29
+ module: "support",
30
+ specifier: "@pithy-sh/support/src/client/projection",
31
+ type: "SupportClientProjection"
32
+ },
33
+ {
34
+ module: "turnstile",
35
+ specifier: "@pithy-sh/turnstile/src/client/projection",
36
+ type: "TurnstileClientProjection"
37
+ }
38
+ ];
39
+ /**
40
+ * **Fixed text, item one: the preamble.**
41
+ *
42
+ * The `vite/client` reference and the paragraph explaining why the default export is a union are
43
+ * policy about how the modules are consumed, not a projection of anything, so there is nothing to
44
+ * derive them from. They are written here.
45
+ */
46
+ const PREAMBLE = `/// <reference types="vite/client" />
47
+
48
+ // The client-safe projection of this worker's composed capabilities, served by @pithy-sh/vite.
49
+ //
50
+ // Each module is a DEFAULT export whose type is a union discriminated on \`enabled\`. That shape is
51
+ // deliberate. A capability that is not composed projects \`{ enabled: false }\` and nothing else, so a
52
+ // NAMED import of any other key would be a missing export and the build would fail — on exactly the
53
+ // case this mechanism exists to make survivable. Importing the default and narrowing cannot fail:
54
+ //
55
+ // import turnstile from "virtual:pithy/turnstile";
56
+ // if (!turnstile.enabled) return null; // narrowed: sitekey, mode and token exist below this line
57
+ //
58
+ // The \`virtual:pithy/*\` modules are never written to disk. These declarations describe modules the
59
+ // Vite plugin serves, built from the Worker's own pithy.config.ts.
60
+ //
61
+ // **Generated, and copied here as it was emitted (#398).** Each declaration below is a capability's
62
+ // declared client projection — \`src/client/projection.ts\` in @pithy-sh/auth, @pithy-sh/payments,
63
+ // @pithy-sh/support and @pithy-sh/turnstile — written out by @pithy-sh/vite's
64
+ // \`src/clientEnvDeclaration.ts\` at kit build time. In the kit that is one statement of each shape and
65
+ // nothing to keep in step: a field a projection stops emitting is a compile error where it is
66
+ // projected, and this file moves in the same commit.
67
+ //
68
+ // In your repository it is yours, like every other seeded file. Editing it changes what your compiler
69
+ // believes and nothing about what the plugin serves.
70
+ `;
71
+ /**
72
+ * **Fixed text, item two: the one named export.**
73
+ *
74
+ * `enabled` is declared alone on purpose, and the refusal is the feature. A capability nobody composed
75
+ * serves `{ enabled: false }` and nothing else, so `import { sitekey } from "virtual:pithy/turnstile"`
76
+ * has to fail the build — on exactly the case the union exists to make survivable. Deriving the named
77
+ * exports from the projection's keys would declare every one of them and take that refusal away.
78
+ */
79
+ const ENABLED_EXPORT = ` /**
80
+ * Whether this capability is composed and serving on this worker. Also the union's discriminant.
81
+ *
82
+ * The only named export, deliberately. Every other key is reached through the default export and a
83
+ * narrowing — see the note at the top of this file.
84
+ */
85
+ export const enabled: boolean;
86
+ `;
87
+ /**
88
+ * Which characters of a TypeScript source are code — everything outside a comment or a string.
89
+ *
90
+ * Both passes below need it, and for the same reason: a brace in `` `POST {basePath}/feedback` `` and
91
+ * a type name in an `{@link …}` tag are text, and counting or rewriting them would be a silent wrong
92
+ * answer rather than an error. Template literals with `${}` substitutions are not handled, because a
93
+ * type position cannot hold one — `parseTypeAliases` refuses anything it cannot finish instead.
94
+ */
95
+ function codeMask(source) {
96
+ const mask = new Array(source.length).fill(true);
97
+ let index = 0;
98
+ while (index < source.length) {
99
+ if (source.startsWith("//", index)) {
100
+ while (index < source.length && source[index] !== "\n") mask[index++] = false;
101
+ continue;
102
+ }
103
+ if (source.startsWith("/*", index)) {
104
+ const close = source.indexOf("*/", index + 2);
105
+ const stop = close === -1 ? source.length : close + 2;
106
+ while (index < stop) mask[index++] = false;
107
+ continue;
108
+ }
109
+ const quote = source[index];
110
+ if (quote === "\"" || quote === "'" || quote === "`") {
111
+ mask[index++] = false;
112
+ while (index < source.length) {
113
+ const char = source[index];
114
+ mask[index++] = false;
115
+ if (char === "\\") {
116
+ if (index < source.length) mask[index++] = false;
117
+ continue;
118
+ }
119
+ if (char === quote) break;
120
+ }
121
+ continue;
122
+ }
123
+ index += 1;
124
+ }
125
+ return mask;
126
+ }
127
+ /**
128
+ * An identifier, wherever one may start, and the head of a top-level exported type alias.
129
+ *
130
+ * Both are built per call rather than shared. A `g` flag carries `lastIndex` between uses, and
131
+ * {@link inlineAliases} recurses — one shared instance would have an inner call move the outer's
132
+ * cursor, which reads as a reference the generator simply skipped.
133
+ */
134
+ const identifierPattern = () => /[A-Za-z_$][A-Za-z0-9_$]*/g;
135
+ const typeAliasPattern = () => /^export type ([A-Za-z_$][A-Za-z0-9_$]*)\s*=/gm;
136
+ /**
137
+ * The source text of every top-level `export type X = …;` in a module, keyed by name.
138
+ *
139
+ * The value is the type node's own text, verbatim: every doc comment, every union arm, every `| null`
140
+ * exactly as written. It is found by scanning for the `;` that closes the alias at bracket depth zero,
141
+ * over code characters only.
142
+ */
143
+ function parseTypeAliases(source) {
144
+ const mask = codeMask(source);
145
+ const aliases = /* @__PURE__ */ new Map();
146
+ const pattern = typeAliasPattern();
147
+ for (let match = pattern.exec(source); match; match = pattern.exec(source)) {
148
+ const [head, name] = match;
149
+ const start = match.index;
150
+ if (!mask[start] || !name) continue;
151
+ const from = start + head.length;
152
+ let depth = 0;
153
+ let end = -1;
154
+ for (let index = from; index < source.length; index += 1) {
155
+ if (!mask[index]) continue;
156
+ const char = source[index];
157
+ if (char === "{" || char === "(" || char === "[") depth += 1;
158
+ else if (char === "}" || char === ")" || char === "]") depth -= 1;
159
+ else if (char === ";" && depth === 0) {
160
+ end = index;
161
+ break;
162
+ }
163
+ }
164
+ if (end === -1) throw new Error(`\`export type ${name}\` is never closed by a \`;\` at depth zero.`);
165
+ aliases.set(name, source.slice(from, end));
166
+ }
167
+ return aliases;
168
+ }
169
+ /** The leading spaces of the line `index` falls on. What an inlined alias has to be pushed out by. */
170
+ function indentAt(text, index) {
171
+ const lineStart = text.lastIndexOf("\n", index - 1) + 1;
172
+ return /^[ ]*/.exec(text.slice(lineStart, index))?.[0] ?? "";
173
+ }
174
+ /**
175
+ * Replace every reference to a sibling type alias with that alias's own text, re-indented to sit where
176
+ * the reference did.
177
+ *
178
+ * `PaymentsClientProduct` is the only one today, and it is named rather than inlined at the source for
179
+ * a reason #395 recorded — the annotation goes on the `.map` callback, where a fresh literal is still
180
+ * checked in both directions. The declaration an adopter reads has no such seam to hang a name on, so
181
+ * the shape is written where it is used.
182
+ */
183
+ function inlineAliases(text, aliases, seen = []) {
184
+ const mask = codeMask(text);
185
+ const parts = [];
186
+ let cursor = 0;
187
+ const pattern = identifierPattern();
188
+ for (let match = pattern.exec(text); match; match = pattern.exec(text)) {
189
+ const name = match[0];
190
+ const body = aliases.get(name);
191
+ if (body === void 0 || !mask[match.index]) continue;
192
+ if (seen.includes(name)) throw new Error(`\`${name}\` refers to itself through ${seen.join(" → ")}.`);
193
+ const indent = indentAt(text, match.index);
194
+ const inlined = inlineAliases(body, aliases, [...seen, name]);
195
+ parts.push(text.slice(cursor, match.index), reindent(inlined.trim(), indent));
196
+ cursor = match.index + name.length;
197
+ }
198
+ parts.push(text.slice(cursor));
199
+ return parts.join("");
200
+ }
201
+ /** Push every line but the first out by `indent`. The first is already sitting where it was written. */
202
+ function reindent(text, indent) {
203
+ return text.split("\n").map((line, position) => position === 0 || line === "" ? line : `${indent}${line}`).join("\n");
204
+ }
205
+ /**
206
+ * Move a block from the column it was written at to the one it is emitted at.
207
+ *
208
+ * The alias text arrives at whatever depth its file put it — `tsc` prints a union's arms at zero and
209
+ * their members at four — and inside a `declare module` block those want four and eight. So the block
210
+ * is dedented to column zero and then pushed out uniformly, which keeps every relative depth, including
211
+ * the ` *` continuation of a doc comment.
212
+ *
213
+ * **The first line is trimmed rather than measured, and the rest are dedented by their common indent.**
214
+ * A type alias's body begins on the `=` line, so its first line arrives as `{` behind a single space
215
+ * that says nothing about the block's depth. Taking that space as the base — which is what this did
216
+ * until #476 changed the input from source to emitted declaration — shifted every following line one
217
+ * column left of where it belonged, and the misalignment was invisible in the old input only because a
218
+ * hand-written union happened to start on a line of its own.
219
+ */
220
+ function shift(text, indent) {
221
+ const lines = text.split("\n");
222
+ while (lines.length > 0 && (lines[0] ?? "").trim() === "") lines.shift();
223
+ while (lines.length > 0 && (lines.at(-1) ?? "").trim() === "") lines.pop();
224
+ const base = lines.slice(1).filter((line) => line.trim() !== "").reduce((width, line) => Math.min(width, (/^[ ]*/.exec(line)?.[0] ?? "").length), Infinity);
225
+ const dedent = Number.isFinite(base) ? base : 0;
226
+ return lines.map((line, position) => {
227
+ if (line.trim() === "") return "";
228
+ return `${indent}${position === 0 ? line.trim() : line.slice(dedent)}`;
229
+ }).join("\n");
230
+ }
231
+ /** One `declare module "virtual:pithy/<module>"` block, fixed text and declared shape together. */
232
+ function renderModule(module, declaredType) {
233
+ return [
234
+ `declare module "virtual:pithy/${module}" {`,
235
+ ENABLED_EXPORT,
236
+ " const config:",
237
+ `${shift(declaredType, " ")};`,
238
+ " export default config;",
239
+ "}",
240
+ ""
241
+ ].join("\n");
242
+ }
243
+ /**
244
+ * The whole file, from each module's projection source.
245
+ *
246
+ * Pure, and the module list is an argument, so the test can hand it a projection this repository does
247
+ * not contain and read what came out. A generator asserted only against the real four would pass just
248
+ * as well if it ignored them and printed a constant.
249
+ */
250
+ function renderClientEnv(sources, modules = DECLARED_MODULES) {
251
+ const blocks = [];
252
+ for (const declared of modules) {
253
+ const source = sources.get(declared.module);
254
+ if (source === void 0) throw new Error(`No projection source for virtual:pithy/${declared.module}.`);
255
+ const aliases = parseTypeAliases(source);
256
+ const body = aliases.get(declared.type);
257
+ if (body === void 0) throw new Error(`${declared.specifier} exports no type \`${declared.type}\`.`);
258
+ aliases.delete(declared.type);
259
+ blocks.push(renderModule(declared.module, inlineAliases(body, aliases)));
260
+ }
261
+ return `${PREAMBLE}\n${blocks.join("\n")}`;
262
+ }
263
+ /**
264
+ * Read the four projections' sources through their own packages' exports maps.
265
+ *
266
+ * **Resolved, then mirrored back to `src` — Jim, #476.** Until every package shipped a build,
267
+ * `@pithy-sh/auth/src/client/projection` resolved to the TypeScript itself. It resolves to
268
+ * `dist/client/projection.js` now, where the types are stripped and `AuthClientProjection` does not
269
+ * exist, so the resolved path is walked back to the module it was emitted from.
270
+ *
271
+ * The declaration beside it, `dist/client/projection.d.ts`, is the other candidate and was tried first.
272
+ * It is arguably the more truthful input — it is the type an adopter can actually see — but it makes
273
+ * this generator's build wait on five capability packages' builds, and `@pithy-sh/vite` depends on none
274
+ * of them. That is five cross-package `dependsOn` edges in `turbo.jsonc` that a sixth capability's
275
+ * projection would silently need and silently not have: the generator would read a stale declaration,
276
+ * emit a stale template, and say nothing. Source has no such ordering, `turbo.jsonc` already names
277
+ * every `packages/<name>/src/client/projection.ts` as this build's input, and the authored text is
278
+ * what the file's own header promises an adopter is emitted verbatim.
279
+ *
280
+ * `dist/x.js` → `src/x.ts` is not a guess about the layout: `tsdown` and `tsc -p tsconfig.build.json`
281
+ * both mirror `src/` into `dist/`, so the two are the same path under a different root, and
282
+ * `packaging.test.ts` fails on any published module missing either half.
283
+ */
284
+ async function readProjectionSources() {
285
+ const sources = /* @__PURE__ */ new Map();
286
+ for (const declared of DECLARED_MODULES) {
287
+ const built = fileURLToPath(import.meta.resolve(declared.specifier));
288
+ const source = built.replace(/([\\/])dist\1(.+)\.js$/, "$1src$1$2.ts");
289
+ if (source === built) throw new Error(`${declared.specifier} did not resolve into a build: ${built}`);
290
+ sources.set(declared.module, await readFile(source, "utf8"));
291
+ }
292
+ return sources;
293
+ }
294
+ /** The declaration as it should be on disk. What `bun run generate` writes and the gate compares. */
295
+ async function generateClientEnv() {
296
+ return renderClientEnv(await readProjectionSources());
297
+ }
298
+ //#endregion
299
+ export { DECLARED_MODULES, generateClientEnv, inlineAliases, parseTypeAliases, readProjectionSources, renderClientEnv, renderModule };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The slice of `@cloudflare/vite-plugin`'s worker config this touches.
3
+ *
4
+ * Structural rather than imported. `@pithy-sh/vite` does not depend on that plugin — an adopter picks
5
+ * its own version — and naming one field is enough to be checked against it at the call site, where the
6
+ * real type applies.
7
+ */
8
+ export interface WorkerVars {
9
+ /** The Worker's plain environment variables, as `wrangler.jsonc` declared them. */
10
+ vars?: Record<string, WorkerVarValue>;
11
+ }
12
+ /**
13
+ * What a wrangler `vars` entry may hold — wrangler's own `Json`, restated.
14
+ *
15
+ * Restated rather than imported for the reason {@link WorkerVars} is structural, and it has to be this
16
+ * exact shape rather than `unknown`: the customizer's return type is checked against
17
+ * `Partial<WorkerConfig>` at the call site, and `Record<string, unknown>` is not assignable to
18
+ * `Record<string, Json>` — an index signature is invariant. A looser type here compiles in this package
19
+ * and fails in every adopter's, which is the worst place to find out.
20
+ */
21
+ export type WorkerVarValue = string | number | boolean | null | WorkerVarValue[] | {
22
+ [key: string]: WorkerVarValue;
23
+ };
24
+ /**
25
+ * Give the Worker the origin `pithy dev` allocated this checkout, as `BASE_URL`.
26
+ *
27
+ * Spread into `@cloudflare/vite-plugin`'s `config` customizer:
28
+ *
29
+ * ```ts
30
+ * cloudflare({ config: devWorkerConfig() })
31
+ * ```
32
+ *
33
+ * **Why an adopter writes a line at all.** A Worker launched through `wrangler dev` is handed its own
34
+ * origin on the argv, by `pithy dev`, with nothing to configure. A Worker launched through a custom
35
+ * `dev.command` is a Vite dev server, and there is no argv to append a `--var` to: `@cloudflare/vite-plugin`
36
+ * takes the Worker's `vars` from `wrangler.jsonc`, and the only documented way in is this customizer,
37
+ * which lives in the adopter's `vite.config.ts`. `pithy init` scaffolds it; an existing project adds
38
+ * the line once and `pithy doctor` says so until it does.
39
+ *
40
+ * **Why it has to be given rather than written down.** A dev port is *allocated* — every checkout
41
+ * reserves its own block — so a `vars.BASE_URL` in `wrangler.jsonc` is right in the first checkout on a
42
+ * machine and wrong in every other one. That is not cosmetic: `BASE_URL` is the `iss` on every
43
+ * control-plane token a Worker signs and the origin its callback links are built against, so a second
44
+ * checkout signed tokens as the first one and its own seam denied every call, 401, with every stored
45
+ * value agreeing (#462, `pithy-sh/dashboard#95`).
46
+ *
47
+ * **Outside `pithy dev` it does nothing at all.** A plain `vite dev`, a build, or CI has no
48
+ * {@link WORKER_ORIGIN_VAR} in its environment, and the Worker keeps whatever `wrangler.jsonc` says —
49
+ * which for a deployed environment is the value `applyDomains` generated from `domains`, and is
50
+ * correct. This never invents an origin; it only passes on one that was allocated.
51
+ */
52
+ export declare function devWorkerConfig(env?: NodeJS.ProcessEnv): (config: WorkerVars) => WorkerVars;
53
+ //# sourceMappingURL=devOrigin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devOrigin.d.ts","sourceRoot":"","sources":["../src/devOrigin.ts"],"names":[],"mappings":"AAMA;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,mFAAmF;IACnF,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;CACvC;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,cAAc,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAAA;CAAE,CAAC;AAErH;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,CAAC,MAAM,EAAE,UAAU,KAAK,UAAU,CAuBxG"}
@@ -0,0 +1,45 @@
1
+ import { LOCAL_ENVIRONMENT } from "@pithy-sh/core/src/naming/environment";
2
+ import { ENVIRONMENT_VAR, WORKER_ORIGIN_VAR } from "@pithy-sh/core/src/worker/identity";
3
+ //#region src/devOrigin.ts
4
+ /**
5
+ * Give the Worker the origin `pithy dev` allocated this checkout, as `BASE_URL`.
6
+ *
7
+ * Spread into `@cloudflare/vite-plugin`'s `config` customizer:
8
+ *
9
+ * ```ts
10
+ * cloudflare({ config: devWorkerConfig() })
11
+ * ```
12
+ *
13
+ * **Why an adopter writes a line at all.** A Worker launched through `wrangler dev` is handed its own
14
+ * origin on the argv, by `pithy dev`, with nothing to configure. A Worker launched through a custom
15
+ * `dev.command` is a Vite dev server, and there is no argv to append a `--var` to: `@cloudflare/vite-plugin`
16
+ * takes the Worker's `vars` from `wrangler.jsonc`, and the only documented way in is this customizer,
17
+ * which lives in the adopter's `vite.config.ts`. `pithy init` scaffolds it; an existing project adds
18
+ * the line once and `pithy doctor` says so until it does.
19
+ *
20
+ * **Why it has to be given rather than written down.** A dev port is *allocated* — every checkout
21
+ * reserves its own block — so a `vars.BASE_URL` in `wrangler.jsonc` is right in the first checkout on a
22
+ * machine and wrong in every other one. That is not cosmetic: `BASE_URL` is the `iss` on every
23
+ * control-plane token a Worker signs and the origin its callback links are built against, so a second
24
+ * checkout signed tokens as the first one and its own seam denied every call, 401, with every stored
25
+ * value agreeing (#462, `pithy-sh/dashboard#95`).
26
+ *
27
+ * **Outside `pithy dev` it does nothing at all.** A plain `vite dev`, a build, or CI has no
28
+ * {@link WORKER_ORIGIN_VAR} in its environment, and the Worker keeps whatever `wrangler.jsonc` says —
29
+ * which for a deployed environment is the value `applyDomains` generated from `domains`, and is
30
+ * correct. This never invents an origin; it only passes on one that was allocated.
31
+ */
32
+ function devWorkerConfig(env = process.env) {
33
+ return (config) => {
34
+ const origin = env[WORKER_ORIGIN_VAR];
35
+ if (origin === void 0 || origin.trim() === "") return {};
36
+ const environment = config.vars?.[ENVIRONMENT_VAR];
37
+ if (typeof environment === "string" && environment !== LOCAL_ENVIRONMENT) return {};
38
+ return { vars: {
39
+ ...config.vars,
40
+ BASE_URL: origin
41
+ } };
42
+ };
43
+ }
44
+ //#endregion
45
+ export { devWorkerConfig };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * What {@link pithy} hands back: a Vite plugin, described without naming a Vite type.
3
+ *
4
+ * **The return type used to be Vite's own `Plugin`, and that made the peer range a lie — Jim,
5
+ * 2026-08-21 (#414).** `package.json` declares `vite: ^6.1.0 || ^7.0.0 || ^8.0.0`, but a `Plugin` in
6
+ * the signature is a `Plugin` out of *this package's* `node_modules`. An adopter who resolved any
7
+ * other copy — which is every adopter developing against a symlinked kit, and every adopter whose
8
+ * install did not deduplicate — had to prove the kit's copy assignable to theirs, field by field,
9
+ * before `plugins: [pithy()]` would compile. Across majors that comparison fails outright: `hotUpdate`
10
+ * does not carry the same `this` in 6 as in 8, so this plugin was not a Vite 6 plugin however identical
11
+ * the hook body is. Within a major it does not even finish — `Plugin` is deep and recursive enough to
12
+ * exhaust tsc's depth budget, which is the `TS2321: Excessive stack depth` that stopped
13
+ * `pithy-sh/dashboard` typechecking the `vite.config.ts` `pithy init` had just written for it. A peer
14
+ * dependency that only compiles when the install deduplicates is not a peer dependency.
15
+ *
16
+ * So the signature promises the two things every Vite in that range agrees on and an adopter's
17
+ * checker can settle in two comparisons: the plugin has a name, and it runs early. No recursion, no
18
+ * hook signatures, nothing that can differ between two copies of the same library.
19
+ *
20
+ * **What is still checked.** The object below is written `satisfies Plugin` against the Vite this
21
+ * package develops against, so every hook keeps its parameter types, its `this`, and its return type.
22
+ * A hook that reads the wrong field off `config`, returns the wrong shape, or is spelled with a name
23
+ * Vite does not call is still a red build — here, where the kit is compiled, and in
24
+ * `tooling/vite-adopter`, which compiles this file too.
25
+ *
26
+ * **What is not.** The adopter's checker no longer re-derives any of that against their copy. It sees
27
+ * two properties and takes the hooks on trust. Four things pay for that trust and none of them is a
28
+ * type an adopter has to compare: `tooling/vite-adopter` compiles the return against every major in
29
+ * the peer range and runs a real `vite build` through the plugin at each of them, {@link
30
+ * PITHY_PLUGIN_HOOKS} holds every hook name to something all three call, `plugin.test.ts` drives each
31
+ * hook by hand, and it runs a real `vite build` and reads the bundle.
32
+ *
33
+ * **One gap, named rather than papered over — Jim, 2026-08-21.** The hook *signatures* are checked
34
+ * against one Vite, and there is no arrangement in which they could be checked against three. Vite 8
35
+ * is rolldown-based and Vite 6 and 7 are rollup-based, so `hotUpdate`'s `this` is
36
+ * `MinimalPluginContext & { environment: DevEnvironment }` out of two different bundlers — `meta`
37
+ * carries `rolldownVersion` in one and not the other. A single object cannot be written `satisfies
38
+ * Plugin` against both; restoring `pithy(): Plugin` reports exactly that at 6.1.6 and 7.0.0, and it is
39
+ * a fact about the two Vites rather than about this plugin. What is checked across the range is the
40
+ * hook *set* and the hook *behavior*: a name Vite 6 never calls is a red build, and a build that does
41
+ * not inline the projection at 6.1.6 is a red test. What is taken on trust across the range is that a
42
+ * hook Vite 6 calls with an argument of its own shape reaches the same field.
43
+ *
44
+ * **Why the floor is `^6.1.0` is not recorded here, because the reason first written down was wrong —
45
+ * Jim, 2026-08-21.** It said `hotUpdate` arrived in 6.1 and that below it this plugin would be silently
46
+ * inert. It did not: `npm pack vite@6.0.0` carries `hotUpdate?: ObjectHook<…>` in `index.d.ts`, and its
47
+ * `HotUpdatePluginContext` is byte-identical to 6.1.6's. The floor may still be right for a reason
48
+ * nobody has written down, and `6.0.0` is not pinned in `tooling/vite-adopter`, so nothing here has ever
49
+ * compiled against it.
50
+ *
51
+ * Two honest next steps, neither taken in this change: pin `6.0.0` in the fixture and find out, or drop
52
+ * the floor to `^6.0.0` and let the fixture say whether that holds. **An invented reason is worse than
53
+ * an absent one**, because the next person reads it instead of measuring.
54
+ */
55
+ export interface PithyPlugin {
56
+ /** `"pithy"`. The plugin's identity in Vite's plugin list, and in any error raised from a hook. */
57
+ name: string;
58
+ /**
59
+ * `"pre"`, always. `virtual:pithy/*` is claimed before any other plugin can, so the order of an
60
+ * adopter's `plugins` array never decides whether the front end can read its own backend.
61
+ */
62
+ enforce: "pre";
63
+ }
64
+ /**
65
+ * Every hook {@link pithy} defines, by name.
66
+ *
67
+ * **The one thing about the hooks an adopter's checker can be given for free.** {@link PithyPlugin}
68
+ * deliberately names no Vite type, so nothing in the return says which hooks exist — and a hook Vite
69
+ * never calls is dead code that looks like a feature. A name is not a Vite type: `tooling/vite-adopter`
70
+ * asserts each of these is a `keyof Plugin` in 6, 7 and 8, which costs an adopter nothing and catches
71
+ * the regression that matters here. Adding a `buildApp` hook, which Vite 7 introduced and Vite 6 has no
72
+ * name for, reddens the fixture rather than shipping under a range that claims 6.
73
+ *
74
+ * `plugin.test.ts` holds this list to the object itself, both directions, at runtime — a hook added
75
+ * without listing it is a failure there, and a name listed that no longer exists is one too. A list
76
+ * that can drift from what it lists is a worse gate than none.
77
+ */
78
+ export declare const PITHY_PLUGIN_HOOKS: readonly ["configResolved", "configureServer", "resolveId", "load", "hotUpdate"];
79
+ /** One of the names {@link PITHY_PLUGIN_HOOKS} lists. */
80
+ export type PithyPluginHook = (typeof PITHY_PLUGIN_HOOKS)[number];
81
+ /** Options for {@link pithy}. Every one has a working default; `pithy()` is the usual call. */
82
+ export interface PithyPluginOptions {
83
+ /**
84
+ * The Worker's `pithy.config.ts`. Relative paths resolve against the Vite root, which for a Pithy
85
+ * Worker is `apps/<name>/` — so the default, `pithy.config.ts`, is already the right file.
86
+ */
87
+ configFile?: string;
88
+ /**
89
+ * The environment this bundle is built for. Defaults to `ENVIRONMENT` in the process env, then
90
+ * `dev`. `pithy deploy --env <name>` is what threads a real value in, so a production bundle sees
91
+ * production's projection and a dev bundle sees dev's.
92
+ */
93
+ environment?: string;
94
+ }
95
+ /**
96
+ * Serve one `virtual:pithy/<capability>` module per capability, projected from the Worker's own
97
+ * `pithy.config.ts`.
98
+ *
99
+ * A screen imports what it needs — `import auth from "virtual:pithy/auth"` — and gets that
100
+ * capability's client-safe projection inlined at build time. A capability the Worker does not compose
101
+ * resolves to `{ enabled: false }` rather than failing the import, including a name nobody has ever
102
+ * heard of: a screen branches on `enabled`, it does not guard on whether the module exists.
103
+ */
104
+ export declare function pithy(options?: PithyPluginOptions): PithyPlugin;
105
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAcA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,MAAM,WAAW,WAAW;IAC1B,mGAAmG;IACnG,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,OAAO,EAAE,KAAK,CAAC;CAChB;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB,YAAI,gBAAgB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,CAAU,CAAC;AAEnH,yDAAyD;AACzD,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE,+FAA+F;AAC/F,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,kBAAuB,GAAG,WAAW,CA4FnE"}