@pithy-sh/vite 0.1.2 → 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.
package/dist/plugin.js ADDED
@@ -0,0 +1,96 @@
1
+ import { capabilityNameFromResolvedId, isResolvedVirtualId, renderVirtualModule, resolveVirtualId } from "./virtualModule.js";
2
+ import { loadWorkerConfig } from "./workerConfig.js";
3
+ import { resolve } from "node:path";
4
+ import { resolveClientProjection } from "@pithy-sh/core/src/capability/client";
5
+ import { normalizePath } from "vite";
6
+ //#region src/plugin.ts
7
+ /**
8
+ * Every hook {@link pithy} defines, by name.
9
+ *
10
+ * **The one thing about the hooks an adopter's checker can be given for free.** {@link PithyPlugin}
11
+ * deliberately names no Vite type, so nothing in the return says which hooks exist — and a hook Vite
12
+ * never calls is dead code that looks like a feature. A name is not a Vite type: `tooling/vite-adopter`
13
+ * asserts each of these is a `keyof Plugin` in 6, 7 and 8, which costs an adopter nothing and catches
14
+ * the regression that matters here. Adding a `buildApp` hook, which Vite 7 introduced and Vite 6 has no
15
+ * name for, reddens the fixture rather than shipping under a range that claims 6.
16
+ *
17
+ * `plugin.test.ts` holds this list to the object itself, both directions, at runtime — a hook added
18
+ * without listing it is a failure there, and a name listed that no longer exists is one too. A list
19
+ * that can drift from what it lists is a worse gate than none.
20
+ */
21
+ const PITHY_PLUGIN_HOOKS = [
22
+ "configResolved",
23
+ "configureServer",
24
+ "resolveId",
25
+ "load",
26
+ "hotUpdate"
27
+ ];
28
+ /**
29
+ * Serve one `virtual:pithy/<capability>` module per capability, projected from the Worker's own
30
+ * `pithy.config.ts`.
31
+ *
32
+ * A screen imports what it needs — `import auth from "virtual:pithy/auth"` — and gets that
33
+ * capability's client-safe projection inlined at build time. A capability the Worker does not compose
34
+ * resolves to `{ enabled: false }` rather than failing the import, including a name nobody has ever
35
+ * heard of: a screen branches on `enabled`, it does not guard on whether the module exists.
36
+ */
37
+ function pithy(options = {}) {
38
+ const environment = options.environment ?? process.env.ENVIRONMENT ?? "dev";
39
+ let configFile = "";
40
+ let loading = null;
41
+ let server;
42
+ const watched = /* @__PURE__ */ new Set();
43
+ function watch(files) {
44
+ for (const file of files) {
45
+ const path = normalizePath(file);
46
+ if (watched.has(path)) continue;
47
+ watched.add(path);
48
+ server?.watcher.add(path);
49
+ }
50
+ }
51
+ function loadOnce() {
52
+ loading ??= loadWorkerConfig(configFile).then((loaded) => {
53
+ watch(loaded.dependencies);
54
+ return loaded;
55
+ }).catch((cause) => {
56
+ loading = null;
57
+ throw cause;
58
+ });
59
+ return loading;
60
+ }
61
+ return {
62
+ name: "pithy",
63
+ enforce: "pre",
64
+ configResolved(config) {
65
+ configFile = normalizePath(resolve(config.root, options.configFile ?? "pithy.config.ts"));
66
+ watch([configFile]);
67
+ },
68
+ configureServer(devServer) {
69
+ server = devServer;
70
+ for (const path of watched) devServer.watcher.add(path);
71
+ },
72
+ resolveId(id) {
73
+ return resolveVirtualId(id);
74
+ },
75
+ async load(id) {
76
+ const name = capabilityNameFromResolvedId(id);
77
+ if (name === null) return null;
78
+ const { capabilities } = await loadOnce();
79
+ return renderVirtualModule(resolveClientProjection(capabilities.get(name), { environment }));
80
+ },
81
+ hotUpdate({ file }) {
82
+ if (!watched.has(normalizePath(file))) return;
83
+ loading = null;
84
+ const invalidated = [];
85
+ for (const [id, module] of this.environment.moduleGraph.idToModuleMap) {
86
+ if (!isResolvedVirtualId(id)) continue;
87
+ this.environment.moduleGraph.invalidateModule(module);
88
+ invalidated.push(module);
89
+ }
90
+ this.environment.hot.send({ type: "full-reload" });
91
+ return invalidated;
92
+ }
93
+ };
94
+ }
95
+ //#endregion
96
+ export { PITHY_PLUGIN_HOOKS, pithy };
@@ -0,0 +1,54 @@
1
+ import { type PithyPlugin, type PithyPluginOptions } from "./plugin";
2
+ /**
3
+ * The Pithy plugin, for a test runner's config.
4
+ *
5
+ * **It is not a second plugin.** It calls {@link pithy} and hands back what that returns — same config,
6
+ * same `resolveClientProjection`, same `renderVirtualModule`, same bytes. There is no fixture here, and
7
+ * therefore nothing that can drift from the projection.
8
+ *
9
+ * ## It is now a one-line redirect, and it was not always — Jim, #476
10
+ *
11
+ * Vitest loads its config by bundling it and leaving every **bare** specifier to node. Until this
12
+ * package shipped a build, `import { pithy } from "@pithy-sh/vite/src/plugin"` in a `vitest.config.ts`
13
+ * therefore handed node `plugin.ts` — TypeScript, under `node_modules`, where node refuses to strip
14
+ * types — and its `@pithy-sh/core/src/capability/client` import had no extension for node to resolve
15
+ * either. The config never loaded and no test ran, transitively taking every screen that read a
16
+ * projection with it. Three lanes in `pithy-sh/dashboard` hit it independently, and this module was the
17
+ * way round: its whole static graph was `vite` and `node:url`, and the plugin was reached through
18
+ * vite's own loader rather than node's.
19
+ *
20
+ * `dist/plugin.js` is JavaScript with real extensions on its imports, so node loads it, and
21
+ * `vitestConfig.test.ts` makes a real child `vitest` run say so — it drives the **build** plugin from a
22
+ * bare specifier in an adopter-shaped project. The wall is gone and the loader hop with it.
23
+ *
24
+ * **What stays is the name.** `pithyTest` is exported from 0.1.0 and `pithy-sh/dashboard` names it in
25
+ * its own configs, so removing it would break a working install to save a line. It is kept as the
26
+ * documented spelling for a test config, and can be dropped whenever nothing imports it — there is no
27
+ * behavior behind it left to maintain.
28
+ *
29
+ * ## What an adopter writes
30
+ *
31
+ * One line, in the test project that mounts client modules — never per consumer:
32
+ *
33
+ * ```ts
34
+ * import { pithyTest } from "@pithy-sh/vite/src/testPlugin";
35
+ *
36
+ * export default defineConfig({
37
+ * plugins: [pithyTest({ configFile: "apps/board/pithy.config.ts" })],
38
+ * test: { environment: "happy-dom" },
39
+ * });
40
+ * ```
41
+ *
42
+ * `configFile` resolves against the Vite root, which for a test run is usually the repository root
43
+ * rather than the Worker's directory — so a monorepo names the Worker, and a single-Worker project takes
44
+ * the default.
45
+ *
46
+ * Still a promise, and still `Promise<PithyPlugin>` rather than `Promise<Plugin>`. The promise is part
47
+ * of the published signature and a documented `PluginOption`, so awaiting nothing is cheaper than
48
+ * changing what an adopter's config is typed against; the return type is for the reason
49
+ * {@link PithyPlugin} gives (#414) — a Vite type in this signature is *this package's* Vite type, and an
50
+ * adopter's `vitest.config.ts` cannot be checked against it unless their install happened to
51
+ * deduplicate onto the same copy.
52
+ */
53
+ export declare function pithyTest(options?: PithyPluginOptions): Promise<PithyPlugin>;
54
+ //# sourceMappingURL=testPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testPlugin.d.ts","sourceRoot":"","sources":["../src/testPlugin.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,kBAAkB,EAAS,MAAM,UAAU,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,wBAAgB,SAAS,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,WAAW,CAAC,CAEhF"}
@@ -0,0 +1,58 @@
1
+ import { pithy } from "./plugin.js";
2
+ //#region src/testPlugin.ts
3
+ /**
4
+ * The Pithy plugin, for a test runner's config.
5
+ *
6
+ * **It is not a second plugin.** It calls {@link pithy} and hands back what that returns — same config,
7
+ * same `resolveClientProjection`, same `renderVirtualModule`, same bytes. There is no fixture here, and
8
+ * therefore nothing that can drift from the projection.
9
+ *
10
+ * ## It is now a one-line redirect, and it was not always — Jim, #476
11
+ *
12
+ * Vitest loads its config by bundling it and leaving every **bare** specifier to node. Until this
13
+ * package shipped a build, `import { pithy } from "@pithy-sh/vite/src/plugin"` in a `vitest.config.ts`
14
+ * therefore handed node `plugin.ts` — TypeScript, under `node_modules`, where node refuses to strip
15
+ * types — and its `@pithy-sh/core/src/capability/client` import had no extension for node to resolve
16
+ * either. The config never loaded and no test ran, transitively taking every screen that read a
17
+ * projection with it. Three lanes in `pithy-sh/dashboard` hit it independently, and this module was the
18
+ * way round: its whole static graph was `vite` and `node:url`, and the plugin was reached through
19
+ * vite's own loader rather than node's.
20
+ *
21
+ * `dist/plugin.js` is JavaScript with real extensions on its imports, so node loads it, and
22
+ * `vitestConfig.test.ts` makes a real child `vitest` run say so — it drives the **build** plugin from a
23
+ * bare specifier in an adopter-shaped project. The wall is gone and the loader hop with it.
24
+ *
25
+ * **What stays is the name.** `pithyTest` is exported from 0.1.0 and `pithy-sh/dashboard` names it in
26
+ * its own configs, so removing it would break a working install to save a line. It is kept as the
27
+ * documented spelling for a test config, and can be dropped whenever nothing imports it — there is no
28
+ * behavior behind it left to maintain.
29
+ *
30
+ * ## What an adopter writes
31
+ *
32
+ * One line, in the test project that mounts client modules — never per consumer:
33
+ *
34
+ * ```ts
35
+ * import { pithyTest } from "@pithy-sh/vite/src/testPlugin";
36
+ *
37
+ * export default defineConfig({
38
+ * plugins: [pithyTest({ configFile: "apps/board/pithy.config.ts" })],
39
+ * test: { environment: "happy-dom" },
40
+ * });
41
+ * ```
42
+ *
43
+ * `configFile` resolves against the Vite root, which for a test run is usually the repository root
44
+ * rather than the Worker's directory — so a monorepo names the Worker, and a single-Worker project takes
45
+ * the default.
46
+ *
47
+ * Still a promise, and still `Promise<PithyPlugin>` rather than `Promise<Plugin>`. The promise is part
48
+ * of the published signature and a documented `PluginOption`, so awaiting nothing is cheaper than
49
+ * changing what an adopter's config is typed against; the return type is for the reason
50
+ * {@link PithyPlugin} gives (#414) — a Vite type in this signature is *this package's* Vite type, and an
51
+ * adopter's `vitest.config.ts` cannot be checked against it unless their install happened to
52
+ * deduplicate onto the same copy.
53
+ */
54
+ function pithyTest(options = {}) {
55
+ return Promise.resolve(pithy(options));
56
+ }
57
+ //#endregion
58
+ export { pithyTest };
@@ -0,0 +1,28 @@
1
+ import type { ClientProjection } from "@pithy-sh/core/src/capability/client";
2
+ /**
3
+ * The import specifier a screen writes: `import auth from "virtual:pithy/auth"`. One module per
4
+ * capability, resolved from the Worker's own `pithy.config.ts` — so a front end reads the backend it
5
+ * is served by, and never a hand-copied duplicate of its config.
6
+ */
7
+ export declare const VIRTUAL_PREFIX = "virtual:pithy/";
8
+ /**
9
+ * The resolved id. The leading NUL is Rollup's convention for "this module has no file" — it stops
10
+ * other plugins and the filesystem resolver from touching it.
11
+ */
12
+ export declare const RESOLVED_PREFIX = "\0virtual:pithy/";
13
+ /** Map an import specifier to its resolved id, or `null` when the specifier is not a Pithy virtual module. */
14
+ export declare function resolveVirtualId(id: string): string | null;
15
+ /** The capability name carried by a resolved id, or `null` when the id is not one of ours. */
16
+ export declare function capabilityNameFromResolvedId(id: string): string | null;
17
+ /** Whether a module id is a resolved Pithy virtual module — used to sweep the dev module graph. */
18
+ export declare function isResolvedVirtualId(id: string): boolean;
19
+ /**
20
+ * Render a projection as module source. Both shapes are emitted on purpose: the default export for
21
+ * `import auth from "virtual:pithy/auth"`, and one named export per key so a bundler can tree-shake
22
+ * `import { enabled } from "virtual:pithy/auth"` down to a single inlined literal.
23
+ *
24
+ * Every value is emitted through `JSON.stringify`, which is the second half of the security boundary
25
+ * — the projection was already validated as JSON, and nothing but JSON can be written here.
26
+ */
27
+ export declare function renderVirtualModule(projection: ClientProjection): string;
28
+ //# sourceMappingURL=virtualModule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"virtualModule.d.ts","sourceRoot":"","sources":["../src/virtualModule.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAE7E;;;;GAIG;AACH,eAAO,MAAM,cAAc,mBAAmB,CAAC;AAE/C;;;GAGG;AACH,eAAO,MAAM,eAAe,qBAAqB,CAAC;AAiElD,8GAA8G;AAC9G,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK1D;AAED,8FAA8F;AAC9F,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAItE;AAED,mGAAmG;AACnG,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,gBAAgB,GAAG,MAAM,CAQxE"}
@@ -0,0 +1,108 @@
1
+ //#region src/virtualModule.ts
2
+ /**
3
+ * The import specifier a screen writes: `import auth from "virtual:pithy/auth"`. One module per
4
+ * capability, resolved from the Worker's own `pithy.config.ts` — so a front end reads the backend it
5
+ * is served by, and never a hand-copied duplicate of its config.
6
+ */
7
+ const VIRTUAL_PREFIX = "virtual:pithy/";
8
+ /**
9
+ * The resolved id. The leading NUL is Rollup's convention for "this module has no file" — it stops
10
+ * other plugins and the filesystem resolver from touching it.
11
+ */
12
+ const RESOLVED_PREFIX = "\0virtual:pithy/";
13
+ /**
14
+ * A capability name as it may appear after the prefix: one segment, the same lowercase-hyphenated
15
+ * shape as `pithy add <capability>`. Anything else (a path, an empty name, a traversal) is not ours,
16
+ * so `resolveVirtualId` declines it and Vite reports its usual unresolved-import error.
17
+ */
18
+ const CAPABILITY_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
19
+ /** A key that can legally be re-exported as `export const <key>`. */
20
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
21
+ /**
22
+ * Keys that are valid object keys but not valid binding names. A projection carrying one still
23
+ * reaches the default export; only its named export is skipped, so nothing is silently lost.
24
+ */
25
+ const RESERVED = /* @__PURE__ */ new Set([
26
+ "await",
27
+ "break",
28
+ "case",
29
+ "catch",
30
+ "class",
31
+ "const",
32
+ "continue",
33
+ "debugger",
34
+ "default",
35
+ "delete",
36
+ "do",
37
+ "else",
38
+ "enum",
39
+ "export",
40
+ "extends",
41
+ "false",
42
+ "finally",
43
+ "for",
44
+ "function",
45
+ "if",
46
+ "implements",
47
+ "import",
48
+ "in",
49
+ "instanceof",
50
+ "interface",
51
+ "let",
52
+ "new",
53
+ "null",
54
+ "package",
55
+ "private",
56
+ "protected",
57
+ "public",
58
+ "return",
59
+ "static",
60
+ "super",
61
+ "switch",
62
+ "this",
63
+ "throw",
64
+ "true",
65
+ "try",
66
+ "typeof",
67
+ "var",
68
+ "void",
69
+ "while",
70
+ "with",
71
+ "yield"
72
+ ]);
73
+ /** Map an import specifier to its resolved id, or `null` when the specifier is not a Pithy virtual module. */
74
+ function resolveVirtualId(id) {
75
+ if (!id.startsWith("virtual:pithy/")) return null;
76
+ const name = id.slice(14);
77
+ if (!CAPABILITY_NAME.test(name)) return null;
78
+ return `${RESOLVED_PREFIX}${name}`;
79
+ }
80
+ /** The capability name carried by a resolved id, or `null` when the id is not one of ours. */
81
+ function capabilityNameFromResolvedId(id) {
82
+ if (!id.startsWith("\0virtual:pithy/")) return null;
83
+ const name = id.slice(15);
84
+ return CAPABILITY_NAME.test(name) ? name : null;
85
+ }
86
+ /** Whether a module id is a resolved Pithy virtual module — used to sweep the dev module graph. */
87
+ function isResolvedVirtualId(id) {
88
+ return capabilityNameFromResolvedId(id) !== null;
89
+ }
90
+ /**
91
+ * Render a projection as module source. Both shapes are emitted on purpose: the default export for
92
+ * `import auth from "virtual:pithy/auth"`, and one named export per key so a bundler can tree-shake
93
+ * `import { enabled } from "virtual:pithy/auth"` down to a single inlined literal.
94
+ *
95
+ * Every value is emitted through `JSON.stringify`, which is the second half of the security boundary
96
+ * — the projection was already validated as JSON, and nothing but JSON can be written here.
97
+ */
98
+ function renderVirtualModule(projection) {
99
+ const lines = [`export default ${JSON.stringify(projection)};`];
100
+ for (const [key, value] of Object.entries(projection)) {
101
+ if (value === void 0) continue;
102
+ if (!IDENTIFIER.test(key) || RESERVED.has(key)) continue;
103
+ lines.push(`export const ${key} = ${JSON.stringify(value)};`);
104
+ }
105
+ return `${lines.join("\n")}\n`;
106
+ }
107
+ //#endregion
108
+ export { RESOLVED_PREFIX, VIRTUAL_PREFIX, capabilityNameFromResolvedId, isResolvedVirtualId, renderVirtualModule, resolveVirtualId };
@@ -0,0 +1,58 @@
1
+ import type { Capability } from "@pithy-sh/core/src/capability/capability";
2
+ /** One Worker's composed capabilities, indexed by name, plus the files the config was built from. */
3
+ export interface LoadedWorkerConfig {
4
+ /** Capability name → the capability, library capabilities first and the app capability last. */
5
+ capabilities: Map<string, Capability>;
6
+ /** Every file the config transitively imported — what the dev server watches so an edit reloads. */
7
+ dependencies: readonly string[];
8
+ }
9
+ /**
10
+ * Why a `pithy.config.ts` would not load — and therefore what may be said about it (#217).
11
+ *
12
+ * This loader built the same refusal the CLI's did, with the same defect. #207 fixed the CLI's
13
+ * (`packages/cli/src/project/config.ts`, `classifyConfigLoadFailure`); this file could not import that
14
+ * fix, because `@pithy-sh/vite` depends on `@pithy-sh/core` and must not depend on the CLI. So it is
15
+ * restated here, with the same rule behind it: **a `catch` reachable by more than one underlying failure
16
+ * may not name a single specific remedy.** Classify, or hedge. The convention is in `docs/CONVENTIONS.md`.
17
+ *
18
+ * What the copies *cannot* be allowed to disagree about is anything that is not a sentence. Two kinds of
19
+ * that were found, one issue apart, and both live in `@pithy-sh/core` — the one package this plugin
20
+ * already depends on. What a runtime does to an error on its way out is the first: `rootCause`, `prop`
21
+ * and `isBuildFailureWrapper`, facts no copy can derive and every copy got wrong (#223). Whether one of
22
+ * its strings is safe to show is the second: `safeReason`, `failurePosition`, `unresolvedSpecifier` and
23
+ * `causeMessage`, because a path or a stack frame is a property of the string and not of the surface
24
+ * quoting it — three near-verbatim copies of that filter meant the hole #223 found in it had to be closed
25
+ * three times (#228).
26
+ *
27
+ * What stays restated here is the policy: which causes this loader recognizes, and what it says about
28
+ * each. Those are genuinely per-surface, and this plugin still cannot import the CLI's.
29
+ */
30
+ export type WorkerConfigFailureKind =
31
+ /** An import in the config does not resolve. `bun install`, or a corrected specifier. */
32
+ "unresolved-import"
33
+ /** The file does not parse. Installing will not help, and the adopter has read the opposite for years. */
34
+ | "parse-error"
35
+ /** The config loaded far enough to throw its own error. Neither of the above. */
36
+ | "threw-on-load"
37
+ /** None of the above. No remedy: a wrong action is worse than no action, because it is followed. */
38
+ | "unknown";
39
+ /** What may be said about a config that would not load. */
40
+ export interface WorkerConfigFailure {
41
+ /** Which failure this was, decided from the cause and never asserted over it. */
42
+ kind: WorkerConfigFailureKind;
43
+ /** The remedy, chosen from the kind. Carries no absolute path, no source line, no stack. */
44
+ action: string;
45
+ }
46
+ /** Choose the refusal's `action` **from** the failure rather than asserting one over it (#217). */
47
+ export declare function classifyWorkerConfigFailure(wrapped: unknown): WorkerConfigFailure;
48
+ /**
49
+ * Load a Worker's `pithy.config.ts` and index the capabilities it composes.
50
+ *
51
+ * `runnerImport` is Vite's own loader: it builds a throwaway runnable environment with
52
+ * `configFile: false`, so the config's TypeScript, its extensionless relative imports, and its
53
+ * `@pithy-sh/*` source imports all resolve — none of which a bare dynamic `import()` handles — and it
54
+ * behaves identically under `vite dev` and `vite build`. It also reports the transitive files it
55
+ * touched, which is what makes HMR on a config edit possible.
56
+ */
57
+ export declare function loadWorkerConfig(configFile: string): Promise<LoadedWorkerConfig>;
58
+ //# sourceMappingURL=workerConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workerConfig.d.ts","sourceRoot":"","sources":["../src/workerConfig.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAC;AAkB3E,qGAAqG;AACrG,MAAM,WAAW,kBAAkB;IACjC,gGAAgG;IAChG,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACtC,oGAAoG;IACpG,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,uBAAuB;AACjC,yFAAyF;AACvF,mBAAmB;AACrB,0GAA0G;GACxG,aAAa;AACf,iFAAiF;GAC/E,eAAe;AACjB,oGAAoG;GAClG,SAAS,CAAC;AAEd,2DAA2D;AAC3D,MAAM,WAAW,mBAAmB;IAClC,iFAAiF;IACjF,IAAI,EAAE,uBAAuB,CAAC;IAC9B,4FAA4F;IAC5F,MAAM,EAAE,MAAM,CAAC;CAChB;AA4CD,mGAAmG;AACnG,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,mBAAmB,CAqCjF;AAMD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAkDtF"}
@@ -0,0 +1,117 @@
1
+ import { access } from "node:fs/promises";
2
+ import { runnerImport } from "vite";
3
+ import { causeMessage, failurePosition, isBuildFailureWrapper, prop, rootCause, safeReason, unresolvedSpecifier } from "@pithy-sh/core/src/error/cause";
4
+ import { InternalError, NotFoundError } from "@pithy-sh/core/src/error/pithyError";
5
+ //#region src/workerConfig.ts
6
+ /**
7
+ * The message, de-colored, for this classifier's own pattern tests — never for output.
8
+ *
9
+ * What may be *said* is core's {@link safeReason}, and only core's. This plugin cannot import the CLI's
10
+ * refusals and never could, which is why the sentences below are restated here — but the *filter* those
11
+ * sentences pass a string through is not a sentence. Whether a string carries an absolute path or half a
12
+ * parser's ANSI box is a property of the string, so it is decided in `@pithy-sh/core`, the one package
13
+ * this plugin depends on, and the suppression #223 had to write three times is written once (#228).
14
+ */
15
+ function rawMessage(cause) {
16
+ return causeMessage(cause) ?? "";
17
+ }
18
+ function isUnresolvedImport(cause) {
19
+ const code = prop(cause, "code");
20
+ if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND" || code === "ERR_PACKAGE_PATH_NOT_EXPORTED" || code === "ERR_UNSUPPORTED_DIR_IMPORT") return true;
21
+ if (prop(cause, "name") === "ResolveMessage") return true;
22
+ return /Cannot find (?:package|module) |Failed to resolve (?:import|module)|Failed to load url /.test(rawMessage(cause));
23
+ }
24
+ function isParseError(cause) {
25
+ const name = prop(cause, "name");
26
+ if (name === "SyntaxError" || name === "BuildMessage") return true;
27
+ if (prop(cause, "code") === "PARSE_ERROR") return true;
28
+ if (isBuildFailureWrapper(cause)) return true;
29
+ return /Transform failed|\[PARSE_ERROR]|Parse (?:error|failure)|Unexpected (?:token|end of input)/.test(rawMessage(cause));
30
+ }
31
+ /** Choose the refusal's `action` **from** the failure rather than asserting one over it (#217). */
32
+ function classifyWorkerConfigFailure(wrapped) {
33
+ const cause = rootCause(wrapped);
34
+ if (isUnresolvedImport(cause)) {
35
+ const specifier = unresolvedSpecifier(cause);
36
+ return {
37
+ kind: "unresolved-import",
38
+ action: specifier ? `Nothing resolves "${specifier}". Install the project's dependencies (bun install), or correct that import.` : "An import in the config does not resolve. Install the project's dependencies (bun install), then check its imports."
39
+ };
40
+ }
41
+ if (isParseError(cause)) {
42
+ const reason = safeReason(cause);
43
+ const at = failurePosition(cause);
44
+ const where = at ? ` Line ${at.line}, column ${at.column}.` : "";
45
+ return {
46
+ kind: "parse-error",
47
+ action: `The config does not parse${reason ? `: ${reason}` : ""}.${where} Fix the file — installing dependencies will not help.`
48
+ };
49
+ }
50
+ if (causeMessage(cause) !== void 0) {
51
+ const reason = safeReason(cause);
52
+ return {
53
+ kind: "threw-on-load",
54
+ action: reason ? `The config threw while loading: ${reason}. Fix that in the config.` : "The config threw while loading. Run the file directly to see what it throws."
55
+ };
56
+ }
57
+ return {
58
+ kind: "unknown",
59
+ action: "Check pithy.config.ts. Run the file directly to see how it fails."
60
+ };
61
+ }
62
+ function isCapability(value) {
63
+ return typeof value === "object" && value !== null && typeof value.name === "string";
64
+ }
65
+ /**
66
+ * Load a Worker's `pithy.config.ts` and index the capabilities it composes.
67
+ *
68
+ * `runnerImport` is Vite's own loader: it builds a throwaway runnable environment with
69
+ * `configFile: false`, so the config's TypeScript, its extensionless relative imports, and its
70
+ * `@pithy-sh/*` source imports all resolve — none of which a bare dynamic `import()` handles — and it
71
+ * behaves identically under `vite dev` and `vite build`. It also reports the transitive files it
72
+ * touched, which is what makes HMR on a config edit possible.
73
+ */
74
+ async function loadWorkerConfig(configFile) {
75
+ try {
76
+ await access(configFile);
77
+ } catch {
78
+ throw new NotFoundError({
79
+ message: `No pithy.config.ts at ${configFile}.`,
80
+ action: "Run pithy ui add from the project root, or point the plugin at it with pithy({ configFile })."
81
+ });
82
+ }
83
+ let module;
84
+ let dependencies;
85
+ try {
86
+ const imported = await runnerImport(configFile);
87
+ module = imported.module;
88
+ dependencies = imported.dependencies.filter((file) => typeof file === "string" && file.length > 0);
89
+ } catch (cause) {
90
+ const { kind, action } = classifyWorkerConfigFailure(cause);
91
+ throw new InternalError({
92
+ message: `Could not load ${configFile}.`,
93
+ action,
94
+ detail: `${kind}: ${cause instanceof Error ? cause.message : String(cause)}`
95
+ });
96
+ }
97
+ const config = module.default;
98
+ if (!config || !Array.isArray(config.capabilities)) throw new InternalError({
99
+ message: `${configFile} doesn't default-export a worker config.`,
100
+ action: "Export default { capabilities, app }."
101
+ });
102
+ const capabilities = /* @__PURE__ */ new Map();
103
+ for (const entry of config.capabilities) {
104
+ if (!isCapability(entry)) throw new InternalError({
105
+ message: `${configFile} lists something that isn't a capability.`,
106
+ action: "Every entry in capabilities is a capability object with a name. Remove the odd one out."
107
+ });
108
+ capabilities.set(entry.name, entry);
109
+ }
110
+ if (isCapability(config.app)) capabilities.set(config.app.name, config.app);
111
+ return {
112
+ capabilities,
113
+ dependencies
114
+ };
115
+ }
116
+ //#endregion
117
+ export { classifyWorkerConfigFailure, loadWorkerConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pithy-sh/vite",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,19 +8,24 @@
8
8
  "directory": "packages/vite"
9
9
  },
