@ploaness/cli 1.0.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/LICENSE +21 -0
- package/README.md +13 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +161 -0
- package/dist/bin.js.map +1 -0
- package/dist/checks/assets.d.ts +30 -0
- package/dist/checks/assets.d.ts.map +1 -0
- package/dist/checks/assets.js +169 -0
- package/dist/checks/assets.js.map +1 -0
- package/dist/checks/containers.d.ts +9 -0
- package/dist/checks/containers.d.ts.map +1 -0
- package/dist/checks/containers.js +194 -0
- package/dist/checks/containers.js.map +1 -0
- package/dist/checks/conventions.d.ts +8 -0
- package/dist/checks/conventions.d.ts.map +1 -0
- package/dist/checks/conventions.js +77 -0
- package/dist/checks/conventions.js.map +1 -0
- package/dist/checks/dependencies.d.ts +11 -0
- package/dist/checks/dependencies.d.ts.map +1 -0
- package/dist/checks/dependencies.js +389 -0
- package/dist/checks/dependencies.js.map +1 -0
- package/dist/checks/editorconfig.d.ts +5 -0
- package/dist/checks/editorconfig.d.ts.map +1 -0
- package/dist/checks/editorconfig.js +38 -0
- package/dist/checks/editorconfig.js.map +1 -0
- package/dist/checks/generated.d.ts +10 -0
- package/dist/checks/generated.d.ts.map +1 -0
- package/dist/checks/generated.js +65 -0
- package/dist/checks/generated.js.map +1 -0
- package/dist/checks/history.d.ts +19 -0
- package/dist/checks/history.d.ts.map +1 -0
- package/dist/checks/history.js +71 -0
- package/dist/checks/history.js.map +1 -0
- package/dist/checks/images.d.ts +14 -0
- package/dist/checks/images.d.ts.map +1 -0
- package/dist/checks/images.js +100 -0
- package/dist/checks/images.js.map +1 -0
- package/dist/checks/install.d.ts +5 -0
- package/dist/checks/install.d.ts.map +1 -0
- package/dist/checks/install.js +21 -0
- package/dist/checks/install.js.map +1 -0
- package/dist/checks/integrity.d.ts +11 -0
- package/dist/checks/integrity.d.ts.map +1 -0
- package/dist/checks/integrity.js +66 -0
- package/dist/checks/integrity.js.map +1 -0
- package/dist/checks/payload.d.ts +7 -0
- package/dist/checks/payload.d.ts.map +1 -0
- package/dist/checks/payload.js +81 -0
- package/dist/checks/payload.js.map +1 -0
- package/dist/checks/preflight.d.ts +5 -0
- package/dist/checks/preflight.d.ts.map +1 -0
- package/dist/checks/preflight.js +48 -0
- package/dist/checks/preflight.js.map +1 -0
- package/dist/checks/references.d.ts +18 -0
- package/dist/checks/references.d.ts.map +1 -0
- package/dist/checks/references.js +100 -0
- package/dist/checks/references.js.map +1 -0
- package/dist/checks/suppressions.d.ts +5 -0
- package/dist/checks/suppressions.d.ts.map +1 -0
- package/dist/checks/suppressions.js +50 -0
- package/dist/checks/suppressions.js.map +1 -0
- package/dist/checks/tailwind.d.ts +5 -0
- package/dist/checks/tailwind.d.ts.map +1 -0
- package/dist/checks/tailwind.js +29 -0
- package/dist/checks/tailwind.js.map +1 -0
- package/dist/checks/tests.d.ts +8 -0
- package/dist/checks/tests.d.ts.map +1 -0
- package/dist/checks/tests.js +123 -0
- package/dist/checks/tests.js.map +1 -0
- package/dist/checks/toolchain.d.ts +25 -0
- package/dist/checks/toolchain.d.ts.map +1 -0
- package/dist/checks/toolchain.js +165 -0
- package/dist/checks/toolchain.js.map +1 -0
- package/dist/checks/tree.d.ts +6 -0
- package/dist/checks/tree.d.ts.map +1 -0
- package/dist/checks/tree.js +52 -0
- package/dist/checks/tree.js.map +1 -0
- package/dist/checks/wiring.d.ts +5 -0
- package/dist/checks/wiring.d.ts.map +1 -0
- package/dist/checks/wiring.js +163 -0
- package/dist/checks/wiring.js.map +1 -0
- package/dist/commands/commit-message.d.ts +11 -0
- package/dist/commands/commit-message.d.ts.map +1 -0
- package/dist/commands/commit-message.js +57 -0
- package/dist/commands/commit-message.js.map +1 -0
- package/dist/commands/format.d.ts +4 -0
- package/dist/commands/format.d.ts.map +1 -0
- package/dist/commands/format.js +35 -0
- package/dist/commands/format.js.map +1 -0
- package/dist/commands/hooks.d.ts +9 -0
- package/dist/commands/hooks.d.ts.map +1 -0
- package/dist/commands/hooks.js +57 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +92 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/sync.d.ts +4 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +30 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/verify.d.ts +18 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +96 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/context.d.ts +157 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +345 -0
- package/dist/context.js.map +1 -0
- package/dist/exec.d.ts +80 -0
- package/dist/exec.d.ts.map +1 -0
- package/dist/exec.js +117 -0
- package/dist/exec.js.map +1 -0
- package/dist/gates.d.ts +61 -0
- package/dist/gates.d.ts.map +1 -0
- package/dist/gates.js +298 -0
- package/dist/gates.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/report.d.ts +40 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +224 -0
- package/dist/report.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Repository as Repo } from '../context.js';
|
|
2
|
+
import { type PlannedGate } from '../gates.js';
|
|
3
|
+
/**
|
|
4
|
+
* Run Default or Extended verification.
|
|
5
|
+
* @param repository the resolved repository environment.
|
|
6
|
+
* @param isExtended whether to include the history, build, bundle, and end-to-end gates.
|
|
7
|
+
* @returns the process exit code.
|
|
8
|
+
*/
|
|
9
|
+
export declare const verify: (repository: Repo, isExtended: boolean) => Promise<number>;
|
|
10
|
+
/**
|
|
11
|
+
* Run one gate by identifier. A single gate is a debugging aid, never a verdict.
|
|
12
|
+
* @param repository the resolved repository environment.
|
|
13
|
+
* @param planned the gate to run, with the member it is about.
|
|
14
|
+
* @param isVerbose whether to print what the gate's tool wrote, not only the verdict it produced.
|
|
15
|
+
* @returns the process exit code.
|
|
16
|
+
*/
|
|
17
|
+
export declare const verifyOne: (repository: Repo, planned: PlannedGate, isVerbose?: boolean) => Promise<number>;
|
|
18
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/commands/verify.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAU,UAAU,IAAI,IAAI,EAAE,MAAM,eAAe,CAAA;AAE/D,OAAO,EAAE,KAAK,WAAW,EAAW,MAAM,aAAa,CAAA;AAsFvD;;;;;GAKG;AACH,eAAO,MAAM,MAAM,GAAU,YAAY,IAAI,EAAE,YAAY,OAAO,KAAG,OAAO,CAAC,MAAM,CAOlF,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,GACpB,YAAY,IAAI,EAChB,SAAS,WAAW,EACpB,YAAW,OAAe,KACzB,OAAO,CAAC,MAAM,CAQhB,CAAA"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Verification: run the gates in order and report one verdict. A gate that throws is a failed gate, not
|
|
2
|
+
// a crashed run, because a tool that cannot start is indistinguishable from a tool that found a defect:
|
|
3
|
+
// either way the project is not verified.
|
|
4
|
+
import { endsRun } from '@ploaness/governance';
|
|
5
|
+
import { failed } from '../exec.js';
|
|
6
|
+
import { planFor } from '../gates.js';
|
|
7
|
+
import { beginGate, reportGate, reportHalt, reportHeader, reportNote, reportOutput, reportVerdict, } from '../report.js';
|
|
8
|
+
const asMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
9
|
+
// The union is narrowed exactly here, once. Everywhere else a gate is a gate; this is the one place
|
|
10
|
+
// that has to know a repository-scope gate is handed the repository and a member-scope gate its member.
|
|
11
|
+
const invoke = async (planned, repo) => {
|
|
12
|
+
if (planned.gate.scope === 'repository') {
|
|
13
|
+
return await planned.gate.run(repo);
|
|
14
|
+
}
|
|
15
|
+
const member = planned.member;
|
|
16
|
+
if (member === undefined) {
|
|
17
|
+
return failed(`the ${planned.gate.id} gate was planned without a member`, [
|
|
18
|
+
'this is a ploaness defect; the run plan and the registry disagree about this gate',
|
|
19
|
+
]);
|
|
20
|
+
}
|
|
21
|
+
return await planned.gate.run(member);
|
|
22
|
+
};
|
|
23
|
+
const runGate = async (planned, repo) => {
|
|
24
|
+
try {
|
|
25
|
+
return await invoke(planned, repo);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return failed(`the ${planned.gate.id} gate could not run`, [asMessage(error)]);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
/** Time one gate and package it as the outcome the report layer prints. */
|
|
32
|
+
const timeGate = async (planned, repo) => {
|
|
33
|
+
const started = Date.now();
|
|
34
|
+
const result = await runGate(planned, repo);
|
|
35
|
+
return {
|
|
36
|
+
gate: planned.gate,
|
|
37
|
+
result,
|
|
38
|
+
durationMs: Date.now() - started,
|
|
39
|
+
member: planned.member?.path,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
// The identifier column is sized to the widest gate in this run rather than to a fixed constant, so
|
|
43
|
+
// adding a longer gate identifier cannot silently push the summaries out of alignment.
|
|
44
|
+
const identifierWidth = (planned) => planned.reduce((widest, step) => Math.max(widest, step.gate.id.length), 0);
|
|
45
|
+
// A sequence with an exit rather than a plain map, because a run does not always reach the end. The
|
|
46
|
+
// rule that decides is `endsRun`, in governance; this supplies the outcome and the mode.
|
|
47
|
+
const runPlan = async (planned, repo, width) => {
|
|
48
|
+
const [step, ...rest] = planned;
|
|
49
|
+
if (step === undefined) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
beginGate(step.gate, width);
|
|
53
|
+
const outcome = await timeGate(step, repo);
|
|
54
|
+
reportGate(outcome, width);
|
|
55
|
+
const isPrecondition = step.gate.isPrecondition === true;
|
|
56
|
+
if (endsRun({
|
|
57
|
+
isFailure: !outcome.result.ok,
|
|
58
|
+
isPrecondition,
|
|
59
|
+
isEnforced: repo.isEnforced,
|
|
60
|
+
})) {
|
|
61
|
+
reportHalt(step.gate, rest.length, isPrecondition);
|
|
62
|
+
return [outcome];
|
|
63
|
+
}
|
|
64
|
+
return [outcome, ...(await runPlan(rest, repo, width))];
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Run Default or Extended verification.
|
|
68
|
+
* @param repository the resolved repository environment.
|
|
69
|
+
* @param isExtended whether to include the history, build, bundle, and end-to-end gates.
|
|
70
|
+
* @returns the process exit code.
|
|
71
|
+
*/
|
|
72
|
+
export const verify = async (repository, isExtended) => {
|
|
73
|
+
const started = Date.now();
|
|
74
|
+
const planned = planFor(repository, isExtended);
|
|
75
|
+
const width = identifierWidth(planned);
|
|
76
|
+
reportHeader(isExtended, planned.length);
|
|
77
|
+
const outcomes = await runPlan(planned, repository, width);
|
|
78
|
+
return reportVerdict(outcomes, isExtended, repository.isEnforced, Date.now() - started);
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Run one gate by identifier. A single gate is a debugging aid, never a verdict.
|
|
82
|
+
* @param repository the resolved repository environment.
|
|
83
|
+
* @param planned the gate to run, with the member it is about.
|
|
84
|
+
* @param isVerbose whether to print what the gate's tool wrote, not only the verdict it produced.
|
|
85
|
+
* @returns the process exit code.
|
|
86
|
+
*/
|
|
87
|
+
export const verifyOne = async (repository, planned, isVerbose = false) => {
|
|
88
|
+
const outcome = await timeGate(planned, repository);
|
|
89
|
+
reportGate(outcome, planned.gate.id.length);
|
|
90
|
+
if (isVerbose) {
|
|
91
|
+
reportOutput(outcome);
|
|
92
|
+
}
|
|
93
|
+
reportNote('A single gate is a debugging aid. Run `ploaness verify` for a verdict.');
|
|
94
|
+
return outcome.result.ok || !repository.isEnforced ? 0 : 1;
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/commands/verify.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { type ProjectManifest, type Settings } from '@ploaness/governance';
|
|
2
|
+
/** The resolved environment of one ploaness run. */
|
|
3
|
+
export interface Context {
|
|
4
|
+
/** The consuming project's repository root. */
|
|
5
|
+
readonly root: string;
|
|
6
|
+
/** The project's parsed package.json, or undefined when it has none. */
|
|
7
|
+
readonly packageJson: unknown;
|
|
8
|
+
/** The effective settings the project declared. */
|
|
9
|
+
readonly settings: Settings;
|
|
10
|
+
/** False in report-only mode, where findings print but the run still exits 0. */
|
|
11
|
+
readonly isEnforced: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* One governed package.
|
|
15
|
+
*
|
|
16
|
+
* `root` is the member's own directory, which is why every check that joins a path onto it kept working
|
|
17
|
+
* unchanged when members arrived: for a single-package repository the member root IS the repository
|
|
18
|
+
* root, so the field means exactly what it always meant.
|
|
19
|
+
*/
|
|
20
|
+
export interface Member extends Context {
|
|
21
|
+
/** Repo-relative, `.` for the member at the repository root. */
|
|
22
|
+
readonly path: string;
|
|
23
|
+
/** Whether this member declares Payload, which decides if the Payload-scope gates ask about it. */
|
|
24
|
+
readonly isPayload: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The other members' paths, relative to THIS member's directory.
|
|
27
|
+
*
|
|
28
|
+
* An analyzer that walks everything below where it runs has to be told where this package stops. At
|
|
29
|
+
* a workspace root every sibling sits underneath, and without this each sibling's entry point is
|
|
30
|
+
* reported as an unused file by a run that was never analysing it.
|
|
31
|
+
*/
|
|
32
|
+
readonly siblingPaths: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The repository a run judges, and every member inside it.
|
|
36
|
+
*
|
|
37
|
+
* The distinction this type exists to make is the one ploaness did not have: an install-script
|
|
38
|
+
* allowlist, an overrides block and a managed dotfile are facts about the REPOSITORY, and reading them
|
|
39
|
+
* from a member's directory produced a verdict about a file that was never there.
|
|
40
|
+
*/
|
|
41
|
+
export interface Repository extends Context {
|
|
42
|
+
/** The workspace file at the repository root - the only place pnpm honours these keys. */
|
|
43
|
+
readonly workspaceFile: string;
|
|
44
|
+
readonly members: readonly Member[];
|
|
45
|
+
/** Every pnpm project the workspace selects, governed or not, for the anti-bypass rule. */
|
|
46
|
+
readonly projects: readonly ProjectManifest[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Read a JSON file, returning undefined when it is absent or unparseable.
|
|
50
|
+
*
|
|
51
|
+
* Comments and trailing commas are tolerated, because the files this reads - a tsconfig, a Biome
|
|
52
|
+
* config, a runtime settings file - legally carry them, and a reader that refused them would report the
|
|
53
|
+
* project as malformed for writing what its own tools accept.
|
|
54
|
+
* @param file the absolute path.
|
|
55
|
+
* @returns the parsed value, or undefined.
|
|
56
|
+
*/
|
|
57
|
+
export declare const readJson: (file: string) => unknown;
|
|
58
|
+
/** Read a text file, returning undefined when it is absent. */
|
|
59
|
+
export declare const readText: (file: string) => string | undefined;
|
|
60
|
+
/** Build the run context for a project root. */
|
|
61
|
+
export declare const createContext: (root: string, isEnforced: boolean) => Context;
|
|
62
|
+
/**
|
|
63
|
+
* Resolve an executable script from a package, independent of hoisting. pnpm keeps a package's own
|
|
64
|
+
* dependencies out of the consumer's `.bin`, so a bare tool name is not reliably on PATH; reading the
|
|
65
|
+
* tool's manifest finds its entry point wherever the store put it.
|
|
66
|
+
* @param packageName the npm package providing the tool.
|
|
67
|
+
* @param binName the bin entry to resolve, defaulting to the package name.
|
|
68
|
+
* @param resolveFrom the require used to locate the package; defaults to the ploaness install.
|
|
69
|
+
* @returns the absolute path to the tool's JavaScript entry point.
|
|
70
|
+
*/
|
|
71
|
+
export declare const resolveTool: (packageName: string, binName?: string, resolveFrom?: NodeJS.Require) => string;
|
|
72
|
+
/**
|
|
73
|
+
* Whether this member has a server of its own, and so a build, a client bundle and a browser to drive.
|
|
74
|
+
*
|
|
75
|
+
* Asked of the MANIFEST rather than of the install. `next` resolves from a workspace root that declares
|
|
76
|
+
* nothing at all, because pnpm keeps a compatibility hoist at `node_modules/.pnpm/node_modules` that
|
|
77
|
+
* node's resolution walks into - so "is the tool reachable" answers yes for every member of a repository
|
|
78
|
+
* where any one member is an application. That is what sent `next build` at a root holding no `app`
|
|
79
|
+
* directory, and the failure named the missing directory rather than the member that should never have
|
|
80
|
+
* been built.
|
|
81
|
+
* @param context the member to judge.
|
|
82
|
+
* @returns true for a Payload or Next application, false for a library.
|
|
83
|
+
*/
|
|
84
|
+
export declare const hasOwnRuntime: (context: Context) => boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Resolve a tool from the CONSUMER's install rather than the harness's. Vitest, Playwright, Next, and
|
|
87
|
+
* the Payload CLI must be the project's own instance: the project's specs and config import those
|
|
88
|
+
* packages directly, and a second copy would load a different module registry.
|
|
89
|
+
*/
|
|
90
|
+
export declare const resolveProjectTool: (context: Context, packageName: string, binName?: string) => string;
|
|
91
|
+
/**
|
|
92
|
+
* Locate a package's manifest as the package at `fromManifest` resolves it.
|
|
93
|
+
*
|
|
94
|
+
* Resolution root matters here rather than being an implementation detail: under pnpm's strict layout
|
|
95
|
+
* `@ploaness/config` is reachable from `ploaness` and from nowhere else, so walking the manifests a
|
|
96
|
+
* project inherits has to re-root at each step. Undefined rather than a throw, because a package that
|
|
97
|
+
* does not resolve is a fact the caller reports rather than a broken run.
|
|
98
|
+
* @param packageName the package to locate.
|
|
99
|
+
* @param fromManifest the manifest whose resolution rules apply.
|
|
100
|
+
* @returns the absolute path to the package's own manifest, or undefined.
|
|
101
|
+
*/
|
|
102
|
+
export declare const manifestPathFrom: (packageName: string, fromManifest: string) => string | undefined;
|
|
103
|
+
/** Locate a directory inside a package ploaness depends on, for shipped configs and assets. */
|
|
104
|
+
export declare const shippedDirectory: (packageName: string) => string;
|
|
105
|
+
/** Locate this module's own package directory, used to report the running harness version. */
|
|
106
|
+
export declare const cliDirectory: () => string;
|
|
107
|
+
/** List the repository's tracked files, NUL-delimited so paths with spaces survive intact. */
|
|
108
|
+
export declare const trackedFiles: (root: string) => readonly string[];
|
|
109
|
+
/** Run a git command in the project and return its trimmed stdout. */
|
|
110
|
+
export declare const git: (context: Context, commandArguments: readonly string[]) => string;
|
|
111
|
+
/**
|
|
112
|
+
* The pinned versions ploaness owns, read from the shipped `pins.json`.
|
|
113
|
+
*
|
|
114
|
+
* One reader rather than one per gate: `preflight` decided the Node floor from a constant of its own,
|
|
115
|
+
* which is a rule living in the I/O layer AND a second copy of what this file already states.
|
|
116
|
+
* @returns the parsed pins, or an empty record when the file cannot be read.
|
|
117
|
+
*/
|
|
118
|
+
export declare const readPins: () => Record<string, unknown>;
|
|
119
|
+
/**
|
|
120
|
+
* Build the run context for a repository, discovering its governed members.
|
|
121
|
+
* @param cwd the directory ploaness was invoked from, which need not be the repository root.
|
|
122
|
+
* @param isEnforced false in report-only mode.
|
|
123
|
+
* @returns the repository, with one member for a single-package project.
|
|
124
|
+
*/
|
|
125
|
+
export declare const createRepository: (cwd: string, isEnforced: boolean) => Repository;
|
|
126
|
+
/**
|
|
127
|
+
* The member a single-gate invocation is about.
|
|
128
|
+
*
|
|
129
|
+
* `ploaness gate <id>` names no member, so it acts on the package containing the working directory. It
|
|
130
|
+
* resolves against every pnpm project rather than only the governed members, because a single gate is
|
|
131
|
+
* the tool an UNGOVERNED repository is surveyed with: before `ploaness` is declared anywhere there are
|
|
132
|
+
* no governed members but the root, and resolving to the root would silently measure the wrong package
|
|
133
|
+
* - reporting the repository's suppression ceiling to someone standing in an application asking about
|
|
134
|
+
* that application. A whole `verify` run still judges only what the repository actually governs.
|
|
135
|
+
* @param repository the repository being judged.
|
|
136
|
+
* @param cwd the directory ploaness was invoked from.
|
|
137
|
+
* @returns the member for the innermost project containing cwd, or the first governed member.
|
|
138
|
+
*/
|
|
139
|
+
export declare const memberAt: (repository: Repository, cwd: string) => Member | undefined;
|
|
140
|
+
/**
|
|
141
|
+
* The environment variables a gate must hand a child process that BOOTS the project.
|
|
142
|
+
*
|
|
143
|
+
* A Payload configuration validates `process.env` at module scope, so a tool that evaluates it needs
|
|
144
|
+
* the project's own environment before it can run at all - and the Payload CLI, unlike Next, reads no
|
|
145
|
+
* `.env` itself. The other three gates that boot the project were each covered by something that does:
|
|
146
|
+
* `next build` reads them, and the runners ploaness ships load them from `vitest-setup.ts` and
|
|
147
|
+
* `playwright.ts`. The generator was the one path with nothing on it, so it evaluated the
|
|
148
|
+
* configuration with those variables absent and produced the artefacts of a project that does not
|
|
149
|
+
* exist.
|
|
150
|
+
*
|
|
151
|
+
* Parsing is `node:util`'s, which is the parser `process.loadEnvFile` uses, so a file read here and the
|
|
152
|
+
* same file read by the runners cannot be read two different ways.
|
|
153
|
+
* @param context the member whose environment files are read.
|
|
154
|
+
* @returns the variables to add, already narrowed to those the run does not carry.
|
|
155
|
+
*/
|
|
156
|
+
export declare const runEnvironment: (context: Context) => Readonly<Record<string, string>>;
|
|
157
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAOA,OAAO,EAUL,KAAK,eAAe,EAWpB,KAAK,QAAQ,EAEd,MAAM,sBAAsB,CAAA;AAS7B,oDAAoD;AACpD,MAAM,WAAW,OAAO;IACtB,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,wEAAwE;IACxE,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;IAC7B,mDAAmD;IACnD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;IAC3B,iFAAiF;IACjF,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAA;CAC7B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,MAAO,SAAQ,OAAO;IACrC,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,mGAAmG;IACnG,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;CACzC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,UAAW,SAAQ,OAAO;IACzC,0FAA0F;IAC1F,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,2FAA2F;IAC3F,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAA;CAC9C;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,KAAG,OACqC,CAAA;AAE7E,+DAA+D;AAC/D,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,KAAG,MAAM,GAAG,SACU,CAAA;AAE3D,gDAAgD;AAChD,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,EAAE,YAAY,OAAO,KAAG,OAGjE,CAAA;AA0CD;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,GACtB,aAAa,MAAM,EACnB,UAAU,MAAM,EAChB,cAAa,MAAM,CAAC,OAAqB,KACxC,MAoBF,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,GAAI,SAAS,OAAO,KAAG,OACF,CAAA;AAE/C;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,OAAO,EAChB,aAAa,MAAM,EACnB,UAAU,MAAM,KACf,MACwF,CAAA;AAE3F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,EAAE,cAAc,MAAM,KAAG,MAAM,GAAG,SAMrF,CAAA;AAED,+FAA+F;AAC/F,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,KAAG,MACX,CAAA;AAE5C,8FAA8F;AAC9F,eAAO,MAAM,YAAY,QAAO,MACkC,CAAA;AAElE,8FAA8F;AAC9F,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,SAAS,MAAM,EASR,CAAA;AAEnD,sEAAsE;AACtE,eAAO,MAAM,GAAG,GAAI,SAAS,OAAO,EAAE,kBAAkB,SAAS,MAAM,EAAE,KAAG,MAQjE,CAAA;AAEX;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,QAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAGjD,CAAA;AAsHD;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,KAAK,MAAM,EAAE,YAAY,OAAO,KAAG,UAiCnE,CAAA;AAuBD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,QAAQ,GAAI,YAAY,UAAU,EAAE,KAAK,MAAM,KAAG,MAAM,GAAG,SAyBvE,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,OAAO,KAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAS9E,CAAA"}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
// Everything a gate needs to know about the project it is judging, resolved once per run.
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { parseEnv } from 'node:util';
|
|
8
|
+
import { asRecord, asStringRecord, findGovernedMembers, findRepositoryRoot, hasRuntime, isPayloadProject, memberKindOf, parseJsonc, ploanessBlock, ROOT_MEMBER_PATH, readKey, readMemberSettings, readSettings, readWorkspacePackages, rebaseExclusion, runEnvironmentFiles, runEnvironmentOverrides, selectProjects, } from '@ploaness/governance';
|
|
9
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
10
|
+
const BYTES_PER_KIB = 1024;
|
|
11
|
+
const KIB_PER_MIB = 1024;
|
|
12
|
+
// Large enough that no analyzer's output is truncated before it can be reported.
|
|
13
|
+
const MAX_OUTPUT_MIB = 64;
|
|
14
|
+
const MAX_OUTPUT_BYTES = MAX_OUTPUT_MIB * KIB_PER_MIB * BYTES_PER_KIB;
|
|
15
|
+
/**
|
|
16
|
+
* Read a JSON file, returning undefined when it is absent or unparseable.
|
|
17
|
+
*
|
|
18
|
+
* Comments and trailing commas are tolerated, because the files this reads - a tsconfig, a Biome
|
|
19
|
+
* config, a runtime settings file - legally carry them, and a reader that refused them would report the
|
|
20
|
+
* project as malformed for writing what its own tools accept.
|
|
21
|
+
* @param file the absolute path.
|
|
22
|
+
* @returns the parsed value, or undefined.
|
|
23
|
+
*/
|
|
24
|
+
export const readJson = (file) => existsSync(file) ? parseJsonc(readFileSync(file, 'utf8')).value : undefined;
|
|
25
|
+
/** Read a text file, returning undefined when it is absent. */
|
|
26
|
+
export const readText = (file) => existsSync(file) ? readFileSync(file, 'utf8') : undefined;
|
|
27
|
+
/** Build the run context for a project root. */
|
|
28
|
+
export const createContext = (root, isEnforced) => {
|
|
29
|
+
const packageJson = readJson(path.join(root, 'package.json'));
|
|
30
|
+
return { root, packageJson, settings: readSettings(packageJson), isEnforced };
|
|
31
|
+
};
|
|
32
|
+
// Locating a tool is harder than it looks. A package may restrict its `exports` so `pkg/package.json` is
|
|
33
|
+
// not addressable (dependency-cruiser), and it may publish an `import` condition only, so CJS resolution
|
|
34
|
+
// of its entry point fails too. Three routes are tried in turn, and the last one is the one that always
|
|
35
|
+
// works: find the entry point through ESM resolution and walk up to the manifest beside it.
|
|
36
|
+
const manifestFrom = (directory) => {
|
|
37
|
+
const candidate = path.join(directory, 'package.json');
|
|
38
|
+
if (existsSync(candidate)) {
|
|
39
|
+
return candidate;
|
|
40
|
+
}
|
|
41
|
+
const parent = path.dirname(directory);
|
|
42
|
+
if (parent === directory) {
|
|
43
|
+
throw new Error(`no package.json found above ${directory}`);
|
|
44
|
+
}
|
|
45
|
+
return manifestFrom(parent);
|
|
46
|
+
};
|
|
47
|
+
const manifestBeside = (entry) => manifestFrom(path.dirname(entry));
|
|
48
|
+
const manifestOf = (packageName, resolveFrom) => {
|
|
49
|
+
try {
|
|
50
|
+
return resolveFrom.resolve(`${packageName}/package.json`);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// fall through
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return manifestBeside(resolveFrom.resolve(packageName));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// fall through
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return manifestBeside(fileURLToPath(import.meta.resolve(packageName)));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new Error(`ploaness could not locate the manifest of ${packageName}`, { cause: error });
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/** Resolve a path inside a package, from a chosen resolution root. */
|
|
69
|
+
const packageDirectory = (packageName, resolveFrom) => path.dirname(manifestOf(packageName, resolveFrom));
|
|
70
|
+
/**
|
|
71
|
+
* Resolve an executable script from a package, independent of hoisting. pnpm keeps a package's own
|
|
72
|
+
* dependencies out of the consumer's `.bin`, so a bare tool name is not reliably on PATH; reading the
|
|
73
|
+
* tool's manifest finds its entry point wherever the store put it.
|
|
74
|
+
* @param packageName the npm package providing the tool.
|
|
75
|
+
* @param binName the bin entry to resolve, defaulting to the package name.
|
|
76
|
+
* @param resolveFrom the require used to locate the package; defaults to the ploaness install.
|
|
77
|
+
* @returns the absolute path to the tool's JavaScript entry point.
|
|
78
|
+
*/
|
|
79
|
+
export const resolveTool = (packageName, binName, resolveFrom = nodeRequire) => {
|
|
80
|
+
const manifestPath = manifestOf(packageName, resolveFrom);
|
|
81
|
+
// A tool's own manifest, so an unreadable one is the installation being broken rather than the
|
|
82
|
+
// project being wrong. `format` resolves a tool outside any gate, where a raw SyntaxError would reach
|
|
83
|
+
// the user as a stack trace with no mention of which package it came from.
|
|
84
|
+
const read = parseJsonc(readFileSync(manifestPath, 'utf8'));
|
|
85
|
+
if (read.problem !== undefined) {
|
|
86
|
+
throw new Error(`ploaness could not read ${manifestPath}: ${read.problem}`);
|
|
87
|
+
}
|
|
88
|
+
// Narrowed through the governance guards rather than asserted. `bin` is either a path or a map of
|
|
89
|
+
// names to paths, and both cases are read here as what they are: an assertion would be a claim the
|
|
90
|
+
// compiler cannot check, and `type-coverage --strict` counts every one as untyped.
|
|
91
|
+
const declaredBin = readKey(read.value, 'bin');
|
|
92
|
+
const wanted = binName ?? packageName;
|
|
93
|
+
const bin = typeof declaredBin === 'string' ? declaredBin : asStringRecord(declaredBin)[wanted];
|
|
94
|
+
if (bin === undefined) {
|
|
95
|
+
throw new Error(`ploaness could not resolve the "${wanted}" executable from ${packageName}`);
|
|
96
|
+
}
|
|
97
|
+
return path.join(path.dirname(manifestPath), bin);
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Whether this member has a server of its own, and so a build, a client bundle and a browser to drive.
|
|
101
|
+
*
|
|
102
|
+
* Asked of the MANIFEST rather than of the install. `next` resolves from a workspace root that declares
|
|
103
|
+
* nothing at all, because pnpm keeps a compatibility hoist at `node_modules/.pnpm/node_modules` that
|
|
104
|
+
* node's resolution walks into - so "is the tool reachable" answers yes for every member of a repository
|
|
105
|
+
* where any one member is an application. That is what sent `next build` at a root holding no `app`
|
|
106
|
+
* directory, and the failure named the missing directory rather than the member that should never have
|
|
107
|
+
* been built.
|
|
108
|
+
* @param context the member to judge.
|
|
109
|
+
* @returns true for a Payload or Next application, false for a library.
|
|
110
|
+
*/
|
|
111
|
+
export const hasOwnRuntime = (context) => hasRuntime(memberKindOf(context.packageJson));
|
|
112
|
+
/**
|
|
113
|
+
* Resolve a tool from the CONSUMER's install rather than the harness's. Vitest, Playwright, Next, and
|
|
114
|
+
* the Payload CLI must be the project's own instance: the project's specs and config import those
|
|
115
|
+
* packages directly, and a second copy would load a different module registry.
|
|
116
|
+
*/
|
|
117
|
+
export const resolveProjectTool = (context, packageName, binName) => resolveTool(packageName, binName, createRequire(path.join(context.root, 'package.json')));
|
|
118
|
+
/**
|
|
119
|
+
* Locate a package's manifest as the package at `fromManifest` resolves it.
|
|
120
|
+
*
|
|
121
|
+
* Resolution root matters here rather than being an implementation detail: under pnpm's strict layout
|
|
122
|
+
* `@ploaness/config` is reachable from `ploaness` and from nowhere else, so walking the manifests a
|
|
123
|
+
* project inherits has to re-root at each step. Undefined rather than a throw, because a package that
|
|
124
|
+
* does not resolve is a fact the caller reports rather than a broken run.
|
|
125
|
+
* @param packageName the package to locate.
|
|
126
|
+
* @param fromManifest the manifest whose resolution rules apply.
|
|
127
|
+
* @returns the absolute path to the package's own manifest, or undefined.
|
|
128
|
+
*/
|
|
129
|
+
export const manifestPathFrom = (packageName, fromManifest) => {
|
|
130
|
+
try {
|
|
131
|
+
return manifestOf(packageName, createRequire(fromManifest));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
/** Locate a directory inside a package ploaness depends on, for shipped configs and assets. */
|
|
138
|
+
export const shippedDirectory = (packageName) => packageDirectory(packageName, nodeRequire);
|
|
139
|
+
/** Locate this module's own package directory, used to report the running harness version. */
|
|
140
|
+
export const cliDirectory = () => path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
141
|
+
/** List the repository's tracked files, NUL-delimited so paths with spaces survive intact. */
|
|
142
|
+
export const trackedFiles = (root) => execFileSync('git', ['ls-files', '-z'], {
|
|
143
|
+
cwd: root,
|
|
144
|
+
encoding: 'utf8',
|
|
145
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
146
|
+
// A directory that is not a repository is a case the caller handles, not a diagnosis to print.
|
|
147
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
148
|
+
})
|
|
149
|
+
.split('\0')
|
|
150
|
+
.filter((file) => file !== '');
|
|
151
|
+
/** Run a git command in the project and return its trimmed stdout. */
|
|
152
|
+
export const git = (context, commandArguments) => execFileSync('git', [...commandArguments], {
|
|
153
|
+
cwd: context.root,
|
|
154
|
+
encoding: 'utf8',
|
|
155
|
+
// git writes its own diagnosis to stderr, which for a directory that is not a repository is not a
|
|
156
|
+
// finding but noise around one the caller reports itself.
|
|
157
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
158
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
159
|
+
}).trim();
|
|
160
|
+
/**
|
|
161
|
+
* The pinned versions ploaness owns, read from the shipped `pins.json`.
|
|
162
|
+
*
|
|
163
|
+
* One reader rather than one per gate: `preflight` decided the Node floor from a constant of its own,
|
|
164
|
+
* which is a rule living in the I/O layer AND a second copy of what this file already states.
|
|
165
|
+
* @returns the parsed pins, or an empty record when the file cannot be read.
|
|
166
|
+
*/
|
|
167
|
+
export const readPins = () => {
|
|
168
|
+
const pinsFile = path.join(shippedDirectory('@ploaness/config'), 'pins.json');
|
|
169
|
+
return asRecord(readJson(pinsFile));
|
|
170
|
+
};
|
|
171
|
+
const MANIFEST = 'package.json';
|
|
172
|
+
const WORKSPACE_FILE = 'pnpm-workspace.yaml';
|
|
173
|
+
// Every directory from the working directory up to the filesystem root, nearest first, which is the
|
|
174
|
+
// order `findRepositoryRoot` searches.
|
|
175
|
+
const ancestorsOf = (directory) => {
|
|
176
|
+
const parent = path.dirname(directory);
|
|
177
|
+
return parent === directory ? [directory] : [directory, ...ancestorsOf(parent)];
|
|
178
|
+
};
|
|
179
|
+
const hasEntry = (directory, entry) => existsSync(path.join(directory, entry));
|
|
180
|
+
// Candidate project directories come from the tracked tree rather than from a filesystem walk, so a
|
|
181
|
+
// directory git does not know about - a build output, an ignored scratch copy, an uninstalled example -
|
|
182
|
+
// can never become a governed member.
|
|
183
|
+
// Candidate project directories come from the tracked tree rather than a filesystem walk, so a
|
|
184
|
+
// directory git does not know about - a build output, an ignored scratch copy - can never become a
|
|
185
|
+
// governed member. A directory that is not a repository at all has exactly one project, itself: the
|
|
186
|
+
// integration fixtures install into such a directory before any commit exists, and asking git there
|
|
187
|
+
// fails rather than answering.
|
|
188
|
+
const manifestDirectories = (root) => {
|
|
189
|
+
try {
|
|
190
|
+
return trackedManifestDirectories(root);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// Not a repository yet. `ploaness init` is run on exactly such a tree - a fresh checkout, or a
|
|
194
|
+
// workspace being adopted before its first commit - and answering "one project, the root" there
|
|
195
|
+
// would scaffold the root and silently leave every other package unwired.
|
|
196
|
+
return walkManifestDirectories(root);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
// Bounded and exclusionary on purpose. A full walk of a workspace would descend into every dependency's
|
|
200
|
+
// own dependencies; nothing ploaness governs is nested deeper than a couple of directories, and neither
|
|
201
|
+
// an installed package nor a tool's cache is a project this repository owns.
|
|
202
|
+
const WALK_DEPTH = 3;
|
|
203
|
+
const isSkipped = (name) => name.startsWith('.') || name === 'node_modules';
|
|
204
|
+
const walkManifestDirectories = (root, relative = ROOT_MEMBER_PATH, depth = 0) => {
|
|
205
|
+
const absolute = relative === ROOT_MEMBER_PATH ? root : path.join(root, relative);
|
|
206
|
+
const here = existsSync(path.join(absolute, MANIFEST)) ? [relative] : [];
|
|
207
|
+
if (depth >= WALK_DEPTH) {
|
|
208
|
+
return here;
|
|
209
|
+
}
|
|
210
|
+
const children = readdirSync(absolute, { withFileTypes: true })
|
|
211
|
+
.filter((entry) => entry.isDirectory() && !isSkipped(entry.name))
|
|
212
|
+
.flatMap((entry) => walkManifestDirectories(root, relative === ROOT_MEMBER_PATH ? entry.name : path.join(relative, entry.name), depth + 1));
|
|
213
|
+
return [...here, ...children];
|
|
214
|
+
};
|
|
215
|
+
const trackedManifestDirectories = (root) => trackedFiles(root)
|
|
216
|
+
.filter((file) => path.basename(file) === MANIFEST)
|
|
217
|
+
.map((file) => {
|
|
218
|
+
const directory = path.dirname(file);
|
|
219
|
+
return directory === '' ? ROOT_MEMBER_PATH : directory;
|
|
220
|
+
});
|
|
221
|
+
const readManifest = (root, projectPath) => readJson(path.join(root, projectPath, MANIFEST));
|
|
222
|
+
// A member's settings sit on top of the repository's rather than replacing them, so a fact declared
|
|
223
|
+
// once at the root - a generated directory the typography ban must skip, a stricter bundle budget -
|
|
224
|
+
// reaches every package without being restated in each.
|
|
225
|
+
// The members that sit INSIDE this one, named relative to it. A member elsewhere in the tree is not
|
|
226
|
+
// below the directory an analyzer walks, so naming it would ignore a path that never appears; and one
|
|
227
|
+
// that is below must be named the way the walk will see it, from here rather than from the root.
|
|
228
|
+
const nestedWithin = (memberPath, every) => {
|
|
229
|
+
if (memberPath === ROOT_MEMBER_PATH) {
|
|
230
|
+
return every.filter((other) => other !== ROOT_MEMBER_PATH);
|
|
231
|
+
}
|
|
232
|
+
const prefix = `${memberPath}/`;
|
|
233
|
+
return every
|
|
234
|
+
.filter((other) => other.startsWith(prefix))
|
|
235
|
+
.map((other) => other.slice(prefix.length));
|
|
236
|
+
};
|
|
237
|
+
const createMember = ({ root, projectPath, isEnforced, repositoryBlock = {}, siblings = [], }) => {
|
|
238
|
+
const packageJson = readManifest(root, projectPath);
|
|
239
|
+
return {
|
|
240
|
+
root: projectPath === ROOT_MEMBER_PATH ? root : path.join(root, projectPath),
|
|
241
|
+
packageJson,
|
|
242
|
+
settings: readMemberSettings(repositoryBlock, ploanessBlock(packageJson)),
|
|
243
|
+
isEnforced,
|
|
244
|
+
path: projectPath,
|
|
245
|
+
isPayload: isPayloadProject(packageJson),
|
|
246
|
+
siblingPaths: siblings,
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Build the run context for a repository, discovering its governed members.
|
|
251
|
+
* @param cwd the directory ploaness was invoked from, which need not be the repository root.
|
|
252
|
+
* @param isEnforced false in report-only mode.
|
|
253
|
+
* @returns the repository, with one member for a single-package project.
|
|
254
|
+
*/
|
|
255
|
+
export const createRepository = (cwd, isEnforced) => {
|
|
256
|
+
const root = findRepositoryRoot(ancestorsOf(path.resolve(cwd)), hasEntry);
|
|
257
|
+
const workspaceFile = readText(path.join(root, WORKSPACE_FILE)) ?? '';
|
|
258
|
+
const projects = selectProjects(readWorkspacePackages(workspaceFile), manifestDirectories(root)).map((projectPath) => ({
|
|
259
|
+
path: projectPath,
|
|
260
|
+
packageJson: readManifest(root, projectPath),
|
|
261
|
+
}));
|
|
262
|
+
const packageJson = readManifest(root, ROOT_MEMBER_PATH);
|
|
263
|
+
const memberPaths = findGovernedMembers(projects);
|
|
264
|
+
const members = memberPaths.map((projectPath) => createMember({
|
|
265
|
+
root,
|
|
266
|
+
projectPath,
|
|
267
|
+
isEnforced,
|
|
268
|
+
repositoryBlock: ploanessBlock(packageJson),
|
|
269
|
+
siblings: nestedWithin(projectPath, memberPaths),
|
|
270
|
+
}));
|
|
271
|
+
return {
|
|
272
|
+
root,
|
|
273
|
+
packageJson,
|
|
274
|
+
settings: withMemberExclusions(readSettings(packageJson), members),
|
|
275
|
+
isEnforced,
|
|
276
|
+
workspaceFile,
|
|
277
|
+
projects,
|
|
278
|
+
members,
|
|
279
|
+
};
|
|
280
|
+
};
|
|
281
|
+
// The gates that walk the tracked tree run once, at the root, while a member declares its exclusions
|
|
282
|
+
// relative to itself. Without this a generated directory a member correctly excused starts failing
|
|
283
|
+
// them, because the pattern it wrote is anchored one directory above where the gate is looking.
|
|
284
|
+
const withMemberExclusions = (base, members) => {
|
|
285
|
+
const rebased = members.flatMap((member) => member.settings.declaredExclusions.map((entry) => rebaseExclusion(member.path, entry)));
|
|
286
|
+
const patternsFor = (setting) => rebased
|
|
287
|
+
.filter((entry) => entry.setting === setting)
|
|
288
|
+
.map((entry) => entry.pattern);
|
|
289
|
+
return {
|
|
290
|
+
...base,
|
|
291
|
+
typographyExclusions: [...base.typographyExclusions, ...patternsFor('typographyExclusions')],
|
|
292
|
+
javascriptAllowlist: [...base.javascriptAllowlist, ...patternsFor('javascriptAllowlist')],
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* The member a single-gate invocation is about.
|
|
297
|
+
*
|
|
298
|
+
* `ploaness gate <id>` names no member, so it acts on the package containing the working directory. It
|
|
299
|
+
* resolves against every pnpm project rather than only the governed members, because a single gate is
|
|
300
|
+
* the tool an UNGOVERNED repository is surveyed with: before `ploaness` is declared anywhere there are
|
|
301
|
+
* no governed members but the root, and resolving to the root would silently measure the wrong package
|
|
302
|
+
* - reporting the repository's suppression ceiling to someone standing in an application asking about
|
|
303
|
+
* that application. A whole `verify` run still judges only what the repository actually governs.
|
|
304
|
+
* @param repository the repository being judged.
|
|
305
|
+
* @param cwd the directory ploaness was invoked from.
|
|
306
|
+
* @returns the member for the innermost project containing cwd, or the first governed member.
|
|
307
|
+
*/
|
|
308
|
+
export const memberAt = (repository, cwd) => {
|
|
309
|
+
const resolved = path.resolve(cwd);
|
|
310
|
+
const containsCwd = (projectPath) => {
|
|
311
|
+
const full = projectPath === ROOT_MEMBER_PATH ? repository.root : path.join(repository.root, projectPath);
|
|
312
|
+
return resolved === full || resolved.startsWith(`${full}${path.sep}`);
|
|
313
|
+
};
|
|
314
|
+
// The deepest match, so a nested project wins over the root that also contains it.
|
|
315
|
+
const innermost = [...repository.projects]
|
|
316
|
+
.filter((project) => containsCwd(project.path))
|
|
317
|
+
.sort((left, right) => right.path.length - left.path.length)[0];
|
|
318
|
+
if (innermost === undefined) {
|
|
319
|
+
return repository.members[0];
|
|
320
|
+
}
|
|
321
|
+
return (repository.members.find((member) => member.path === innermost.path) ??
|
|
322
|
+
createMember({
|
|
323
|
+
root: repository.root,
|
|
324
|
+
projectPath: innermost.path,
|
|
325
|
+
isEnforced: repository.isEnforced,
|
|
326
|
+
}));
|
|
327
|
+
};
|
|
328
|
+
/**
|
|
329
|
+
* The environment variables a gate must hand a child process that BOOTS the project.
|
|
330
|
+
*
|
|
331
|
+
* A Payload configuration validates `process.env` at module scope, so a tool that evaluates it needs
|
|
332
|
+
* the project's own environment before it can run at all - and the Payload CLI, unlike Next, reads no
|
|
333
|
+
* `.env` itself. The other three gates that boot the project were each covered by something that does:
|
|
334
|
+
* `next build` reads them, and the runners ploaness ships load them from `vitest-setup.ts` and
|
|
335
|
+
* `playwright.ts`. The generator was the one path with nothing on it, so it evaluated the
|
|
336
|
+
* configuration with those variables absent and produced the artefacts of a project that does not
|
|
337
|
+
* exist.
|
|
338
|
+
*
|
|
339
|
+
* Parsing is `node:util`'s, which is the parser `process.loadEnvFile` uses, so a file read here and the
|
|
340
|
+
* same file read by the runners cannot be read two different ways.
|
|
341
|
+
* @param context the member whose environment files are read.
|
|
342
|
+
* @returns the variables to add, already narrowed to those the run does not carry.
|
|
343
|
+
*/
|
|
344
|
+
export const runEnvironment = (context) => runEnvironmentOverrides(process.env, runEnvironmentFiles((relativePath) => existsSync(path.join(context.root, relativePath))).map((file) => parseEnv(readFileSync(path.join(context.root, file), 'utf8'))));
|
|
345
|
+
//# sourceMappingURL=context.js.map
|