@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.
- package/dist/clientEnvDeclaration.d.ts +102 -0
- package/dist/clientEnvDeclaration.d.ts.map +1 -0
- package/dist/clientEnvDeclaration.js +299 -0
- package/dist/devOrigin.d.ts +53 -0
- package/dist/devOrigin.d.ts.map +1 -0
- package/dist/devOrigin.js +45 -0
- package/dist/plugin.d.ts +105 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +96 -0
- package/dist/testPlugin.d.ts +54 -0
- package/dist/testPlugin.d.ts.map +1 -0
- package/dist/testPlugin.js +58 -0
- package/dist/virtualModule.d.ts +28 -0
- package/dist/virtualModule.d.ts.map +1 -0
- package/dist/virtualModule.js +108 -0
- package/dist/workerConfig.d.ts +58 -0
- package/dist/workerConfig.d.ts.map +1 -0
- package/dist/workerConfig.js +117 -0
- package/package.json +12 -6
- package/src/clientEnvDeclaration.ts +46 -9
- package/src/testPlugin.ts +28 -37
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.
|
|
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/*":
|
|
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
|
|
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": "
|
|
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.
|
|
55
|
+
"zod": "^4.4.0"
|
|
50
56
|
}
|
|
51
57
|
}
|