10
10
  "files": [
11
+ "dist",
11
12
  "src",
12
- "!src/**/*.test.*"
13
+ "!src/**/*.test.*",
14
+ "!dist/**/*.test.*"
13
15
  ],
14
16
  "type": "module",
15
17
  "engines": {
16
18
  "node": ">=22"
17
19
  },
18
20
  "exports": {
19
- "./src/*": "./src/*.ts"
21
+ "./src/*": {
22
+ "types": "./dist/*.d.ts",
23
+ "default": "./dist/*.js"
24
+ }
20
25
  },
21
26
  "scripts": {
22
27
  "generate": "bun scripts/generateClientEnv.ts",
23
- "build": "bun run generate && tsc -p tsconfig.json --noEmit false --outDir dist",
28
+ "build": "bun run generate && tsdown && tsc -p tsconfig.build.json",
24
29
  "typecheck": "tsc -p tsconfig.json",
25
30
  "test": "vitest run",
26
31
  "test:node": "vitest run --project=node",
@@ -31,7 +36,7 @@
31
36
  "vite": "^6.1.0 || ^7.0.0 || ^8.0.0"
32
37
  },
33
38
  "dependencies": {
34
- "@pithy-sh/core": "^0.1.2"
39
+ "@pithy-sh/core": "^0.1.3"
35
40
  },
36
41
  "devDependencies": {
37
42
  "@pithy-sh/auth": "workspace:*",
@@ -43,9 +48,10 @@
43
48
  "@pithy-sh/ui-react": "workspace:*",
44
49
  "@types/node": "^22.15.0",
45
50
  "@vitest/coverage-v8": "^4.1.0",
51
+ "tsdown": "^0.23.0",
46
52
  "typescript": "^7.0.2",
47
53
  "vite": "^8.0.16",
48
54
  "vitest": "^4.1.0",
49
- "zod": "^4.0.0"
55
+ "zod": "^4.4.0"
50
56
  }
51
57
  }