limina 0.0.1
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/bin/limina.js +25 -0
- package/chunks/dep-DoSHsBSP.js +2026 -0
- package/chunks/dep-jgc7X0zw.js +377 -0
- package/chunks/dep-lkQg1P9Q.js +3 -0
- package/cli.js +2287 -0
- package/config.d.ts +483 -0
- package/config.js +4 -0
- package/index.d.ts +144 -0
- package/index.js +5 -0
- package/package.json +56 -0
package/cli.js
ADDED
|
@@ -0,0 +1,2287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./chunks/dep-lkQg1P9Q.js";
|
|
3
|
+
import { d as normalizeSlashes, f as normalizeWorkspacePath, h as toRelativePath, i as loadConfig, l as isPathInsideDirectory, m as toPosixPath, n as getActiveCheckerExtensions, p as toAbsolutePath, r as getActiveCheckers, u as normalizeAbsolutePath } from "./chunks/dep-jgc7X0zw.js";
|
|
4
|
+
import { A as collectImporters, B as createFormatHost, C as inferPackageProject$1, D as parseProject$1, E as isWorkspacePackageFile, F as collectGraphProjectRoute, G as isOrdinaryTypecheckConfigPath, H as getDtsCompanionConfigPath, I as collectGraphProjectRouteFromRoot, J as readJsonConfig, K as parseProjectFileNames, L as collectGraphProjectRoutes, M as findPackageForSpecifier, N as getPackageRootSpecifier, O as resolveInternalImport$1, P as collectCheckerEntryProjectRoutes, S as getTypecheckConfigPath, T as isRelativeSpecifier, U as getRawReferencePaths, V as formatReferences, W as isDtsConfigPath, X as resolveReferencePath, Y as resolveProjectConfigPath, _ as createFileOwnerLookup$1, a as runSourceCheck, b as findTargetProject, c as PackageLogger, d as clearCliScreen, f as formatErrorMessage$1, g as collectImportsFromFile$1, h as normalizeGraphRules, i as runCheckerTypecheck, j as collectWorkspacePackages, k as shouldResolveThroughGraph$1, l as PathsLogger, m as getDeniedWorkspaceDepRule, n as createLiminaFlowReporter, o as CliLogger, p as getDeniedRefRule, q as parseProjectFileNamesForExtensions, r as runCheckerBuild, s as GraphLogger, u as ProofLogger, v as findImporterForFile$1, w as isDtsProjectConfig, x as formatArtifactDependencyPolicy, y as findPackageForFile, z as createExtensionPattern } from "./chunks/dep-DoSHsBSP.js";
|
|
5
|
+
import { builtinModules } from "node:module";
|
|
6
|
+
import { cac } from "cac";
|
|
7
|
+
import { createElapsedTimer } from "@docs-islands/logger/helper";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import ts from "typescript";
|
|
11
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
12
|
+
import { glob } from "tinyglobby";
|
|
13
|
+
import { checkPackage, createPackageFromTarballData } from "@arethetypeswrong/core";
|
|
14
|
+
import { pack } from "@publint/pack";
|
|
15
|
+
import { init, parse } from "es-module-lexer";
|
|
16
|
+
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { publint } from "publint";
|
|
19
|
+
import { formatMessage } from "publint/utils";
|
|
20
|
+
|
|
21
|
+
//#region src/commands/graph.ts
|
|
22
|
+
const requiredDtsCompilerOptions = [
|
|
23
|
+
["composite", true],
|
|
24
|
+
["incremental", true],
|
|
25
|
+
["noEmit", false],
|
|
26
|
+
["declaration", true],
|
|
27
|
+
["emitDeclarationOnly", true]
|
|
28
|
+
];
|
|
29
|
+
const requiredDtsPathOptions = [
|
|
30
|
+
"rootDir",
|
|
31
|
+
"outDir",
|
|
32
|
+
"tsBuildInfoFile"
|
|
33
|
+
];
|
|
34
|
+
const comparableTypecheckOptions = [
|
|
35
|
+
"allowArbitraryExtensions",
|
|
36
|
+
"allowImportingTsExtensions",
|
|
37
|
+
"allowJs",
|
|
38
|
+
"allowSyntheticDefaultImports",
|
|
39
|
+
"checkJs",
|
|
40
|
+
"customConditions",
|
|
41
|
+
"esModuleInterop",
|
|
42
|
+
"exactOptionalPropertyTypes",
|
|
43
|
+
"forceConsistentCasingInFileNames",
|
|
44
|
+
"isolatedDeclarations",
|
|
45
|
+
"isolatedModules",
|
|
46
|
+
"jsx",
|
|
47
|
+
"jsxImportSource",
|
|
48
|
+
"lib",
|
|
49
|
+
"module",
|
|
50
|
+
"moduleDetection",
|
|
51
|
+
"moduleResolution",
|
|
52
|
+
"noFallthroughCasesInSwitch",
|
|
53
|
+
"noImplicitAny",
|
|
54
|
+
"noImplicitOverride",
|
|
55
|
+
"noImplicitReturns",
|
|
56
|
+
"noImplicitThis",
|
|
57
|
+
"noPropertyAccessFromIndexSignature",
|
|
58
|
+
"noUncheckedIndexedAccess",
|
|
59
|
+
"resolveJsonModule",
|
|
60
|
+
"skipLibCheck",
|
|
61
|
+
"strict",
|
|
62
|
+
"strictBindCallApply",
|
|
63
|
+
"strictFunctionTypes",
|
|
64
|
+
"strictNullChecks",
|
|
65
|
+
"strictPropertyInitialization",
|
|
66
|
+
"target",
|
|
67
|
+
"typeRoots",
|
|
68
|
+
"types",
|
|
69
|
+
"useDefineForClassFields",
|
|
70
|
+
"verbatimModuleSyntax"
|
|
71
|
+
];
|
|
72
|
+
function formatCompilerOptionValue(value) {
|
|
73
|
+
if (value === void 0) return "undefined";
|
|
74
|
+
return JSON.stringify(value);
|
|
75
|
+
}
|
|
76
|
+
function compilerOptionEquals(left, right) {
|
|
77
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
78
|
+
}
|
|
79
|
+
function addDtsOptionProblems(config, project, problems) {
|
|
80
|
+
if (!isDtsProjectConfig(project.configPath)) return;
|
|
81
|
+
for (const [optionName, expected] of requiredDtsCompilerOptions) {
|
|
82
|
+
const actual = project.options[optionName];
|
|
83
|
+
if (actual === expected) continue;
|
|
84
|
+
problems.push([
|
|
85
|
+
"Invalid declaration leaf compiler option:",
|
|
86
|
+
` project: ${toRelativePath(config.rootDir, project.configPath)}`,
|
|
87
|
+
` option: compilerOptions.${optionName}`,
|
|
88
|
+
` expected: ${formatCompilerOptionValue(expected)}`,
|
|
89
|
+
` actual: ${formatCompilerOptionValue(actual)}`,
|
|
90
|
+
" reason: tsconfig*.dts.json projects are consumed by tsc -b and must emit declarations through composite incremental builds."
|
|
91
|
+
].join("\n"));
|
|
92
|
+
}
|
|
93
|
+
for (const optionName of requiredDtsPathOptions) {
|
|
94
|
+
if (project.options[optionName]) continue;
|
|
95
|
+
problems.push([
|
|
96
|
+
"Missing declaration leaf output option:",
|
|
97
|
+
` project: ${toRelativePath(config.rootDir, project.configPath)}`,
|
|
98
|
+
` option: compilerOptions.${optionName}`,
|
|
99
|
+
" reason: declaration leaves need explicit root/output state so declaration output and tsbuildinfo files do not collide."
|
|
100
|
+
].join("\n"));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function addTypecheckParityProblems(config, dtsProject, problems) {
|
|
104
|
+
if (!isDtsProjectConfig(dtsProject.configPath)) return;
|
|
105
|
+
const typecheckConfigPath = getTypecheckConfigPath(dtsProject.configPath);
|
|
106
|
+
if (!existsSync(typecheckConfigPath)) {
|
|
107
|
+
problems.push([
|
|
108
|
+
"Missing typecheck companion config:",
|
|
109
|
+
` declaration leaf: ${toRelativePath(config.rootDir, dtsProject.configPath)}`,
|
|
110
|
+
` expected typecheck config: ${toRelativePath(config.rootDir, typecheckConfigPath)}`,
|
|
111
|
+
" reason: every tsconfig*.dts.json project should have a matching tsconfig*.json file with the same typechecking semantics."
|
|
112
|
+
].join("\n"));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const typecheckProject = parseProject$1(config, typecheckConfigPath);
|
|
116
|
+
for (const optionName of comparableTypecheckOptions) {
|
|
117
|
+
const buildValue = dtsProject.options[optionName];
|
|
118
|
+
const typecheckValue = typecheckProject.options[optionName];
|
|
119
|
+
if (compilerOptionEquals(buildValue, typecheckValue)) continue;
|
|
120
|
+
problems.push([
|
|
121
|
+
"Typecheck option mismatch between declaration leaf and companion config:",
|
|
122
|
+
` declaration leaf: ${toRelativePath(config.rootDir, dtsProject.configPath)}`,
|
|
123
|
+
` typecheck config: ${toRelativePath(config.rootDir, typecheckConfigPath)}`,
|
|
124
|
+
` option: compilerOptions.${optionName}`,
|
|
125
|
+
` declaration value: ${formatCompilerOptionValue(buildValue)}`,
|
|
126
|
+
` typecheck value: ${formatCompilerOptionValue(typecheckValue)}`,
|
|
127
|
+
" reason: tsconfig*.dts.json should emit with the same typechecking semantics as its matching tsconfig*.json companion."
|
|
128
|
+
].join("\n"));
|
|
129
|
+
}
|
|
130
|
+
const typecheckFiles = new Set(typecheckProject.fileNames);
|
|
131
|
+
const missingFiles = dtsProject.fileNames.filter((fileName) => !typecheckFiles.has(fileName));
|
|
132
|
+
if (missingFiles.length === 0) return;
|
|
133
|
+
problems.push([
|
|
134
|
+
"Declaration leaf includes files missing from its companion typecheck config:",
|
|
135
|
+
` declaration leaf: ${toRelativePath(config.rootDir, dtsProject.configPath)}`,
|
|
136
|
+
` typecheck config: ${toRelativePath(config.rootDir, typecheckConfigPath)}`,
|
|
137
|
+
" files:",
|
|
138
|
+
...missingFiles.slice(0, 10).map((fileName) => ` - ${toRelativePath(config.rootDir, fileName)}`),
|
|
139
|
+
...missingFiles.length > 10 ? [` ...and ${missingFiles.length - 10} more`] : [],
|
|
140
|
+
" reason: a declaration leaf must not emit declarations for files that are not covered by the matching typecheck target."
|
|
141
|
+
].join("\n"));
|
|
142
|
+
}
|
|
143
|
+
function addDeniedReferenceProblems(options) {
|
|
144
|
+
if (!options.project.label) return;
|
|
145
|
+
for (const referencePath of options.project.references) {
|
|
146
|
+
if (!options.projectsByPath.has(referencePath)) continue;
|
|
147
|
+
const deniedRefRule = getDeniedRefRule(options.rules, options.project.label, referencePath);
|
|
148
|
+
const targetPackage = findPackageForFile(referencePath, options.packages);
|
|
149
|
+
const deniedDepRule = targetPackage ? getDeniedWorkspaceDepRule(options.rules, options.project.label, targetPackage.name) : null;
|
|
150
|
+
if (!deniedRefRule && !deniedDepRule) continue;
|
|
151
|
+
const lines = [
|
|
152
|
+
"Denied graph access:",
|
|
153
|
+
` rule: ${options.project.label}`,
|
|
154
|
+
` referencing project: ${toRelativePath(options.config.rootDir, options.project.configPath)}`,
|
|
155
|
+
` referenced project: ${toRelativePath(options.config.rootDir, referencePath)}`
|
|
156
|
+
];
|
|
157
|
+
if (deniedRefRule) lines.push(` denied ref: ${toRelativePath(options.config.rootDir, deniedRefRule.path)}`, ` reason: ${deniedRefRule.reason}`);
|
|
158
|
+
else if (deniedDepRule) lines.push(` denied dependency: ${deniedDepRule.name}`, ` reason: ${deniedDepRule.reason}`);
|
|
159
|
+
options.problems.push(lines.join("\n"));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function addDeniedPackageImportProblem(options) {
|
|
163
|
+
options.problems.push([
|
|
164
|
+
"Denied graph access:",
|
|
165
|
+
` rule: ${options.project.label}`,
|
|
166
|
+
` importing project: ${toRelativePath(options.config.rootDir, options.project.configPath)}`,
|
|
167
|
+
` file: ${toRelativePath(options.config.rootDir, options.importRecord.filePath)}:${options.importRecord.line}`,
|
|
168
|
+
` imported specifier: ${options.importRecord.specifier}`,
|
|
169
|
+
` denied dependency: ${options.rule.name}`,
|
|
170
|
+
` reason: ${options.rule.reason}`
|
|
171
|
+
].join("\n"));
|
|
172
|
+
}
|
|
173
|
+
function addDeniedRefImportProblem(options) {
|
|
174
|
+
options.problems.push([
|
|
175
|
+
"Denied graph access:",
|
|
176
|
+
` rule: ${options.project.label}`,
|
|
177
|
+
` importing project: ${toRelativePath(options.config.rootDir, options.project.configPath)}`,
|
|
178
|
+
` file: ${toRelativePath(options.config.rootDir, options.importRecord.filePath)}:${options.importRecord.line}`,
|
|
179
|
+
` imported specifier: ${options.importRecord.specifier}`,
|
|
180
|
+
` target project: ${toRelativePath(options.config.rootDir, options.targetProjectPath)}`,
|
|
181
|
+
` denied ref: ${toRelativePath(options.config.rootDir, options.rule.path)}`,
|
|
182
|
+
` reason: ${options.rule.reason}`
|
|
183
|
+
].join("\n"));
|
|
184
|
+
}
|
|
185
|
+
function addWorkspaceReferenceDependencyProblems(config, project, projectsByPath, packages, importers, problems) {
|
|
186
|
+
if (!isDtsProjectConfig(project.configPath)) return;
|
|
187
|
+
const sourcePackage = findPackageForFile(project.configPath, packages);
|
|
188
|
+
const importer = sourcePackage ? findImporterForFile$1(project.configPath, importers) : null;
|
|
189
|
+
if (!sourcePackage) return;
|
|
190
|
+
for (const referencePath of project.references) {
|
|
191
|
+
if (!projectsByPath.has(referencePath)) continue;
|
|
192
|
+
const targetPackage = findPackageForFile(referencePath, packages);
|
|
193
|
+
if (!targetPackage || targetPackage.name === sourcePackage.name) continue;
|
|
194
|
+
if (importer?.workspaceDependencies.has(targetPackage.name)) continue;
|
|
195
|
+
problems.push([
|
|
196
|
+
"Project reference crosses workspace packages without a workspace:* dependency:",
|
|
197
|
+
` referencing project: ${toRelativePath(config.rootDir, project.configPath)}`,
|
|
198
|
+
` referenced project: ${toRelativePath(config.rootDir, referencePath)}`,
|
|
199
|
+
` referencing package: ${sourcePackage.name}`,
|
|
200
|
+
` referenced package: ${targetPackage.name}`,
|
|
201
|
+
` package manifest: ${toRelativePath(config.rootDir, path.join(sourcePackage.directory, "package.json"))}`,
|
|
202
|
+
` reason: a cross-package tsconfig*.dts.json reference is a source dependency edge, so ${sourcePackage.name} must declare ${targetPackage.name} with the workspace: protocol.`,
|
|
203
|
+
` fix: add "${targetPackage.name}": "workspace:*" to dependencies, devDependencies, peerDependencies, or optionalDependencies in the referencing package manifest. If this package intentionally consumes built artifacts, remove the project reference; ${formatArtifactDependencyPolicy(targetPackage)}`
|
|
204
|
+
].join("\n"));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function runGraphCheckInternal(config, options = {}) {
|
|
208
|
+
const graphRoute = collectGraphProjectRoute(config);
|
|
209
|
+
const projectPaths = graphRoute.projectPaths;
|
|
210
|
+
const projects = projectPaths.map((projectPath) => parseProject$1(config, projectPath));
|
|
211
|
+
const projectsByPath = new Map(projects.map((project) => [project.configPath, project]));
|
|
212
|
+
const fileOwnerLookup = createFileOwnerLookup$1(projects);
|
|
213
|
+
const packages = await collectWorkspacePackages(config);
|
|
214
|
+
const importers = collectImporters(config, packages);
|
|
215
|
+
const problems = [...graphRoute.problems];
|
|
216
|
+
const graphRules = normalizeGraphRules({
|
|
217
|
+
config,
|
|
218
|
+
include: {
|
|
219
|
+
nodeBuiltins: false,
|
|
220
|
+
refs: true,
|
|
221
|
+
workspaceDeps: true
|
|
222
|
+
},
|
|
223
|
+
packages,
|
|
224
|
+
problems,
|
|
225
|
+
projectPaths
|
|
226
|
+
});
|
|
227
|
+
for (const project of projects) {
|
|
228
|
+
if (project.labelProblem) problems.push(project.labelProblem);
|
|
229
|
+
addDtsOptionProblems(config, project, problems);
|
|
230
|
+
addTypecheckParityProblems(config, project, problems);
|
|
231
|
+
addDeniedReferenceProblems({
|
|
232
|
+
config,
|
|
233
|
+
packages,
|
|
234
|
+
problems,
|
|
235
|
+
project,
|
|
236
|
+
projectsByPath,
|
|
237
|
+
rules: graphRules
|
|
238
|
+
});
|
|
239
|
+
addWorkspaceReferenceDependencyProblems(config, project, projectsByPath, packages, importers, problems);
|
|
240
|
+
for (const filePath of project.fileNames) for (const importRecord of collectImportsFromFile$1(filePath)) {
|
|
241
|
+
const resolvedFilePath = resolveInternalImport$1(importRecord.specifier, filePath, project.options);
|
|
242
|
+
const targetPackage = findPackageForSpecifier(importRecord.specifier, packages);
|
|
243
|
+
const importer = targetPackage ? findImporterForFile$1(importRecord.filePath, importers) : null;
|
|
244
|
+
const unresolvedDeniedDepRule = targetPackage ? getDeniedWorkspaceDepRule(graphRules, project.label, targetPackage.name) : null;
|
|
245
|
+
if (!resolvedFilePath) {
|
|
246
|
+
if (unresolvedDeniedDepRule) {
|
|
247
|
+
addDeniedPackageImportProblem({
|
|
248
|
+
config,
|
|
249
|
+
importRecord,
|
|
250
|
+
problems,
|
|
251
|
+
project,
|
|
252
|
+
rule: unresolvedDeniedDepRule
|
|
253
|
+
});
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (!targetPackage) continue;
|
|
257
|
+
problems.push([
|
|
258
|
+
"Unresolved workspace import:",
|
|
259
|
+
` importing project: ${toRelativePath(config.rootDir, project.configPath)}`,
|
|
260
|
+
` file: ${toRelativePath(config.rootDir, importRecord.filePath)}:${importRecord.line}`,
|
|
261
|
+
` imported specifier: ${importRecord.specifier}`,
|
|
262
|
+
` matched workspace package: ${targetPackage.name}`,
|
|
263
|
+
` current references: ${formatReferences(config.rootDir, project.references)}`
|
|
264
|
+
].join("\n"));
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const targetWorkspacePackageForResolved = findPackageForFile(resolvedFilePath, packages);
|
|
268
|
+
const deniedDepRule = (targetPackage ? getDeniedWorkspaceDepRule(graphRules, project.label, targetPackage.name) : null) ?? (targetWorkspacePackageForResolved ? getDeniedWorkspaceDepRule(graphRules, project.label, targetWorkspacePackageForResolved.name) : null);
|
|
269
|
+
if (deniedDepRule) {
|
|
270
|
+
addDeniedPackageImportProblem({
|
|
271
|
+
config,
|
|
272
|
+
importRecord,
|
|
273
|
+
problems,
|
|
274
|
+
project,
|
|
275
|
+
rule: deniedDepRule
|
|
276
|
+
});
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (isRelativeSpecifier(importRecord.specifier)) {
|
|
280
|
+
const sourcePackage = findPackageForFile(importRecord.filePath, packages);
|
|
281
|
+
if (sourcePackage && targetWorkspacePackageForResolved && sourcePackage.name !== targetWorkspacePackageForResolved.name) {
|
|
282
|
+
problems.push([
|
|
283
|
+
"Cross-package relative import:",
|
|
284
|
+
` importing project: ${toRelativePath(config.rootDir, project.configPath)}`,
|
|
285
|
+
` file: ${toRelativePath(config.rootDir, importRecord.filePath)}:${importRecord.line}`,
|
|
286
|
+
` imported specifier: ${importRecord.specifier}`,
|
|
287
|
+
` source package: ${sourcePackage.name}`,
|
|
288
|
+
` target package: ${targetWorkspacePackageForResolved.name}`,
|
|
289
|
+
` resolved file: ${toRelativePath(config.rootDir, resolvedFilePath)}`,
|
|
290
|
+
" reason: workspace packages must depend through package exports."
|
|
291
|
+
].join("\n"));
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (targetPackage && !shouldResolveThroughGraph$1(importer, targetPackage)) continue;
|
|
296
|
+
if (targetPackage && shouldResolveThroughGraph$1(importer, targetPackage) && !fileOwnerLookup.has(resolvedFilePath)) {
|
|
297
|
+
const referencedProjectPath = inferPackageProject$1(resolvedFilePath, targetPackage, projectPaths);
|
|
298
|
+
const hasProjectReference = referencedProjectPath && project.references.has(referencedProjectPath);
|
|
299
|
+
problems.push([
|
|
300
|
+
hasProjectReference ? "Referenced workspace dependency resolves through package exports to a build artifact:" : "Workspace source dependency resolved outside the source graph:",
|
|
301
|
+
` importing project: ${toRelativePath(config.rootDir, project.configPath)}`,
|
|
302
|
+
...referencedProjectPath ? [` referenced project: ${toRelativePath(config.rootDir, referencedProjectPath)}`, ` project reference present: ${hasProjectReference ? "yes" : "no"}`] : [],
|
|
303
|
+
` file: ${toRelativePath(config.rootDir, importRecord.filePath)}:${importRecord.line}`,
|
|
304
|
+
` imported specifier: ${importRecord.specifier}`,
|
|
305
|
+
` resolved file: ${toRelativePath(config.rootDir, resolvedFilePath)}`,
|
|
306
|
+
" reason: workspace:* dependencies are source dependencies, but TypeScript resolved this package export to a file not owned by the source graph. tsc -b does not rewrite package exports through project references.",
|
|
307
|
+
` fix: expose source files from the dependency package exports, add a source paths config to this declaration leaf extends, or stop using workspace:* plus project references for artifact consumption; ${formatArtifactDependencyPolicy(targetPackage)}`,
|
|
308
|
+
" hint: run `limina paths generate` to create a compatibility paths file, then manually add it to the first position of the listed tsconfig*.dts.json extends array."
|
|
309
|
+
].join("\n"));
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const targetProjectPath = findTargetProject({
|
|
313
|
+
fileOwnerLookup,
|
|
314
|
+
packages,
|
|
315
|
+
projectPaths,
|
|
316
|
+
resolvedFilePath,
|
|
317
|
+
specifier: importRecord.specifier
|
|
318
|
+
});
|
|
319
|
+
if (!targetProjectPath) {
|
|
320
|
+
if (!targetPackage) continue;
|
|
321
|
+
if (!isWorkspacePackageFile(resolvedFilePath, packages)) {
|
|
322
|
+
if (targetPackage && shouldResolveThroughGraph$1(importer, targetPackage)) problems.push([
|
|
323
|
+
"Workspace source import resolved outside the workspace graph:",
|
|
324
|
+
` importing project: ${toRelativePath(config.rootDir, project.configPath)}`,
|
|
325
|
+
` file: ${toRelativePath(config.rootDir, importRecord.filePath)}:${importRecord.line}`,
|
|
326
|
+
` imported specifier: ${importRecord.specifier}`,
|
|
327
|
+
` resolved file: ${toRelativePath(config.rootDir, resolvedFilePath)}`,
|
|
328
|
+
` reason: workspace:* dependencies are source dependency edges and must resolve to files owned by the source graph; ${formatArtifactDependencyPolicy(targetPackage)}`
|
|
329
|
+
].join("\n"));
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
problems.push([
|
|
333
|
+
"Unable to map workspace import to a graph project:",
|
|
334
|
+
` importing project: ${toRelativePath(config.rootDir, project.configPath)}`,
|
|
335
|
+
` file: ${toRelativePath(config.rootDir, importRecord.filePath)}:${importRecord.line}`,
|
|
336
|
+
` imported specifier: ${importRecord.specifier}`,
|
|
337
|
+
` resolved file: ${toRelativePath(config.rootDir, resolvedFilePath)}`,
|
|
338
|
+
` current references: ${formatReferences(config.rootDir, project.references)}`
|
|
339
|
+
].join("\n"));
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (targetProjectPath === project.configPath) continue;
|
|
343
|
+
const deniedRefRule = getDeniedRefRule(graphRules, project.label, targetProjectPath);
|
|
344
|
+
if (deniedRefRule) {
|
|
345
|
+
addDeniedRefImportProblem({
|
|
346
|
+
config,
|
|
347
|
+
importRecord,
|
|
348
|
+
problems,
|
|
349
|
+
project,
|
|
350
|
+
rule: deniedRefRule,
|
|
351
|
+
targetProjectPath
|
|
352
|
+
});
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (!projectsByPath.has(targetProjectPath)) {
|
|
356
|
+
problems.push([
|
|
357
|
+
"Expected graph target is not reachable from any checker entry:",
|
|
358
|
+
` importing project: ${toRelativePath(config.rootDir, project.configPath)}`,
|
|
359
|
+
` file: ${toRelativePath(config.rootDir, importRecord.filePath)}:${importRecord.line}`,
|
|
360
|
+
` imported specifier: ${importRecord.specifier}`,
|
|
361
|
+
` expected graph project: ${toRelativePath(config.rootDir, targetProjectPath)}`
|
|
362
|
+
].join("\n"));
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (!project.references.has(targetProjectPath)) problems.push([
|
|
366
|
+
"Missing project reference for workspace import:",
|
|
367
|
+
` importing project: ${toRelativePath(config.rootDir, project.configPath)}`,
|
|
368
|
+
` file: ${toRelativePath(config.rootDir, importRecord.filePath)}:${importRecord.line}`,
|
|
369
|
+
` imported specifier: ${importRecord.specifier}`,
|
|
370
|
+
` expected reference: ${toRelativePath(config.rootDir, targetProjectPath)}`,
|
|
371
|
+
` current references: ${formatReferences(config.rootDir, project.references)}`
|
|
372
|
+
].join("\n"));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (problems.length > 0) {
|
|
376
|
+
GraphLogger.error(problems.join("\n\n"));
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
if (options.logSuccess ?? true) GraphLogger.success(`Checked ${projects.length} graph projects; references are valid.`);
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
async function runGraphCheck(config, options = {}) {
|
|
383
|
+
if (options.clearScreen ?? true) clearCliScreen();
|
|
384
|
+
const elapsed = createElapsedTimer();
|
|
385
|
+
const task = options.flow?.start("graph check", { depth: options.flowDepth ?? 0 });
|
|
386
|
+
GraphLogger.info("graph check started");
|
|
387
|
+
try {
|
|
388
|
+
const logSuccess = !options.flow?.interactive;
|
|
389
|
+
const passed = await runGraphCheckInternal(config, { logSuccess });
|
|
390
|
+
if (passed) {
|
|
391
|
+
if (logSuccess) GraphLogger.success("graph check finished", elapsed());
|
|
392
|
+
task?.pass();
|
|
393
|
+
} else {
|
|
394
|
+
GraphLogger.error("graph check finished with failures", elapsed());
|
|
395
|
+
task?.fail("graph check finished with failures");
|
|
396
|
+
}
|
|
397
|
+
return passed;
|
|
398
|
+
} catch (error) {
|
|
399
|
+
GraphLogger.error(`graph check failed: ${formatErrorMessage$1(error)}`, elapsed());
|
|
400
|
+
task?.fail("graph check failed", { error });
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
//#endregion
|
|
406
|
+
//#region src/commands/package.ts
|
|
407
|
+
const DEFAULT_PACKAGE_CHECKS = [
|
|
408
|
+
"publint",
|
|
409
|
+
"attw",
|
|
410
|
+
"boundary"
|
|
411
|
+
];
|
|
412
|
+
const PACKAGE_CHECK_TOOLS = new Set(DEFAULT_PACKAGE_CHECKS);
|
|
413
|
+
const ATTW_PROFILE_IGNORED_RESOLUTIONS = {
|
|
414
|
+
strict: [],
|
|
415
|
+
node16: [],
|
|
416
|
+
"esm-only": ["node16-cjs"]
|
|
417
|
+
};
|
|
418
|
+
const nodeBuiltinSpecifiers = new Set(builtinModules.flatMap((specifier) => specifier.startsWith("node:") ? [specifier, specifier.slice(5)] : [specifier, `node:${specifier}`]));
|
|
419
|
+
function isRecord(value) {
|
|
420
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
421
|
+
}
|
|
422
|
+
function isPackageCheckTool(value) {
|
|
423
|
+
return PACKAGE_CHECK_TOOLS.has(value);
|
|
424
|
+
}
|
|
425
|
+
function isRelativeOrAbsoluteSpecifier(specifier) {
|
|
426
|
+
return specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("http:") || specifier.startsWith("https:") || specifier.startsWith("data:");
|
|
427
|
+
}
|
|
428
|
+
function toArrayBuffer(buffer) {
|
|
429
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
430
|
+
}
|
|
431
|
+
function collectSelfSpecifierMatchers(packageName, exportsField) {
|
|
432
|
+
const exact = new Set([packageName]);
|
|
433
|
+
const prefixes = [];
|
|
434
|
+
if (!isRecord(exportsField)) return {
|
|
435
|
+
exact,
|
|
436
|
+
prefixes
|
|
437
|
+
};
|
|
438
|
+
for (const exportKey of Object.keys(exportsField)) {
|
|
439
|
+
if (exportKey === ".") {
|
|
440
|
+
exact.add(packageName);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (!exportKey.startsWith("./")) continue;
|
|
444
|
+
const normalizedSubpath = exportKey.slice(2);
|
|
445
|
+
if (normalizedSubpath.length === 0) continue;
|
|
446
|
+
const wildcardIndex = normalizedSubpath.indexOf("*");
|
|
447
|
+
if (wildcardIndex !== -1) {
|
|
448
|
+
prefixes.push(`${packageName}/${normalizedSubpath.slice(0, wildcardIndex)}`);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
exact.add(`${packageName}/${normalizedSubpath}`);
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
exact,
|
|
455
|
+
prefixes
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function isAllowedSelfSpecifier(specifier, matchers) {
|
|
459
|
+
return matchers.exact.has(specifier) || matchers.prefixes.some((prefix) => specifier.startsWith(prefix));
|
|
460
|
+
}
|
|
461
|
+
function normalizePublishedModulePath(relativeFilePath) {
|
|
462
|
+
return relativeFilePath.replaceAll("\\", "/");
|
|
463
|
+
}
|
|
464
|
+
function classifyRuntimeEnvironment(target, relativeFilePath) {
|
|
465
|
+
if (typeof target.environment === "function") return target.environment(relativeFilePath);
|
|
466
|
+
if (target.environment) return target.environment;
|
|
467
|
+
const normalizedPath = normalizePublishedModulePath(relativeFilePath);
|
|
468
|
+
return normalizedPath.startsWith("node/") || normalizedPath.startsWith("plugin/") ? "node" : "browser";
|
|
469
|
+
}
|
|
470
|
+
async function collectPublishedModuleFiles(directoryPath) {
|
|
471
|
+
const entries = await readdir(directoryPath, { withFileTypes: true });
|
|
472
|
+
const files = [];
|
|
473
|
+
for (const entry of entries) {
|
|
474
|
+
const absolutePath = path.join(directoryPath, entry.name);
|
|
475
|
+
if (entry.isDirectory()) {
|
|
476
|
+
files.push(...await collectPublishedModuleFiles(absolutePath));
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (/\.[cm]?js$/u.test(entry.name)) files.push(absolutePath);
|
|
480
|
+
}
|
|
481
|
+
return files;
|
|
482
|
+
}
|
|
483
|
+
function validatePublishedSpecifier(options) {
|
|
484
|
+
const { allowedExternalPackages, environment, packageName, selfSpecifiers, specifier } = options;
|
|
485
|
+
if (isRelativeOrAbsoluteSpecifier(specifier)) return null;
|
|
486
|
+
if (nodeBuiltinSpecifiers.has(specifier)) {
|
|
487
|
+
if (environment === "node") return null;
|
|
488
|
+
return `browser/runtime output must not import Node builtin "${specifier}"`;
|
|
489
|
+
}
|
|
490
|
+
const packageRoot = getPackageRootSpecifier(specifier);
|
|
491
|
+
if (packageRoot === packageName) {
|
|
492
|
+
if (isAllowedSelfSpecifier(specifier, selfSpecifiers)) return null;
|
|
493
|
+
return `self import "${specifier}" is not exposed by output package.json exports`;
|
|
494
|
+
}
|
|
495
|
+
if (allowedExternalPackages.has(packageRoot)) return null;
|
|
496
|
+
return `"${specifier}" resolves to package "${packageRoot}" which is not listed in dependencies, peerDependencies, optionalDependencies, or self exports`;
|
|
497
|
+
}
|
|
498
|
+
function formatAttwProblem(problem) {
|
|
499
|
+
const resolutionKind = "resolutionKind" in problem ? ` [resolution: ${problem.resolutionKind}]` : "";
|
|
500
|
+
const entrypoint = "entrypoint" in problem ? ` [entrypoint: ${problem.entrypoint}]` : "";
|
|
501
|
+
switch (problem.kind) {
|
|
502
|
+
case "NoResolution": return `No resolution${resolutionKind}${entrypoint}`;
|
|
503
|
+
case "UntypedResolution": return `Untyped resolution${resolutionKind}${entrypoint}`;
|
|
504
|
+
case "FalseESM": return `False ESM: ${problem.typesFileName} -> ${problem.implementationFileName}`;
|
|
505
|
+
case "FalseCJS": return `False CJS: ${problem.typesFileName} -> ${problem.implementationFileName}`;
|
|
506
|
+
case "CJSResolvesToESM": return `CJS resolves to ESM${resolutionKind}${entrypoint}`;
|
|
507
|
+
case "FallbackCondition": return `Fallback condition used${resolutionKind}${entrypoint}`;
|
|
508
|
+
case "NamedExports": return problem.isMissingAllNamed ? `Named exports missing: all named exports [types: ${problem.typesFileName}] [implementation: ${problem.implementationFileName}]` : `Named exports missing: ${problem.missing.join(", ") || "(none)"} [types: ${problem.typesFileName}] [implementation: ${problem.implementationFileName}]`;
|
|
509
|
+
case "FalseExportDefault": return `False export default [types: ${problem.typesFileName}] [implementation: ${problem.implementationFileName}]`;
|
|
510
|
+
case "MissingExportEquals": return `Missing export equals [types: ${problem.typesFileName}] [implementation: ${problem.implementationFileName}]`;
|
|
511
|
+
case "InternalResolutionError": return `Internal resolution error in ${problem.fileName} [option: ${problem.resolutionOption}] [module: ${problem.moduleSpecifier}]`;
|
|
512
|
+
case "UnexpectedModuleSyntax": return `Unexpected module syntax in ${problem.fileName}`;
|
|
513
|
+
case "CJSOnlyExportsDefault": return `CJS only exports default in ${problem.fileName}`;
|
|
514
|
+
default: return `Unknown ATTW problem: ${JSON.stringify(problem)}`;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function normalizeTargetChecks(target) {
|
|
518
|
+
const checks = target.checks ?? DEFAULT_PACKAGE_CHECKS;
|
|
519
|
+
const normalizedChecks = [];
|
|
520
|
+
for (const check of checks) {
|
|
521
|
+
if (!isPackageCheckTool(check)) throw new Error(`Invalid package check "${check}". Expected one of: publint, attw, boundary.`);
|
|
522
|
+
if (!normalizedChecks.includes(check)) normalizedChecks.push(check);
|
|
523
|
+
}
|
|
524
|
+
return normalizedChecks;
|
|
525
|
+
}
|
|
526
|
+
function selectTargetChecks(target, requestedTool) {
|
|
527
|
+
const configuredChecks = normalizeTargetChecks(target);
|
|
528
|
+
if (!requestedTool || requestedTool === "all") return configuredChecks;
|
|
529
|
+
return configuredChecks.includes(requestedTool) ? [requestedTool] : [];
|
|
530
|
+
}
|
|
531
|
+
function findNearestPackageJsonPath(cwd, rootDir) {
|
|
532
|
+
const resolvedRootDir = path.resolve(rootDir);
|
|
533
|
+
let currentDir = path.resolve(cwd);
|
|
534
|
+
while (true) {
|
|
535
|
+
const relativeToRoot = path.relative(resolvedRootDir, currentDir);
|
|
536
|
+
if (!(relativeToRoot === "" || relativeToRoot !== ".." && !relativeToRoot.startsWith(`..${path.sep}`) && !path.isAbsolute(relativeToRoot))) return;
|
|
537
|
+
const packageJsonPath = path.join(currentDir, "package.json");
|
|
538
|
+
if (existsSync(packageJsonPath)) return packageJsonPath;
|
|
539
|
+
if (currentDir === resolvedRootDir) return;
|
|
540
|
+
const parentDir = path.dirname(currentDir);
|
|
541
|
+
if (parentDir === currentDir) return;
|
|
542
|
+
currentDir = parentDir;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function readCwdPackageName(cwd, rootDir) {
|
|
546
|
+
const packageJsonPath = findNearestPackageJsonPath(cwd, rootDir);
|
|
547
|
+
if (!packageJsonPath) return;
|
|
548
|
+
try {
|
|
549
|
+
const manifest = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
550
|
+
return typeof manifest.name === "string" && manifest.name.trim() ? manifest.name.trim() : void 0;
|
|
551
|
+
} catch (error) {
|
|
552
|
+
throw new Error(`Unable to read package name from ${packageJsonPath}: ${formatErrorMessage$1(error)}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function formatConfiguredTargetNames(targets) {
|
|
556
|
+
const names = targets.map((target) => target.name).filter((name) => Boolean(name));
|
|
557
|
+
return names.length > 0 ? names.join(", ") : "(none)";
|
|
558
|
+
}
|
|
559
|
+
function resolveTargetOutDir(options) {
|
|
560
|
+
const outDir = options.target.outDir;
|
|
561
|
+
if (typeof outDir !== "string" || outDir.trim().length === 0) throw new Error(`Invalid package check target at packageChecks.targets[${options.targetIndex}].outDir. Expected a non-empty string.`);
|
|
562
|
+
return path.resolve(options.config.rootDir, outDir);
|
|
563
|
+
}
|
|
564
|
+
function getTargetLabel(config, target, outDir) {
|
|
565
|
+
return target.name ?? toRelativePath(config.rootDir, outDir);
|
|
566
|
+
}
|
|
567
|
+
function createTargetPlan(options) {
|
|
568
|
+
const outDir = resolveTargetOutDir({
|
|
569
|
+
config: options.config,
|
|
570
|
+
target: options.target,
|
|
571
|
+
targetIndex: options.targetIndex
|
|
572
|
+
});
|
|
573
|
+
return {
|
|
574
|
+
checks: selectTargetChecks(options.target, options.requestedTool),
|
|
575
|
+
label: getTargetLabel(options.config, options.target, outDir),
|
|
576
|
+
outDir,
|
|
577
|
+
rawTarget: options.target
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function createPackageCheckPlan(options) {
|
|
581
|
+
const targets = options.config.packageChecks?.targets ?? [];
|
|
582
|
+
if (targets.length === 0) throw new Error("No package check targets are configured.");
|
|
583
|
+
let selectedTargets;
|
|
584
|
+
let selectionReason;
|
|
585
|
+
if (options.targetName) {
|
|
586
|
+
selectedTargets = targets.filter((target) => target.name === options.targetName);
|
|
587
|
+
if (selectedTargets.length === 0) throw new Error([`No package check target named "${options.targetName}" is configured.`, `Configured target names: ${formatConfiguredTargetNames(targets)}.`].join(" "));
|
|
588
|
+
selectionReason = `--package "${options.targetName}" matched configured target name.`;
|
|
589
|
+
} else {
|
|
590
|
+
const cwdPackageName = readCwdPackageName(options.cwd, options.config.rootDir);
|
|
591
|
+
if (cwdPackageName) {
|
|
592
|
+
selectedTargets = targets.filter((target) => target.name === cwdPackageName);
|
|
593
|
+
if (selectedTargets.length > 0) selectionReason = `nearest package.json name "${cwdPackageName}" matched configured target name.`;
|
|
594
|
+
else {
|
|
595
|
+
selectedTargets = targets;
|
|
596
|
+
selectionReason = `nearest package.json name "${cwdPackageName}" did not match configured target names; running all configured targets.`;
|
|
597
|
+
}
|
|
598
|
+
} else {
|
|
599
|
+
selectedTargets = targets;
|
|
600
|
+
selectionReason = "No package name was found from cwd up to the workspace root; running all configured targets.";
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
selectionReason,
|
|
605
|
+
targets: selectedTargets.map((target) => createTargetPlan({
|
|
606
|
+
config: options.config,
|
|
607
|
+
requestedTool: options.tool,
|
|
608
|
+
target,
|
|
609
|
+
targetIndex: targets.indexOf(target)
|
|
610
|
+
}))
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
function logPackageCheckPlan(options) {
|
|
614
|
+
PackageLogger.info([
|
|
615
|
+
"Package check plan:",
|
|
616
|
+
` config: ${toRelativePath(options.config.rootDir, options.config.configPath)}`,
|
|
617
|
+
` cwd: ${toRelativePath(options.config.rootDir, options.cwd)}`,
|
|
618
|
+
` selection: ${options.plan.selectionReason}`,
|
|
619
|
+
" targets:",
|
|
620
|
+
...options.plan.targets.map((target) => [
|
|
621
|
+
` - ${target.label}`,
|
|
622
|
+
` outDir: ${toRelativePath(options.config.rootDir, target.outDir)}`,
|
|
623
|
+
` checks: ${target.checks.length > 0 ? target.checks.join(", ") : "(none)"}`
|
|
624
|
+
].join("\n"))
|
|
625
|
+
].join("\n"));
|
|
626
|
+
}
|
|
627
|
+
async function packOutputTarball(outDir) {
|
|
628
|
+
const destination = await mkdtemp(path.join(tmpdir(), "__LATTICE_PACKAGE__"));
|
|
629
|
+
return {
|
|
630
|
+
cleanup: async () => {
|
|
631
|
+
await rm(destination, {
|
|
632
|
+
force: true,
|
|
633
|
+
recursive: true
|
|
634
|
+
}).catch(() => null);
|
|
635
|
+
},
|
|
636
|
+
tarball: await readFile(await pack(outDir, {
|
|
637
|
+
destination,
|
|
638
|
+
ignoreScripts: true,
|
|
639
|
+
packageManager: "pnpm"
|
|
640
|
+
}))
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
async function runPublintCheck(options) {
|
|
644
|
+
const task = options.flow?.start(`publint: ${options.label}`, { depth: options.flowDepth ?? 0 });
|
|
645
|
+
PackageLogger.info(`publint started: ${options.label}`);
|
|
646
|
+
const publintElapsed = createElapsedTimer();
|
|
647
|
+
const { messages, pkg } = await publint({
|
|
648
|
+
pack: { tarball: toArrayBuffer(options.tarball) },
|
|
649
|
+
strict: options.strict
|
|
650
|
+
});
|
|
651
|
+
if (messages.length === 0) {
|
|
652
|
+
if (!options.flow?.interactive) PackageLogger.success(`publint passed: ${options.label}`, publintElapsed());
|
|
653
|
+
task?.pass();
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
for (const message of messages) {
|
|
657
|
+
const rendered = formatMessage(message, pkg) ?? message.code;
|
|
658
|
+
if (message.type === "error") {
|
|
659
|
+
PackageLogger.error(`[${options.label}] [publint] ${rendered}`);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (message.type === "warning") {
|
|
663
|
+
PackageLogger.warn(`[${options.label}] [publint] ${rendered}`);
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
PackageLogger.info(`[${options.label}] [publint] ${rendered}`);
|
|
667
|
+
}
|
|
668
|
+
PackageLogger.error(`publint found ${messages.length} issue(s): ${options.label}`, publintElapsed());
|
|
669
|
+
task?.fail(`publint found ${messages.length} issue(s): ${options.label}`);
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
async function runAttwCheck(options) {
|
|
673
|
+
const task = options.flow?.start(`attw: ${options.label}`, { depth: options.flowDepth ?? 0 });
|
|
674
|
+
PackageLogger.info(`attw started: ${options.label} (profile: ${options.profile})`);
|
|
675
|
+
const attwElapsed = createElapsedTimer();
|
|
676
|
+
const result = await checkPackage(createPackageFromTarballData(options.tarball));
|
|
677
|
+
if (!result.types) {
|
|
678
|
+
PackageLogger.error(`[${options.label}] [attw] package has no types`);
|
|
679
|
+
PackageLogger.error(`attw failed: ${options.label}`, attwElapsed());
|
|
680
|
+
task?.fail(`attw failed: ${options.label}`);
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
const ignoredResolutions = ATTW_PROFILE_IGNORED_RESOLUTIONS[options.profile];
|
|
684
|
+
const problems = result.problems.filter((problem) => {
|
|
685
|
+
if ("resolutionKind" in problem) return !ignoredResolutions.includes(problem.resolutionKind);
|
|
686
|
+
return true;
|
|
687
|
+
});
|
|
688
|
+
if (problems.length === 0) {
|
|
689
|
+
if (!options.flow?.interactive) PackageLogger.success(`attw passed: ${options.label}`, attwElapsed());
|
|
690
|
+
task?.pass();
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
for (const problem of problems) PackageLogger.error(`[${options.label}] [attw] ${formatAttwProblem(problem)}`);
|
|
694
|
+
PackageLogger.error(`attw found ${problems.length} problem(s): ${options.label}`, attwElapsed());
|
|
695
|
+
task?.fail(`attw found ${problems.length} problem(s): ${options.label}`);
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
async function runBoundaryCheck(target, label, options = {}) {
|
|
699
|
+
const task = options.flow?.start(`package boundary: ${label}`, { depth: options.flowDepth ?? 0 });
|
|
700
|
+
PackageLogger.info(`package boundary started: ${label}`);
|
|
701
|
+
const boundaryElapsed = createElapsedTimer();
|
|
702
|
+
const violations = await auditPublishedPackageBoundaries(target);
|
|
703
|
+
if (violations.length === 0) {
|
|
704
|
+
if (!options.flow?.interactive) PackageLogger.success(`package boundary passed: ${label}`, boundaryElapsed());
|
|
705
|
+
task?.pass();
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
for (const violation of violations) PackageLogger.error(`[${label}] [boundary] ${violation.filePath} (${violation.environment}) imports "${violation.specifier}": ${violation.message}`);
|
|
709
|
+
PackageLogger.error(`package boundary found ${violations.length} issue(s): ${label}`, boundaryElapsed());
|
|
710
|
+
task?.fail(`package boundary found ${violations.length} issue(s): ${label}`);
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
async function runPackageCheckTarget(options) {
|
|
714
|
+
const target = {
|
|
715
|
+
...options.rawTarget,
|
|
716
|
+
outDir: options.outDir
|
|
717
|
+
};
|
|
718
|
+
const label = options.label;
|
|
719
|
+
const outputPackageJsonPath = path.join(target.outDir, "package.json");
|
|
720
|
+
const task = options.flow?.start(`package target: ${label}`, { depth: options.flowDepth ?? 0 });
|
|
721
|
+
let packedDist;
|
|
722
|
+
try {
|
|
723
|
+
if (!existsSync(outputPackageJsonPath)) throw new Error(`outDir package.json not found for ${label} at ${toRelativePath(options.config.rootDir, outputPackageJsonPath)}. Run the package build first.`);
|
|
724
|
+
if (options.checks.includes("publint") || options.checks.includes("attw")) {
|
|
725
|
+
const packTask = options.flow?.start(`package tarball: ${label}`, { depth: (options.flowDepth ?? 0) + 1 });
|
|
726
|
+
PackageLogger.info(`package tarball packing started: ${label}`);
|
|
727
|
+
const packElapsed = createElapsedTimer();
|
|
728
|
+
try {
|
|
729
|
+
packedDist = await packOutputTarball(target.outDir);
|
|
730
|
+
} catch (error) {
|
|
731
|
+
PackageLogger.error(`package tarball failed: ${label}: ${formatErrorMessage$1(error)}`, packElapsed());
|
|
732
|
+
packTask?.fail(`package tarball failed: ${label}`, { error });
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
if (!options.flow?.interactive) PackageLogger.success(`package tarball packed: ${label}`, packElapsed());
|
|
736
|
+
packTask?.pass();
|
|
737
|
+
}
|
|
738
|
+
let passed = true;
|
|
739
|
+
if (options.checks.includes("publint")) passed = await runPublintCheck({
|
|
740
|
+
flow: options.flow,
|
|
741
|
+
flowDepth: (options.flowDepth ?? 0) + 1,
|
|
742
|
+
label,
|
|
743
|
+
strict: target.publint?.strict ?? true,
|
|
744
|
+
tarball: packedDist.tarball
|
|
745
|
+
}) && passed;
|
|
746
|
+
if (options.checks.includes("attw")) passed = await runAttwCheck({
|
|
747
|
+
flow: options.flow,
|
|
748
|
+
flowDepth: (options.flowDepth ?? 0) + 1,
|
|
749
|
+
label,
|
|
750
|
+
profile: options.attwProfile ?? target.attw?.profile ?? "esm-only",
|
|
751
|
+
tarball: packedDist.tarball
|
|
752
|
+
}) && passed;
|
|
753
|
+
if (options.checks.includes("boundary")) passed = await runBoundaryCheck({
|
|
754
|
+
...target.boundary,
|
|
755
|
+
outDir: target.outDir
|
|
756
|
+
}, label, {
|
|
757
|
+
flow: options.flow,
|
|
758
|
+
flowDepth: (options.flowDepth ?? 0) + 1
|
|
759
|
+
}) && passed;
|
|
760
|
+
if (passed) {
|
|
761
|
+
if (!options.flow?.interactive) PackageLogger.success(`package checks passed: ${label}`);
|
|
762
|
+
task?.pass();
|
|
763
|
+
} else {
|
|
764
|
+
PackageLogger.error(`package checks failed: ${label}`);
|
|
765
|
+
task?.fail(`package checks failed: ${label}`);
|
|
766
|
+
}
|
|
767
|
+
return passed;
|
|
768
|
+
} catch (error) {
|
|
769
|
+
PackageLogger.error(`package checks failed: ${label}: ${formatErrorMessage$1(error)}`);
|
|
770
|
+
task?.fail(`package checks failed: ${label}`, { error });
|
|
771
|
+
throw error;
|
|
772
|
+
} finally {
|
|
773
|
+
if (packedDist) await packedDist.cleanup();
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async function auditPublishedPackageBoundaries(target) {
|
|
777
|
+
const manifestPath = path.join(target.outDir, "package.json");
|
|
778
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
779
|
+
const allowedExternalPackages = new Set([
|
|
780
|
+
...Object.keys(manifest.dependencies ?? {}),
|
|
781
|
+
...Object.keys(manifest.peerDependencies ?? {}),
|
|
782
|
+
...Object.keys(manifest.optionalDependencies ?? {}),
|
|
783
|
+
...target.ignoredExternalPackages ?? []
|
|
784
|
+
]);
|
|
785
|
+
const selfSpecifiers = collectSelfSpecifierMatchers(manifest.name, manifest.exports);
|
|
786
|
+
const publishedFiles = await collectPublishedModuleFiles(target.outDir);
|
|
787
|
+
const violations = [];
|
|
788
|
+
await init;
|
|
789
|
+
for (const filePath of publishedFiles) {
|
|
790
|
+
const relativeFilePath = path.relative(target.outDir, filePath);
|
|
791
|
+
const environment = classifyRuntimeEnvironment(target, relativeFilePath);
|
|
792
|
+
const [importSpecifiers] = parse(await readFile(filePath, "utf8"));
|
|
793
|
+
for (const importSpecifier of importSpecifiers) {
|
|
794
|
+
if (!importSpecifier.n) continue;
|
|
795
|
+
const message = validatePublishedSpecifier({
|
|
796
|
+
allowedExternalPackages,
|
|
797
|
+
environment,
|
|
798
|
+
packageName: manifest.name,
|
|
799
|
+
selfSpecifiers,
|
|
800
|
+
specifier: importSpecifier.n
|
|
801
|
+
});
|
|
802
|
+
if (!message) continue;
|
|
803
|
+
violations.push({
|
|
804
|
+
environment,
|
|
805
|
+
filePath: relativeFilePath,
|
|
806
|
+
message,
|
|
807
|
+
specifier: importSpecifier.n
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return violations.toSorted((left, right) => {
|
|
812
|
+
if (left.filePath === right.filePath) return left.specifier.localeCompare(right.specifier);
|
|
813
|
+
return left.filePath.localeCompare(right.filePath);
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
async function runPackageCheck(options) {
|
|
817
|
+
if (options.clearScreen ?? true) clearCliScreen();
|
|
818
|
+
const elapsed = createElapsedTimer();
|
|
819
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
820
|
+
const task = options.flow?.start("package check", { depth: options.flowDepth ?? 0 });
|
|
821
|
+
try {
|
|
822
|
+
PackageLogger.info("package check started");
|
|
823
|
+
const plan = createPackageCheckPlan({
|
|
824
|
+
config: options.config,
|
|
825
|
+
cwd,
|
|
826
|
+
targetName: options.targetName,
|
|
827
|
+
tool: options.tool
|
|
828
|
+
});
|
|
829
|
+
logPackageCheckPlan({
|
|
830
|
+
config: options.config,
|
|
831
|
+
cwd,
|
|
832
|
+
plan
|
|
833
|
+
});
|
|
834
|
+
const runnableTargets = plan.targets.filter((target) => target.checks.length > 0);
|
|
835
|
+
if (runnableTargets.length === 0) throw new Error(options.tool && options.tool !== "all" ? `No package check targets have "${options.tool}" enabled.` : "No package checks are enabled.");
|
|
836
|
+
let passed = true;
|
|
837
|
+
for (const target of runnableTargets) passed = await runPackageCheckTarget({
|
|
838
|
+
attwProfile: options.attwProfile,
|
|
839
|
+
checks: target.checks,
|
|
840
|
+
config: options.config,
|
|
841
|
+
flow: options.flow,
|
|
842
|
+
flowDepth: (options.flowDepth ?? 0) + 1,
|
|
843
|
+
label: target.label,
|
|
844
|
+
outDir: target.outDir,
|
|
845
|
+
rawTarget: target.rawTarget
|
|
846
|
+
}) && passed;
|
|
847
|
+
if (passed) {
|
|
848
|
+
if (!options.flow?.interactive) PackageLogger.success("package check finished", elapsed());
|
|
849
|
+
task?.pass();
|
|
850
|
+
} else {
|
|
851
|
+
PackageLogger.error("package check finished with failures", elapsed());
|
|
852
|
+
task?.fail("package check finished with failures");
|
|
853
|
+
}
|
|
854
|
+
return passed;
|
|
855
|
+
} catch (error) {
|
|
856
|
+
PackageLogger.error(`package check failed: ${formatErrorMessage$1(error)}`, elapsed());
|
|
857
|
+
task?.fail("package check failed", { error });
|
|
858
|
+
throw error;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
//#endregion
|
|
863
|
+
//#region src/commands/paths.ts
|
|
864
|
+
const defaultSourceExtensions = [
|
|
865
|
+
".ts",
|
|
866
|
+
".tsx",
|
|
867
|
+
".mts",
|
|
868
|
+
".cts",
|
|
869
|
+
".d.ts",
|
|
870
|
+
".d.mts",
|
|
871
|
+
".d.cts"
|
|
872
|
+
];
|
|
873
|
+
const defaultConditionPriority = [
|
|
874
|
+
"source",
|
|
875
|
+
"development",
|
|
876
|
+
"types",
|
|
877
|
+
"import",
|
|
878
|
+
"module",
|
|
879
|
+
"default",
|
|
880
|
+
"require"
|
|
881
|
+
];
|
|
882
|
+
const defaultArtifactDirectories = [
|
|
883
|
+
"dist",
|
|
884
|
+
"build",
|
|
885
|
+
"lib",
|
|
886
|
+
"esm",
|
|
887
|
+
"cjs",
|
|
888
|
+
"out"
|
|
889
|
+
];
|
|
890
|
+
function generatedFileName(config) {
|
|
891
|
+
return config.paths?.generatedFileName ?? "tsconfig.dts.paths.generated.json";
|
|
892
|
+
}
|
|
893
|
+
function generatedFileMarker(config) {
|
|
894
|
+
return config.paths?.generatedFileMarker ?? "GENERATED FILE - DO NOT EDIT BY HAND.";
|
|
895
|
+
}
|
|
896
|
+
function parseProject(config, configPath) {
|
|
897
|
+
const diagnostics = [];
|
|
898
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, {
|
|
899
|
+
...ts.sys,
|
|
900
|
+
onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
|
|
901
|
+
diagnostics.push(diagnostic);
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
if (!parsed) throw new Error(ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
905
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
906
|
+
getCurrentDirectory: () => config.rootDir,
|
|
907
|
+
getNewLine: () => "\n"
|
|
908
|
+
}));
|
|
909
|
+
if (parsed.errors.length > 0) throw new Error(ts.formatDiagnosticsWithColorAndContext(parsed.errors, {
|
|
910
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
911
|
+
getCurrentDirectory: () => config.rootDir,
|
|
912
|
+
getNewLine: () => "\n"
|
|
913
|
+
}));
|
|
914
|
+
return {
|
|
915
|
+
configPath: normalizeAbsolutePath(configPath),
|
|
916
|
+
fileNames: parsed.fileNames.filter((fileName) => /\.(?:[cm]?tsx?|d\.[cm]?ts)$/u.test(fileName)).map(normalizeAbsolutePath),
|
|
917
|
+
options: parsed.options,
|
|
918
|
+
references: new Set(getRawReferencePaths(config, configPath))
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
function getSourceFileKind(filePath) {
|
|
922
|
+
if (filePath.endsWith(".tsx")) return ts.ScriptKind.TSX;
|
|
923
|
+
if (filePath.endsWith(".jsx")) return ts.ScriptKind.JSX;
|
|
924
|
+
return ts.ScriptKind.TS;
|
|
925
|
+
}
|
|
926
|
+
function stringLiteralValue(node) {
|
|
927
|
+
return node && ts.isStringLiteralLike(node) ? node.text : null;
|
|
928
|
+
}
|
|
929
|
+
function collectImportsFromFile(filePath) {
|
|
930
|
+
const sourceText = readFileSync(filePath, "utf8");
|
|
931
|
+
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, getSourceFileKind(filePath));
|
|
932
|
+
const imports = [];
|
|
933
|
+
const addImport = (specifier, node) => {
|
|
934
|
+
const location = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
935
|
+
imports.push({
|
|
936
|
+
filePath,
|
|
937
|
+
line: location.line + 1,
|
|
938
|
+
specifier
|
|
939
|
+
});
|
|
940
|
+
};
|
|
941
|
+
const visit = (node) => {
|
|
942
|
+
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
|
|
943
|
+
const specifier = stringLiteralValue(node.moduleSpecifier);
|
|
944
|
+
if (specifier) addImport(specifier, node);
|
|
945
|
+
} else if (ts.isImportTypeNode(node)) {
|
|
946
|
+
const specifier = ts.isLiteralTypeNode(node.argument) ? stringLiteralValue(node.argument.literal) : null;
|
|
947
|
+
if (specifier) addImport(specifier, node);
|
|
948
|
+
} else if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
949
|
+
const specifier = stringLiteralValue(node.arguments[0]);
|
|
950
|
+
if (specifier) addImport(specifier, node);
|
|
951
|
+
}
|
|
952
|
+
ts.forEachChild(node, visit);
|
|
953
|
+
};
|
|
954
|
+
visit(sourceFile);
|
|
955
|
+
return imports;
|
|
956
|
+
}
|
|
957
|
+
function resolveInternalImport(specifier, containingFile, options) {
|
|
958
|
+
const resolved = ts.resolveModuleName(specifier, containingFile, options, ts.sys).resolvedModule;
|
|
959
|
+
return resolved?.resolvedFileName ? normalizeAbsolutePath(resolved.resolvedFileName) : null;
|
|
960
|
+
}
|
|
961
|
+
function resolveImportWithoutMatchingPaths(specifier, containingFile, options) {
|
|
962
|
+
if (!options.paths) return resolveInternalImport(specifier, containingFile, options);
|
|
963
|
+
const paths = Object.fromEntries(Object.entries(options.paths).filter(([alias]) => !aliasMatchesSpecifier(alias, specifier)));
|
|
964
|
+
return resolveInternalImport(specifier, containingFile, {
|
|
965
|
+
...options,
|
|
966
|
+
paths: Object.keys(paths).length > 0 ? paths : void 0
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
function createFileOwnerLookup(projects) {
|
|
970
|
+
const ownerLookup = /* @__PURE__ */ new Map();
|
|
971
|
+
for (const project of projects) for (const fileName of project.fileNames) {
|
|
972
|
+
const owners = ownerLookup.get(fileName) ?? [];
|
|
973
|
+
owners.push(project.configPath);
|
|
974
|
+
ownerLookup.set(fileName, owners);
|
|
975
|
+
}
|
|
976
|
+
return ownerLookup;
|
|
977
|
+
}
|
|
978
|
+
function projectExtendsGeneratedConfig(config, configPath) {
|
|
979
|
+
const extendsValue = readJsonConfig(config, configPath).extends;
|
|
980
|
+
return (typeof extendsValue === "string" ? [extendsValue] : Array.isArray(extendsValue) ? extendsValue : []).some((entry) => typeof entry === "string" && path.basename(entry) === generatedFileName(config));
|
|
981
|
+
}
|
|
982
|
+
function findImporterForFile(filePath, importers) {
|
|
983
|
+
return importers.find((importer) => isPathInsideDirectory(filePath, importer.directory)) ?? null;
|
|
984
|
+
}
|
|
985
|
+
function shouldResolveThroughGraph(importer, targetPackage) {
|
|
986
|
+
if (!importer || !targetPackage) return false;
|
|
987
|
+
return importer.name === targetPackage.name || importer.workspaceDependencies.has(targetPackage.name);
|
|
988
|
+
}
|
|
989
|
+
function inferPackageProject(resolvedFilePath, workspacePackage, projectPaths) {
|
|
990
|
+
if (!isPathInsideDirectory(resolvedFilePath, workspacePackage.directory)) return null;
|
|
991
|
+
return projectPaths.find((projectPath) => {
|
|
992
|
+
return projectPath.startsWith(`${workspacePackage.directory}/`) && projectPath.endsWith("/tsconfig.lib.dts.json");
|
|
993
|
+
}) ?? null;
|
|
994
|
+
}
|
|
995
|
+
function collectTargetCandidates(config, value) {
|
|
996
|
+
if (typeof value === "string") return [value];
|
|
997
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
|
|
998
|
+
const record = value;
|
|
999
|
+
const candidates = [];
|
|
1000
|
+
const visitedKeys = /* @__PURE__ */ new Set();
|
|
1001
|
+
for (const key of config.paths?.conditionPriority ?? defaultConditionPriority) {
|
|
1002
|
+
if (!(key in record)) continue;
|
|
1003
|
+
visitedKeys.add(key);
|
|
1004
|
+
candidates.push(...collectTargetCandidates(config, record[key]));
|
|
1005
|
+
}
|
|
1006
|
+
for (const key of Object.keys(record).sort()) {
|
|
1007
|
+
if (visitedKeys.has(key)) continue;
|
|
1008
|
+
candidates.push(...collectTargetCandidates(config, record[key]));
|
|
1009
|
+
}
|
|
1010
|
+
return candidates;
|
|
1011
|
+
}
|
|
1012
|
+
function normalizePackageTarget(target) {
|
|
1013
|
+
if (!target.startsWith("./")) return null;
|
|
1014
|
+
return target.slice(2);
|
|
1015
|
+
}
|
|
1016
|
+
function packageExportKeyToAlias(packageName, exportKey) {
|
|
1017
|
+
if (exportKey === ".") return packageName;
|
|
1018
|
+
if (!exportKey.startsWith("./")) return "";
|
|
1019
|
+
return `${packageName}/${exportKey.slice(2)}`;
|
|
1020
|
+
}
|
|
1021
|
+
function removeKnownExtension(filePath) {
|
|
1022
|
+
for (const extension of [
|
|
1023
|
+
".d.mts",
|
|
1024
|
+
".d.cts",
|
|
1025
|
+
".d.ts",
|
|
1026
|
+
".mts",
|
|
1027
|
+
".cts",
|
|
1028
|
+
".mjs",
|
|
1029
|
+
".cjs",
|
|
1030
|
+
".js",
|
|
1031
|
+
".tsx",
|
|
1032
|
+
".ts"
|
|
1033
|
+
]) if (filePath.endsWith(extension)) return filePath.slice(0, -extension.length);
|
|
1034
|
+
return filePath;
|
|
1035
|
+
}
|
|
1036
|
+
function configuredArtifactDirectories(config) {
|
|
1037
|
+
return config.paths?.artifactDirectories ?? defaultArtifactDirectories;
|
|
1038
|
+
}
|
|
1039
|
+
function configuredSourceExtensions(config) {
|
|
1040
|
+
return config.paths?.sourceExtensions ?? defaultSourceExtensions;
|
|
1041
|
+
}
|
|
1042
|
+
function hasConfiguredSourceExtension(config, target) {
|
|
1043
|
+
return configuredSourceExtensions(config).some((extension) => target.endsWith(extension));
|
|
1044
|
+
}
|
|
1045
|
+
function isInsideArtifactDirectory(config, target) {
|
|
1046
|
+
const normalizedTarget = normalizeSlashes(target);
|
|
1047
|
+
return configuredArtifactDirectories(config).some((directoryName) => {
|
|
1048
|
+
const normalizedDirectoryName = normalizeSlashes(directoryName).replace(/\/+$/u, "");
|
|
1049
|
+
return normalizedTarget === normalizedDirectoryName || normalizedTarget.startsWith(`${normalizedDirectoryName}/`);
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
function isLikelySourceTarget(config, target) {
|
|
1053
|
+
return hasConfiguredSourceExtension(config, target) && !isInsideArtifactDirectory(config, target);
|
|
1054
|
+
}
|
|
1055
|
+
function stripArtifactPrefix(config, target) {
|
|
1056
|
+
const normalizedTarget = normalizeSlashes(target);
|
|
1057
|
+
for (const directoryName of configuredArtifactDirectories(config)) {
|
|
1058
|
+
const normalizedDirectoryName = normalizeSlashes(directoryName).replace(/\/+$/u, "");
|
|
1059
|
+
if (normalizedTarget.startsWith(`${normalizedDirectoryName}/`)) return normalizedTarget.slice(normalizedDirectoryName.length + 1);
|
|
1060
|
+
}
|
|
1061
|
+
return normalizedTarget;
|
|
1062
|
+
}
|
|
1063
|
+
function sourceFileCandidates(config, target) {
|
|
1064
|
+
const normalizedTarget = normalizeSlashes(target);
|
|
1065
|
+
const withoutKnownExtension = removeKnownExtension(normalizedTarget);
|
|
1066
|
+
const withoutArtifactPrefix = stripArtifactPrefix(config, normalizedTarget);
|
|
1067
|
+
const sourceBase = removeKnownExtension(withoutArtifactPrefix);
|
|
1068
|
+
const bases = withoutArtifactPrefix === normalizedTarget ? [
|
|
1069
|
+
withoutKnownExtension,
|
|
1070
|
+
sourceBase,
|
|
1071
|
+
`src/${sourceBase}`,
|
|
1072
|
+
`${sourceBase}/index`,
|
|
1073
|
+
`src/${sourceBase}/index`
|
|
1074
|
+
] : [
|
|
1075
|
+
sourceBase,
|
|
1076
|
+
`src/${sourceBase}`,
|
|
1077
|
+
`${sourceBase}/index`,
|
|
1078
|
+
`src/${sourceBase}/index`
|
|
1079
|
+
];
|
|
1080
|
+
const candidates = [];
|
|
1081
|
+
for (const base of bases) for (const extension of configuredSourceExtensions(config)) candidates.push(`${base}${extension}`);
|
|
1082
|
+
return [...new Set(candidates)];
|
|
1083
|
+
}
|
|
1084
|
+
function wildcardBaseDirectory(pattern) {
|
|
1085
|
+
const wildcardIndex = pattern.indexOf("*");
|
|
1086
|
+
const prefix = wildcardIndex === -1 ? pattern : pattern.slice(0, wildcardIndex);
|
|
1087
|
+
const lastSlashIndex = prefix.lastIndexOf("/");
|
|
1088
|
+
return lastSlashIndex === -1 ? "." : prefix.slice(0, lastSlashIndex);
|
|
1089
|
+
}
|
|
1090
|
+
function sourceWildcardPatternCandidates(config, target) {
|
|
1091
|
+
const strippedPattern = stripArtifactPrefix(config, target);
|
|
1092
|
+
const sourcePattern = removeKnownExtension(strippedPattern);
|
|
1093
|
+
const preferSrcPrefix = strippedPattern !== normalizeSlashes(target);
|
|
1094
|
+
const candidates = [];
|
|
1095
|
+
for (const extension of configuredSourceExtensions(config)) if (preferSrcPrefix) {
|
|
1096
|
+
candidates.push(`src/${sourcePattern}${extension}`);
|
|
1097
|
+
candidates.push(`${sourcePattern}${extension}`);
|
|
1098
|
+
} else {
|
|
1099
|
+
candidates.push(`${sourcePattern}${extension}`);
|
|
1100
|
+
candidates.push(`src/${sourcePattern}${extension}`);
|
|
1101
|
+
}
|
|
1102
|
+
return [...new Set(candidates)];
|
|
1103
|
+
}
|
|
1104
|
+
function resolveWildcardTarget(config, packageDirectory, target) {
|
|
1105
|
+
const sourcePatterns = sourceWildcardPatternCandidates(config, target);
|
|
1106
|
+
const candidatePatterns = isLikelySourceTarget(config, target) ? [target, ...sourcePatterns] : sourcePatterns;
|
|
1107
|
+
for (const candidatePattern of candidatePatterns) {
|
|
1108
|
+
const baseDirectory = wildcardBaseDirectory(candidatePattern);
|
|
1109
|
+
if (existsSync(path.join(packageDirectory, baseDirectory))) return toPosixPath(path.join(toRelativePath(config.rootDir, packageDirectory), candidatePattern));
|
|
1110
|
+
}
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1113
|
+
function resolveExactTarget(config, packageDirectory, target) {
|
|
1114
|
+
const absoluteTarget = path.join(packageDirectory, target);
|
|
1115
|
+
if (existsSync(absoluteTarget) && isLikelySourceTarget(config, target)) return normalizeWorkspacePath(config.rootDir, absoluteTarget);
|
|
1116
|
+
for (const candidate of sourceFileCandidates(config, target)) {
|
|
1117
|
+
const absoluteCandidate = path.join(packageDirectory, candidate);
|
|
1118
|
+
if (existsSync(absoluteCandidate)) return normalizeWorkspacePath(config.rootDir, absoluteCandidate);
|
|
1119
|
+
}
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
function resolvePackageTarget(config, packageDirectory, rawTarget) {
|
|
1123
|
+
const target = normalizePackageTarget(rawTarget);
|
|
1124
|
+
if (!target) return null;
|
|
1125
|
+
if (target.includes("*")) return resolveWildcardTarget(config, packageDirectory, target);
|
|
1126
|
+
return resolveExactTarget(config, packageDirectory, target);
|
|
1127
|
+
}
|
|
1128
|
+
function collectExportEntries(config, workspacePackage) {
|
|
1129
|
+
const exportsField = workspacePackage.manifest.exports;
|
|
1130
|
+
if (!exportsField) return [];
|
|
1131
|
+
const exportEntries = typeof exportsField === "object" && exportsField !== null && !Array.isArray(exportsField) && Object.keys(exportsField).some((key) => key.startsWith(".")) ? Object.entries(exportsField) : [[".", exportsField]];
|
|
1132
|
+
const entries = [];
|
|
1133
|
+
for (const [exportKey, exportValue] of exportEntries.sort(([left], [right]) => left.localeCompare(right))) {
|
|
1134
|
+
const alias = packageExportKeyToAlias(workspacePackage.name, exportKey);
|
|
1135
|
+
if (!alias) continue;
|
|
1136
|
+
for (const candidate of collectTargetCandidates(config, exportValue)) {
|
|
1137
|
+
const resolvedTarget = resolvePackageTarget(config, workspacePackage.directory, candidate);
|
|
1138
|
+
if (resolvedTarget) {
|
|
1139
|
+
entries.push([alias, resolvedTarget]);
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return entries;
|
|
1145
|
+
}
|
|
1146
|
+
function aliasMatchesSpecifier(alias, specifier) {
|
|
1147
|
+
if (alias === specifier) return true;
|
|
1148
|
+
const wildcardIndex = alias.indexOf("*");
|
|
1149
|
+
if (wildcardIndex === -1) return false;
|
|
1150
|
+
const prefix = alias.slice(0, wildcardIndex);
|
|
1151
|
+
const suffix = alias.slice(wildcardIndex + 1);
|
|
1152
|
+
return specifier.startsWith(prefix) && specifier.endsWith(suffix);
|
|
1153
|
+
}
|
|
1154
|
+
function addPathEntry(paths, alias, target) {
|
|
1155
|
+
const targets = paths.get(alias) ?? [];
|
|
1156
|
+
if (!targets.includes(target)) targets.push(target);
|
|
1157
|
+
paths.set(alias, targets);
|
|
1158
|
+
}
|
|
1159
|
+
function compareAliases(left, right) {
|
|
1160
|
+
const leftGroup = left.startsWith("@") ? 1 : 0;
|
|
1161
|
+
const rightGroup = right.startsWith("@") ? 1 : 0;
|
|
1162
|
+
if (leftGroup !== rightGroup) return leftGroup - rightGroup;
|
|
1163
|
+
if ((left.startsWith("@") ? left.split("/").slice(0, 2).join("/") : left.split("/")[0] ?? left) === (right.startsWith("@") ? right.split("/").slice(0, 2).join("/") : right.split("/")[0] ?? right)) {
|
|
1164
|
+
const leftPrefixLength = left.split("*")[0]?.length ?? left.length;
|
|
1165
|
+
const rightPrefixLength = right.split("*")[0]?.length ?? right.length;
|
|
1166
|
+
if (leftPrefixLength !== rightPrefixLength) return rightPrefixLength - leftPrefixLength;
|
|
1167
|
+
}
|
|
1168
|
+
return left.localeCompare(right);
|
|
1169
|
+
}
|
|
1170
|
+
function toTsconfigPathTarget(config, outputDirectory, target) {
|
|
1171
|
+
const relativeTarget = toPosixPath(path.relative(outputDirectory, toAbsolutePath(config.rootDir, target)));
|
|
1172
|
+
if (relativeTarget.startsWith("./") || relativeTarget.startsWith("../")) return relativeTarget;
|
|
1173
|
+
return `./${relativeTarget}`;
|
|
1174
|
+
}
|
|
1175
|
+
function formatPaths(config, paths, outputDirectory) {
|
|
1176
|
+
const lines = [];
|
|
1177
|
+
const entries = [...paths.entries()].sort(([left], [right]) => compareAliases(left, right));
|
|
1178
|
+
for (const [alias, targets] of entries) {
|
|
1179
|
+
const pathTargets = targets.map((target) => toTsconfigPathTarget(config, outputDirectory, target));
|
|
1180
|
+
if (pathTargets.length === 1 && `${JSON.stringify(alias)}: [${JSON.stringify(pathTargets[0])}],`.length < 80) {
|
|
1181
|
+
lines.push(` ${JSON.stringify(alias)}: [${JSON.stringify(pathTargets[0])}],`);
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
lines.push(` ${JSON.stringify(alias)}: [`);
|
|
1185
|
+
for (const target of pathTargets) lines.push(` ${JSON.stringify(target)},`);
|
|
1186
|
+
lines.push(" ],");
|
|
1187
|
+
}
|
|
1188
|
+
return lines.join("\n");
|
|
1189
|
+
}
|
|
1190
|
+
function formatGeneratedConfig(config, paths, outputPath) {
|
|
1191
|
+
const outputDirectory = path.dirname(outputPath);
|
|
1192
|
+
return `{
|
|
1193
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
1194
|
+
/**
|
|
1195
|
+
* ${generatedFileMarker(config)}
|
|
1196
|
+
*
|
|
1197
|
+
* Compatibility paths for workspace:* source dependencies whose package
|
|
1198
|
+
* exports resolve to build artifacts. Run \`limina paths generate\` to
|
|
1199
|
+
* refresh this file, then manually extend it from the relevant
|
|
1200
|
+
* tsconfig*.dts.json files.
|
|
1201
|
+
*/
|
|
1202
|
+
"compilerOptions": {
|
|
1203
|
+
"paths": {
|
|
1204
|
+
${formatPaths(config, paths, outputDirectory)}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
`;
|
|
1209
|
+
}
|
|
1210
|
+
function getDraft(drafts, outputPath) {
|
|
1211
|
+
const existing = drafts.get(outputPath);
|
|
1212
|
+
if (existing) return existing;
|
|
1213
|
+
const created = {
|
|
1214
|
+
configPaths: /* @__PURE__ */ new Set(),
|
|
1215
|
+
paths: /* @__PURE__ */ new Map()
|
|
1216
|
+
};
|
|
1217
|
+
drafts.set(outputPath, created);
|
|
1218
|
+
return created;
|
|
1219
|
+
}
|
|
1220
|
+
function createGeneratedConfigs(config, drafts) {
|
|
1221
|
+
return [...drafts.entries()].map(([outputPath, draft]) => ({
|
|
1222
|
+
aliasCount: draft.paths.size,
|
|
1223
|
+
configPaths: [...draft.configPaths].sort((left, right) => left.localeCompare(right)),
|
|
1224
|
+
content: formatGeneratedConfig(config, draft.paths, outputPath),
|
|
1225
|
+
outputPath
|
|
1226
|
+
})).filter((generatedConfig) => generatedConfig.aliasCount > 0).sort((left, right) => left.outputPath.localeCompare(right.outputPath));
|
|
1227
|
+
}
|
|
1228
|
+
async function collectGeneratedConfigs(config) {
|
|
1229
|
+
const graphRoute = collectGraphProjectRoute(config);
|
|
1230
|
+
const projectPaths = graphRoute.projectPaths;
|
|
1231
|
+
if (graphRoute.problems.length > 0) throw new Error(graphRoute.problems.join("\n\n"));
|
|
1232
|
+
const projects = projectPaths.map((projectPath) => parseProject(config, projectPath));
|
|
1233
|
+
const fileOwnerLookup = createFileOwnerLookup(projects);
|
|
1234
|
+
const packages = await collectWorkspacePackages(config);
|
|
1235
|
+
const importers = collectImporters(config, packages);
|
|
1236
|
+
const exportEntriesByPackage = /* @__PURE__ */ new Map();
|
|
1237
|
+
const drafts = /* @__PURE__ */ new Map();
|
|
1238
|
+
for (const project of projects) {
|
|
1239
|
+
const keepsGeneratedPaths = projectExtendsGeneratedConfig(config, project.configPath);
|
|
1240
|
+
for (const filePath of project.fileNames) for (const importRecord of collectImportsFromFile(filePath)) {
|
|
1241
|
+
const targetPackage = findPackageForSpecifier(importRecord.specifier, packages);
|
|
1242
|
+
const importer = targetPackage ? findImporterForFile(importRecord.filePath, importers) : null;
|
|
1243
|
+
if (!targetPackage || !shouldResolveThroughGraph(importer, targetPackage)) continue;
|
|
1244
|
+
const resolvedFilePath = resolveInternalImport(importRecord.specifier, filePath, project.options);
|
|
1245
|
+
if (!resolvedFilePath) continue;
|
|
1246
|
+
const artifactResolvedFilePath = fileOwnerLookup.has(resolvedFilePath) && keepsGeneratedPaths ? resolveImportWithoutMatchingPaths(importRecord.specifier, filePath, project.options) : resolvedFilePath;
|
|
1247
|
+
if (!artifactResolvedFilePath || fileOwnerLookup.has(artifactResolvedFilePath)) continue;
|
|
1248
|
+
const targetProjectPath = inferPackageProject(artifactResolvedFilePath, targetPackage, projectPaths);
|
|
1249
|
+
if (!targetProjectPath || !project.references.has(targetProjectPath)) continue;
|
|
1250
|
+
const exportEntries = exportEntriesByPackage.get(targetPackage.name) ?? collectExportEntries(config, targetPackage);
|
|
1251
|
+
exportEntriesByPackage.set(targetPackage.name, exportEntries);
|
|
1252
|
+
if (!exportEntries.some(([alias]) => aliasMatchesSpecifier(alias, importRecord.specifier))) continue;
|
|
1253
|
+
const draft = getDraft(drafts, path.join(path.dirname(project.configPath), generatedFileName(config)));
|
|
1254
|
+
draft.configPaths.add(project.configPath);
|
|
1255
|
+
for (const [alias, target] of exportEntries) addPathEntry(draft.paths, alias, target);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return createGeneratedConfigs(config, drafts);
|
|
1259
|
+
}
|
|
1260
|
+
async function isGeneratedTsconfigPathsFile(config, filePath) {
|
|
1261
|
+
try {
|
|
1262
|
+
return (await readFile(filePath, "utf8")).includes(generatedFileMarker(config));
|
|
1263
|
+
} catch {
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
async function collectExistingGeneratedConfigPaths(config) {
|
|
1268
|
+
return (await glob(`**/${generatedFileName(config)}`, {
|
|
1269
|
+
cwd: config.rootDir,
|
|
1270
|
+
absolute: true,
|
|
1271
|
+
ignore: ["**/node_modules/**", "**/dist/**"]
|
|
1272
|
+
})).map((filePath) => path.resolve(filePath)).sort();
|
|
1273
|
+
}
|
|
1274
|
+
async function writeGeneratedConfigs(config, generatedConfigs) {
|
|
1275
|
+
let didChange = false;
|
|
1276
|
+
const expectedOutputPaths = new Set(generatedConfigs.map((generatedConfig) => path.resolve(generatedConfig.outputPath)));
|
|
1277
|
+
for (const existingFile of await collectExistingGeneratedConfigPaths(config)) {
|
|
1278
|
+
if (expectedOutputPaths.has(existingFile)) continue;
|
|
1279
|
+
if (await isGeneratedTsconfigPathsFile(config, existingFile)) {
|
|
1280
|
+
await rm(existingFile);
|
|
1281
|
+
didChange = true;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
for (const generatedConfig of generatedConfigs) {
|
|
1285
|
+
if ((existsSync(generatedConfig.outputPath) ? await readFile(generatedConfig.outputPath, "utf8") : null) === generatedConfig.content) continue;
|
|
1286
|
+
await mkdir(path.dirname(generatedConfig.outputPath), { recursive: true });
|
|
1287
|
+
await writeFile(generatedConfig.outputPath, generatedConfig.content);
|
|
1288
|
+
didChange = true;
|
|
1289
|
+
}
|
|
1290
|
+
return didChange;
|
|
1291
|
+
}
|
|
1292
|
+
async function checkGeneratedConfigs(config, generatedConfigs) {
|
|
1293
|
+
const expectedOutputPaths = new Set(generatedConfigs.map((generatedConfig) => path.resolve(generatedConfig.outputPath)));
|
|
1294
|
+
for (const existingFile of await collectExistingGeneratedConfigPaths(config)) {
|
|
1295
|
+
if (expectedOutputPaths.has(existingFile)) continue;
|
|
1296
|
+
if (await isGeneratedTsconfigPathsFile(config, existingFile)) return true;
|
|
1297
|
+
}
|
|
1298
|
+
for (const generatedConfig of generatedConfigs) if ((existsSync(generatedConfig.outputPath) ? await readFile(generatedConfig.outputPath, "utf8") : null) !== generatedConfig.content) return true;
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
function toTsconfigExtendsPath(configPath, targetPath) {
|
|
1302
|
+
const relativeTarget = toPosixPath(path.relative(path.dirname(configPath), targetPath));
|
|
1303
|
+
if (relativeTarget.startsWith("./") || relativeTarget.startsWith("../")) return relativeTarget;
|
|
1304
|
+
return `./${relativeTarget}`;
|
|
1305
|
+
}
|
|
1306
|
+
function logManualExtendsSuggestions(config, generatedConfigs) {
|
|
1307
|
+
if (generatedConfigs.reduce((total, generatedConfig) => total + generatedConfig.configPaths.length, 0) === 0) {
|
|
1308
|
+
PathsLogger.info("No workspace:* artifact-export compatibility paths are needed.");
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
PathsLogger.info("Generated path configs are not injected automatically. Add them manually to the first position of each listed extends array:");
|
|
1312
|
+
for (const generatedConfig of generatedConfigs) {
|
|
1313
|
+
PathsLogger.info(` ${toRelativePath(config.rootDir, generatedConfig.outputPath)} (${generatedConfig.aliasCount} aliases)`);
|
|
1314
|
+
for (const configPath of generatedConfig.configPaths) PathsLogger.info(` - ${toRelativePath(config.rootDir, configPath)} extends ${toTsconfigExtendsPath(configPath, generatedConfig.outputPath)}`);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
async function runPathsInternal(config, options = {}) {
|
|
1318
|
+
const generatedConfigs = await collectGeneratedConfigs(config);
|
|
1319
|
+
const didChange = options.check ? await checkGeneratedConfigs(config, generatedConfigs) : await writeGeneratedConfigs(config, generatedConfigs);
|
|
1320
|
+
const aliasCount = generatedConfigs.reduce((total, generatedConfig) => total + generatedConfig.aliasCount, 0);
|
|
1321
|
+
const suggestionCount = generatedConfigs.reduce((total, generatedConfig) => total + generatedConfig.configPaths.length, 0);
|
|
1322
|
+
const action = options.check ? didChange ? "Would update" : "Checked unchanged" : didChange ? "Generated" : "Skipped unchanged";
|
|
1323
|
+
PathsLogger.info(`${action} ${generatedConfigs.length} TypeScript graph path config files with ${aliasCount} path aliases.`);
|
|
1324
|
+
logManualExtendsSuggestions(config, generatedConfigs);
|
|
1325
|
+
if (options.check && didChange) PathsLogger.error("TypeScript graph path state is stale; run `limina paths generate`, then manually extend the listed tsconfig*.dts.json files.");
|
|
1326
|
+
return {
|
|
1327
|
+
aliasCount,
|
|
1328
|
+
changed: didChange,
|
|
1329
|
+
outputCount: generatedConfigs.length,
|
|
1330
|
+
suggestionCount
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
async function runPaths(config, options = {}) {
|
|
1334
|
+
if (options.clearScreen ?? true) clearCliScreen();
|
|
1335
|
+
const elapsed = createElapsedTimer();
|
|
1336
|
+
const action = options.check ? "paths check" : "paths generate";
|
|
1337
|
+
const task = options.flow?.start(action, { depth: options.flowDepth ?? 0 });
|
|
1338
|
+
PathsLogger.info(`${action} started`);
|
|
1339
|
+
try {
|
|
1340
|
+
const result = await runPathsInternal(config, options);
|
|
1341
|
+
if (options.check && result.changed) {
|
|
1342
|
+
PathsLogger.error(`${action} finished with stale files`, elapsed());
|
|
1343
|
+
task?.fail(`${action} finished with stale files`);
|
|
1344
|
+
} else {
|
|
1345
|
+
if (!options.flow?.interactive) PathsLogger.success(`${action} finished`, elapsed());
|
|
1346
|
+
task?.pass();
|
|
1347
|
+
}
|
|
1348
|
+
return result;
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
PathsLogger.error(`${action} failed: ${formatErrorMessage$1(error)}`, elapsed());
|
|
1351
|
+
task?.fail(`${action} failed`, { error });
|
|
1352
|
+
throw error;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
//#endregion
|
|
1357
|
+
//#region src/commands/proof.ts
|
|
1358
|
+
const dtsConfigPattern = "**/tsconfig*.dts.json";
|
|
1359
|
+
const buildGraphConfigPattern = "**/tsconfig*.build.json";
|
|
1360
|
+
const tsconfigJsonPattern = "**/tsconfig.json";
|
|
1361
|
+
const tsconfigFilePattern = "**/tsconfig*.json";
|
|
1362
|
+
const defaultSourceExclude = [
|
|
1363
|
+
"node_modules",
|
|
1364
|
+
"dist",
|
|
1365
|
+
".git",
|
|
1366
|
+
".tsbuild",
|
|
1367
|
+
"coverage",
|
|
1368
|
+
"**/tsconfig*.json",
|
|
1369
|
+
"**/package.json",
|
|
1370
|
+
".prettierrc.json",
|
|
1371
|
+
".markdownlint.json",
|
|
1372
|
+
"vercel.json"
|
|
1373
|
+
];
|
|
1374
|
+
const ignoredSemanticCompilerOptions = new Set([
|
|
1375
|
+
"baseUrl",
|
|
1376
|
+
"build",
|
|
1377
|
+
"composite",
|
|
1378
|
+
"configFilePath",
|
|
1379
|
+
"declaration",
|
|
1380
|
+
"declarationDir",
|
|
1381
|
+
"declarationMap",
|
|
1382
|
+
"emitBOM",
|
|
1383
|
+
"emitDeclarationOnly",
|
|
1384
|
+
"incremental",
|
|
1385
|
+
"inlineSourceMap",
|
|
1386
|
+
"inlineSources",
|
|
1387
|
+
"mapRoot",
|
|
1388
|
+
"newLine",
|
|
1389
|
+
"noEmit",
|
|
1390
|
+
"noEmitOnError",
|
|
1391
|
+
"out",
|
|
1392
|
+
"outDir",
|
|
1393
|
+
"outFile",
|
|
1394
|
+
"paths",
|
|
1395
|
+
"pathsBasePath",
|
|
1396
|
+
"preserveConstEnums",
|
|
1397
|
+
"project",
|
|
1398
|
+
"removeComments",
|
|
1399
|
+
"rootDir",
|
|
1400
|
+
"showConfig",
|
|
1401
|
+
"sourceMap",
|
|
1402
|
+
"sourceRoot",
|
|
1403
|
+
"tsBuildInfoFile"
|
|
1404
|
+
]);
|
|
1405
|
+
async function collectTsconfigPaths(config, pattern) {
|
|
1406
|
+
return (await glob(pattern, {
|
|
1407
|
+
cwd: config.rootDir,
|
|
1408
|
+
absolute: true,
|
|
1409
|
+
ignore: [
|
|
1410
|
+
"**/.git/**",
|
|
1411
|
+
"**/.tsbuild/**",
|
|
1412
|
+
"**/coverage/**",
|
|
1413
|
+
"**/dist/**",
|
|
1414
|
+
"**/node_modules/**"
|
|
1415
|
+
]
|
|
1416
|
+
})).map(normalizeAbsolutePath).sort();
|
|
1417
|
+
}
|
|
1418
|
+
async function collectDtsConfigPaths(config) {
|
|
1419
|
+
return collectTsconfigPaths(config, dtsConfigPattern);
|
|
1420
|
+
}
|
|
1421
|
+
async function collectBuildGraphConfigPaths(config) {
|
|
1422
|
+
return collectTsconfigPaths(config, buildGraphConfigPattern);
|
|
1423
|
+
}
|
|
1424
|
+
async function collectDefaultTsconfigPaths(config) {
|
|
1425
|
+
return collectTsconfigPaths(config, tsconfigJsonPattern);
|
|
1426
|
+
}
|
|
1427
|
+
async function collectOrdinaryTypecheckConfigPaths(config) {
|
|
1428
|
+
return (await collectTsconfigPaths(config, tsconfigFilePattern)).filter(isOrdinaryTypecheckConfigPath);
|
|
1429
|
+
}
|
|
1430
|
+
function isPlainRecord(value) {
|
|
1431
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1432
|
+
}
|
|
1433
|
+
function formatUnknownValue(value) {
|
|
1434
|
+
if (value === void 0) return "undefined";
|
|
1435
|
+
return JSON.stringify(value);
|
|
1436
|
+
}
|
|
1437
|
+
function hasGlobSyntax(pattern) {
|
|
1438
|
+
return /[*?[\]{}()!+@]/u.test(pattern);
|
|
1439
|
+
}
|
|
1440
|
+
function isDirectoryShorthand(pattern) {
|
|
1441
|
+
return !hasGlobSyntax(pattern) && !pattern.includes("/") && !path.extname(pattern);
|
|
1442
|
+
}
|
|
1443
|
+
function normalizeSourceExcludePattern(pattern) {
|
|
1444
|
+
const normalized = pattern.replaceAll("\\", "/").replace(/\/+$/u, "");
|
|
1445
|
+
if (!normalized) return [];
|
|
1446
|
+
if (isDirectoryShorthand(normalized)) return [`${normalized}/**`, `**/${normalized}/**`];
|
|
1447
|
+
if (hasGlobSyntax(normalized)) return [normalized];
|
|
1448
|
+
if (normalized.includes("/")) return [normalized, `${normalized}/**`];
|
|
1449
|
+
return [normalized, `**/${normalized}`];
|
|
1450
|
+
}
|
|
1451
|
+
function sourceIncludePatterns(config) {
|
|
1452
|
+
if (config.config?.source?.include) return config.config.source.include;
|
|
1453
|
+
return getActiveCheckerExtensions(config).map((extension) => `**/*${extension}`);
|
|
1454
|
+
}
|
|
1455
|
+
function sourceExcludePatterns(config) {
|
|
1456
|
+
return (config.config?.source?.exclude ?? defaultSourceExclude).flatMap(normalizeSourceExcludePattern);
|
|
1457
|
+
}
|
|
1458
|
+
async function collectExpectedSourceFiles(config) {
|
|
1459
|
+
const proofFilePattern = config.config?.source?.include !== void 0 ? null : createExtensionPattern(getActiveCheckerExtensions(config));
|
|
1460
|
+
const files = await glob(sourceIncludePatterns(config), {
|
|
1461
|
+
cwd: config.rootDir,
|
|
1462
|
+
absolute: true,
|
|
1463
|
+
ignore: sourceExcludePatterns(config),
|
|
1464
|
+
onlyFiles: true
|
|
1465
|
+
});
|
|
1466
|
+
return new Set(files.map(normalizeAbsolutePath).filter((filePath) => proofFilePattern?.test(filePath) ?? true).sort());
|
|
1467
|
+
}
|
|
1468
|
+
function collectCheckerCoverageTargets(config) {
|
|
1469
|
+
const problems = [];
|
|
1470
|
+
const targets = [];
|
|
1471
|
+
for (const checker of getActiveCheckers(config)) {
|
|
1472
|
+
const configPath = resolveProjectConfigPath(config.rootDir, checker.entry);
|
|
1473
|
+
if (!existsSync(configPath)) {
|
|
1474
|
+
problems.push([
|
|
1475
|
+
"Checker proof entry references a missing tsconfig:",
|
|
1476
|
+
` checker: ${checker.name}`,
|
|
1477
|
+
` config: ${toRelativePath(config.rootDir, configPath)}`
|
|
1478
|
+
].join("\n"));
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
const routeCollection = collectGraphProjectRouteFromRoot({
|
|
1482
|
+
rootConfigPath: configPath,
|
|
1483
|
+
rootDir: config.rootDir
|
|
1484
|
+
});
|
|
1485
|
+
problems.push(...routeCollection.problems);
|
|
1486
|
+
targets.push({
|
|
1487
|
+
checker,
|
|
1488
|
+
configPath,
|
|
1489
|
+
coverageConfigPaths: routeCollection.projectPaths,
|
|
1490
|
+
label: `${checker.name}:entry`
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
return {
|
|
1494
|
+
problems,
|
|
1495
|
+
targets
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
function collectConfiguredAllowlistEntries(config) {
|
|
1499
|
+
const entries = [];
|
|
1500
|
+
const problems = [];
|
|
1501
|
+
const rawEntries = config.proof?.allowlist;
|
|
1502
|
+
if (rawEntries === void 0) return {
|
|
1503
|
+
entries,
|
|
1504
|
+
problems
|
|
1505
|
+
};
|
|
1506
|
+
if (!Array.isArray(rawEntries)) {
|
|
1507
|
+
problems.push([
|
|
1508
|
+
"Invalid proof allowlist config:",
|
|
1509
|
+
" field: proof.allowlist",
|
|
1510
|
+
` value: ${formatUnknownValue(rawEntries)}`,
|
|
1511
|
+
" reason: proof.allowlist must be an array."
|
|
1512
|
+
].join("\n"));
|
|
1513
|
+
return {
|
|
1514
|
+
entries,
|
|
1515
|
+
problems
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
rawEntries.forEach((entry, index) => {
|
|
1519
|
+
const field = `proof.allowlist[${index}]`;
|
|
1520
|
+
if (!isPlainRecord(entry)) {
|
|
1521
|
+
problems.push([
|
|
1522
|
+
"Invalid proof allowlist config:",
|
|
1523
|
+
` field: ${field}`,
|
|
1524
|
+
` value: ${formatUnknownValue(entry)}`,
|
|
1525
|
+
" reason: allowlist entries must be objects with non-empty file and reason fields."
|
|
1526
|
+
].join("\n"));
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
const fileValue = entry.file;
|
|
1530
|
+
const reasonValue = entry.reason;
|
|
1531
|
+
if (typeof fileValue !== "string" || fileValue.trim().length === 0) {
|
|
1532
|
+
problems.push([
|
|
1533
|
+
"Invalid proof allowlist config:",
|
|
1534
|
+
` field: ${field}.file`,
|
|
1535
|
+
` value: ${formatUnknownValue(fileValue)}`,
|
|
1536
|
+
" reason: allowlist file must be a non-empty string."
|
|
1537
|
+
].join("\n"));
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
if (typeof reasonValue !== "string" || reasonValue.trim().length === 0) {
|
|
1541
|
+
problems.push([
|
|
1542
|
+
"Invalid proof allowlist config:",
|
|
1543
|
+
` field: ${field}.reason`,
|
|
1544
|
+
` value: ${formatUnknownValue(reasonValue)}`,
|
|
1545
|
+
" reason: allowlist reason must be a non-empty string."
|
|
1546
|
+
].join("\n"));
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
entries.push({
|
|
1550
|
+
filePath: normalizeAbsolutePath(path.join(config.rootDir, fileValue)),
|
|
1551
|
+
reason: reasonValue.trim()
|
|
1552
|
+
});
|
|
1553
|
+
});
|
|
1554
|
+
return {
|
|
1555
|
+
entries,
|
|
1556
|
+
problems
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
function addCoverage(coverageByFile, filePath, source) {
|
|
1560
|
+
const sources = coverageByFile.get(filePath) ?? [];
|
|
1561
|
+
sources.push(source);
|
|
1562
|
+
coverageByFile.set(filePath, sources);
|
|
1563
|
+
}
|
|
1564
|
+
function collectCoverage(options) {
|
|
1565
|
+
const coverageByFile = /* @__PURE__ */ new Map();
|
|
1566
|
+
const proofFilePattern = createExtensionPattern(getActiveCheckerExtensions(options.config));
|
|
1567
|
+
const typeScriptChecker = getActiveCheckers(options.config).find((checker) => checker.preset === "tsc");
|
|
1568
|
+
for (const graphProjectPath of options.graphProjectPaths) for (const filePath of parseProjectFileNames(options.config, graphProjectPath, proofFilePattern)) {
|
|
1569
|
+
if (!options.sourceFiles.has(filePath)) continue;
|
|
1570
|
+
addCoverage(coverageByFile, filePath, {
|
|
1571
|
+
label: toRelativePath(options.config.rootDir, graphProjectPath),
|
|
1572
|
+
type: "graph"
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
for (const checkerTarget of options.checkerTargets) {
|
|
1576
|
+
const checkerExtensions = [...typeScriptChecker?.extensions ?? [], ...checkerTarget.checker.extensions];
|
|
1577
|
+
for (const configPath of checkerTarget.coverageConfigPaths) for (const filePath of parseProjectFileNamesForExtensions(options.config, configPath, checkerExtensions)) {
|
|
1578
|
+
if (!options.sourceFiles.has(filePath)) continue;
|
|
1579
|
+
addCoverage(coverageByFile, filePath, {
|
|
1580
|
+
label: `${toRelativePath(options.config.rootDir, configPath)} via ${checkerTarget.label}`,
|
|
1581
|
+
type: "checker"
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
if (options.includeAllowlist !== false) for (const entry of options.allowlistEntries) {
|
|
1586
|
+
if (!options.sourceFiles.has(entry.filePath)) continue;
|
|
1587
|
+
addCoverage(coverageByFile, entry.filePath, {
|
|
1588
|
+
label: entry.reason,
|
|
1589
|
+
type: "allowlist"
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
return coverageByFile;
|
|
1593
|
+
}
|
|
1594
|
+
function parseConfig(config, configPath) {
|
|
1595
|
+
const diagnostics = [];
|
|
1596
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, {
|
|
1597
|
+
...ts.sys,
|
|
1598
|
+
onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
|
|
1599
|
+
diagnostics.push(diagnostic);
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
if (!parsed) throw new Error(ts.formatDiagnosticsWithColorAndContext(diagnostics, createFormatHost(config.rootDir)));
|
|
1603
|
+
if (parsed.errors.length > 0) throw new Error(ts.formatDiagnosticsWithColorAndContext(parsed.errors, createFormatHost(config.rootDir)));
|
|
1604
|
+
return {
|
|
1605
|
+
fileNames: parsed.fileNames.map(normalizeAbsolutePath).sort(),
|
|
1606
|
+
options: parsed.options
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
function formatJsonValue(value) {
|
|
1610
|
+
return JSON.stringify(value ?? null);
|
|
1611
|
+
}
|
|
1612
|
+
function normalizeCompilerOptionValue(value) {
|
|
1613
|
+
if (Array.isArray(value)) return value;
|
|
1614
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)));
|
|
1615
|
+
return value;
|
|
1616
|
+
}
|
|
1617
|
+
function addDtsConfigSemanticProblems(options) {
|
|
1618
|
+
const dtsFileNames = new Set(options.dtsConfig.fileNames);
|
|
1619
|
+
const localFileNames = new Set(options.localConfig.fileNames);
|
|
1620
|
+
const onlyInDts = options.dtsConfig.fileNames.filter((fileName) => !localFileNames.has(fileName));
|
|
1621
|
+
const onlyInLocal = options.localConfig.fileNames.filter((fileName) => !dtsFileNames.has(fileName));
|
|
1622
|
+
if (onlyInDts.length > 0 || onlyInLocal.length > 0) options.problems.push([
|
|
1623
|
+
"DTS config file set does not match its strict local tsconfig:",
|
|
1624
|
+
` config: ${toRelativePath(options.config.rootDir, options.dtsConfigPath)}`,
|
|
1625
|
+
` local: ${toRelativePath(options.config.rootDir, options.localConfigPath)}`,
|
|
1626
|
+
...onlyInDts.length > 0 ? [
|
|
1627
|
+
" only in dts config:",
|
|
1628
|
+
...onlyInDts.slice(0, 10).map((fileName) => ` - ${toRelativePath(options.config.rootDir, fileName)}`),
|
|
1629
|
+
onlyInDts.length > 10 ? ` ... ${onlyInDts.length - 10} more` : ""
|
|
1630
|
+
] : [],
|
|
1631
|
+
...onlyInLocal.length > 0 ? [
|
|
1632
|
+
" only in local config:",
|
|
1633
|
+
...onlyInLocal.slice(0, 10).map((fileName) => ` - ${toRelativePath(options.config.rootDir, fileName)}`),
|
|
1634
|
+
onlyInLocal.length > 10 ? ` ... ${onlyInLocal.length - 10} more` : ""
|
|
1635
|
+
] : []
|
|
1636
|
+
].filter(Boolean).join("\n"));
|
|
1637
|
+
const optionNames = new Set([...Object.keys(options.localConfig.options), ...Object.keys(options.dtsConfig.options)]);
|
|
1638
|
+
for (const optionName of [...optionNames].sort()) {
|
|
1639
|
+
if (ignoredSemanticCompilerOptions.has(optionName)) continue;
|
|
1640
|
+
const localValue = normalizeCompilerOptionValue(options.localConfig.options[optionName]);
|
|
1641
|
+
const dtsValue = normalizeCompilerOptionValue(options.dtsConfig.options[optionName]);
|
|
1642
|
+
if (formatJsonValue(localValue) === formatJsonValue(dtsValue)) continue;
|
|
1643
|
+
options.problems.push([
|
|
1644
|
+
"DTS config overrides a typecheck compiler option from its strict local tsconfig:",
|
|
1645
|
+
` config: ${toRelativePath(options.config.rootDir, options.dtsConfigPath)}`,
|
|
1646
|
+
` local: ${toRelativePath(options.config.rootDir, options.localConfigPath)}`,
|
|
1647
|
+
` option: compilerOptions.${optionName}`,
|
|
1648
|
+
` local: ${formatJsonValue(localValue)}`,
|
|
1649
|
+
` dts: ${formatJsonValue(dtsValue)}`
|
|
1650
|
+
].join("\n"));
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
function addDtsConfigProblems(options) {
|
|
1654
|
+
for (const configPath of options.dtsConfigPaths) {
|
|
1655
|
+
if (!options.graphProjectPaths.has(configPath)) options.problems.push(["DTS config is not reachable from any checker entry:", ` config: ${toRelativePath(options.config.rootDir, configPath)}`].join("\n"));
|
|
1656
|
+
const localConfigPath = getDtsCompanionConfigPath(configPath);
|
|
1657
|
+
if (!existsSync(localConfigPath)) {
|
|
1658
|
+
options.problems.push([
|
|
1659
|
+
"DTS config is missing its strict local tsconfig:",
|
|
1660
|
+
` config: ${toRelativePath(options.config.rootDir, configPath)}`,
|
|
1661
|
+
` expected: ${toRelativePath(options.config.rootDir, localConfigPath)}`
|
|
1662
|
+
].join("\n"));
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
const dtsConfig = parseConfig(options.config, configPath);
|
|
1666
|
+
const localConfig = parseConfig(options.config, localConfigPath);
|
|
1667
|
+
if (dtsConfig.options.composite !== true) options.problems.push([
|
|
1668
|
+
"DTS config is not valid for tsc -b:",
|
|
1669
|
+
` config: ${toRelativePath(options.config.rootDir, configPath)}`,
|
|
1670
|
+
" reason: final compilerOptions.composite must be true."
|
|
1671
|
+
].join("\n"));
|
|
1672
|
+
if (dtsConfig.options.noEmit === true) options.problems.push([
|
|
1673
|
+
"DTS config is not valid for tsc -b:",
|
|
1674
|
+
` config: ${toRelativePath(options.config.rootDir, configPath)}`,
|
|
1675
|
+
" reason: final compilerOptions.noEmit must not be true."
|
|
1676
|
+
].join("\n"));
|
|
1677
|
+
if (dtsConfig.options.declaration !== true) options.problems.push([
|
|
1678
|
+
"DTS config is not valid for declaration emit:",
|
|
1679
|
+
` config: ${toRelativePath(options.config.rootDir, configPath)}`,
|
|
1680
|
+
" reason: final compilerOptions.declaration must be true."
|
|
1681
|
+
].join("\n"));
|
|
1682
|
+
if (dtsConfig.options.emitDeclarationOnly !== true) options.problems.push([
|
|
1683
|
+
"DTS config is not valid for declaration emit:",
|
|
1684
|
+
` config: ${toRelativePath(options.config.rootDir, configPath)}`,
|
|
1685
|
+
" reason: final compilerOptions.emitDeclarationOnly must be true."
|
|
1686
|
+
].join("\n"));
|
|
1687
|
+
addDtsConfigSemanticProblems({
|
|
1688
|
+
config: options.config,
|
|
1689
|
+
dtsConfig,
|
|
1690
|
+
dtsConfigPath: configPath,
|
|
1691
|
+
localConfig,
|
|
1692
|
+
localConfigPath,
|
|
1693
|
+
problems: options.problems
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
function isEmptyArray(value) {
|
|
1698
|
+
return Array.isArray(value) && value.length === 0;
|
|
1699
|
+
}
|
|
1700
|
+
function formatConfigRole(role) {
|
|
1701
|
+
return role === "build graph" ? "Build graph config" : "Default tsconfig.json";
|
|
1702
|
+
}
|
|
1703
|
+
function addPureAggregatorProblems(options) {
|
|
1704
|
+
const roleLabel = formatConfigRole(options.role);
|
|
1705
|
+
const allowedKeys = new Set([
|
|
1706
|
+
"$schema",
|
|
1707
|
+
"files",
|
|
1708
|
+
"references"
|
|
1709
|
+
]);
|
|
1710
|
+
const extraKeys = Object.keys(options.configObject).filter((key) => !allowedKeys.has(key));
|
|
1711
|
+
if (!Object.hasOwn(options.configObject, "files")) options.problems.push([
|
|
1712
|
+
`${roleLabel} is not a pure aggregator:`,
|
|
1713
|
+
` config: ${toRelativePath(options.config.rootDir, options.configPath)}`,
|
|
1714
|
+
" field: files",
|
|
1715
|
+
" reason: configs with project references must declare files: []."
|
|
1716
|
+
].join("\n"));
|
|
1717
|
+
else if (!isEmptyArray(options.configObject.files)) options.problems.push([
|
|
1718
|
+
`${roleLabel} is not a pure aggregator:`,
|
|
1719
|
+
` config: ${toRelativePath(options.config.rootDir, options.configPath)}`,
|
|
1720
|
+
" field: files",
|
|
1721
|
+
` value: ${formatUnknownValue(options.configObject.files)}`,
|
|
1722
|
+
" reason: configs with project references must declare files: []."
|
|
1723
|
+
].join("\n"));
|
|
1724
|
+
if (extraKeys.length > 0) options.problems.push([
|
|
1725
|
+
`${roleLabel} is not a pure aggregator:`,
|
|
1726
|
+
` config: ${toRelativePath(options.config.rootDir, options.configPath)}`,
|
|
1727
|
+
` fields: ${extraKeys.sort().join(", ")}`,
|
|
1728
|
+
" reason: pure aggregators may only declare $schema, files, and references; move source inputs and compiler options into leaf configs."
|
|
1729
|
+
].join("\n"));
|
|
1730
|
+
}
|
|
1731
|
+
function addBuildGraphConfigProblems(options) {
|
|
1732
|
+
for (const configPath of options.buildGraphConfigPaths) {
|
|
1733
|
+
const configObject = readJsonConfig(options.config, configPath);
|
|
1734
|
+
addPureAggregatorProblems({
|
|
1735
|
+
config: options.config,
|
|
1736
|
+
configObject,
|
|
1737
|
+
configPath,
|
|
1738
|
+
problems: options.problems,
|
|
1739
|
+
role: "build graph"
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
function addDefaultTsconfigShapeProblems(options) {
|
|
1744
|
+
for (const configPath of options.tsconfigPaths) {
|
|
1745
|
+
const configObject = readJsonConfig(options.config, configPath);
|
|
1746
|
+
if (!Object.hasOwn(configObject, "references")) continue;
|
|
1747
|
+
addPureAggregatorProblems({
|
|
1748
|
+
config: options.config,
|
|
1749
|
+
configObject,
|
|
1750
|
+
configPath,
|
|
1751
|
+
problems: options.problems,
|
|
1752
|
+
role: "tsconfig.json"
|
|
1753
|
+
});
|
|
1754
|
+
if (!Array.isArray(configObject.references)) continue;
|
|
1755
|
+
configObject.references.forEach((reference, index) => {
|
|
1756
|
+
if (!isPlainRecord(reference) || typeof reference.path !== "string") return;
|
|
1757
|
+
const referencePath = resolveReferencePath(configPath, reference.path);
|
|
1758
|
+
if (isOrdinaryTypecheckConfigPath(referencePath)) return;
|
|
1759
|
+
options.problems.push([
|
|
1760
|
+
"Default tsconfig.json references a non-typecheck config:",
|
|
1761
|
+
` config: ${toRelativePath(options.config.rootDir, configPath)}`,
|
|
1762
|
+
` field: references[${index}].path`,
|
|
1763
|
+
` reference: ${reference.path}`,
|
|
1764
|
+
` resolved: ${toRelativePath(options.config.rootDir, referencePath)}`,
|
|
1765
|
+
" reason: tsconfig.json is the default IDE/typecheck entry and must not reference declaration build graph configs."
|
|
1766
|
+
].join("\n"));
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
function addDefaultTsconfigEnvironmentProblems(options) {
|
|
1771
|
+
const configsByDirectory = /* @__PURE__ */ new Map();
|
|
1772
|
+
for (const configPath of options.ordinaryConfigPaths) {
|
|
1773
|
+
const directory = path.dirname(configPath);
|
|
1774
|
+
const configs = configsByDirectory.get(directory) ?? [];
|
|
1775
|
+
configs.push(configPath);
|
|
1776
|
+
configsByDirectory.set(directory, configs);
|
|
1777
|
+
}
|
|
1778
|
+
for (const [directory, configPaths] of configsByDirectory.entries()) {
|
|
1779
|
+
const scopedConfigPaths = configPaths.filter((configPath) => path.basename(configPath) !== "tsconfig.json");
|
|
1780
|
+
if (scopedConfigPaths.length === 0) continue;
|
|
1781
|
+
const defaultConfigPath = normalizeAbsolutePath(path.join(directory, "tsconfig.json"));
|
|
1782
|
+
if (!existsSync(defaultConfigPath)) {
|
|
1783
|
+
options.problems.push([
|
|
1784
|
+
"Directory with typecheck environments is missing default tsconfig.json:",
|
|
1785
|
+
` directory: ${toRelativePath(options.config.rootDir, directory)}`,
|
|
1786
|
+
" reason: tsconfig.json is the default IDE/typecheck entry for its directory."
|
|
1787
|
+
].join("\n"));
|
|
1788
|
+
continue;
|
|
1789
|
+
}
|
|
1790
|
+
if (scopedConfigPaths.length === 1) {
|
|
1791
|
+
options.problems.push([
|
|
1792
|
+
"Single typecheck environment should use default tsconfig.json:",
|
|
1793
|
+
` config: ${toRelativePath(options.config.rootDir, scopedConfigPaths[0])}`,
|
|
1794
|
+
` default: ${toRelativePath(options.config.rootDir, defaultConfigPath)}`,
|
|
1795
|
+
" reason: directories with only one type environment should make tsconfig.json the leaf entry."
|
|
1796
|
+
].join("\n"));
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
const defaultConfigObject = readJsonConfig(options.config, defaultConfigPath);
|
|
1800
|
+
if (!Object.hasOwn(defaultConfigObject, "references")) options.problems.push([
|
|
1801
|
+
"Directory with multiple typecheck environments must use tsconfig.json as an aggregator:",
|
|
1802
|
+
` config: ${toRelativePath(options.config.rootDir, defaultConfigPath)}`,
|
|
1803
|
+
" reason: multiple type environments require a default IDE/typecheck aggregator."
|
|
1804
|
+
].join("\n"));
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
function collectConfigFileOwners(config, configPaths, sourceFiles) {
|
|
1808
|
+
const ownersByFile = /* @__PURE__ */ new Map();
|
|
1809
|
+
const proofFilePattern = createExtensionPattern(getActiveCheckerExtensions(config));
|
|
1810
|
+
for (const configPath of configPaths) {
|
|
1811
|
+
if (!existsSync(configPath)) continue;
|
|
1812
|
+
for (const filePath of parseProjectFileNames(config, configPath, proofFilePattern)) {
|
|
1813
|
+
if (!sourceFiles.has(filePath)) continue;
|
|
1814
|
+
const owners = ownersByFile.get(filePath) ?? [];
|
|
1815
|
+
owners.push(configPath);
|
|
1816
|
+
ownersByFile.set(filePath, owners);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
return ownersByFile;
|
|
1820
|
+
}
|
|
1821
|
+
function addDuplicateGraphCoverageProblems(options) {
|
|
1822
|
+
for (const [filePath, owners] of [...options.ownersByFile.entries()].sort(([left], [right]) => toRelativePath(options.config.rootDir, left).localeCompare(toRelativePath(options.config.rootDir, right)))) {
|
|
1823
|
+
const uniqueOwners = [...new Set(owners)];
|
|
1824
|
+
if (uniqueOwners.length <= 1) continue;
|
|
1825
|
+
options.problems.push([
|
|
1826
|
+
"Duplicate checker graph coverage:",
|
|
1827
|
+
` file: ${toRelativePath(options.config.rootDir, filePath)}`,
|
|
1828
|
+
" covered by:",
|
|
1829
|
+
...uniqueOwners.sort((left, right) => toRelativePath(options.config.rootDir, left).localeCompare(toRelativePath(options.config.rootDir, right))).map((configPath) => ` - ${toRelativePath(options.config.rootDir, configPath)}`),
|
|
1830
|
+
" reason: a checker graph file must have a single declaration owner; move the file to one dts leaf or narrow include/exclude patterns."
|
|
1831
|
+
].join("\n"));
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
function addDuplicateGraphOwnerProblems(options) {
|
|
1835
|
+
for (const [configPath, ownerCheckerNames] of options.graphOwnersByConfigPath.entries()) {
|
|
1836
|
+
const uniqueOwnerCheckerNames = [...new Set(ownerCheckerNames)].sort();
|
|
1837
|
+
if (uniqueOwnerCheckerNames.length <= 1) continue;
|
|
1838
|
+
options.problems.push([
|
|
1839
|
+
"Duplicate checker graph declaration owner:",
|
|
1840
|
+
` config: ${toRelativePath(options.config.rootDir, configPath)}`,
|
|
1841
|
+
" owned by:",
|
|
1842
|
+
...uniqueOwnerCheckerNames.map((checkerName) => ` - ${checkerName}`),
|
|
1843
|
+
" reason: each tsconfig*.dts.json must be reached by exactly one graph-capable checker entry."
|
|
1844
|
+
].join("\n"));
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
function addAllowlistProblems(options) {
|
|
1848
|
+
for (const entry of options.allowlistEntries) {
|
|
1849
|
+
if (!existsSync(entry.filePath)) {
|
|
1850
|
+
options.problems.push(["Typecheck proof allowlist references a missing file:", ` file: ${toRelativePath(options.config.rootDir, entry.filePath)}`].join("\n"));
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
if (!options.sourceFiles.has(entry.filePath)) {
|
|
1854
|
+
options.problems.push([
|
|
1855
|
+
"Typecheck proof allowlist file is outside the configured source boundary:",
|
|
1856
|
+
` file: ${toRelativePath(options.config.rootDir, entry.filePath)}`,
|
|
1857
|
+
" reason: allowlist entries should only describe source files that proof would otherwise require coverage for."
|
|
1858
|
+
].join("\n"));
|
|
1859
|
+
continue;
|
|
1860
|
+
}
|
|
1861
|
+
if (options.baseCoverageByFile.has(entry.filePath)) options.problems.push(["Typecheck proof allowlist file is already covered without the allowlist:", ` file: ${toRelativePath(options.config.rootDir, entry.filePath)}`].join("\n"));
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
function addUncoveredSourceProblems(options) {
|
|
1865
|
+
const uncoveredFiles = [...options.sourceFiles].filter((filePath) => !options.coverageByFile.has(filePath));
|
|
1866
|
+
if (uncoveredFiles.length === 0) return;
|
|
1867
|
+
options.problems.push([
|
|
1868
|
+
"Source files are not covered by typecheck proof:",
|
|
1869
|
+
...uncoveredFiles.slice(0, 20).map((filePath) => ` - ${toRelativePath(options.config.rootDir, filePath)}`),
|
|
1870
|
+
uncoveredFiles.length > 20 ? ` ... ${uncoveredFiles.length - 20} more` : "",
|
|
1871
|
+
" reason: every file in config.source must be covered by a checker entry or an explicit allowlist entry."
|
|
1872
|
+
].filter(Boolean).join("\n"));
|
|
1873
|
+
}
|
|
1874
|
+
function addGraphOwner(ownersByConfigPath, configPath, checkerName) {
|
|
1875
|
+
const owners = ownersByConfigPath.get(configPath) ?? [];
|
|
1876
|
+
owners.push(checkerName);
|
|
1877
|
+
ownersByConfigPath.set(configPath, owners);
|
|
1878
|
+
}
|
|
1879
|
+
async function runProofCheckInternal(config, options = {}) {
|
|
1880
|
+
const problems = [];
|
|
1881
|
+
const graphRouteCollection = collectGraphProjectRoutes(config);
|
|
1882
|
+
const entryRouteCollection = collectCheckerEntryProjectRoutes(config);
|
|
1883
|
+
const graphProjectPaths = [...new Set(graphRouteCollection.routes.flatMap((route) => route.projectPaths))].sort();
|
|
1884
|
+
const entryProjectPaths = [...new Set(entryRouteCollection.routes.flatMap((route) => route.projectPaths))].sort();
|
|
1885
|
+
const entryProjectPathSet = new Set(entryProjectPaths);
|
|
1886
|
+
const dtsConfigPaths = await collectDtsConfigPaths(config);
|
|
1887
|
+
const buildGraphConfigPaths = await collectBuildGraphConfigPaths(config);
|
|
1888
|
+
const defaultTsconfigPaths = await collectDefaultTsconfigPaths(config);
|
|
1889
|
+
const ordinaryTypecheckConfigPaths = await collectOrdinaryTypecheckConfigPaths(config);
|
|
1890
|
+
const graphOwnersByConfigPath = /* @__PURE__ */ new Map();
|
|
1891
|
+
problems.push(...graphRouteCollection.problems);
|
|
1892
|
+
problems.push(...entryRouteCollection.problems);
|
|
1893
|
+
for (const route of graphRouteCollection.routes) for (const projectPath of route.projectPaths) {
|
|
1894
|
+
if (!isDtsConfigPath(projectPath)) continue;
|
|
1895
|
+
addGraphOwner(graphOwnersByConfigPath, projectPath, route.checkerName);
|
|
1896
|
+
}
|
|
1897
|
+
addDtsConfigProblems({
|
|
1898
|
+
config,
|
|
1899
|
+
dtsConfigPaths,
|
|
1900
|
+
graphProjectPaths: entryProjectPathSet,
|
|
1901
|
+
problems
|
|
1902
|
+
});
|
|
1903
|
+
addBuildGraphConfigProblems({
|
|
1904
|
+
buildGraphConfigPaths,
|
|
1905
|
+
config,
|
|
1906
|
+
problems
|
|
1907
|
+
});
|
|
1908
|
+
addDefaultTsconfigShapeProblems({
|
|
1909
|
+
config,
|
|
1910
|
+
problems,
|
|
1911
|
+
tsconfigPaths: defaultTsconfigPaths
|
|
1912
|
+
});
|
|
1913
|
+
addDefaultTsconfigEnvironmentProblems({
|
|
1914
|
+
config,
|
|
1915
|
+
ordinaryConfigPaths: ordinaryTypecheckConfigPaths,
|
|
1916
|
+
problems
|
|
1917
|
+
});
|
|
1918
|
+
addDuplicateGraphOwnerProblems({
|
|
1919
|
+
config,
|
|
1920
|
+
graphOwnersByConfigPath,
|
|
1921
|
+
problems
|
|
1922
|
+
});
|
|
1923
|
+
if (problems.length > 0) {
|
|
1924
|
+
ProofLogger.error(problems.join("\n\n"));
|
|
1925
|
+
return false;
|
|
1926
|
+
}
|
|
1927
|
+
const checkerTargetCollection = collectCheckerCoverageTargets(config);
|
|
1928
|
+
const checkerTargets = checkerTargetCollection.targets;
|
|
1929
|
+
problems.push(...checkerTargetCollection.problems);
|
|
1930
|
+
if (problems.length > 0) {
|
|
1931
|
+
ProofLogger.error(problems.join("\n\n"));
|
|
1932
|
+
return false;
|
|
1933
|
+
}
|
|
1934
|
+
const sourceFiles = await collectExpectedSourceFiles(config);
|
|
1935
|
+
const allowlistCollection = collectConfiguredAllowlistEntries(config);
|
|
1936
|
+
const allowlistEntries = allowlistCollection.entries;
|
|
1937
|
+
problems.push(...allowlistCollection.problems);
|
|
1938
|
+
const baseCoverageByFile = collectCoverage({
|
|
1939
|
+
allowlistEntries,
|
|
1940
|
+
checkerTargets,
|
|
1941
|
+
config,
|
|
1942
|
+
graphProjectPaths,
|
|
1943
|
+
includeAllowlist: false,
|
|
1944
|
+
sourceFiles
|
|
1945
|
+
});
|
|
1946
|
+
const coverageByFile = collectCoverage({
|
|
1947
|
+
allowlistEntries,
|
|
1948
|
+
checkerTargets,
|
|
1949
|
+
config,
|
|
1950
|
+
graphProjectPaths,
|
|
1951
|
+
sourceFiles
|
|
1952
|
+
});
|
|
1953
|
+
addDuplicateGraphCoverageProblems({
|
|
1954
|
+
config,
|
|
1955
|
+
ownersByFile: collectConfigFileOwners(config, graphProjectPaths, sourceFiles),
|
|
1956
|
+
problems
|
|
1957
|
+
});
|
|
1958
|
+
addAllowlistProblems({
|
|
1959
|
+
allowlistEntries,
|
|
1960
|
+
baseCoverageByFile,
|
|
1961
|
+
config,
|
|
1962
|
+
problems,
|
|
1963
|
+
sourceFiles
|
|
1964
|
+
});
|
|
1965
|
+
addUncoveredSourceProblems({
|
|
1966
|
+
config,
|
|
1967
|
+
coverageByFile,
|
|
1968
|
+
problems,
|
|
1969
|
+
sourceFiles
|
|
1970
|
+
});
|
|
1971
|
+
if (problems.length > 0) {
|
|
1972
|
+
ProofLogger.error(problems.join("\n\n"));
|
|
1973
|
+
return false;
|
|
1974
|
+
}
|
|
1975
|
+
const graphFileCount = [...coverageByFile.values()].filter((sources) => sources.some((source) => source.type === "graph")).length;
|
|
1976
|
+
const checkerFileCount = [...coverageByFile.values()].filter((sources) => sources.some((source) => source.type === "checker")).length;
|
|
1977
|
+
if (options.logSuccess ?? true) ProofLogger.success([
|
|
1978
|
+
`Checked ${entryProjectPaths.length} checker entry projects and ${dtsConfigPaths.length} dts configs.`,
|
|
1979
|
+
`Graph-capable checker entries cover ${graphFileCount} files; checker entries cover ${checkerFileCount} files.`,
|
|
1980
|
+
`Configured source boundary covers ${sourceFiles.size} files.`
|
|
1981
|
+
].join("\n"));
|
|
1982
|
+
if ((options.logSuccess ?? true) && (config.proof?.allowlist ?? []).length > 0) ProofLogger.info(`Explicit typecheck proof allowlist: ${(config.proof?.allowlist ?? []).map((entry) => entry.file).join(", ")}`);
|
|
1983
|
+
return true;
|
|
1984
|
+
}
|
|
1985
|
+
async function runProofCheck(config, options = {}) {
|
|
1986
|
+
if (options.clearScreen ?? true) clearCliScreen();
|
|
1987
|
+
const elapsed = createElapsedTimer();
|
|
1988
|
+
const task = options.flow?.start("proof check", { depth: options.flowDepth ?? 0 });
|
|
1989
|
+
ProofLogger.info("proof check started");
|
|
1990
|
+
try {
|
|
1991
|
+
const logSuccess = !options.flow?.interactive;
|
|
1992
|
+
const passed = await runProofCheckInternal(config, { logSuccess });
|
|
1993
|
+
if (passed) {
|
|
1994
|
+
if (logSuccess) ProofLogger.success("proof check finished", elapsed());
|
|
1995
|
+
task?.pass();
|
|
1996
|
+
} else {
|
|
1997
|
+
ProofLogger.error("proof check finished with failures", elapsed());
|
|
1998
|
+
task?.fail("proof check finished with failures");
|
|
1999
|
+
}
|
|
2000
|
+
return passed;
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
ProofLogger.error(`proof check failed: ${formatErrorMessage$1(error)}`, elapsed());
|
|
2003
|
+
task?.fail("proof check failed", { error });
|
|
2004
|
+
throw error;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
//#endregion
|
|
2009
|
+
//#region src/pipeline.ts
|
|
2010
|
+
const builtInTaskNames = new Set([
|
|
2011
|
+
"checker:build",
|
|
2012
|
+
"checker:typecheck",
|
|
2013
|
+
"graph:check",
|
|
2014
|
+
"package:check",
|
|
2015
|
+
"proof:check",
|
|
2016
|
+
"source:check"
|
|
2017
|
+
]);
|
|
2018
|
+
function isBuiltinTaskName(value) {
|
|
2019
|
+
return builtInTaskNames.has(value);
|
|
2020
|
+
}
|
|
2021
|
+
function getPipelineStepLabel(step) {
|
|
2022
|
+
if (step.type === "task") return step.name;
|
|
2023
|
+
return [step.command, ...step.args ?? []].join(" ");
|
|
2024
|
+
}
|
|
2025
|
+
async function runBuiltinTask(config, taskName, options = {}) {
|
|
2026
|
+
switch (taskName) {
|
|
2027
|
+
case "graph:check": return runGraphCheck(config, {
|
|
2028
|
+
clearScreen: false,
|
|
2029
|
+
flow: options.flow,
|
|
2030
|
+
flowDepth: 1
|
|
2031
|
+
});
|
|
2032
|
+
case "proof:check": return runProofCheck(config, {
|
|
2033
|
+
clearScreen: false,
|
|
2034
|
+
flow: options.flow,
|
|
2035
|
+
flowDepth: 1
|
|
2036
|
+
});
|
|
2037
|
+
case "source:check": return runSourceCheck(config, {
|
|
2038
|
+
clearScreen: false,
|
|
2039
|
+
flow: options.flow,
|
|
2040
|
+
flowDepth: 1
|
|
2041
|
+
});
|
|
2042
|
+
case "package:check": return runPackageCheck({
|
|
2043
|
+
clearScreen: false,
|
|
2044
|
+
config,
|
|
2045
|
+
cwd: options.cwd,
|
|
2046
|
+
flow: options.flow,
|
|
2047
|
+
flowDepth: 1
|
|
2048
|
+
});
|
|
2049
|
+
case "checker:typecheck": return (await runCheckerTypecheck({
|
|
2050
|
+
clearScreen: false,
|
|
2051
|
+
config,
|
|
2052
|
+
cwd: config.rootDir,
|
|
2053
|
+
flow: options.flow,
|
|
2054
|
+
flowDepth: 1
|
|
2055
|
+
})).passed;
|
|
2056
|
+
case "checker:build": return (await runCheckerBuild({
|
|
2057
|
+
clearScreen: false,
|
|
2058
|
+
config,
|
|
2059
|
+
cwd: config.rootDir,
|
|
2060
|
+
flow: options.flow,
|
|
2061
|
+
flowDepth: 1
|
|
2062
|
+
})).passed;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
function normalizePipelineStep(step) {
|
|
2066
|
+
if (typeof step !== "string") return step;
|
|
2067
|
+
if (isBuiltinTaskName(step)) return {
|
|
2068
|
+
name: step,
|
|
2069
|
+
type: "task"
|
|
2070
|
+
};
|
|
2071
|
+
const [command, ...args] = step.split(/\s+/u).filter(Boolean);
|
|
2072
|
+
if (!command) throw new Error("Pipeline command step must not be empty.");
|
|
2073
|
+
return {
|
|
2074
|
+
args,
|
|
2075
|
+
command,
|
|
2076
|
+
type: "command"
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
function runCommandStep(config, step, options = {}) {
|
|
2080
|
+
const label = getPipelineStepLabel(step);
|
|
2081
|
+
const task = options.flow?.start(`command: ${label}`, { depth: 1 });
|
|
2082
|
+
const commandOptions = {
|
|
2083
|
+
cwd: step.cwd ? path.resolve(config.rootDir, step.cwd) : config.rootDir,
|
|
2084
|
+
env: {
|
|
2085
|
+
...process.env,
|
|
2086
|
+
...step.env
|
|
2087
|
+
},
|
|
2088
|
+
shell: process.platform === "win32"
|
|
2089
|
+
};
|
|
2090
|
+
if (options.flow?.interactive) return new Promise((resolve, reject) => {
|
|
2091
|
+
const child = spawn(step.command, step.args ?? [], {
|
|
2092
|
+
...commandOptions,
|
|
2093
|
+
stdio: [
|
|
2094
|
+
"inherit",
|
|
2095
|
+
"pipe",
|
|
2096
|
+
"pipe"
|
|
2097
|
+
]
|
|
2098
|
+
});
|
|
2099
|
+
child.stdout?.on("data", (chunk) => {
|
|
2100
|
+
options.flow?.writeOutput(chunk, { stream: "stdout" });
|
|
2101
|
+
});
|
|
2102
|
+
child.stderr?.on("data", (chunk) => {
|
|
2103
|
+
options.flow?.writeOutput(chunk, { stream: "stderr" });
|
|
2104
|
+
});
|
|
2105
|
+
child.on("error", (error) => {
|
|
2106
|
+
task?.fail(void 0, { error });
|
|
2107
|
+
reject(error);
|
|
2108
|
+
});
|
|
2109
|
+
child.on("close", (code) => {
|
|
2110
|
+
const passed = (code ?? 1) === 0;
|
|
2111
|
+
if (passed) task?.pass();
|
|
2112
|
+
else task?.fail(`command failed: ${label} exited with code ${code ?? 1}`);
|
|
2113
|
+
resolve(passed);
|
|
2114
|
+
});
|
|
2115
|
+
});
|
|
2116
|
+
const result = spawnSync(step.command, step.args ?? [], {
|
|
2117
|
+
...commandOptions,
|
|
2118
|
+
stdio: "inherit"
|
|
2119
|
+
});
|
|
2120
|
+
if (result.error) {
|
|
2121
|
+
task?.fail(void 0, { error: result.error });
|
|
2122
|
+
throw result.error;
|
|
2123
|
+
}
|
|
2124
|
+
const passed = (result.status ?? 1) === 0;
|
|
2125
|
+
if (passed) task?.pass();
|
|
2126
|
+
else task?.fail(`command failed: ${label} exited with code ${result.status ?? 1}`);
|
|
2127
|
+
return passed;
|
|
2128
|
+
}
|
|
2129
|
+
async function runPipeline(config, pipelineName, options = {}) {
|
|
2130
|
+
const steps = config.pipelines?.[pipelineName];
|
|
2131
|
+
if (!steps) throw new Error(`Unknown limina pipeline "${pipelineName}".`);
|
|
2132
|
+
const normalizedSteps = steps.map(normalizePipelineStep);
|
|
2133
|
+
const pipelineTask = options.flow?.start(`pipeline: ${pipelineName}`, { collapseOnSuccess: false });
|
|
2134
|
+
for (const [stepIndex, step] of normalizedSteps.entries()) if (!(step.type === "task" ? await runBuiltinTask(config, step.name, options) : await runCommandStep(config, step, options))) {
|
|
2135
|
+
const label = getPipelineStepLabel(step);
|
|
2136
|
+
pipelineTask?.fail(`pipeline blocked: ${pipelineName} at ${label}`);
|
|
2137
|
+
for (const remainingStep of normalizedSteps.slice(stepIndex + 1)) options.flow?.skip(`skipped: ${getPipelineStepLabel(remainingStep)}`, { depth: 1 });
|
|
2138
|
+
return false;
|
|
2139
|
+
}
|
|
2140
|
+
pipelineTask?.pass();
|
|
2141
|
+
return true;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
//#endregion
|
|
2145
|
+
//#region src/cli.ts
|
|
2146
|
+
async function load(flags, command) {
|
|
2147
|
+
return loadConfig({
|
|
2148
|
+
command,
|
|
2149
|
+
configPath: flags.config,
|
|
2150
|
+
cwd: process.cwd(),
|
|
2151
|
+
mode: flags.mode
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
function parsePackageTool(tool) {
|
|
2155
|
+
if (!tool) return;
|
|
2156
|
+
if (tool === "all" || tool === "publint" || tool === "attw" || tool === "boundary") return tool;
|
|
2157
|
+
throw new Error(`Invalid package check --tool "${tool}". Expected one of: all, publint, attw, boundary.`);
|
|
2158
|
+
}
|
|
2159
|
+
function parsePackageAttwProfile(profile) {
|
|
2160
|
+
if (!profile) return;
|
|
2161
|
+
if (profile === "strict" || profile === "node16" || profile === "esm-only") return profile;
|
|
2162
|
+
throw new Error(`Invalid package check --attw-profile "${profile}". Expected one of: strict, node16, esm-only.`);
|
|
2163
|
+
}
|
|
2164
|
+
function parseConcurrency(value) {
|
|
2165
|
+
if (value === void 0) return;
|
|
2166
|
+
const parsed = Number(value);
|
|
2167
|
+
if (!Number.isInteger(parsed) || parsed < 1) throw new Error("Invalid --concurrency value. Expected a positive integer.");
|
|
2168
|
+
return parsed;
|
|
2169
|
+
}
|
|
2170
|
+
function createCliFlow() {
|
|
2171
|
+
clearCliScreen();
|
|
2172
|
+
return createLiminaFlowReporter();
|
|
2173
|
+
}
|
|
2174
|
+
async function main() {
|
|
2175
|
+
const cli = cac("limina");
|
|
2176
|
+
cli.option("--config <path>", "Path to limina.config.mjs");
|
|
2177
|
+
cli.option("--mode <mode>", "Mode passed to limina config functions");
|
|
2178
|
+
cli.help();
|
|
2179
|
+
cli.command("check <pipeline>", "Run a configured governance pipeline").action(async (pipeline, flags) => {
|
|
2180
|
+
const flow = createCliFlow();
|
|
2181
|
+
flow.intro("limina check");
|
|
2182
|
+
const passed = await runPipeline(await load(flags, "check"), pipeline, {
|
|
2183
|
+
cwd: process.cwd(),
|
|
2184
|
+
flow
|
|
2185
|
+
});
|
|
2186
|
+
if (!passed) process.exitCode = 1;
|
|
2187
|
+
flow.outro(passed ? "limina check passed" : "limina check failed");
|
|
2188
|
+
});
|
|
2189
|
+
cli.command("paths <action>", "Generate source paths for workspace dependency artifact exports").action(async (action, flags) => {
|
|
2190
|
+
if (action !== "generate" && action !== "apply" && action !== "check") throw new Error(`Unknown paths action "${action}". Expected generate, apply, or check.`);
|
|
2191
|
+
const flow = createCliFlow();
|
|
2192
|
+
flow.intro(`limina paths ${action}`);
|
|
2193
|
+
const result = await runPaths(await load(flags, "paths"), {
|
|
2194
|
+
check: action === "check",
|
|
2195
|
+
clearScreen: false,
|
|
2196
|
+
flow
|
|
2197
|
+
});
|
|
2198
|
+
if (action === "check" && result.changed) process.exitCode = 1;
|
|
2199
|
+
flow.outro(action === "check" && result.changed ? "limina paths failed" : "limina paths passed");
|
|
2200
|
+
});
|
|
2201
|
+
cli.command("graph <action>", "Check TypeScript graph architecture").action(async (action, flags) => {
|
|
2202
|
+
if (action !== "check") throw new Error(`Unknown graph action "${action}". Expected check.`);
|
|
2203
|
+
const flow = createCliFlow();
|
|
2204
|
+
flow.intro("limina graph check");
|
|
2205
|
+
const passed = await runGraphCheck(await load(flags, "graph"), {
|
|
2206
|
+
clearScreen: false,
|
|
2207
|
+
flow
|
|
2208
|
+
});
|
|
2209
|
+
if (!passed) process.exitCode = 1;
|
|
2210
|
+
flow.outro(passed ? "limina graph passed" : "limina graph failed");
|
|
2211
|
+
});
|
|
2212
|
+
cli.command("proof <action>", "Check root typecheck coverage proof").action(async (action, flags) => {
|
|
2213
|
+
if (action !== "check") throw new Error(`Unknown proof action "${action}". Expected check.`);
|
|
2214
|
+
const flow = createCliFlow();
|
|
2215
|
+
flow.intro("limina proof check");
|
|
2216
|
+
const passed = await runProofCheck(await load(flags, "proof"), {
|
|
2217
|
+
clearScreen: false,
|
|
2218
|
+
flow
|
|
2219
|
+
});
|
|
2220
|
+
if (!passed) process.exitCode = 1;
|
|
2221
|
+
flow.outro(passed ? "limina proof passed" : "limina proof failed");
|
|
2222
|
+
});
|
|
2223
|
+
cli.command("source <action>", "Check source package boundaries").action(async (action, flags) => {
|
|
2224
|
+
if (action !== "check") throw new Error(`Unknown source action "${action}". Expected check.`);
|
|
2225
|
+
const flow = createCliFlow();
|
|
2226
|
+
flow.intro("limina source check");
|
|
2227
|
+
const passed = await runSourceCheck(await load(flags, "source"), {
|
|
2228
|
+
clearScreen: false,
|
|
2229
|
+
flow
|
|
2230
|
+
});
|
|
2231
|
+
if (!passed) process.exitCode = 1;
|
|
2232
|
+
flow.outro(passed ? "limina source passed" : "limina source failed");
|
|
2233
|
+
});
|
|
2234
|
+
cli.command("checker <action>", "Run configured checker typecheck or build entries").option("--concurrency <n>", "Maximum concurrent checker processes").action(async (action, flags) => {
|
|
2235
|
+
if (action !== "typecheck" && action !== "build") throw new Error(`Unknown checker action "${action}". Expected typecheck or build.`);
|
|
2236
|
+
const flow = createCliFlow();
|
|
2237
|
+
flow.intro(`limina checker ${action}`);
|
|
2238
|
+
if (action === "build") {
|
|
2239
|
+
const result = await runCheckerBuild({
|
|
2240
|
+
clearScreen: false,
|
|
2241
|
+
config: await load(flags, "check"),
|
|
2242
|
+
cwd: process.cwd(),
|
|
2243
|
+
flow
|
|
2244
|
+
});
|
|
2245
|
+
if (!result.passed) process.exitCode = 1;
|
|
2246
|
+
flow.outro(result.passed ? "limina checker passed" : "limina checker failed");
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
const result = await runCheckerTypecheck({
|
|
2250
|
+
clearScreen: false,
|
|
2251
|
+
config: await load(flags, "check"),
|
|
2252
|
+
concurrency: parseConcurrency(flags.concurrency),
|
|
2253
|
+
cwd: process.cwd(),
|
|
2254
|
+
flow
|
|
2255
|
+
});
|
|
2256
|
+
if (!result.passed) process.exitCode = 1;
|
|
2257
|
+
flow.outro(result.passed ? "limina checker passed" : "limina checker failed");
|
|
2258
|
+
});
|
|
2259
|
+
cli.command("package <action>", "Check configured published package outputs").option("-p, --package <name>", "Run a single package check target").option("--tool <tool>", "Run one package check tool").option("--attw-profile <profile>", "Override the configured ATTW profile").action(async (action, flags) => {
|
|
2260
|
+
if (action !== "check") throw new Error(`Unknown package action "${action}". Expected check.`);
|
|
2261
|
+
const flow = createCliFlow();
|
|
2262
|
+
flow.intro("limina package check");
|
|
2263
|
+
const config = await load(flags, "package");
|
|
2264
|
+
const passed = await runPackageCheck({
|
|
2265
|
+
attwProfile: parsePackageAttwProfile(flags.attwProfile),
|
|
2266
|
+
clearScreen: false,
|
|
2267
|
+
config,
|
|
2268
|
+
cwd: process.cwd(),
|
|
2269
|
+
flow,
|
|
2270
|
+
targetName: flags.package,
|
|
2271
|
+
tool: parsePackageTool(flags.tool)
|
|
2272
|
+
});
|
|
2273
|
+
if (!passed) process.exitCode = 1;
|
|
2274
|
+
flow.outro(passed ? "limina package passed" : "limina package failed");
|
|
2275
|
+
});
|
|
2276
|
+
cli.parse(process.argv, { run: false });
|
|
2277
|
+
try {
|
|
2278
|
+
await cli.runMatchedCommand();
|
|
2279
|
+
} catch (error) {
|
|
2280
|
+
CliLogger.error(`limina failed: ${formatErrorMessage$1(error)}`);
|
|
2281
|
+
process.exitCode = 1;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
await main();
|
|
2285
|
+
|
|
2286
|
+
//#endregion
|
|
2287
|
+
export { };
|