@pithy-sh/vite 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/devOrigin.d.ts +5 -0
- package/dist/devOrigin.d.ts.map +1 -1
- package/dist/devOrigin.js +13 -2
- package/dist/workerConfig.d.ts +19 -17
- package/dist/workerConfig.d.ts.map +1 -1
- package/dist/workerConfig.js +48 -31
- package/package.json +3 -3
- package/src/devOrigin.ts +13 -2
- package/src/workerConfig.ts +69 -70
package/dist/devOrigin.d.ts
CHANGED
|
@@ -46,6 +46,11 @@ export type WorkerVarValue = string | number | boolean | null | WorkerVarValue[]
|
|
|
46
46
|
* checkout signed tokens as the first one and its own seam denied every call, 401, with every stored
|
|
47
47
|
* value agreeing (#462, `pithy-sh/dashboard#95`).
|
|
48
48
|
*
|
|
49
|
+
* **It also hands on where each service binding answers (#617).** `pithy dev` publishes the Worker's
|
|
50
|
+
* {@link SERVICE_ORIGINS_VAR} into this process's environment, for the reason it publishes the origin there:
|
|
51
|
+
* no argv. Passed on as the var of the same name, it is what core's `withLocalServices` reads to send
|
|
52
|
+
* `env.<BINDING>.fetch()` to the target's pinned port. A Worker that binds nothing is handed nothing.
|
|
53
|
+
*
|
|
49
54
|
* **Outside `pithy dev` it does nothing at all.** A plain `vite dev`, a build, or CI has no
|
|
50
55
|
* {@link WORKER_ORIGIN_VAR} in its environment, and the Worker keeps whatever `wrangler.jsonc` says —
|
|
51
56
|
* which for a deployed environment is the value `applyDomains` generated from `domains`, and is
|
package/dist/devOrigin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devOrigin.d.ts","sourceRoot":"","sources":["../src/devOrigin.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"devOrigin.d.ts","sourceRoot":"","sources":["../src/devOrigin.ts"],"names":[],"mappings":"AAOA;;;;;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,CAAC,MAAM,EAAE,UAAU,KAAK,UAAU,CA4BxG"}
|
package/dist/devOrigin.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import { LOCAL_ENVIRONMENT } from "@pithy-sh/core/src/naming/environment";
|
|
4
4
|
import { ENVIRONMENT_VAR, WORKER_ORIGIN_VAR } from "@pithy-sh/core/src/worker/identity";
|
|
5
|
+
import { SERVICE_ORIGINS_VAR } from "@pithy-sh/core/src/worker/serviceLoopback";
|
|
5
6
|
//#region src/devOrigin.ts
|
|
6
7
|
/**
|
|
7
8
|
* Give the Worker the origin `pithy dev` allocated this checkout, as `BASE_URL`.
|
|
@@ -26,6 +27,11 @@ import { ENVIRONMENT_VAR, WORKER_ORIGIN_VAR } from "@pithy-sh/core/src/worker/id
|
|
|
26
27
|
* checkout signed tokens as the first one and its own seam denied every call, 401, with every stored
|
|
27
28
|
* value agreeing (#462, `pithy-sh/dashboard#95`).
|
|
28
29
|
*
|
|
30
|
+
* **It also hands on where each service binding answers (#617).** `pithy dev` publishes the Worker's
|
|
31
|
+
* {@link SERVICE_ORIGINS_VAR} into this process's environment, for the reason it publishes the origin there:
|
|
32
|
+
* no argv. Passed on as the var of the same name, it is what core's `withLocalServices` reads to send
|
|
33
|
+
* `env.<BINDING>.fetch()` to the target's pinned port. A Worker that binds nothing is handed nothing.
|
|
34
|
+
*
|
|
29
35
|
* **Outside `pithy dev` it does nothing at all.** A plain `vite dev`, a build, or CI has no
|
|
30
36
|
* {@link WORKER_ORIGIN_VAR} in its environment, and the Worker keeps whatever `wrangler.jsonc` says —
|
|
31
37
|
* which for a deployed environment is the value `applyDomains` generated from `domains`, and is
|
|
@@ -34,12 +40,17 @@ import { ENVIRONMENT_VAR, WORKER_ORIGIN_VAR } from "@pithy-sh/core/src/worker/id
|
|
|
34
40
|
function devWorkerConfig(env = process.env) {
|
|
35
41
|
return (config) => {
|
|
36
42
|
const origin = env[WORKER_ORIGIN_VAR];
|
|
37
|
-
|
|
43
|
+
const services = env[SERVICE_ORIGINS_VAR];
|
|
44
|
+
const given = {
|
|
45
|
+
...origin === void 0 || origin.trim() === "" ? {} : { BASE_URL: origin },
|
|
46
|
+
...services === void 0 || services.trim() === "" ? {} : { [SERVICE_ORIGINS_VAR]: services }
|
|
47
|
+
};
|
|
48
|
+
if (Object.keys(given).length === 0) return {};
|
|
38
49
|
const environment = config.vars?.[ENVIRONMENT_VAR];
|
|
39
50
|
if (typeof environment === "string" && environment !== LOCAL_ENVIRONMENT) return {};
|
|
40
51
|
return { vars: {
|
|
41
52
|
...config.vars,
|
|
42
|
-
|
|
53
|
+
...given
|
|
43
54
|
} };
|
|
44
55
|
};
|
|
45
56
|
}
|
package/dist/workerConfig.d.ts
CHANGED
|
@@ -19,19 +19,23 @@ export interface LoadedWorkerConfig {
|
|
|
19
19
|
*
|
|
20
20
|
* What the copies *cannot* be allowed to disagree about is anything that is not a sentence. Two kinds of
|
|
21
21
|
* that were found, one issue apart, and both live in `@pithy-sh/core` — the one package this plugin
|
|
22
|
-
* already depends on. What a runtime does to an error on its way out is the first: `rootCause
|
|
23
|
-
*
|
|
24
|
-
* its strings is safe to show is the second: `safeReason`, `failurePosition
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
22
|
+
* already depends on. What a runtime does to an error on its way out is the first: `rootCause` and
|
|
23
|
+
* `prop`, facts no copy can derive and every copy got wrong (#223). Whether one of
|
|
24
|
+
* its strings is safe to show is the second: `safeReason`, `failurePosition` and `causeMessage`, because a
|
|
25
|
+
* path or a stack frame is a property of the string and not of the surface quoting it — three
|
|
26
|
+
* near-verbatim copies of that filter meant the hole #223 found in it had to be closed three times (#228).
|
|
27
|
+
* An import that did not resolve is the third: which kind, whose, and what to do are `unresolvedImport`'s,
|
|
28
|
+
* because three surfaces deciding it kept giving three different remedies (#709). A file inside an
|
|
29
|
+
* installed package that throws as it loads is the fourth, `packageLoadFailure`'s, for the same reason.
|
|
28
30
|
*
|
|
29
|
-
* What stays restated here is the policy:
|
|
30
|
-
*
|
|
31
|
+
* What stays restated here is the rest of the policy: a parse error and the config's own throw. This
|
|
32
|
+
* plugin still cannot import the CLI's.
|
|
31
33
|
*/
|
|
32
34
|
export type WorkerConfigFailureKind =
|
|
33
|
-
/** An import in the config does not resolve.
|
|
35
|
+
/** An import in the config does not resolve. Which cause it was decides whether installing helps. */
|
|
34
36
|
"unresolved-import"
|
|
37
|
+
/** A file inside an installed package would not load: its `"type"` contradicts its code, or it does not parse. */
|
|
38
|
+
| "package-load"
|
|
35
39
|
/** The file does not parse. Installing will not help, and the adopter has read the opposite for years. */
|
|
36
40
|
| "parse-error"
|
|
37
41
|
/** The config loaded far enough to throw its own error. Neither of the above. */
|
|
@@ -48,15 +52,13 @@ export interface WorkerConfigFailure {
|
|
|
48
52
|
/**
|
|
49
53
|
* Choose the refusal's `action` **from** the failure rather than asserting one over it (#217).
|
|
50
54
|
*
|
|
51
|
-
* An unresolved import
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* `importingPackage` is where core records that both runtimes hand it over. **It asserts nothing about
|
|
57
|
-
* which package is absent** — nothing here can know that — only where the import is written.
|
|
55
|
+
* An unresolved import is core's {@link unresolvedImport}: the same sentence the CLI gives for the same
|
|
56
|
+
* cause, naming the package the import is written in when it is not the config (#480, #489), and an
|
|
57
|
+
* install only where an install can help (#709). `relativeTo` is the config's directory, so an adopter's
|
|
58
|
+
* own missing file is shown as the import they wrote and never as an absolute path. A parser's position is
|
|
59
|
+
* quoted only when it is in `configFile`, never when it is in a file the config imports.
|
|
58
60
|
*/
|
|
59
|
-
export declare function classifyWorkerConfigFailure(wrapped: unknown): WorkerConfigFailure;
|
|
61
|
+
export declare function classifyWorkerConfigFailure(wrapped: unknown, relativeTo?: string, configFile?: string): WorkerConfigFailure;
|
|
60
62
|
/**
|
|
61
63
|
* Load a Worker's `pithy.config.ts` and index the capabilities it composes.
|
|
62
64
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workerConfig.d.ts","sourceRoot":"","sources":["../src/workerConfig.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"workerConfig.d.ts","sourceRoot":"","sources":["../src/workerConfig.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAC;AAiB3E,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;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,uBAAuB;AACjC,qGAAqG;AACnG,mBAAmB;AACrB,kHAAkH;GAChH,cAAc;AAChB,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;AA0CD;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,OAAO,EAChB,UAAU,CAAC,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,GAClB,mBAAmB,CAuCrB;AAMD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAoDtF"}
|
package/dist/workerConfig.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import { access } from "node:fs/promises";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
4
5
|
import { runnerImport } from "vite";
|
|
5
|
-
import {
|
|
6
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
7
|
+
import { commandName } from "@pithy-sh/core/src/cli/command";
|
|
8
|
+
import { causeMessage, failurePosition, prop, rootCause, safeReason } from "@pithy-sh/core/src/error/cause";
|
|
6
9
|
import { InternalError, NotFoundError } from "@pithy-sh/core/src/error/pithyError";
|
|
10
|
+
import { nodeModulesProbe, packageEntryProbe, packageLoadFailure, unresolvedImport } from "@pithy-sh/core/src/error/unresolved";
|
|
7
11
|
//#region src/workerConfig.ts
|
|
8
12
|
/**
|
|
9
13
|
* The message, de-colored, for this classifier's own pattern tests — never for output.
|
|
@@ -17,47 +21,60 @@ import { InternalError, NotFoundError } from "@pithy-sh/core/src/error/pithyErro
|
|
|
17
21
|
function rawMessage(cause) {
|
|
18
22
|
return causeMessage(cause) ?? "";
|
|
19
23
|
}
|
|
20
|
-
function isUnresolvedImport(cause) {
|
|
21
|
-
const code = prop(cause, "code");
|
|
22
|
-
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND" || code === "ERR_PACKAGE_PATH_NOT_EXPORTED" || code === "ERR_UNSUPPORTED_DIR_IMPORT") return true;
|
|
23
|
-
if (prop(cause, "name") === "ResolveMessage") return true;
|
|
24
|
-
return /Cannot find (?:package|module) |Failed to resolve (?:import|module)|Failed to load url /.test(rawMessage(cause));
|
|
25
|
-
}
|
|
26
24
|
function isParseError(cause) {
|
|
27
25
|
const name = prop(cause, "name");
|
|
28
26
|
if (name === "SyntaxError" || name === "BuildMessage") return true;
|
|
29
27
|
if (prop(cause, "code") === "PARSE_ERROR") return true;
|
|
30
|
-
if (isBuildFailureWrapper(cause)) return true;
|
|
31
28
|
return /Transform failed|\[PARSE_ERROR]|Parse (?:error|failure)|Unexpected (?:token|end of input)/.test(rawMessage(cause));
|
|
32
29
|
}
|
|
33
30
|
/**
|
|
31
|
+
* Whether a package is installed where the importing file would look — core's walk over `node:fs`, the
|
|
32
|
+
* same one the CLI passes, so a deep import of an absent package is told to install and one of an
|
|
33
|
+
* installed package without that path is not (#709). This plugin runs on Node, never in a Worker.
|
|
34
|
+
*/
|
|
35
|
+
const installed = nodeModulesProbe({
|
|
36
|
+
exists: existsSync,
|
|
37
|
+
realpath: realpathSync
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* What an installed package's bare name resolves to, from its own `package.json`: core's read, the same the
|
|
41
|
+
* CLI passes. Vite's runner says *Failed to resolve entry* for a package that declares no entry and for one
|
|
42
|
+
* whose declared entry is not on disk, and names no importer for either (#709).
|
|
43
|
+
*/
|
|
44
|
+
const entry = packageEntryProbe({
|
|
45
|
+
exists: existsSync,
|
|
46
|
+
realpath: realpathSync,
|
|
47
|
+
read: (path) => readFileSync(path, "utf8")
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
34
50
|
* Choose the refusal's `action` **from** the failure rather than asserting one over it (#217).
|
|
35
51
|
*
|
|
36
|
-
* An unresolved import
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* `importingPackage` is where core records that both runtimes hand it over. **It asserts nothing about
|
|
42
|
-
* which package is absent** — nothing here can know that — only where the import is written.
|
|
52
|
+
* An unresolved import is core's {@link unresolvedImport}: the same sentence the CLI gives for the same
|
|
53
|
+
* cause, naming the package the import is written in when it is not the config (#480, #489), and an
|
|
54
|
+
* install only where an install can help (#709). `relativeTo` is the config's directory, so an adopter's
|
|
55
|
+
* own missing file is shown as the import they wrote and never as an absolute path. A parser's position is
|
|
56
|
+
* quoted only when it is in `configFile`, never when it is in a file the config imports.
|
|
43
57
|
*/
|
|
44
|
-
function classifyWorkerConfigFailure(wrapped) {
|
|
58
|
+
function classifyWorkerConfigFailure(wrapped, relativeTo, configFile) {
|
|
45
59
|
const cause = rootCause(wrapped);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
const unresolved = unresolvedImport(cause, {
|
|
61
|
+
installed,
|
|
62
|
+
entry,
|
|
63
|
+
relativeTo,
|
|
64
|
+
loader: "@pithy-sh/vite"
|
|
65
|
+
});
|
|
66
|
+
if (unresolved !== void 0) return {
|
|
67
|
+
kind: "unresolved-import",
|
|
68
|
+
action: unresolved.remedy
|
|
69
|
+
};
|
|
70
|
+
const foreign = packageLoadFailure(cause, { loader: "@pithy-sh/vite" });
|
|
71
|
+
if (foreign !== void 0) return {
|
|
72
|
+
kind: "package-load",
|
|
73
|
+
action: foreign.remedy
|
|
74
|
+
};
|
|
58
75
|
if (isParseError(cause)) {
|
|
59
76
|
const reason = safeReason(cause);
|
|
60
|
-
const at = failurePosition(cause);
|
|
77
|
+
const at = failurePosition(cause, configFile === void 0 ? void 0 : (file) => resolve(file) === resolve(configFile));
|
|
61
78
|
const where = at ? ` Line ${at.line}, column ${at.column}.` : "";
|
|
62
79
|
return {
|
|
63
80
|
kind: "parse-error",
|
|
@@ -94,7 +111,7 @@ async function loadWorkerConfig(configFile) {
|
|
|
94
111
|
} catch {
|
|
95
112
|
throw new NotFoundError({
|
|
96
113
|
message: `No pithy.config.ts at ${configFile}.`,
|
|
97
|
-
action:
|
|
114
|
+
action: `Point the plugin at the Worker's pithy.config.ts with pithy({ configFile }). A front end scaffolded by ${commandName("ui add")} is pointed at it already.`
|
|
98
115
|
});
|
|
99
116
|
}
|
|
100
117
|
let module;
|
|
@@ -104,7 +121,7 @@ async function loadWorkerConfig(configFile) {
|
|
|
104
121
|
module = imported.module;
|
|
105
122
|
dependencies = imported.dependencies.filter((file) => typeof file === "string" && file.length > 0);
|
|
106
123
|
} catch (cause) {
|
|
107
|
-
const { kind, action } = classifyWorkerConfigFailure(cause);
|
|
124
|
+
const { kind, action } = classifyWorkerConfigFailure(cause, dirname(configFile), configFile);
|
|
108
125
|
throw new InternalError({
|
|
109
126
|
message: `Could not load ${configFile}.`,
|
|
110
127
|
action,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pithy-sh/vite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,14 +26,14 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"generate": "bun scripts/generateClientEnv.ts",
|
|
28
28
|
"build": "bun run generate && tsdown && tsc -p tsconfig.build.json && bun ../../tooling/build/src/stampDeclarations.ts",
|
|
29
|
-
"typecheck": "
|
|
29
|
+
"typecheck": "bun check -p tsconfig.json",
|
|
30
30
|
"test": "vitest run",
|
|
31
31
|
"test:node": "vitest run --project=node",
|
|
32
32
|
"clean": "rm -rf dist .turbo",
|
|
33
33
|
"reset": "bun run clean && rm -rf node_modules"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@pithy-sh/core": "^0.
|
|
36
|
+
"@pithy-sh/core": "^0.11.0",
|
|
37
37
|
"vite": "^6.1.0 || ^7.0.0 || ^8.0.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
package/src/devOrigin.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { LOCAL_ENVIRONMENT } from "@pithy-sh/core/src/naming/environment";
|
|
5
5
|
import { ENVIRONMENT_VAR, WORKER_ORIGIN_VAR } from "@pithy-sh/core/src/worker/identity";
|
|
6
|
+
import { SERVICE_ORIGINS_VAR } from "@pithy-sh/core/src/worker/serviceLoopback";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* The slice of `@cloudflare/vite-plugin`'s worker config this touches.
|
|
@@ -50,6 +51,11 @@ export type WorkerVarValue = string | number | boolean | null | WorkerVarValue[]
|
|
|
50
51
|
* checkout signed tokens as the first one and its own seam denied every call, 401, with every stored
|
|
51
52
|
* value agreeing (#462, `pithy-sh/dashboard#95`).
|
|
52
53
|
*
|
|
54
|
+
* **It also hands on where each service binding answers (#617).** `pithy dev` publishes the Worker's
|
|
55
|
+
* {@link SERVICE_ORIGINS_VAR} into this process's environment, for the reason it publishes the origin there:
|
|
56
|
+
* no argv. Passed on as the var of the same name, it is what core's `withLocalServices` reads to send
|
|
57
|
+
* `env.<BINDING>.fetch()` to the target's pinned port. A Worker that binds nothing is handed nothing.
|
|
58
|
+
*
|
|
53
59
|
* **Outside `pithy dev` it does nothing at all.** A plain `vite dev`, a build, or CI has no
|
|
54
60
|
* {@link WORKER_ORIGIN_VAR} in its environment, and the Worker keeps whatever `wrangler.jsonc` says —
|
|
55
61
|
* which for a deployed environment is the value `applyDomains` generated from `domains`, and is
|
|
@@ -58,10 +64,15 @@ export type WorkerVarValue = string | number | boolean | null | WorkerVarValue[]
|
|
|
58
64
|
export function devWorkerConfig(env: NodeJS.ProcessEnv = process.env): (config: WorkerVars) => WorkerVars {
|
|
59
65
|
return (config) => {
|
|
60
66
|
const origin = env[WORKER_ORIGIN_VAR];
|
|
67
|
+
const services = env[SERVICE_ORIGINS_VAR];
|
|
61
68
|
// Absent, blank, or whitespace is "nobody allocated one", never "use the empty string". An empty
|
|
62
69
|
// `BASE_URL` is worse than a wrong one: it fails a URL parse somewhere far from here rather than
|
|
63
70
|
// being denied at the seam with the origin named.
|
|
64
|
-
|
|
71
|
+
const given = {
|
|
72
|
+
...(origin === undefined || origin.trim() === "" ? {} : { BASE_URL: origin }),
|
|
73
|
+
...(services === undefined || services.trim() === "" ? {} : { [SERVICE_ORIGINS_VAR]: services }),
|
|
74
|
+
};
|
|
75
|
+
if (Object.keys(given).length === 0) return {};
|
|
65
76
|
// And never over a config that says, itself, that it is not dev.
|
|
66
77
|
//
|
|
67
78
|
// The customizer is applied on `build` as well as on `serve`, and it beats the `vars` the file
|
|
@@ -76,6 +87,6 @@ export function devWorkerConfig(env: NodeJS.ProcessEnv = process.env): (config:
|
|
|
76
87
|
// the fix from the dev run it exists for — the failure this whole issue was.
|
|
77
88
|
const environment = config.vars?.[ENVIRONMENT_VAR];
|
|
78
89
|
if (typeof environment === "string" && environment !== LOCAL_ENVIRONMENT) return {};
|
|
79
|
-
return { vars: { ...config.vars,
|
|
90
|
+
return { vars: { ...config.vars, ...given } };
|
|
80
91
|
};
|
|
81
92
|
}
|
package/src/workerConfig.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
4
5
|
import { access } from "node:fs/promises";
|
|
6
|
+
import { dirname, resolve } from "node:path";
|
|
5
7
|
import type { Capability } from "@pithy-sh/core/src/capability/capability";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
failurePosition,
|
|
9
|
-
importingPackage,
|
|
10
|
-
isBuildFailureWrapper,
|
|
11
|
-
prop,
|
|
12
|
-
rootCause,
|
|
13
|
-
safeReason,
|
|
14
|
-
unresolvedSpecifier,
|
|
15
|
-
} from "@pithy-sh/core/src/error/cause";
|
|
8
|
+
import { commandName } from "@pithy-sh/core/src/cli/command";
|
|
9
|
+
import { causeMessage, failurePosition, prop, rootCause, safeReason } from "@pithy-sh/core/src/error/cause";
|
|
16
10
|
import { InternalError, NotFoundError } from "@pithy-sh/core/src/error/pithyError";
|
|
11
|
+
import {
|
|
12
|
+
nodeModulesProbe,
|
|
13
|
+
packageEntryProbe,
|
|
14
|
+
packageLoadFailure,
|
|
15
|
+
unresolvedImport,
|
|
16
|
+
} from "@pithy-sh/core/src/error/unresolved";
|
|
17
17
|
import { runnerImport } from "vite";
|
|
18
18
|
|
|
19
19
|
/** The default export of a Worker's `pithy.config.ts`, before it is trusted. */
|
|
@@ -40,19 +40,23 @@ export interface LoadedWorkerConfig {
|
|
|
40
40
|
*
|
|
41
41
|
* What the copies *cannot* be allowed to disagree about is anything that is not a sentence. Two kinds of
|
|
42
42
|
* that were found, one issue apart, and both live in `@pithy-sh/core` — the one package this plugin
|
|
43
|
-
* already depends on. What a runtime does to an error on its way out is the first: `rootCause
|
|
44
|
-
*
|
|
45
|
-
* its strings is safe to show is the second: `safeReason`, `failurePosition
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
43
|
+
* already depends on. What a runtime does to an error on its way out is the first: `rootCause` and
|
|
44
|
+
* `prop`, facts no copy can derive and every copy got wrong (#223). Whether one of
|
|
45
|
+
* its strings is safe to show is the second: `safeReason`, `failurePosition` and `causeMessage`, because a
|
|
46
|
+
* path or a stack frame is a property of the string and not of the surface quoting it — three
|
|
47
|
+
* near-verbatim copies of that filter meant the hole #223 found in it had to be closed three times (#228).
|
|
48
|
+
* An import that did not resolve is the third: which kind, whose, and what to do are `unresolvedImport`'s,
|
|
49
|
+
* because three surfaces deciding it kept giving three different remedies (#709). A file inside an
|
|
50
|
+
* installed package that throws as it loads is the fourth, `packageLoadFailure`'s, for the same reason.
|
|
49
51
|
*
|
|
50
|
-
* What stays restated here is the policy:
|
|
51
|
-
*
|
|
52
|
+
* What stays restated here is the rest of the policy: a parse error and the config's own throw. This
|
|
53
|
+
* plugin still cannot import the CLI's.
|
|
52
54
|
*/
|
|
53
55
|
export type WorkerConfigFailureKind =
|
|
54
|
-
/** An import in the config does not resolve.
|
|
56
|
+
/** An import in the config does not resolve. Which cause it was decides whether installing helps. */
|
|
55
57
|
| "unresolved-import"
|
|
58
|
+
/** A file inside an installed package would not load: its `"type"` contradicts its code, or it does not parse. */
|
|
59
|
+
| "package-load"
|
|
56
60
|
/** The file does not parse. Installing will not help, and the adopter has read the opposite for years. */
|
|
57
61
|
| "parse-error"
|
|
58
62
|
/** The config loaded far enough to throw its own error. Neither of the above. */
|
|
@@ -81,73 +85,66 @@ function rawMessage(cause: unknown): string {
|
|
|
81
85
|
return causeMessage(cause) ?? "";
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
function isUnresolvedImport(cause: unknown): boolean {
|
|
85
|
-
const code = prop(cause, "code");
|
|
86
|
-
if (
|
|
87
|
-
code === "ERR_MODULE_NOT_FOUND" ||
|
|
88
|
-
code === "MODULE_NOT_FOUND" ||
|
|
89
|
-
code === "ERR_PACKAGE_PATH_NOT_EXPORTED" ||
|
|
90
|
-
code === "ERR_UNSUPPORTED_DIR_IMPORT"
|
|
91
|
-
) {
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
if (prop(cause, "name") === "ResolveMessage") return true;
|
|
95
|
-
return /Cannot find (?:package|module) |Failed to resolve (?:import|module)|Failed to load url /.test(
|
|
96
|
-
rawMessage(cause),
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
88
|
function isParseError(cause: unknown): boolean {
|
|
101
89
|
const name = prop(cause, "name");
|
|
102
90
|
if (name === "SyntaxError" || name === "BuildMessage") return true;
|
|
103
91
|
if (prop(cause, "code") === "PARSE_ERROR") return true;
|
|
104
|
-
// Bun's build wrapper with its diagnostics already dropped — the shape every caller after the first
|
|
105
|
-
// sees, since a failed module is cached and re-thrown emptied out. It proves a build produced
|
|
106
|
-
// diagnostics, so it is a parse error with no reason to quote. See core's `cause.ts` (#223).
|
|
107
|
-
if (isBuildFailureWrapper(cause)) return true;
|
|
108
92
|
return /Transform failed|\[PARSE_ERROR]|Parse (?:error|failure)|Unexpected (?:token|end of input)/.test(
|
|
109
93
|
rawMessage(cause),
|
|
110
94
|
);
|
|
111
95
|
}
|
|
112
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Whether a package is installed where the importing file would look — core's walk over `node:fs`, the
|
|
99
|
+
* same one the CLI passes, so a deep import of an absent package is told to install and one of an
|
|
100
|
+
* installed package without that path is not (#709). This plugin runs on Node, never in a Worker.
|
|
101
|
+
*/
|
|
102
|
+
const installed = nodeModulesProbe({ exists: existsSync, realpath: realpathSync });
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* What an installed package's bare name resolves to, from its own `package.json`: core's read, the same the
|
|
106
|
+
* CLI passes. Vite's runner says *Failed to resolve entry* for a package that declares no entry and for one
|
|
107
|
+
* whose declared entry is not on disk, and names no importer for either (#709).
|
|
108
|
+
*/
|
|
109
|
+
const entry = packageEntryProbe({
|
|
110
|
+
exists: existsSync,
|
|
111
|
+
realpath: realpathSync,
|
|
112
|
+
read: (path) => readFileSync(path, "utf8"),
|
|
113
|
+
});
|
|
114
|
+
|
|
113
115
|
/**
|
|
114
116
|
* Choose the refusal's `action` **from** the failure rather than asserting one over it (#217).
|
|
115
117
|
*
|
|
116
|
-
* An unresolved import
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* `importingPackage` is where core records that both runtimes hand it over. **It asserts nothing about
|
|
122
|
-
* which package is absent** — nothing here can know that — only where the import is written.
|
|
118
|
+
* An unresolved import is core's {@link unresolvedImport}: the same sentence the CLI gives for the same
|
|
119
|
+
* cause, naming the package the import is written in when it is not the config (#480, #489), and an
|
|
120
|
+
* install only where an install can help (#709). `relativeTo` is the config's directory, so an adopter's
|
|
121
|
+
* own missing file is shown as the import they wrote and never as an absolute path. A parser's position is
|
|
122
|
+
* quoted only when it is in `configFile`, never when it is in a file the config imports.
|
|
123
123
|
*/
|
|
124
|
-
export function classifyWorkerConfigFailure(
|
|
124
|
+
export function classifyWorkerConfigFailure(
|
|
125
|
+
wrapped: unknown,
|
|
126
|
+
relativeTo?: string,
|
|
127
|
+
configFile?: string,
|
|
128
|
+
): WorkerConfigFailure {
|
|
125
129
|
// Bun hands `import()` failures over inside an `AggregateError`. Classify what is inside it.
|
|
126
130
|
const cause = rootCause(wrapped);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
action:
|
|
135
|
-
"An import in the config does not resolve. Install the project's dependencies (bun install), then check its imports.",
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
return {
|
|
139
|
-
kind: "unresolved-import",
|
|
140
|
-
action: importer
|
|
141
|
-
? `Nothing resolves "${specifier}". ${importer} is what imports it, not the config. Install the project's dependencies (bun install).`
|
|
142
|
-
: `Nothing resolves "${specifier}". Install the project's dependencies (bun install), or correct that import.`,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
131
|
+
// Which import, whose, and what to do: core's, for all three surfaces that refuse a module (#709).
|
|
132
|
+
const unresolved = unresolvedImport(cause, { installed, entry, relativeTo, loader: "@pithy-sh/vite" });
|
|
133
|
+
if (unresolved !== undefined) return { kind: "unresolved-import", action: unresolved.remedy };
|
|
134
|
+
|
|
135
|
+
// A package's own file that would not load is the package's, before it can read as the config's (#709).
|
|
136
|
+
const foreign = packageLoadFailure(cause, { loader: "@pithy-sh/vite" });
|
|
137
|
+
if (foreign !== undefined) return { kind: "package-load", action: foreign.remedy };
|
|
145
138
|
|
|
146
139
|
if (isParseError(cause)) {
|
|
147
|
-
// Both of these can answer "nothing"
|
|
148
|
-
//
|
|
140
|
+
// Both of these can answer "nothing": a diagnostic that carries a path, a stack frame or a newline is
|
|
141
|
+
// not quoted, and a position is never invented. That filter is core's, once (#228).
|
|
149
142
|
const reason = safeReason(cause);
|
|
150
|
-
|
|
143
|
+
// A position in a file the config imports is not the config's, and is never quoted as if it were (#709).
|
|
144
|
+
const at = failurePosition(
|
|
145
|
+
cause,
|
|
146
|
+
configFile === undefined ? undefined : (file) => resolve(file) === resolve(configFile),
|
|
147
|
+
);
|
|
151
148
|
const where = at ? ` Line ${at.line}, column ${at.column}.` : "";
|
|
152
149
|
return {
|
|
153
150
|
kind: "parse-error",
|
|
@@ -188,7 +185,9 @@ export async function loadWorkerConfig(configFile: string): Promise<LoadedWorker
|
|
|
188
185
|
} catch {
|
|
189
186
|
throw new NotFoundError({
|
|
190
187
|
message: `No pithy.config.ts at ${configFile}.`,
|
|
191
|
-
|
|
188
|
+
// No command here (#711): `pithy ui add` acts on one Worker and takes a framework, and a plugin that has
|
|
189
|
+
// lost its config cannot say which of either. Pointing the plugin is the remedy that always applies.
|
|
190
|
+
action: `Point the plugin at the Worker's pithy.config.ts with pithy({ configFile }). A front end scaffolded by ${commandName("ui add")} is pointed at it already.`,
|
|
192
191
|
});
|
|
193
192
|
}
|
|
194
193
|
|
|
@@ -202,7 +201,7 @@ export async function loadWorkerConfig(configFile: string): Promise<LoadedWorker
|
|
|
202
201
|
// The config is there and would not load. Which of the four ways it failed decides what to tell the
|
|
203
202
|
// adopter — see {@link classifyWorkerConfigFailure}. The raw cause still goes to `detail` and stops
|
|
204
203
|
// there: the CLI renderer prints `message` and `action` only, and the HTTP codec strips `detail`.
|
|
205
|
-
const { kind, action } = classifyWorkerConfigFailure(cause);
|
|
204
|
+
const { kind, action } = classifyWorkerConfigFailure(cause, dirname(configFile), configFile);
|
|
206
205
|
throw new InternalError({
|
|
207
206
|
message: `Could not load ${configFile}.`,
|
|
208
207
|
action,
|