@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.
@@ -246,18 +246,33 @@ function reindent(text: string, indent: string): string {
246
246
  /**
247
247
  * Move a block from the column it was written at to the one it is emitted at.
248
248
  *
249
- * The alias text arrives at whatever depth its source file put it — a union's arms at two spaces, its
250
- * members at six. Inside a `declare module` block those want four and eight. So the block is dedented
251
- * by its own first line's indent and then pushed out uniformly, which keeps every relative depth,
252
- * including the ` *` continuation of a doc comment.
249
+ * The alias text arrives at whatever depth its file put it — `tsc` prints a union's arms at zero and
250
+ * their members at four — and inside a `declare module` block those want four and eight. So the block
251
+ * is dedented to column zero and then pushed out uniformly, which keeps every relative depth, including
252
+ * the ` *` continuation of a doc comment.
253
+ *
254
+ * **The first line is trimmed rather than measured, and the rest are dedented by their common indent.**
255
+ * A type alias's body begins on the `=` line, so its first line arrives as `{` behind a single space
256
+ * that says nothing about the block's depth. Taking that space as the base — which is what this did
257
+ * until #476 changed the input from source to emitted declaration — shifted every following line one
258
+ * column left of where it belonged, and the misalignment was invisible in the old input only because a
259
+ * hand-written union happened to start on a line of its own.
253
260
  */
254
261
  function shift(text: string, indent: string): string {
255
262
  const lines = text.split("\n");
256
263
  while (lines.length > 0 && (lines[0] ?? "").trim() === "") lines.shift();
257
264
  while (lines.length > 0 && (lines.at(-1) ?? "").trim() === "") lines.pop();
258
- const base = /^[ ]*/.exec(lines[0] ?? "")?.[0] ?? "";
265
+
266
+ const rest = lines.slice(1).filter((line) => line.trim() !== "");
267
+ const base = rest.reduce((width, line) => Math.min(width, (/^[ ]*/.exec(line)?.[0] ?? "").length), Infinity);
268
+ const dedent = Number.isFinite(base) ? (base as number) : 0;
269
+
259
270
  return lines
260
- .map((line) => (line.startsWith(base) ? `${indent}${line.slice(base.length)}` : `${indent}${line.trimStart()}`))
271
+ .map((line, position) => {
272
+ if (line.trim() === "") return "";
273
+ const body = position === 0 ? line.trim() : line.slice(dedent);
274
+ return `${indent}${body}`;
275
+ })
261
276
  .join("\n");
262
277
  }
263
278
 
@@ -298,12 +313,34 @@ export function renderClientEnv(
298
313
  return `${PREAMBLE}\n${blocks.join("\n")}`;
299
314
  }
300
315
 
301
- /** Read the four projection sources through their own packages' exports maps. */
316
+ /**
317
+ * Read the four projections' sources through their own packages' exports maps.
318
+ *
319
+ * **Resolved, then mirrored back to `src` — Jim, #476.** Until every package shipped a build,
320
+ * `@pithy-sh/auth/src/client/projection` resolved to the TypeScript itself. It resolves to
321
+ * `dist/client/projection.js` now, where the types are stripped and `AuthClientProjection` does not
322
+ * exist, so the resolved path is walked back to the module it was emitted from.
323
+ *
324
+ * The declaration beside it, `dist/client/projection.d.ts`, is the other candidate and was tried first.
325
+ * It is arguably the more truthful input — it is the type an adopter can actually see — but it makes
326
+ * this generator's build wait on five capability packages' builds, and `@pithy-sh/vite` depends on none
327
+ * of them. That is five cross-package `dependsOn` edges in `turbo.jsonc` that a sixth capability's
328
+ * projection would silently need and silently not have: the generator would read a stale declaration,
329
+ * emit a stale template, and say nothing. Source has no such ordering, `turbo.jsonc` already names
330
+ * every `packages/<name>/src/client/projection.ts` as this build's input, and the authored text is
331
+ * what the file's own header promises an adopter is emitted verbatim.
332
+ *
333
+ * `dist/x.js` → `src/x.ts` is not a guess about the layout: `tsdown` and `tsc -p tsconfig.build.json`
334
+ * both mirror `src/` into `dist/`, so the two are the same path under a different root, and
335
+ * `packaging.test.ts` fails on any published module missing either half.
336
+ */
302
337
  export async function readProjectionSources(): Promise<Map<string, string>> {
303
338
  const sources = new Map<string, string>();
304
339
  for (const declared of DECLARED_MODULES) {
305
- const path = fileURLToPath(import.meta.resolve(declared.specifier));
306
- sources.set(declared.module, await readFile(path, "utf8"));
340
+ const built = fileURLToPath(import.meta.resolve(declared.specifier));
341
+ const source = built.replace(/([\\/])dist\1(.+)\.js$/, "$1src$1$2.ts");
342
+ if (source === built) throw new Error(`${declared.specifier} did not resolve into a build: ${built}`);
343
+ sources.set(declared.module, await readFile(source, "utf8"));
307
344
  }
308
345
  return sources;
309
346
  }
package/src/testPlugin.ts CHANGED
@@ -1,42 +1,34 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
- import { fileURLToPath } from "node:url";
5
- import { runnerImport } from "vite";
6
- import type { PithyPlugin, PithyPluginOptions } from "./plugin";
7
-
8
- /**
9
- * `./plugin.ts`, as an absolute path.
10
- *
11
- * From `import.meta.url` rather than from a bare specifier: this file's own realpath is the one place
12
- * the build plugin is certainly beside, whatever an adopter's `node_modules` looks like — a symlinked
13
- * checkout, a hoisted install, a worktree.
14
- */
15
- const BUILD_PLUGIN = fileURLToPath(new URL("./plugin.ts", import.meta.url));
4
+ import { type PithyPlugin, type PithyPluginOptions, pithy } from "./plugin";
16
5
 
17
6
  /**
18
7
  * The Pithy plugin, for a test runner's config.
19
8
  *
20
- * **It is not a second plugin.** It resolves to the object {@link pithy} returns, built from
21
- * `./plugin.ts` itself, so a test imports the module a build inlines — same config, same
22
- * `resolveClientProjection`, same `renderVirtualModule`, same bytes. There is no fixture here, and
23
- * therefore nothing that can drift from the projection. `testPlugin.test.ts` holds that: it renders a
24
- * module through the build plugin's own `load` hook and makes a real child `vitest` run compare what it
25
- * imports against it.
9
+ * **It is not a second plugin.** It calls {@link pithy} and hands back what that returns — same config,
10
+ * same `resolveClientProjection`, same `renderVirtualModule`, same bytes. There is no fixture here, and
11
+ * therefore nothing that can drift from the projection.
12
+ *
13
+ * ## It is now a one-line redirect, and it was not always — Jim, #476
26
14
  *
27
- * ## Why the indirection exists at all
15
+ * Vitest loads its config by bundling it and leaving every **bare** specifier to node. Until this
16
+ * package shipped a build, `import { pithy } from "@pithy-sh/vite/src/plugin"` in a `vitest.config.ts`
17
+ * therefore handed node `plugin.ts` — TypeScript, under `node_modules`, where node refuses to strip
18
+ * types — and its `@pithy-sh/core/src/capability/client` import had no extension for node to resolve
19
+ * either. The config never loaded and no test ran, transitively taking every screen that read a
20
+ * projection with it. Three lanes in `pithy-sh/dashboard` hit it independently, and this module was the
21
+ * way round: its whole static graph was `vite` and `node:url`, and the plugin was reached through
22
+ * vite's own loader rather than node's.
28
23
  *
29
- * Vitest loads its config by bundling it and leaving every **bare** specifier to node. So
30
- * `import { pithy } from "@pithy-sh/vite/src/plugin"` in a `vitest.config.ts` is handed to node, which
31
- * loads `plugin.ts` — and stops at its `@pithy-sh/core/src/capability/client` import, whose own
32
- * `../error/pithyError` has no extension for node to resolve. The config never loads and no test runs.
33
- * `vitest.shared.ts` in this repository documents the same wall from the other side, for the same reason.
24
+ * `dist/plugin.js` is JavaScript with real extensions on its imports, so node loads it, and
25
+ * `vitestConfig.test.ts` makes a real child `vitest` run say so — it drives the **build** plugin from a
26
+ * bare specifier in an adopter-shaped project. The wall is gone and the loader hop with it.
34
27
  *
35
- * This module's whole static graph is `vite` and `node:url` — both things node loads — and the kit
36
- * source is reached through {@link runnerImport}, which is vite's own loader and resolves extensionless
37
- * TypeScript the way every other Pithy import site does. That constraint is load-bearing: **any
38
- * `@pithy-sh/*` import added to this file breaks every adopter's test run at config load.** The child
39
- * run in `testPlugin.test.ts` is what says so out loud.
28
+ * **What stays is the name.** `pithyTest` is exported from 0.1.0 and `pithy-sh/dashboard` names it in
29
+ * its own configs, so removing it would break a working install to save a line. It is kept as the
30
+ * documented spelling for a test config, and can be dropped whenever nothing imports it — there is no
31
+ * behavior behind it left to maintain.
40
32
  *
41
33
  * ## What an adopter writes
42
34
  *
@@ -55,14 +47,13 @@ const BUILD_PLUGIN = fileURLToPath(new URL("./plugin.ts", import.meta.url));
55
47
  * rather than the Worker's directory — so a monorepo names the Worker, and a single-Worker project takes
56
48
  * the default.
57
49
  *
58
- * A promise, because vite awaits a plugin. That is a documented `PluginOption`, and it is what lets the
59
- * real plugin be built by vite's loader instead of restated for node's.
60
- *
61
- * `Promise<PithyPlugin>` rather than `Promise<Plugin>` for the reason {@link PithyPlugin} gives (#414):
62
- * a Vite type in this signature is *this package's* Vite type, and an adopter's `vitest.config.ts`
63
- * cannot be typechecked against it unless their install happened to deduplicate onto the same copy.
64
- * The test config is the same surface as the build config and takes the same rule.
50
+ * Still a promise, and still `Promise<PithyPlugin>` rather than `Promise<Plugin>`. The promise is part
51
+ * of the published signature and a documented `PluginOption`, so awaiting nothing is cheaper than
52
+ * changing what an adopter's config is typed against; the return type is for the reason
53
+ * {@link PithyPlugin} gives (#414) — a Vite type in this signature is *this package's* Vite type, and an
54
+ * adopter's `vitest.config.ts` cannot be checked against it unless their install happened to
55
+ * deduplicate onto the same copy.
65
56
  */
66
57
  export function pithyTest(options: PithyPluginOptions = {}): Promise<PithyPlugin> {
67
- return runnerImport<typeof import("./plugin")>(BUILD_PLUGIN).then(({ module }) => module.pithy(options));
58
+ return Promise.resolve(pithy(options));
68
59
  }