@raypx-kit/devkit 0.0.0-alpha.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/CHANGELOG.md +21 -0
- package/LICENSE +201 -0
- package/README.md +189 -0
- package/dist/architecture.d.mts +15 -0
- package/dist/architecture.mjs +266 -0
- package/dist/architecture.mjs.map +1 -0
- package/dist/artifact.d.mts +25 -0
- package/dist/artifact.mjs +104 -0
- package/dist/artifact.mjs.map +1 -0
- package/dist/catalogs.d.mts +13 -0
- package/dist/catalogs.mjs +49 -0
- package/dist/catalogs.mjs.map +1 -0
- package/dist/docs.d.mts +5 -0
- package/dist/docs.mjs +21 -0
- package/dist/docs.mjs.map +1 -0
- package/dist/process.d.mts +23 -0
- package/dist/process.mjs +82 -0
- package/dist/process.mjs.map +1 -0
- package/dist/toolchain.d.mts +27 -0
- package/dist/toolchain.mjs +213 -0
- package/dist/toolchain.mjs.map +1 -0
- package/dist/workspaces.d.mts +71 -0
- package/dist/workspaces.mjs +139 -0
- package/dist/workspaces.mjs.map +1 -0
- package/package.json +79 -0
package/dist/process.mjs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { config } from "@dotenvx/dotenvx";
|
|
5
|
+
//#region src/process.ts
|
|
6
|
+
/**
|
|
7
|
+
* Copies the current environment and layers local dotenv files beneath it; values
|
|
8
|
+
* already set in the process win. Loaded secrets are never printed.
|
|
9
|
+
*/
|
|
10
|
+
function developmentEnvironment(root, files = [".env.local", ".env"]) {
|
|
11
|
+
const env = Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== void 0));
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
const path = join(root, file);
|
|
14
|
+
if (existsSync(path)) config({
|
|
15
|
+
path,
|
|
16
|
+
processEnv: env,
|
|
17
|
+
quiet: true
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return env;
|
|
21
|
+
}
|
|
22
|
+
/** Runs a command with inherited output, forwarding interrupts and rejecting on failure. */
|
|
23
|
+
function run(command, args, cwd, env = process.env) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const child = spawn(command, args, {
|
|
26
|
+
cwd,
|
|
27
|
+
env,
|
|
28
|
+
stdio: "inherit",
|
|
29
|
+
windowsHide: true
|
|
30
|
+
});
|
|
31
|
+
const forward = (signal) => {
|
|
32
|
+
child.kill(signal);
|
|
33
|
+
};
|
|
34
|
+
const interrupt = () => forward("SIGINT");
|
|
35
|
+
const terminate = () => forward("SIGTERM");
|
|
36
|
+
process.once("SIGINT", interrupt);
|
|
37
|
+
process.once("SIGTERM", terminate);
|
|
38
|
+
const cleanup = () => {
|
|
39
|
+
process.off("SIGINT", interrupt);
|
|
40
|
+
process.off("SIGTERM", terminate);
|
|
41
|
+
};
|
|
42
|
+
child.once("error", (error) => {
|
|
43
|
+
cleanup();
|
|
44
|
+
reject(error);
|
|
45
|
+
});
|
|
46
|
+
child.once("exit", (code, signal) => {
|
|
47
|
+
cleanup();
|
|
48
|
+
if (code === 0) resolve();
|
|
49
|
+
else reject(/* @__PURE__ */ new Error(`${command} exited ${code ?? signal}`));
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Runs a Turbo task under Bun. Untrusted runs drop remote cache credentials and use
|
|
55
|
+
* the local cache only, so forks never read or write the shared remote cache.
|
|
56
|
+
*/
|
|
57
|
+
function turbo(root, task, args = [], { developmentTasks = ["dev", "build"], env: extra = {}, trustedCacheEnv = "DEVKIT_REMOTE_CACHE_TRUSTED" } = {}) {
|
|
58
|
+
const env = {
|
|
59
|
+
...developmentTasks.includes(task) ? developmentEnvironment(root) : process.env,
|
|
60
|
+
...extra
|
|
61
|
+
};
|
|
62
|
+
if (env[trustedCacheEnv] !== "true") {
|
|
63
|
+
for (const key of [
|
|
64
|
+
"TURBO_TOKEN",
|
|
65
|
+
"TURBO_TEAM",
|
|
66
|
+
"TURBO_REMOTE_CACHE_SIGNATURE_KEY"
|
|
67
|
+
]) delete env[key];
|
|
68
|
+
delete env.TURBO_REMOTE_ONLY;
|
|
69
|
+
args = ["--cache=local:rw", ...args];
|
|
70
|
+
}
|
|
71
|
+
return run(process.execPath, [
|
|
72
|
+
"--bun",
|
|
73
|
+
"turbo",
|
|
74
|
+
"run",
|
|
75
|
+
task,
|
|
76
|
+
...args
|
|
77
|
+
], root, env);
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { developmentEnvironment, run, turbo };
|
|
81
|
+
|
|
82
|
+
//# sourceMappingURL=process.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process.mjs","names":[],"sources":["../src/process.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { config } from \"@dotenvx/dotenvx\";\n\n/**\n * Copies the current environment and layers local dotenv files beneath it; values\n * already set in the process win. Loaded secrets are never printed.\n */\nexport function developmentEnvironment(\n root: string,\n files: readonly string[] = [\".env.local\", \".env\"],\n): NodeJS.ProcessEnv {\n const env: Record<string, string> = Object.fromEntries(\n Object.entries(process.env).filter(\n (entry): entry is [string, string] => entry[1] !== undefined,\n ),\n );\n for (const file of files) {\n const path = join(root, file);\n if (existsSync(path)) config({ path, processEnv: env, quiet: true });\n }\n return env;\n}\n\n/** Runs a command with inherited output, forwarding interrupts and rejecting on failure. */\nexport function run(\n command: string,\n args: string[],\n cwd: string,\n env = process.env,\n): Promise<void> {\n return new Promise((resolve, reject) => {\n const child = spawn(command, args, { cwd, env, stdio: \"inherit\", windowsHide: true });\n const forward = (signal: NodeJS.Signals) => {\n child.kill(signal);\n };\n const interrupt = () => forward(\"SIGINT\");\n const terminate = () => forward(\"SIGTERM\");\n process.once(\"SIGINT\", interrupt);\n process.once(\"SIGTERM\", terminate);\n const cleanup = () => {\n process.off(\"SIGINT\", interrupt);\n process.off(\"SIGTERM\", terminate);\n };\n child.once(\"error\", (error) => {\n cleanup();\n reject(error);\n });\n child.once(\"exit\", (code, signal) => {\n cleanup();\n if (code === 0) resolve();\n else reject(new Error(`${command} exited ${code ?? signal}`));\n });\n });\n}\n\nexport interface TurboOptions {\n /** Load development dotenv files for these tasks. */\n developmentTasks?: readonly string[];\n /** Environment entries added to every task, such as a cache platform key. */\n env?: Record<string, string>;\n /** Only when this variable is `\"true\"` are remote cache credentials passed through. */\n trustedCacheEnv?: string;\n}\n\n/**\n * Runs a Turbo task under Bun. Untrusted runs drop remote cache credentials and use\n * the local cache only, so forks never read or write the shared remote cache.\n */\nexport function turbo(\n root: string,\n task: string,\n args: string[] = [],\n {\n developmentTasks = [\"dev\", \"build\"],\n env: extra = {},\n trustedCacheEnv = \"DEVKIT_REMOTE_CACHE_TRUSTED\",\n }: TurboOptions = {},\n): Promise<void> {\n const env = {\n ...(developmentTasks.includes(task) ? developmentEnvironment(root) : process.env),\n ...extra,\n };\n if (env[trustedCacheEnv] !== \"true\") {\n for (const key of [\"TURBO_TOKEN\", \"TURBO_TEAM\", \"TURBO_REMOTE_CACHE_SIGNATURE_KEY\"])\n delete env[key];\n delete env.TURBO_REMOTE_ONLY;\n args = [\"--cache=local:rw\", ...args];\n }\n return run(process.execPath, [\"--bun\", \"turbo\", \"run\", task, ...args], root, env);\n}\n"],"mappings":";;;;;;;;;AASA,SAAgB,uBACd,MACA,QAA2B,CAAC,cAAc,MAAM,GAC7B;CACnB,MAAM,MAA8B,OAAO,YACzC,OAAO,QAAQ,QAAQ,GAAG,CAAC,CAAC,QACzB,UAAqC,MAAM,OAAO,KAAA,CACrD,CACF;CACA,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAO,KAAK,MAAM,IAAI;EAC5B,IAAI,WAAW,IAAI,GAAG,OAAO;GAAE;GAAM,YAAY;GAAK,OAAO;EAAK,CAAC;CACrE;CACA,OAAO;AACT;;AAGA,SAAgB,IACd,SACA,MACA,KACA,MAAM,QAAQ,KACC;CACf,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,QAAQ,MAAM,SAAS,MAAM;GAAE;GAAK;GAAK,OAAO;GAAW,aAAa;EAAK,CAAC;EACpF,MAAM,WAAW,WAA2B;GAC1C,MAAM,KAAK,MAAM;EACnB;EACA,MAAM,kBAAkB,QAAQ,QAAQ;EACxC,MAAM,kBAAkB,QAAQ,SAAS;EACzC,QAAQ,KAAK,UAAU,SAAS;EAChC,QAAQ,KAAK,WAAW,SAAS;EACjC,MAAM,gBAAgB;GACpB,QAAQ,IAAI,UAAU,SAAS;GAC/B,QAAQ,IAAI,WAAW,SAAS;EAClC;EACA,MAAM,KAAK,UAAU,UAAU;GAC7B,QAAQ;GACR,OAAO,KAAK;EACd,CAAC;EACD,MAAM,KAAK,SAAS,MAAM,WAAW;GACnC,QAAQ;GACR,IAAI,SAAS,GAAG,QAAQ;QACnB,uBAAO,IAAI,MAAM,GAAG,QAAQ,UAAU,QAAQ,QAAQ,CAAC;EAC9D,CAAC;CACH,CAAC;AACH;;;;;AAeA,SAAgB,MACd,MACA,MACA,OAAiB,CAAC,GAClB,EACE,mBAAmB,CAAC,OAAO,OAAO,GAClC,KAAK,QAAQ,CAAC,GACd,kBAAkB,kCACF,CAAC,GACJ;CACf,MAAM,MAAM;EACV,GAAI,iBAAiB,SAAS,IAAI,IAAI,uBAAuB,IAAI,IAAI,QAAQ;EAC7E,GAAG;CACL;CACA,IAAI,IAAI,qBAAqB,QAAQ;EACnC,KAAK,MAAM,OAAO;GAAC;GAAe;GAAc;EAAkC,GAChF,OAAO,IAAI;EACb,OAAO,IAAI;EACX,OAAO,CAAC,oBAAoB,GAAG,IAAI;CACrC;CACA,OAAO,IAAI,QAAQ,UAAU;EAAC;EAAS;EAAS;EAAO;EAAM,GAAG;CAAI,GAAG,MAAM,GAAG;AAClF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/toolchain.d.ts
|
|
2
|
+
export interface Toolchain {
|
|
3
|
+
node: string;
|
|
4
|
+
bun: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ToolchainOptions {
|
|
7
|
+
/** Repository-relative documents whose `Node x.y.z` and `Bun x.y.z` mentions must match. */
|
|
8
|
+
documents?: readonly string[];
|
|
9
|
+
/** Files every Turbo cache key must include when `turbo.json` exists. */
|
|
10
|
+
turboGlobalDependencies?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export interface RuntimeOptions {
|
|
13
|
+
/** Environment variable that may select a Node executable outside PATH. */
|
|
14
|
+
nodeBinaryEnv?: string;
|
|
15
|
+
}
|
|
16
|
+
/** `.nvmrc` pins Node and the root `packageManager` pins Bun; every other pin follows them. */
|
|
17
|
+
export declare function readToolchain(root: string): Toolchain;
|
|
18
|
+
/** Returns the Node executable after confirming it provides the pinned release. */
|
|
19
|
+
export declare function pinnedNode(root: string, { nodeBinaryEnv }?: RuntimeOptions): string;
|
|
20
|
+
/** The binary override changes the consumer executable, never the required version. */
|
|
21
|
+
export declare function assertToolchainRuntime(root: string, options?: RuntimeOptions): Toolchain & {
|
|
22
|
+
nodeBinary: string;
|
|
23
|
+
};
|
|
24
|
+
/** Check duplicated declarations without installing tools or running application code. */
|
|
25
|
+
export declare function checkToolchain(root: string, { documents, turboGlobalDependencies }?: ToolchainOptions): string[];
|
|
26
|
+
//#endregion
|
|
27
|
+
//# sourceMappingURL=toolchain.d.mts.map
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { workspacePatterns } from "./workspaces.mjs";
|
|
2
|
+
import { existsSync, globSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { parseTOML } from "confbox/toml";
|
|
6
|
+
import { parseYAML } from "confbox/yaml";
|
|
7
|
+
//#region src/toolchain.ts
|
|
8
|
+
const exactVersion = /^\d+\.\d+\.\d+$/;
|
|
9
|
+
const manifestAt = (root, file = "package.json") => JSON.parse(readFileSync(join(root, file), "utf8"));
|
|
10
|
+
const catalogsOf = (manifest) => {
|
|
11
|
+
const workspaces = manifest.workspaces;
|
|
12
|
+
if (!workspaces || typeof workspaces !== "object" || Array.isArray(workspaces)) return [];
|
|
13
|
+
const { catalog, catalogs } = workspaces;
|
|
14
|
+
return [...catalog ? [catalog] : [], ...Object.values(catalogs ?? {})];
|
|
15
|
+
};
|
|
16
|
+
/** `.nvmrc` pins Node and the root `packageManager` pins Bun; every other pin follows them. */
|
|
17
|
+
function readToolchain(root) {
|
|
18
|
+
const node = readFileSync(join(root, ".nvmrc"), "utf8").trim().replace(/^v/, "");
|
|
19
|
+
const bun = /^bun@(.+)$/.exec(manifestAt(root).packageManager ?? "")?.[1] ?? "";
|
|
20
|
+
if (!exactVersion.test(node) || !exactVersion.test(bun)) throw new Error("Pin exact Node and Bun versions in .nvmrc and package.json packageManager.");
|
|
21
|
+
return {
|
|
22
|
+
node,
|
|
23
|
+
bun
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Returns the Node executable after confirming it provides the pinned release. */
|
|
27
|
+
function pinnedNode(root, { nodeBinaryEnv = "DEVKIT_NODE_BINARY" } = {}) {
|
|
28
|
+
const pins = readToolchain(root);
|
|
29
|
+
const nodeBinary = process.env[nodeBinaryEnv] || "node";
|
|
30
|
+
let node;
|
|
31
|
+
try {
|
|
32
|
+
node = execFileSync(nodeBinary, ["--version"], {
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
stdio: "pipe"
|
|
35
|
+
}).trim();
|
|
36
|
+
} catch {
|
|
37
|
+
throw new Error(`Node is unavailable; select Node ${pins.node} from .nvmrc or set ${nodeBinaryEnv}.`);
|
|
38
|
+
}
|
|
39
|
+
if (node !== `v${pins.node}`) throw new Error(`Expected Node ${pins.node}, got ${node}; select the pinned Node from .nvmrc or set ${nodeBinaryEnv}.`);
|
|
40
|
+
return nodeBinary;
|
|
41
|
+
}
|
|
42
|
+
/** The binary override changes the consumer executable, never the required version. */
|
|
43
|
+
function assertToolchainRuntime(root, options = {}) {
|
|
44
|
+
const pins = readToolchain(root);
|
|
45
|
+
if (process.versions.bun !== pins.bun) throw new Error(`Expected Bun ${pins.bun}, got ${process.versions.bun ?? "none"}.`);
|
|
46
|
+
return {
|
|
47
|
+
...pins,
|
|
48
|
+
nodeBinary: pinnedNode(root, options)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function isRecord(value) {
|
|
52
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
53
|
+
}
|
|
54
|
+
function checkMise(root) {
|
|
55
|
+
const file = "mise.toml";
|
|
56
|
+
const path = join(root, file);
|
|
57
|
+
if (!existsSync(path)) return [`${file}: missing runtime manager configuration`];
|
|
58
|
+
let mise;
|
|
59
|
+
try {
|
|
60
|
+
mise = parseTOML(readFileSync(path, "utf8"));
|
|
61
|
+
} catch {
|
|
62
|
+
return [`${file}: invalid TOML configuration`];
|
|
63
|
+
}
|
|
64
|
+
if (!isRecord(mise)) return [`${file}: configuration must be a table`];
|
|
65
|
+
const findings = [];
|
|
66
|
+
if (Object.hasOwn(mise, "tools")) findings.push(`${file}: read existing pins instead of declaring [tools] versions`);
|
|
67
|
+
const tools = isRecord(mise.settings) ? mise.settings.idiomatic_version_file_enable_tools : void 0;
|
|
68
|
+
if (!Array.isArray(tools) || tools.length !== 2 || !["node", "bun"].every((tool) => tools.includes(tool))) findings.push(`${file}: enable idiomatic version files for node and bun`);
|
|
69
|
+
return findings;
|
|
70
|
+
}
|
|
71
|
+
function checkSetupStep(file, location, step) {
|
|
72
|
+
if (typeof step.uses !== "string") return [];
|
|
73
|
+
const refSeparator = step.uses.indexOf("@");
|
|
74
|
+
if (refSeparator < 0) return [];
|
|
75
|
+
const repository = step.uses.slice(0, refSeparator).toLowerCase();
|
|
76
|
+
const setup = [[
|
|
77
|
+
"node",
|
|
78
|
+
"actions/setup-node",
|
|
79
|
+
".nvmrc"
|
|
80
|
+
], [
|
|
81
|
+
"bun",
|
|
82
|
+
"oven-sh/setup-bun",
|
|
83
|
+
"package.json"
|
|
84
|
+
]].find(([, action]) => repository === action);
|
|
85
|
+
if (!setup) return [];
|
|
86
|
+
const [tool, , source] = setup;
|
|
87
|
+
const findings = [];
|
|
88
|
+
if (step.with !== void 0 && !isRecord(step.with)) findings.push(`${file}: ${location}.with must be a mapping`);
|
|
89
|
+
const inputs = isRecord(step.with) ? step.with : {};
|
|
90
|
+
if (Object.hasOwn(inputs, `${tool}-version`)) findings.push(`${file}: read ${tool}-version-file: ${source} instead of a literal version`);
|
|
91
|
+
const versionFile = inputs[`${tool}-version-file`];
|
|
92
|
+
if (versionFile === void 0) findings.push(`${file}: every ${tool} setup must read ${source}`);
|
|
93
|
+
else if (versionFile !== source) findings.push(`${file}: ${tool}-version-file must be ${source}`);
|
|
94
|
+
return findings;
|
|
95
|
+
}
|
|
96
|
+
function checkSetupSteps(file, location, steps) {
|
|
97
|
+
if (!Array.isArray(steps)) return [`${file}: ${location} must be an array`];
|
|
98
|
+
const findings = [];
|
|
99
|
+
for (const [index, step] of steps.entries()) {
|
|
100
|
+
const stepLocation = `${location}[${index}]`;
|
|
101
|
+
if (!isRecord(step)) {
|
|
102
|
+
findings.push(`${file}: ${stepLocation} must be a mapping`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
findings.push(...checkSetupStep(file, stepLocation, step));
|
|
106
|
+
}
|
|
107
|
+
return findings;
|
|
108
|
+
}
|
|
109
|
+
function checkWorkflows(root) {
|
|
110
|
+
const findings = [];
|
|
111
|
+
for (const file of globSync(".github/workflows/*.{yml,yaml}", { cwd: root })) {
|
|
112
|
+
let workflow;
|
|
113
|
+
try {
|
|
114
|
+
workflow = parseYAML(readFileSync(join(root, file), "utf8"), { filename: file });
|
|
115
|
+
} catch {
|
|
116
|
+
findings.push(`${file}: invalid YAML configuration`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!isRecord(workflow) || !isRecord(workflow.jobs)) {
|
|
120
|
+
findings.push(`${file}: workflow jobs must be a mapping`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
for (const [name, job] of Object.entries(workflow.jobs)) {
|
|
124
|
+
const location = `jobs.${name}`;
|
|
125
|
+
if (!isRecord(job)) {
|
|
126
|
+
findings.push(`${file}: ${location} must be a mapping`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (job.steps === void 0) continue;
|
|
130
|
+
findings.push(...checkSetupSteps(file, `${location}.steps`, job.steps));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return findings;
|
|
134
|
+
}
|
|
135
|
+
function checkCompositeActions(root) {
|
|
136
|
+
const findings = [];
|
|
137
|
+
for (const file of globSync(".github/actions/**/action.{yml,yaml}", { cwd: root })) {
|
|
138
|
+
let action;
|
|
139
|
+
try {
|
|
140
|
+
action = parseYAML(readFileSync(join(root, file), "utf8"), { filename: file });
|
|
141
|
+
} catch {
|
|
142
|
+
findings.push(`${file}: invalid YAML configuration`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!isRecord(action) || !isRecord(action.runs)) {
|
|
146
|
+
findings.push(`${file}: action runs must be a mapping`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (action.runs.using !== "composite") continue;
|
|
150
|
+
findings.push(...checkSetupSteps(file, "runs.steps", action.runs.steps));
|
|
151
|
+
}
|
|
152
|
+
return findings;
|
|
153
|
+
}
|
|
154
|
+
/** Check duplicated declarations without installing tools or running application code. */
|
|
155
|
+
function checkToolchain(root, { documents = ["README.md", "AGENTS.md"], turboGlobalDependencies = [".nvmrc", "mise.toml"] } = {}) {
|
|
156
|
+
let pins;
|
|
157
|
+
try {
|
|
158
|
+
pins = readToolchain(root);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return [error instanceof Error ? error.message : String(error)];
|
|
161
|
+
}
|
|
162
|
+
const findings = [];
|
|
163
|
+
const manifest = manifestAt(root);
|
|
164
|
+
const expect = (file, label, actual, expected) => {
|
|
165
|
+
if (actual !== expected) findings.push(`${file}: ${label} is ${actual ?? "missing"}, expected ${expected}`);
|
|
166
|
+
};
|
|
167
|
+
expect("package.json", "engines.node", manifest.engines?.node, pins.node);
|
|
168
|
+
for (const key of ["packageManager", "runtime"]) {
|
|
169
|
+
const engine = manifest.devEngines?.[key];
|
|
170
|
+
expect("package.json", `devEngines.${key}`, `${engine?.name}@${engine?.version}`, `bun@${pins.bun}`);
|
|
171
|
+
expect("package.json", `devEngines.${key}.onFail`, engine?.onFail, "error");
|
|
172
|
+
}
|
|
173
|
+
for (const file of [
|
|
174
|
+
".node-version",
|
|
175
|
+
".bun-version",
|
|
176
|
+
".tool-versions"
|
|
177
|
+
]) if (existsSync(join(root, file))) findings.push(`${file}: use .nvmrc and package.json instead of a second version source`);
|
|
178
|
+
findings.push(...checkMise(root));
|
|
179
|
+
const nodeMajor = pins.node.split(".")[0];
|
|
180
|
+
const version = (range) => range && !/^[a-z]+:/.test(range) ? range.replace(/^[\^~]/, "") : void 0;
|
|
181
|
+
const checkTypes = (file, dependencies) => {
|
|
182
|
+
const node = version(dependencies?.["@types/node"]);
|
|
183
|
+
if (node && node.split(".")[0] !== nodeMajor) findings.push(`${file}: @types/node must match the Node ${nodeMajor} major`);
|
|
184
|
+
const bun = version(dependencies?.["@types/bun"]);
|
|
185
|
+
if (bun) expect(file, "@types/bun", bun, pins.bun);
|
|
186
|
+
};
|
|
187
|
+
for (const catalog of catalogsOf(manifest)) checkTypes("package.json catalog", catalog);
|
|
188
|
+
for (const file of ["package.json", ...globSync(workspacePatterns(manifest).map((path) => `${path}/package.json`), { cwd: root })]) checkTypes(file, manifestAt(root, file).devDependencies);
|
|
189
|
+
for (const file of globSync("**/Dockerfile", {
|
|
190
|
+
cwd: root,
|
|
191
|
+
exclude: ["**/node_modules/**"]
|
|
192
|
+
})) {
|
|
193
|
+
const source = readFileSync(join(root, file), "utf8");
|
|
194
|
+
for (const [, version] of source.matchAll(/^FROM\s+node:(\d+\.\d+\.\d+)/gm)) expect(file, "node image", version, pins.node);
|
|
195
|
+
for (const [, version] of source.matchAll(/^FROM\s+oven\/bun:(\d+\.\d+\.\d+)/gm)) expect(file, "bun image", version, pins.bun);
|
|
196
|
+
}
|
|
197
|
+
findings.push(...checkWorkflows(root), ...checkCompositeActions(root));
|
|
198
|
+
for (const file of documents) {
|
|
199
|
+
if (!existsSync(join(root, file))) continue;
|
|
200
|
+
const document = readFileSync(join(root, file), "utf8").replaceAll("**", "");
|
|
201
|
+
for (const [, tool, version] of document.matchAll(/\b(Node|Bun) v?(\d+\.\d+\.\d+)\b/g)) expect(file, `documented ${tool}`, version, tool === "Node" ? pins.node : pins.bun);
|
|
202
|
+
}
|
|
203
|
+
const turboFile = join(root, "turbo.json");
|
|
204
|
+
if (existsSync(turboFile)) {
|
|
205
|
+
const turbo = JSON.parse(readFileSync(turboFile, "utf8"));
|
|
206
|
+
for (const file of turboGlobalDependencies) if (!turbo.globalDependencies?.includes(file)) findings.push(`turbo.json: include ${file} in globalDependencies`);
|
|
207
|
+
}
|
|
208
|
+
return [...new Set(findings)];
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
export { assertToolchainRuntime, checkToolchain, pinnedNode, readToolchain };
|
|
212
|
+
|
|
213
|
+
//# sourceMappingURL=toolchain.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toolchain.mjs","names":[],"sources":["../src/toolchain.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { existsSync, globSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { parseTOML } from \"confbox/toml\";\nimport { parseYAML } from \"confbox/yaml\";\nimport { workspacePatterns } from \"./workspaces.js\";\n\nexport interface Toolchain {\n node: string;\n bun: string;\n}\n\nexport interface ToolchainOptions {\n /** Repository-relative documents whose `Node x.y.z` and `Bun x.y.z` mentions must match. */\n documents?: readonly string[];\n /** Files every Turbo cache key must include when `turbo.json` exists. */\n turboGlobalDependencies?: readonly string[];\n}\n\nexport interface RuntimeOptions {\n /** Environment variable that may select a Node executable outside PATH. */\n nodeBinaryEnv?: string;\n}\n\ninterface Manifest {\n packageManager?: string;\n engines?: { node?: string };\n devEngines?: Record<string, { name?: string; version?: string; onFail?: string }>;\n devDependencies?: Record<string, string>;\n workspaces?: unknown;\n}\n\nconst exactVersion = /^\\d+\\.\\d+\\.\\d+$/;\nconst manifestAt = (root: string, file = \"package.json\"): Manifest =>\n JSON.parse(readFileSync(join(root, file), \"utf8\")) as Manifest;\nconst catalogsOf = (manifest: Manifest): Record<string, string>[] => {\n const workspaces = manifest.workspaces;\n if (!workspaces || typeof workspaces !== \"object\" || Array.isArray(workspaces)) return [];\n const { catalog, catalogs } = workspaces as {\n catalog?: Record<string, string>;\n catalogs?: Record<string, Record<string, string>>;\n };\n return [...(catalog ? [catalog] : []), ...Object.values(catalogs ?? {})];\n};\n\n/** `.nvmrc` pins Node and the root `packageManager` pins Bun; every other pin follows them. */\nexport function readToolchain(root: string): Toolchain {\n const node = readFileSync(join(root, \".nvmrc\"), \"utf8\").trim().replace(/^v/, \"\");\n const bun = /^bun@(.+)$/.exec(manifestAt(root).packageManager ?? \"\")?.[1] ?? \"\";\n if (!exactVersion.test(node) || !exactVersion.test(bun))\n throw new Error(\"Pin exact Node and Bun versions in .nvmrc and package.json packageManager.\");\n return { node, bun };\n}\n\n/** Returns the Node executable after confirming it provides the pinned release. */\nexport function pinnedNode(\n root: string,\n { nodeBinaryEnv = \"DEVKIT_NODE_BINARY\" }: RuntimeOptions = {},\n) {\n const pins = readToolchain(root);\n const nodeBinary = process.env[nodeBinaryEnv] || \"node\";\n let node: string;\n try {\n node = execFileSync(nodeBinary, [\"--version\"], { encoding: \"utf8\", stdio: \"pipe\" }).trim();\n } catch {\n throw new Error(\n `Node is unavailable; select Node ${pins.node} from .nvmrc or set ${nodeBinaryEnv}.`,\n );\n }\n if (node !== `v${pins.node}`)\n throw new Error(\n `Expected Node ${pins.node}, got ${node}; select the pinned Node from .nvmrc or set ${nodeBinaryEnv}.`,\n );\n return nodeBinary;\n}\n\n/** The binary override changes the consumer executable, never the required version. */\nexport function assertToolchainRuntime(\n root: string,\n options: RuntimeOptions = {},\n): Toolchain & { nodeBinary: string } {\n const pins = readToolchain(root);\n if (process.versions.bun !== pins.bun)\n throw new Error(`Expected Bun ${pins.bun}, got ${process.versions.bun ?? \"none\"}.`);\n return { ...pins, nodeBinary: pinnedNode(root, options) };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction checkMise(root: string): string[] {\n const file = \"mise.toml\";\n const path = join(root, file);\n if (!existsSync(path)) return [`${file}: missing runtime manager configuration`];\n let mise: unknown;\n try {\n mise = parseTOML(readFileSync(path, \"utf8\"));\n } catch {\n return [`${file}: invalid TOML configuration`];\n }\n if (!isRecord(mise)) return [`${file}: configuration must be a table`];\n const findings: string[] = [];\n if (Object.hasOwn(mise, \"tools\"))\n findings.push(`${file}: read existing pins instead of declaring [tools] versions`);\n const tools = isRecord(mise.settings)\n ? mise.settings.idiomatic_version_file_enable_tools\n : undefined;\n if (\n !Array.isArray(tools) ||\n tools.length !== 2 ||\n ![\"node\", \"bun\"].every((tool) => tools.includes(tool))\n )\n findings.push(`${file}: enable idiomatic version files for node and bun`);\n return findings;\n}\n\nfunction checkSetupStep(file: string, location: string, step: Record<string, unknown>): string[] {\n if (typeof step.uses !== \"string\") return [];\n const refSeparator = step.uses.indexOf(\"@\");\n if (refSeparator < 0) return [];\n const repository = step.uses.slice(0, refSeparator).toLowerCase();\n const setup = (\n [\n [\"node\", \"actions/setup-node\", \".nvmrc\"],\n [\"bun\", \"oven-sh/setup-bun\", \"package.json\"],\n ] as const\n ).find(([, action]) => repository === action);\n if (!setup) return [];\n const [tool, , source] = setup;\n const findings: string[] = [];\n if (step.with !== undefined && !isRecord(step.with))\n findings.push(`${file}: ${location}.with must be a mapping`);\n const inputs = isRecord(step.with) ? step.with : {};\n if (Object.hasOwn(inputs, `${tool}-version`))\n findings.push(`${file}: read ${tool}-version-file: ${source} instead of a literal version`);\n const versionFile = inputs[`${tool}-version-file`];\n if (versionFile === undefined) findings.push(`${file}: every ${tool} setup must read ${source}`);\n else if (versionFile !== source) findings.push(`${file}: ${tool}-version-file must be ${source}`);\n return findings;\n}\n\nfunction checkSetupSteps(file: string, location: string, steps: unknown): string[] {\n if (!Array.isArray(steps)) return [`${file}: ${location} must be an array`];\n const findings: string[] = [];\n for (const [index, step] of steps.entries()) {\n const stepLocation = `${location}[${index}]`;\n if (!isRecord(step)) {\n findings.push(`${file}: ${stepLocation} must be a mapping`);\n continue;\n }\n findings.push(...checkSetupStep(file, stepLocation, step));\n }\n return findings;\n}\n\nfunction checkWorkflows(root: string): string[] {\n const findings: string[] = [];\n for (const file of globSync(\".github/workflows/*.{yml,yaml}\", { cwd: root })) {\n let workflow: unknown;\n try {\n workflow = parseYAML(readFileSync(join(root, file), \"utf8\"), { filename: file });\n } catch {\n findings.push(`${file}: invalid YAML configuration`);\n continue;\n }\n if (!isRecord(workflow) || !isRecord(workflow.jobs)) {\n findings.push(`${file}: workflow jobs must be a mapping`);\n continue;\n }\n for (const [name, job] of Object.entries(workflow.jobs)) {\n const location = `jobs.${name}`;\n if (!isRecord(job)) {\n findings.push(`${file}: ${location} must be a mapping`);\n continue;\n }\n if (job.steps === undefined) continue;\n findings.push(...checkSetupSteps(file, `${location}.steps`, job.steps));\n }\n }\n return findings;\n}\n\nfunction checkCompositeActions(root: string): string[] {\n const findings: string[] = [];\n for (const file of globSync(\".github/actions/**/action.{yml,yaml}\", { cwd: root })) {\n let action: unknown;\n try {\n action = parseYAML(readFileSync(join(root, file), \"utf8\"), { filename: file });\n } catch {\n findings.push(`${file}: invalid YAML configuration`);\n continue;\n }\n if (!isRecord(action) || !isRecord(action.runs)) {\n findings.push(`${file}: action runs must be a mapping`);\n continue;\n }\n if (action.runs.using !== \"composite\") continue;\n findings.push(...checkSetupSteps(file, \"runs.steps\", action.runs.steps));\n }\n return findings;\n}\n\n/** Check duplicated declarations without installing tools or running application code. */\nexport function checkToolchain(\n root: string,\n {\n documents = [\"README.md\", \"AGENTS.md\"],\n turboGlobalDependencies = [\".nvmrc\", \"mise.toml\"],\n }: ToolchainOptions = {},\n): string[] {\n let pins: Toolchain;\n try {\n pins = readToolchain(root);\n } catch (error) {\n return [error instanceof Error ? error.message : String(error)];\n }\n const findings: string[] = [];\n const manifest = manifestAt(root);\n const expect = (file: string, label: string, actual: string | undefined, expected: string) => {\n if (actual !== expected)\n findings.push(`${file}: ${label} is ${actual ?? \"missing\"}, expected ${expected}`);\n };\n expect(\"package.json\", \"engines.node\", manifest.engines?.node, pins.node);\n for (const key of [\"packageManager\", \"runtime\"]) {\n const engine = manifest.devEngines?.[key];\n expect(\n \"package.json\",\n `devEngines.${key}`,\n `${engine?.name}@${engine?.version}`,\n `bun@${pins.bun}`,\n );\n expect(\"package.json\", `devEngines.${key}.onFail`, engine?.onFail, \"error\");\n }\n for (const file of [\".node-version\", \".bun-version\", \".tool-versions\"])\n if (existsSync(join(root, file)))\n findings.push(`${file}: use .nvmrc and package.json instead of a second version source`);\n findings.push(...checkMise(root));\n\n // Runtime type packages follow the pins: @types/node by major, @types/bun exactly.\n const nodeMajor = pins.node.split(\".\")[0]!;\n // Protocol ranges such as catalog: and workspace: are checked where they resolve.\n const version = (range: string | undefined) =>\n range && !/^[a-z]+:/.test(range) ? range.replace(/^[\\^~]/, \"\") : undefined;\n const checkTypes = (file: string, dependencies: Record<string, string> | undefined) => {\n const node = version(dependencies?.[\"@types/node\"]);\n if (node && node.split(\".\")[0] !== nodeMajor)\n findings.push(`${file}: @types/node must match the Node ${nodeMajor} major`);\n const bun = version(dependencies?.[\"@types/bun\"]);\n if (bun) expect(file, \"@types/bun\", bun, pins.bun);\n };\n for (const catalog of catalogsOf(manifest)) checkTypes(\"package.json catalog\", catalog);\n for (const file of [\n \"package.json\",\n ...globSync(\n workspacePatterns(manifest).map((path) => `${path}/package.json`),\n { cwd: root },\n ),\n ])\n checkTypes(file, manifestAt(root, file).devDependencies);\n\n for (const file of globSync(\"**/Dockerfile\", { cwd: root, exclude: [\"**/node_modules/**\"] })) {\n const source = readFileSync(join(root, file), \"utf8\");\n for (const [, version] of source.matchAll(/^FROM\\s+node:(\\d+\\.\\d+\\.\\d+)/gm))\n expect(file, \"node image\", version, pins.node);\n for (const [, version] of source.matchAll(/^FROM\\s+oven\\/bun:(\\d+\\.\\d+\\.\\d+)/gm))\n expect(file, \"bun image\", version, pins.bun);\n }\n findings.push(...checkWorkflows(root), ...checkCompositeActions(root));\n for (const file of documents) {\n if (!existsSync(join(root, file))) continue;\n const document = readFileSync(join(root, file), \"utf8\").replaceAll(\"**\", \"\");\n for (const [, tool, version] of document.matchAll(/\\b(Node|Bun) v?(\\d+\\.\\d+\\.\\d+)\\b/g))\n expect(file, `documented ${tool}`, version, tool === \"Node\" ? pins.node : pins.bun);\n }\n const turboFile = join(root, \"turbo.json\");\n if (existsSync(turboFile)) {\n const turbo = JSON.parse(readFileSync(turboFile, \"utf8\")) as { globalDependencies?: string[] };\n for (const file of turboGlobalDependencies)\n if (!turbo.globalDependencies?.includes(file))\n findings.push(`turbo.json: include ${file} in globalDependencies`);\n }\n return [...new Set(findings)];\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//#region src/workspaces.d.ts
|
|
2
|
+
export declare const targets: readonly ["universal", "browser", "native", "node", "bun"];
|
|
3
|
+
export type Target = (typeof targets)[number];
|
|
4
|
+
export declare const kinds: readonly ["app", "library", "tooling"];
|
|
5
|
+
export type Kind = (typeof kinds)[number];
|
|
6
|
+
/** Per-workspace boundary metadata, supplied explicitly or read from package.json. */
|
|
7
|
+
export interface Metadata {
|
|
8
|
+
kind: Kind;
|
|
9
|
+
target: Target;
|
|
10
|
+
entryTargets?: Record<string, Target>;
|
|
11
|
+
sourceTargets?: Record<string, Target>;
|
|
12
|
+
/** Maintained source behind an export whose published target is generated. */
|
|
13
|
+
sourceEntries?: Record<string, string>;
|
|
14
|
+
/** Development dependencies explicitly bundled into the published runtime. */
|
|
15
|
+
bundledDependencies?: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface Manifest {
|
|
18
|
+
name: string;
|
|
19
|
+
private?: boolean;
|
|
20
|
+
type?: string;
|
|
21
|
+
exports?: Record<string, unknown>;
|
|
22
|
+
imports?: Record<string, string>;
|
|
23
|
+
scripts?: Record<string, string>;
|
|
24
|
+
dependencies?: Record<string, string>;
|
|
25
|
+
devDependencies?: Record<string, string>;
|
|
26
|
+
peerDependencies?: Record<string, string>;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
export interface Workspace {
|
|
30
|
+
path: string;
|
|
31
|
+
manifest: Manifest;
|
|
32
|
+
metadata: Metadata | undefined;
|
|
33
|
+
aliases: Record<string, string[]>;
|
|
34
|
+
}
|
|
35
|
+
export interface WorkspaceIndex {
|
|
36
|
+
root: string;
|
|
37
|
+
metadataKey: string;
|
|
38
|
+
/** Origin of workspace metadata; omitted legacy indexes use the manifest. */
|
|
39
|
+
metadataSource?: "manifest" | "options";
|
|
40
|
+
workspaces: Workspace[];
|
|
41
|
+
byName: Map<string, Workspace>;
|
|
42
|
+
findings: string[];
|
|
43
|
+
}
|
|
44
|
+
export interface DiscoverOptions {
|
|
45
|
+
/** Complete metadata by exact workspace-relative path; never merged with manifests. */
|
|
46
|
+
metadata?: Readonly<Record<string, Metadata>>;
|
|
47
|
+
/** The package.json field holding {@link Metadata}, used only when metadata is omitted. */
|
|
48
|
+
metadataKey?: string;
|
|
49
|
+
/** Directories whose children must each be a workspace. */
|
|
50
|
+
containers?: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
/** Matches package export, import-map and tsconfig path patterns, including `**`. */
|
|
53
|
+
export declare function matches(pattern: string, value: string): boolean;
|
|
54
|
+
/** Reads workspace patterns from the array or Bun/Yarn object form of `workspaces`. */
|
|
55
|
+
export declare function workspacePatterns(manifest: {
|
|
56
|
+
workspaces?: unknown;
|
|
57
|
+
}): string[];
|
|
58
|
+
export declare function discover(root: string, { metadata, metadataKey, containers }?: DiscoverOptions): WorkspaceIndex;
|
|
59
|
+
export declare function workspaceFor(index: WorkspaceIndex, file: string): Workspace | undefined;
|
|
60
|
+
export declare function sourceTarget(workspace: Workspace, file: string): Target;
|
|
61
|
+
export declare function compatible(from: Target, to: Target): boolean;
|
|
62
|
+
export declare function exportPaths(value: unknown): string[];
|
|
63
|
+
export declare function resolveImport(index: WorkspaceIndex, from: Workspace, file: string, request: string): {
|
|
64
|
+
target: Workspace | undefined;
|
|
65
|
+
subpath: string;
|
|
66
|
+
internalPath: boolean;
|
|
67
|
+
paths: string[];
|
|
68
|
+
targetPlatform: "browser" | "bun" | "native" | "node" | "universal";
|
|
69
|
+
};
|
|
70
|
+
//#endregion
|
|
71
|
+
//# sourceMappingURL=workspaces.d.mts.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { existsSync, globSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join, posix } from "node:path";
|
|
3
|
+
//#region src/workspaces.ts
|
|
4
|
+
const targets = [
|
|
5
|
+
"universal",
|
|
6
|
+
"browser",
|
|
7
|
+
"native",
|
|
8
|
+
"node",
|
|
9
|
+
"bun"
|
|
10
|
+
];
|
|
11
|
+
const kinds = [
|
|
12
|
+
"app",
|
|
13
|
+
"library",
|
|
14
|
+
"tooling"
|
|
15
|
+
];
|
|
16
|
+
/** Matches package export, import-map and tsconfig path patterns, including `**`. */
|
|
17
|
+
function matches(pattern, value) {
|
|
18
|
+
let regex = "^";
|
|
19
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
20
|
+
const char = pattern[i];
|
|
21
|
+
if (char === "*" && pattern[i + 1] === "*") {
|
|
22
|
+
if (pattern[i + 2] === "/") {
|
|
23
|
+
regex += "(?:.*/)?";
|
|
24
|
+
i += 2;
|
|
25
|
+
} else {
|
|
26
|
+
regex += ".*";
|
|
27
|
+
i++;
|
|
28
|
+
}
|
|
29
|
+
} else if (char === "*") regex += "[^/]*";
|
|
30
|
+
else regex += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
31
|
+
}
|
|
32
|
+
return new RegExp(`${regex}$`).test(value);
|
|
33
|
+
}
|
|
34
|
+
/** Reads workspace patterns from the array or Bun/Yarn object form of `workspaces`. */
|
|
35
|
+
function workspacePatterns(manifest) {
|
|
36
|
+
const value = manifest.workspaces;
|
|
37
|
+
if (Array.isArray(value)) return value;
|
|
38
|
+
if (value && typeof value === "object" && Array.isArray(value.packages)) return value.packages;
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
function discover(root, { metadata, metadataKey = "devkit", containers = ["apps", "packages"] } = {}) {
|
|
42
|
+
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
43
|
+
const findings = [];
|
|
44
|
+
const workspaces = [];
|
|
45
|
+
for (const file of globSync(workspacePatterns(manifest).map((p) => `${p}/package.json`), { cwd: root }).sort()) {
|
|
46
|
+
const path = file.replaceAll("\\", "/").slice(0, -13);
|
|
47
|
+
const entry = JSON.parse(readFileSync(join(root, file), "utf8"));
|
|
48
|
+
const workspaceMetadata = metadata === void 0 ? entry[metadataKey] : Object.hasOwn(metadata, path) ? metadata[path] : void 0;
|
|
49
|
+
if (metadata !== void 0 && workspaceMetadata === void 0) findings.push(`${path}: missing supplied workspace metadata`);
|
|
50
|
+
let aliases = {};
|
|
51
|
+
const config = join(root, path, "tsconfig.json");
|
|
52
|
+
if (existsSync(config)) try {
|
|
53
|
+
aliases = JSON.parse(readFileSync(config, "utf8")).compilerOptions?.paths ?? {};
|
|
54
|
+
} catch {
|
|
55
|
+
findings.push(`${path}/tsconfig.json: expected valid JSON`);
|
|
56
|
+
}
|
|
57
|
+
workspaces.push({
|
|
58
|
+
path,
|
|
59
|
+
manifest: entry,
|
|
60
|
+
metadata: workspaceMetadata,
|
|
61
|
+
aliases
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const byName = /* @__PURE__ */ new Map();
|
|
65
|
+
for (const workspace of workspaces) {
|
|
66
|
+
if (byName.has(workspace.manifest.name)) findings.push(`duplicate workspace: ${workspace.manifest.name}`);
|
|
67
|
+
byName.set(workspace.manifest.name, workspace);
|
|
68
|
+
}
|
|
69
|
+
if (metadata !== void 0) {
|
|
70
|
+
const paths = new Set(workspaces.map((workspace) => workspace.path));
|
|
71
|
+
for (const path of Object.keys(metadata).sort()) if (!paths.has(path)) findings.push(`${path}: metadata supplied for unknown workspace`);
|
|
72
|
+
}
|
|
73
|
+
for (const parent of containers) {
|
|
74
|
+
if (!existsSync(join(root, parent))) continue;
|
|
75
|
+
for (const child of readdirSync(join(root, parent), { withFileTypes: true })) {
|
|
76
|
+
if (!child.isDirectory() || child.name.startsWith(".")) continue;
|
|
77
|
+
const path = `${parent}/${child.name}`;
|
|
78
|
+
if (!workspaces.some((w) => w.path === path)) findings.push(`${path}: missing workspace manifest`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
root,
|
|
83
|
+
metadataKey,
|
|
84
|
+
metadataSource: metadata === void 0 ? "manifest" : "options",
|
|
85
|
+
workspaces,
|
|
86
|
+
byName,
|
|
87
|
+
findings
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function workspaceFor(index, file) {
|
|
91
|
+
return index.workspaces.find((w) => file.startsWith(`${w.path}/`));
|
|
92
|
+
}
|
|
93
|
+
function sourceTarget(workspace, file) {
|
|
94
|
+
const relative = file.slice(workspace.path.length + 1);
|
|
95
|
+
return Object.entries(workspace.metadata?.sourceTargets ?? {}).filter(([pattern]) => matches(pattern, relative)).sort(([a], [b]) => b.replaceAll("*", "").length - a.replaceAll("*", "").length)[0]?.[1] ?? workspace.metadata?.target ?? "universal";
|
|
96
|
+
}
|
|
97
|
+
function compatible(from, to) {
|
|
98
|
+
return to === "universal" || from === to || from === "bun" && to === "node";
|
|
99
|
+
}
|
|
100
|
+
function exportPaths(value) {
|
|
101
|
+
if (typeof value === "string") return [value];
|
|
102
|
+
if (value && typeof value === "object") return Object.values(value).flatMap(exportPaths);
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
function resolveImport(index, from, file, request) {
|
|
106
|
+
const name = request.startsWith("@") ? request.split("/").slice(0, 2).join("/") : request.split("/")[0];
|
|
107
|
+
const target = index.byName.get(name);
|
|
108
|
+
if (target) {
|
|
109
|
+
const subpath = request === name ? "." : `.${request.slice(name.length)}`;
|
|
110
|
+
const entry = Object.entries(target.manifest.exports ?? {}).find(([pattern]) => matches(pattern, subpath));
|
|
111
|
+
const suffix = entry?.[0].includes("*") ? subpath.slice(entry[0].split("*")[0].length) : "";
|
|
112
|
+
const source = entry && target.metadata?.sourceEntries?.[entry[0]];
|
|
113
|
+
return {
|
|
114
|
+
target,
|
|
115
|
+
subpath,
|
|
116
|
+
internalPath: !entry,
|
|
117
|
+
paths: entry ? (source ? [source] : exportPaths(entry[1])).map((p) => posix.join(target.path, p.replace("*", suffix))) : [],
|
|
118
|
+
targetPlatform: target.metadata?.entryTargets?.[entry?.[0] ?? subpath] ?? target.metadata?.target ?? "universal"
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const alias = Object.entries(from.aliases).find(([pattern]) => matches(pattern, request));
|
|
122
|
+
const internal = Object.entries(from.manifest.imports ?? {}).find(([pattern]) => matches(pattern, request));
|
|
123
|
+
const pattern = internal?.[0] ?? alias?.[0];
|
|
124
|
+
const mapped = internal?.[1] ?? alias?.[1][0];
|
|
125
|
+
const suffix = pattern?.includes("*") ? request.slice(pattern.split("*")[0].length) : "";
|
|
126
|
+
const path = request.startsWith(".") ? posix.join(posix.dirname(file), request) : mapped ? posix.join(from.path, mapped.replace("*", suffix)) : void 0;
|
|
127
|
+
const owner = path ? workspaceFor(index, path) : void 0;
|
|
128
|
+
return {
|
|
129
|
+
target: owner,
|
|
130
|
+
subpath: ".",
|
|
131
|
+
internalPath: !!owner && owner !== from,
|
|
132
|
+
paths: path ? [path] : [],
|
|
133
|
+
targetPlatform: owner ? sourceTarget(owner, path) : "universal"
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { compatible, discover, exportPaths, kinds, matches, resolveImport, sourceTarget, targets, workspaceFor, workspacePatterns };
|
|
138
|
+
|
|
139
|
+
//# sourceMappingURL=workspaces.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspaces.mjs","names":[],"sources":["../src/workspaces.ts"],"sourcesContent":["import { existsSync, globSync, readFileSync, readdirSync } from \"node:fs\";\nimport { join, posix } from \"node:path\";\n\nexport const targets = [\"universal\", \"browser\", \"native\", \"node\", \"bun\"] as const;\nexport type Target = (typeof targets)[number];\nexport const kinds = [\"app\", \"library\", \"tooling\"] as const;\nexport type Kind = (typeof kinds)[number];\n\n/** Per-workspace boundary metadata, supplied explicitly or read from package.json. */\nexport interface Metadata {\n kind: Kind;\n target: Target;\n entryTargets?: Record<string, Target>;\n sourceTargets?: Record<string, Target>;\n /** Maintained source behind an export whose published target is generated. */\n sourceEntries?: Record<string, string>;\n /** Development dependencies explicitly bundled into the published runtime. */\n bundledDependencies?: string[];\n}\nexport interface Manifest {\n name: string;\n private?: boolean;\n type?: string;\n exports?: Record<string, unknown>;\n imports?: Record<string, string>;\n scripts?: Record<string, string>;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n [key: string]: unknown;\n}\nexport interface Workspace {\n path: string;\n manifest: Manifest;\n metadata: Metadata | undefined;\n aliases: Record<string, string[]>;\n}\nexport interface WorkspaceIndex {\n root: string;\n metadataKey: string;\n /** Origin of workspace metadata; omitted legacy indexes use the manifest. */\n metadataSource?: \"manifest\" | \"options\";\n workspaces: Workspace[];\n byName: Map<string, Workspace>;\n findings: string[];\n}\nexport interface DiscoverOptions {\n /** Complete metadata by exact workspace-relative path; never merged with manifests. */\n metadata?: Readonly<Record<string, Metadata>>;\n /** The package.json field holding {@link Metadata}, used only when metadata is omitted. */\n metadataKey?: string;\n /** Directories whose children must each be a workspace. */\n containers?: readonly string[];\n}\n\n/** Matches package export, import-map and tsconfig path patterns, including `**`. */\nexport function matches(pattern: string, value: string): boolean {\n let regex = \"^\";\n for (let i = 0; i < pattern.length; i++) {\n const char = pattern[i]!;\n if (char === \"*\" && pattern[i + 1] === \"*\") {\n if (pattern[i + 2] === \"/\") {\n regex += \"(?:.*/)?\";\n i += 2;\n } else {\n regex += \".*\";\n i++;\n }\n } else if (char === \"*\") regex += \"[^/]*\";\n else regex += char.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n }\n return new RegExp(`${regex}$`).test(value);\n}\n\n/** Reads workspace patterns from the array or Bun/Yarn object form of `workspaces`. */\nexport function workspacePatterns(manifest: { workspaces?: unknown }): string[] {\n const value = manifest.workspaces;\n if (Array.isArray(value)) return value as string[];\n if (\n value &&\n typeof value === \"object\" &&\n Array.isArray((value as { packages?: unknown }).packages)\n )\n return (value as { packages: string[] }).packages;\n return [];\n}\n\nexport function discover(\n root: string,\n { metadata, metadataKey = \"devkit\", containers = [\"apps\", \"packages\"] }: DiscoverOptions = {},\n): WorkspaceIndex {\n const manifest = JSON.parse(readFileSync(join(root, \"package.json\"), \"utf8\"));\n const findings: string[] = [];\n const workspaces: Workspace[] = [];\n for (const file of globSync(\n workspacePatterns(manifest).map((p) => `${p}/package.json`),\n { cwd: root },\n ).sort()) {\n const path = file.replaceAll(\"\\\\\", \"/\").slice(0, -13);\n const entry = JSON.parse(readFileSync(join(root, file), \"utf8\")) as Manifest;\n const workspaceMetadata =\n metadata === undefined\n ? (entry[metadataKey] as Metadata | undefined)\n : Object.hasOwn(metadata, path)\n ? metadata[path]\n : undefined;\n if (metadata !== undefined && workspaceMetadata === undefined)\n findings.push(`${path}: missing supplied workspace metadata`);\n let aliases: Record<string, string[]> = {};\n const config = join(root, path, \"tsconfig.json\");\n if (existsSync(config)) {\n try {\n aliases = JSON.parse(readFileSync(config, \"utf8\")).compilerOptions?.paths ?? {};\n } catch {\n findings.push(`${path}/tsconfig.json: expected valid JSON`);\n }\n }\n workspaces.push({\n path,\n manifest: entry,\n metadata: workspaceMetadata,\n aliases,\n });\n }\n const byName = new Map<string, Workspace>();\n for (const workspace of workspaces) {\n if (byName.has(workspace.manifest.name))\n findings.push(`duplicate workspace: ${workspace.manifest.name}`);\n byName.set(workspace.manifest.name, workspace);\n }\n if (metadata !== undefined) {\n const paths = new Set(workspaces.map((workspace) => workspace.path));\n for (const path of Object.keys(metadata).sort()) {\n if (!paths.has(path)) findings.push(`${path}: metadata supplied for unknown workspace`);\n }\n }\n for (const parent of containers) {\n if (!existsSync(join(root, parent))) continue;\n for (const child of readdirSync(join(root, parent), { withFileTypes: true })) {\n if (!child.isDirectory() || child.name.startsWith(\".\")) continue;\n const path = `${parent}/${child.name}`;\n if (!workspaces.some((w) => w.path === path))\n findings.push(`${path}: missing workspace manifest`);\n }\n }\n return {\n root,\n metadataKey,\n metadataSource: metadata === undefined ? \"manifest\" : \"options\",\n workspaces,\n byName,\n findings,\n };\n}\n\nexport function workspaceFor(index: WorkspaceIndex, file: string) {\n return index.workspaces.find((w) => file.startsWith(`${w.path}/`));\n}\n\nexport function sourceTarget(workspace: Workspace, file: string): Target {\n const relative = file.slice(workspace.path.length + 1);\n const scopes = Object.entries(workspace.metadata?.sourceTargets ?? {})\n .filter(([pattern]) => matches(pattern, relative))\n .sort(([a], [b]) => b.replaceAll(\"*\", \"\").length - a.replaceAll(\"*\", \"\").length);\n return scopes[0]?.[1] ?? workspace.metadata?.target ?? \"universal\";\n}\n\nexport function compatible(from: Target, to: Target): boolean {\n return to === \"universal\" || from === to || (from === \"bun\" && to === \"node\");\n}\n\nexport function exportPaths(value: unknown): string[] {\n if (typeof value === \"string\") return [value];\n if (value && typeof value === \"object\") return Object.values(value).flatMap(exportPaths);\n return [];\n}\n\nexport function resolveImport(\n index: WorkspaceIndex,\n from: Workspace,\n file: string,\n request: string,\n) {\n const name = request.startsWith(\"@\")\n ? request.split(\"/\").slice(0, 2).join(\"/\")\n : request.split(\"/\")[0]!;\n const target = index.byName.get(name);\n if (target) {\n const subpath = request === name ? \".\" : `.${request.slice(name.length)}`;\n const entry = Object.entries(target.manifest.exports ?? {}).find(([pattern]) =>\n matches(pattern, subpath),\n );\n const suffix = entry?.[0].includes(\"*\") ? subpath.slice(entry[0].split(\"*\")[0]!.length) : \"\";\n const source = entry && target.metadata?.sourceEntries?.[entry[0]];\n return {\n target,\n subpath,\n internalPath: !entry,\n paths: entry\n ? (source ? [source] : exportPaths(entry[1])).map((p) =>\n posix.join(target.path, p.replace(\"*\", suffix)),\n )\n : [],\n targetPlatform:\n target.metadata?.entryTargets?.[entry?.[0] ?? subpath] ??\n target.metadata?.target ??\n \"universal\",\n };\n }\n const alias = Object.entries(from.aliases).find(([pattern]) => matches(pattern, request));\n const internal = Object.entries(from.manifest.imports ?? {}).find(([pattern]) =>\n matches(pattern, request),\n );\n const pattern = internal?.[0] ?? alias?.[0];\n const mapped = internal?.[1] ?? alias?.[1][0];\n const suffix = pattern?.includes(\"*\") ? request.slice(pattern.split(\"*\")[0]!.length) : \"\";\n const path = request.startsWith(\".\")\n ? posix.join(posix.dirname(file), request)\n : mapped\n ? posix.join(from.path, mapped.replace(\"*\", suffix))\n : undefined;\n const owner = path ? workspaceFor(index, path) : undefined;\n return {\n target: owner,\n subpath: \".\",\n internalPath: !!owner && owner !== from,\n paths: path ? [path] : [],\n targetPlatform: owner ? sourceTarget(owner, path!) : (\"universal\" as Target),\n };\n}\n"],"mappings":"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"}
|