limina 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +384 -0
- package/bin/limina.js +0 -0
- package/chunks/{dep-DoSHsBSP.js → dep-uPXyoC0V.js} +654 -206
- package/cli.js +225 -150
- package/config.d.ts +7 -41
- package/index.d.ts +20 -2
- package/index.js +2 -2
- package/package.json +14 -2
package/cli.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./chunks/dep-lkQg1P9Q.js";
|
|
3
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
|
|
4
|
+
import { A as shouldResolveThroughGraph$1, B as createExtensionPattern, C as formatArtifactDependencyPolicy, D as isRelativeSpecifier, E as isDtsProjectConfig, F as collectCheckerEntryProjectRoutes, G as isDtsConfigPath, H as formatReferences, I as collectGraphProjectRoute, J as parseProjectFileNamesForExtensions, K as isOrdinaryTypecheckConfigPath, L as collectGraphProjectRouteFromRoot, M as collectWorkspacePackages, N as findPackageForSpecifier, O as parseProject$1, P as getPackageRootSpecifier, R as collectGraphProjectRoutes, S as findTargetProject, T as inferPackageProject$1, U as getDtsCompanionConfigPath, V as createFormatHost, W as getRawReferencePaths, X as resolveProjectConfigPath, Y as readJsonConfig, Z as resolveReferencePath, _ as normalizeGraphRules, a as runSourceCheck, b as findImporterForFile$1, c as GraphLogger, d as ProofLogger, f as clearCliScreen, g as getDeniedRefRule, h as getDeniedDepRuleForSpecifier, i as runCheckerTypecheck, j as collectImporters, k as resolveInternalImport$1, l as PackageLogger, m as getDeniedDepRuleForPackage, n as createLiminaFlowReporter, o as runInit, p as formatErrorMessage$1, q as parseProjectFileNames, r as runCheckerBuild, s as CliLogger, u as PathsLogger, v as collectImportsFromFile$1, w as getTypecheckConfigPath, x as findPackageForFile, y as createFileOwnerLookup$1 } from "./chunks/dep-uPXyoC0V.js";
|
|
5
5
|
import { builtinModules } from "node:module";
|
|
6
6
|
import { cac } from "cac";
|
|
7
|
-
import { createElapsedTimer } from "
|
|
7
|
+
import { createElapsedTimer } from "logaria/helper";
|
|
8
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import ts from "typescript";
|
|
11
11
|
import { spawn, spawnSync } from "node:child_process";
|
|
12
12
|
import { glob } from "tinyglobby";
|
|
13
|
+
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
13
14
|
import { checkPackage, createPackageFromTarballData } from "@arethetypeswrong/core";
|
|
14
15
|
import { pack } from "@publint/pack";
|
|
15
16
|
import { init, parse } from "es-module-lexer";
|
|
16
|
-
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
17
17
|
import { tmpdir } from "node:os";
|
|
18
18
|
import { publint } from "publint";
|
|
19
19
|
import { formatMessage } from "publint/utils";
|
|
@@ -146,7 +146,7 @@ function addDeniedReferenceProblems(options) {
|
|
|
146
146
|
if (!options.projectsByPath.has(referencePath)) continue;
|
|
147
147
|
const deniedRefRule = getDeniedRefRule(options.rules, options.project.label, referencePath);
|
|
148
148
|
const targetPackage = findPackageForFile(referencePath, options.packages);
|
|
149
|
-
const deniedDepRule = targetPackage ?
|
|
149
|
+
const deniedDepRule = targetPackage ? getDeniedDepRuleForPackage(options.rules, options.project.label, targetPackage.name) : null;
|
|
150
150
|
if (!deniedRefRule && !deniedDepRule) continue;
|
|
151
151
|
const lines = [
|
|
152
152
|
"Denied graph access:",
|
|
@@ -154,12 +154,12 @@ function addDeniedReferenceProblems(options) {
|
|
|
154
154
|
` referencing project: ${toRelativePath(options.config.rootDir, options.project.configPath)}`,
|
|
155
155
|
` referenced project: ${toRelativePath(options.config.rootDir, referencePath)}`
|
|
156
156
|
];
|
|
157
|
-
if (
|
|
158
|
-
else if (
|
|
157
|
+
if (deniedDepRule) lines.push(` denied dependency: ${deniedDepRule.name}`, ` reason: ${deniedDepRule.reason}`);
|
|
158
|
+
else if (deniedRefRule) lines.push(` denied ref: ${toRelativePath(options.config.rootDir, deniedRefRule.path)}`, ` reason: ${deniedRefRule.reason}`);
|
|
159
159
|
options.problems.push(lines.join("\n"));
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
-
function
|
|
162
|
+
function addDeniedDepImportProblem(options) {
|
|
163
163
|
options.problems.push([
|
|
164
164
|
"Denied graph access:",
|
|
165
165
|
` rule: ${options.project.label}`,
|
|
@@ -182,6 +182,25 @@ function addDeniedRefImportProblem(options) {
|
|
|
182
182
|
` reason: ${options.rule.reason}`
|
|
183
183
|
].join("\n"));
|
|
184
184
|
}
|
|
185
|
+
function getNodeModulesPackageName(filePath) {
|
|
186
|
+
const parts = filePath.split("/");
|
|
187
|
+
const nodeModulesIndex = parts.lastIndexOf("node_modules");
|
|
188
|
+
if (nodeModulesIndex === -1) return null;
|
|
189
|
+
const packageName = parts[nodeModulesIndex + 1];
|
|
190
|
+
if (!packageName) return null;
|
|
191
|
+
if (packageName.startsWith("@")) {
|
|
192
|
+
const scopedName = parts[nodeModulesIndex + 2];
|
|
193
|
+
return scopedName ? `${packageName}/${scopedName}` : null;
|
|
194
|
+
}
|
|
195
|
+
return packageName;
|
|
196
|
+
}
|
|
197
|
+
function getResolvedPackageName(filePath, packages) {
|
|
198
|
+
return getNodeModulesPackageName(filePath) ?? findPackageForFile(filePath, packages)?.name ?? null;
|
|
199
|
+
}
|
|
200
|
+
function getResolvedWorkspacePackage(filePath, packages) {
|
|
201
|
+
if (getNodeModulesPackageName(filePath)) return null;
|
|
202
|
+
return findPackageForFile(filePath, packages);
|
|
203
|
+
}
|
|
185
204
|
function addWorkspaceReferenceDependencyProblems(config, project, projectsByPath, packages, importers, problems) {
|
|
186
205
|
if (!isDtsProjectConfig(project.configPath)) return;
|
|
187
206
|
const sourcePackage = findPackageForFile(project.configPath, packages);
|
|
@@ -216,9 +235,8 @@ async function runGraphCheckInternal(config, options = {}) {
|
|
|
216
235
|
const graphRules = normalizeGraphRules({
|
|
217
236
|
config,
|
|
218
237
|
include: {
|
|
219
|
-
|
|
220
|
-
refs: true
|
|
221
|
-
workspaceDeps: true
|
|
238
|
+
deps: true,
|
|
239
|
+
refs: true
|
|
222
240
|
},
|
|
223
241
|
packages,
|
|
224
242
|
problems,
|
|
@@ -238,21 +256,21 @@ async function runGraphCheckInternal(config, options = {}) {
|
|
|
238
256
|
});
|
|
239
257
|
addWorkspaceReferenceDependencyProblems(config, project, projectsByPath, packages, importers, problems);
|
|
240
258
|
for (const filePath of project.fileNames) for (const importRecord of collectImportsFromFile$1(filePath)) {
|
|
259
|
+
const rawDeniedDepRule = getDeniedDepRuleForSpecifier(graphRules, project.label, importRecord.specifier);
|
|
260
|
+
if (rawDeniedDepRule) {
|
|
261
|
+
addDeniedDepImportProblem({
|
|
262
|
+
config,
|
|
263
|
+
importRecord,
|
|
264
|
+
problems,
|
|
265
|
+
project,
|
|
266
|
+
rule: rawDeniedDepRule
|
|
267
|
+
});
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
241
270
|
const resolvedFilePath = resolveInternalImport$1(importRecord.specifier, filePath, project.options);
|
|
242
271
|
const targetPackage = findPackageForSpecifier(importRecord.specifier, packages);
|
|
243
|
-
const importer =
|
|
244
|
-
const unresolvedDeniedDepRule = targetPackage ? getDeniedWorkspaceDepRule(graphRules, project.label, targetPackage.name) : null;
|
|
272
|
+
const importer = findImporterForFile$1(importRecord.filePath, importers);
|
|
245
273
|
if (!resolvedFilePath) {
|
|
246
|
-
if (unresolvedDeniedDepRule) {
|
|
247
|
-
addDeniedPackageImportProblem({
|
|
248
|
-
config,
|
|
249
|
-
importRecord,
|
|
250
|
-
problems,
|
|
251
|
-
project,
|
|
252
|
-
rule: unresolvedDeniedDepRule
|
|
253
|
-
});
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
274
|
if (!targetPackage) continue;
|
|
257
275
|
problems.push([
|
|
258
276
|
"Unresolved workspace import:",
|
|
@@ -264,10 +282,12 @@ async function runGraphCheckInternal(config, options = {}) {
|
|
|
264
282
|
].join("\n"));
|
|
265
283
|
continue;
|
|
266
284
|
}
|
|
267
|
-
const targetWorkspacePackageForResolved =
|
|
268
|
-
const
|
|
285
|
+
const targetWorkspacePackageForResolved = getResolvedWorkspacePackage(resolvedFilePath, packages);
|
|
286
|
+
const targetPackageForGraph = targetPackage;
|
|
287
|
+
const resolvedPackageName = getResolvedPackageName(resolvedFilePath, packages);
|
|
288
|
+
const deniedDepRule = resolvedPackageName ? getDeniedDepRuleForPackage(graphRules, project.label, resolvedPackageName) : null;
|
|
269
289
|
if (deniedDepRule) {
|
|
270
|
-
|
|
290
|
+
addDeniedDepImportProblem({
|
|
271
291
|
config,
|
|
272
292
|
importRecord,
|
|
273
293
|
problems,
|
|
@@ -292,9 +312,8 @@ async function runGraphCheckInternal(config, options = {}) {
|
|
|
292
312
|
continue;
|
|
293
313
|
}
|
|
294
314
|
}
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
const referencedProjectPath = inferPackageProject$1(resolvedFilePath, targetPackage, projectPaths);
|
|
315
|
+
if (targetPackageForGraph && shouldResolveThroughGraph$1(importer, targetPackageForGraph) && !fileOwnerLookup.has(resolvedFilePath)) {
|
|
316
|
+
const referencedProjectPath = inferPackageProject$1(resolvedFilePath, targetPackageForGraph, projectPaths);
|
|
298
317
|
const hasProjectReference = referencedProjectPath && project.references.has(referencedProjectPath);
|
|
299
318
|
problems.push([
|
|
300
319
|
hasProjectReference ? "Referenced workspace dependency resolves through package exports to a build artifact:" : "Workspace source dependency resolved outside the source graph:",
|
|
@@ -304,7 +323,7 @@ async function runGraphCheckInternal(config, options = {}) {
|
|
|
304
323
|
` imported specifier: ${importRecord.specifier}`,
|
|
305
324
|
` resolved file: ${toRelativePath(config.rootDir, resolvedFilePath)}`,
|
|
306
325
|
" 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(
|
|
326
|
+
` 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(targetPackageForGraph)}`,
|
|
308
327
|
" 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
328
|
].join("\n"));
|
|
310
329
|
continue;
|
|
@@ -317,15 +336,15 @@ async function runGraphCheckInternal(config, options = {}) {
|
|
|
317
336
|
specifier: importRecord.specifier
|
|
318
337
|
});
|
|
319
338
|
if (!targetProjectPath) {
|
|
320
|
-
if (!
|
|
321
|
-
if (!
|
|
322
|
-
if (
|
|
339
|
+
if (!targetPackageForGraph) continue;
|
|
340
|
+
if (!targetWorkspacePackageForResolved) {
|
|
341
|
+
if (targetPackageForGraph && shouldResolveThroughGraph$1(importer, targetPackageForGraph)) problems.push([
|
|
323
342
|
"Workspace source import resolved outside the workspace graph:",
|
|
324
343
|
` importing project: ${toRelativePath(config.rootDir, project.configPath)}`,
|
|
325
344
|
` file: ${toRelativePath(config.rootDir, importRecord.filePath)}:${importRecord.line}`,
|
|
326
345
|
` imported specifier: ${importRecord.specifier}`,
|
|
327
346
|
` 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(
|
|
347
|
+
` reason: workspace:* dependencies are source dependency edges and must resolve to files owned by the source graph; ${formatArtifactDependencyPolicy(targetPackageForGraph)}`
|
|
329
348
|
].join("\n"));
|
|
330
349
|
continue;
|
|
331
350
|
}
|
|
@@ -352,6 +371,7 @@ async function runGraphCheckInternal(config, options = {}) {
|
|
|
352
371
|
});
|
|
353
372
|
continue;
|
|
354
373
|
}
|
|
374
|
+
if (targetPackageForGraph && !shouldResolveThroughGraph$1(importer, targetPackageForGraph)) continue;
|
|
355
375
|
if (!projectsByPath.has(targetProjectPath)) {
|
|
356
376
|
problems.push([
|
|
357
377
|
"Expected graph target is not reachable from any checker entry:",
|
|
@@ -416,6 +436,7 @@ const ATTW_PROFILE_IGNORED_RESOLUTIONS = {
|
|
|
416
436
|
"esm-only": ["node16-cjs"]
|
|
417
437
|
};
|
|
418
438
|
const nodeBuiltinSpecifiers = new Set(builtinModules.flatMap((specifier) => specifier.startsWith("node:") ? [specifier, specifier.slice(5)] : [specifier, `node:${specifier}`]));
|
|
439
|
+
const REQUIRED_PUBLIC_PACKAGE_FILES = ["README.md", "LICENSE.md"];
|
|
419
440
|
function isRecord(value) {
|
|
420
441
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
421
442
|
}
|
|
@@ -625,7 +646,7 @@ function logPackageCheckPlan(options) {
|
|
|
625
646
|
].join("\n"));
|
|
626
647
|
}
|
|
627
648
|
async function packOutputTarball(outDir) {
|
|
628
|
-
const destination = await mkdtemp(path.join(tmpdir(), "
|
|
649
|
+
const destination = await mkdtemp(path.join(tmpdir(), "__LIMINA_PACKAGE__"));
|
|
629
650
|
return {
|
|
630
651
|
cleanup: async () => {
|
|
631
652
|
await rm(destination, {
|
|
@@ -640,6 +661,20 @@ async function packOutputTarball(outDir) {
|
|
|
640
661
|
}))
|
|
641
662
|
};
|
|
642
663
|
}
|
|
664
|
+
async function readDistPackageJson(options) {
|
|
665
|
+
if (!existsSync(options.packageJsonPath)) throw new Error(`outDir package.json not found${options.label ? ` for ${options.label}` : ""} at ${options.config ? toRelativePath(options.config.rootDir, options.packageJsonPath) : options.packageJsonPath}. Run the package build first.`);
|
|
666
|
+
return JSON.parse(await readFile(options.packageJsonPath, "utf8"));
|
|
667
|
+
}
|
|
668
|
+
async function assertPublicPackageMetadata(options) {
|
|
669
|
+
if ((await readDistPackageJson({
|
|
670
|
+
config: options.config,
|
|
671
|
+
label: options.label,
|
|
672
|
+
packageJsonPath: options.packageJsonPath
|
|
673
|
+
})).private === true) return;
|
|
674
|
+
const missingFiles = REQUIRED_PUBLIC_PACKAGE_FILES.filter((fileName) => !existsSync(path.join(options.outDir, fileName)));
|
|
675
|
+
if (missingFiles.length === 0) return;
|
|
676
|
+
throw new Error(`publishable package output for ${options.label} at ${toRelativePath(options.config.rootDir, options.outDir)} is missing required file(s): ${missingFiles.join(", ")}. Add them to the built output or set "private": true in the output package.json.`);
|
|
677
|
+
}
|
|
643
678
|
async function runPublintCheck(options) {
|
|
644
679
|
const task = options.flow?.start(`publint: ${options.label}`, { depth: options.flowDepth ?? 0 });
|
|
645
680
|
PackageLogger.info(`publint started: ${options.label}`);
|
|
@@ -720,7 +755,12 @@ async function runPackageCheckTarget(options) {
|
|
|
720
755
|
const task = options.flow?.start(`package target: ${label}`, { depth: options.flowDepth ?? 0 });
|
|
721
756
|
let packedDist;
|
|
722
757
|
try {
|
|
723
|
-
|
|
758
|
+
await assertPublicPackageMetadata({
|
|
759
|
+
config: options.config,
|
|
760
|
+
label,
|
|
761
|
+
outDir: target.outDir,
|
|
762
|
+
packageJsonPath: outputPackageJsonPath
|
|
763
|
+
});
|
|
724
764
|
if (options.checks.includes("publint") || options.checks.includes("attw")) {
|
|
725
765
|
const packTask = options.flow?.start(`package tarball: ${label}`, { depth: (options.flowDepth ?? 0) + 1 });
|
|
726
766
|
PackageLogger.info(`package tarball packing started: ${label}`);
|
|
@@ -774,8 +814,7 @@ async function runPackageCheckTarget(options) {
|
|
|
774
814
|
}
|
|
775
815
|
}
|
|
776
816
|
async function auditPublishedPackageBoundaries(target) {
|
|
777
|
-
const
|
|
778
|
-
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
817
|
+
const manifest = await readDistPackageJson({ packageJsonPath: path.join(target.outDir, "package.json") });
|
|
779
818
|
const allowedExternalPackages = new Set([
|
|
780
819
|
...Object.keys(manifest.dependencies ?? {}),
|
|
781
820
|
...Object.keys(manifest.peerDependencies ?? {}),
|
|
@@ -860,7 +899,7 @@ async function runPackageCheck(options) {
|
|
|
860
899
|
}
|
|
861
900
|
|
|
862
901
|
//#endregion
|
|
863
|
-
//#region src/
|
|
902
|
+
//#region src/package-exports.ts
|
|
864
903
|
const defaultSourceExtensions = [
|
|
865
904
|
".ts",
|
|
866
905
|
".tsx",
|
|
@@ -887,110 +926,11 @@ const defaultArtifactDirectories = [
|
|
|
887
926
|
"cjs",
|
|
888
927
|
"out"
|
|
889
928
|
];
|
|
890
|
-
function
|
|
891
|
-
return config.paths?.
|
|
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);
|
|
929
|
+
function configuredArtifactDirectories(config) {
|
|
930
|
+
return config.paths?.artifactDirectories ?? defaultArtifactDirectories;
|
|
988
931
|
}
|
|
989
|
-
function
|
|
990
|
-
|
|
991
|
-
return projectPaths.find((projectPath) => {
|
|
992
|
-
return projectPath.startsWith(`${workspacePackage.directory}/`) && projectPath.endsWith("/tsconfig.lib.dts.json");
|
|
993
|
-
}) ?? null;
|
|
932
|
+
function configuredSourceExtensions(config) {
|
|
933
|
+
return config.paths?.sourceExtensions ?? defaultSourceExtensions;
|
|
994
934
|
}
|
|
995
935
|
function collectTargetCandidates(config, value) {
|
|
996
936
|
if (typeof value === "string") return [value];
|
|
@@ -1033,12 +973,6 @@ function removeKnownExtension(filePath) {
|
|
|
1033
973
|
]) if (filePath.endsWith(extension)) return filePath.slice(0, -extension.length);
|
|
1034
974
|
return filePath;
|
|
1035
975
|
}
|
|
1036
|
-
function configuredArtifactDirectories(config) {
|
|
1037
|
-
return config.paths?.artifactDirectories ?? defaultArtifactDirectories;
|
|
1038
|
-
}
|
|
1039
|
-
function configuredSourceExtensions(config) {
|
|
1040
|
-
return config.paths?.sourceExtensions ?? defaultSourceExtensions;
|
|
1041
|
-
}
|
|
1042
976
|
function hasConfiguredSourceExtension(config, target) {
|
|
1043
977
|
return configuredSourceExtensions(config).some((extension) => target.endsWith(extension));
|
|
1044
978
|
}
|
|
@@ -1151,6 +1085,114 @@ function aliasMatchesSpecifier(alias, specifier) {
|
|
|
1151
1085
|
const suffix = alias.slice(wildcardIndex + 1);
|
|
1152
1086
|
return specifier.startsWith(prefix) && specifier.endsWith(suffix);
|
|
1153
1087
|
}
|
|
1088
|
+
|
|
1089
|
+
//#endregion
|
|
1090
|
+
//#region src/commands/paths.ts
|
|
1091
|
+
function generatedFileName(config) {
|
|
1092
|
+
return config.paths?.generatedFileName ?? "tsconfig.dts.paths.generated.json";
|
|
1093
|
+
}
|
|
1094
|
+
function generatedFileMarker(config) {
|
|
1095
|
+
return config.paths?.generatedFileMarker ?? "GENERATED FILE - DO NOT EDIT BY HAND.";
|
|
1096
|
+
}
|
|
1097
|
+
function parseProject(config, configPath) {
|
|
1098
|
+
const diagnostics = [];
|
|
1099
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, {
|
|
1100
|
+
...ts.sys,
|
|
1101
|
+
onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
|
|
1102
|
+
diagnostics.push(diagnostic);
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
if (!parsed) throw new Error(ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
1106
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
1107
|
+
getCurrentDirectory: () => config.rootDir,
|
|
1108
|
+
getNewLine: () => "\n"
|
|
1109
|
+
}));
|
|
1110
|
+
if (parsed.errors.length > 0) throw new Error(ts.formatDiagnosticsWithColorAndContext(parsed.errors, {
|
|
1111
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
1112
|
+
getCurrentDirectory: () => config.rootDir,
|
|
1113
|
+
getNewLine: () => "\n"
|
|
1114
|
+
}));
|
|
1115
|
+
return {
|
|
1116
|
+
configPath: normalizeAbsolutePath(configPath),
|
|
1117
|
+
fileNames: parsed.fileNames.filter((fileName) => /\.(?:[cm]?tsx?|d\.[cm]?ts)$/u.test(fileName)).map(normalizeAbsolutePath),
|
|
1118
|
+
options: parsed.options,
|
|
1119
|
+
references: new Set(getRawReferencePaths(config, configPath))
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function getSourceFileKind(filePath) {
|
|
1123
|
+
if (filePath.endsWith(".tsx")) return ts.ScriptKind.TSX;
|
|
1124
|
+
if (filePath.endsWith(".jsx")) return ts.ScriptKind.JSX;
|
|
1125
|
+
return ts.ScriptKind.TS;
|
|
1126
|
+
}
|
|
1127
|
+
function stringLiteralValue(node) {
|
|
1128
|
+
return node && ts.isStringLiteralLike(node) ? node.text : null;
|
|
1129
|
+
}
|
|
1130
|
+
function collectImportsFromFile(filePath) {
|
|
1131
|
+
const sourceText = readFileSync(filePath, "utf8");
|
|
1132
|
+
const sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true, getSourceFileKind(filePath));
|
|
1133
|
+
const imports = [];
|
|
1134
|
+
const addImport = (specifier, node) => {
|
|
1135
|
+
const location = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
1136
|
+
imports.push({
|
|
1137
|
+
filePath,
|
|
1138
|
+
line: location.line + 1,
|
|
1139
|
+
specifier
|
|
1140
|
+
});
|
|
1141
|
+
};
|
|
1142
|
+
const visit = (node) => {
|
|
1143
|
+
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
|
|
1144
|
+
const specifier = stringLiteralValue(node.moduleSpecifier);
|
|
1145
|
+
if (specifier) addImport(specifier, node);
|
|
1146
|
+
} else if (ts.isImportTypeNode(node)) {
|
|
1147
|
+
const specifier = ts.isLiteralTypeNode(node.argument) ? stringLiteralValue(node.argument.literal) : null;
|
|
1148
|
+
if (specifier) addImport(specifier, node);
|
|
1149
|
+
} else if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
1150
|
+
const specifier = stringLiteralValue(node.arguments[0]);
|
|
1151
|
+
if (specifier) addImport(specifier, node);
|
|
1152
|
+
}
|
|
1153
|
+
ts.forEachChild(node, visit);
|
|
1154
|
+
};
|
|
1155
|
+
visit(sourceFile);
|
|
1156
|
+
return imports;
|
|
1157
|
+
}
|
|
1158
|
+
function resolveInternalImport(specifier, containingFile, options) {
|
|
1159
|
+
const resolved = ts.resolveModuleName(specifier, containingFile, options, ts.sys).resolvedModule;
|
|
1160
|
+
return resolved?.resolvedFileName ? normalizeAbsolutePath(resolved.resolvedFileName) : null;
|
|
1161
|
+
}
|
|
1162
|
+
function resolveImportWithoutMatchingPaths(specifier, containingFile, options) {
|
|
1163
|
+
if (!options.paths) return resolveInternalImport(specifier, containingFile, options);
|
|
1164
|
+
const paths = Object.fromEntries(Object.entries(options.paths).filter(([alias]) => !aliasMatchesSpecifier(alias, specifier)));
|
|
1165
|
+
return resolveInternalImport(specifier, containingFile, {
|
|
1166
|
+
...options,
|
|
1167
|
+
paths: Object.keys(paths).length > 0 ? paths : void 0
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
function createFileOwnerLookup(projects) {
|
|
1171
|
+
const ownerLookup = /* @__PURE__ */ new Map();
|
|
1172
|
+
for (const project of projects) for (const fileName of project.fileNames) {
|
|
1173
|
+
const owners = ownerLookup.get(fileName) ?? [];
|
|
1174
|
+
owners.push(project.configPath);
|
|
1175
|
+
ownerLookup.set(fileName, owners);
|
|
1176
|
+
}
|
|
1177
|
+
return ownerLookup;
|
|
1178
|
+
}
|
|
1179
|
+
function projectExtendsGeneratedConfig(config, configPath) {
|
|
1180
|
+
const extendsValue = readJsonConfig(config, configPath).extends;
|
|
1181
|
+
return (typeof extendsValue === "string" ? [extendsValue] : Array.isArray(extendsValue) ? extendsValue : []).some((entry) => typeof entry === "string" && path.basename(entry) === generatedFileName(config));
|
|
1182
|
+
}
|
|
1183
|
+
function findImporterForFile(filePath, importers) {
|
|
1184
|
+
return importers.find((importer) => isPathInsideDirectory(filePath, importer.directory)) ?? null;
|
|
1185
|
+
}
|
|
1186
|
+
function shouldResolveThroughGraph(importer, targetPackage) {
|
|
1187
|
+
if (!importer || !targetPackage) return false;
|
|
1188
|
+
return importer.name === targetPackage.name || importer.workspaceDependencies.has(targetPackage.name);
|
|
1189
|
+
}
|
|
1190
|
+
function inferPackageProject(resolvedFilePath, workspacePackage, projectPaths) {
|
|
1191
|
+
if (!isPathInsideDirectory(resolvedFilePath, workspacePackage.directory)) return null;
|
|
1192
|
+
return projectPaths.find((projectPath) => {
|
|
1193
|
+
return projectPath.startsWith(`${workspacePackage.directory}/`) && projectPath.endsWith("/tsconfig.lib.dts.json");
|
|
1194
|
+
}) ?? null;
|
|
1195
|
+
}
|
|
1154
1196
|
function addPathEntry(paths, alias, target) {
|
|
1155
1197
|
const targets = paths.get(alias) ?? [];
|
|
1156
1198
|
if (!targets.includes(target)) targets.push(target);
|
|
@@ -2015,6 +2057,12 @@ const builtInTaskNames = new Set([
|
|
|
2015
2057
|
"proof:check",
|
|
2016
2058
|
"source:check"
|
|
2017
2059
|
]);
|
|
2060
|
+
const defaultCheckPipeline = [
|
|
2061
|
+
"graph:check",
|
|
2062
|
+
"source:check",
|
|
2063
|
+
"proof:check",
|
|
2064
|
+
"checker:typecheck"
|
|
2065
|
+
];
|
|
2018
2066
|
function isBuiltinTaskName(value) {
|
|
2019
2067
|
return builtInTaskNames.has(value);
|
|
2020
2068
|
}
|
|
@@ -2128,7 +2176,7 @@ function runCommandStep(config, step, options = {}) {
|
|
|
2128
2176
|
}
|
|
2129
2177
|
async function runPipeline(config, pipelineName, options = {}) {
|
|
2130
2178
|
const steps = config.pipelines?.[pipelineName];
|
|
2131
|
-
if (!steps) throw new Error(`
|
|
2179
|
+
if (!steps) throw new Error([`Pipeline instruction "${pipelineName}" was not found.`, `Define it in ${path.relative(config.rootDir, config.configPath)} under the "pipelines" field, then run "limina check ${pipelineName}" again.`].join("\n"));
|
|
2132
2180
|
const normalizedSteps = steps.map(normalizePipelineStep);
|
|
2133
2181
|
const pipelineTask = options.flow?.start(`pipeline: ${pipelineName}`, { collapseOnSuccess: false });
|
|
2134
2182
|
for (const [stepIndex, step] of normalizedSteps.entries()) if (!(step.type === "task" ? await runBuiltinTask(config, step.name, options) : await runCommandStep(config, step, options))) {
|
|
@@ -2140,6 +2188,18 @@ async function runPipeline(config, pipelineName, options = {}) {
|
|
|
2140
2188
|
pipelineTask?.pass();
|
|
2141
2189
|
return true;
|
|
2142
2190
|
}
|
|
2191
|
+
async function runDefaultCheck(config, options = {}) {
|
|
2192
|
+
const normalizedSteps = defaultCheckPipeline.map(normalizePipelineStep);
|
|
2193
|
+
const pipelineTask = options.flow?.start("default check", { collapseOnSuccess: false });
|
|
2194
|
+
for (const [stepIndex, step] of normalizedSteps.entries()) if (!(step.type === "task" ? await runBuiltinTask(config, step.name, options) : await runCommandStep(config, step, options))) {
|
|
2195
|
+
const label = getPipelineStepLabel(step);
|
|
2196
|
+
pipelineTask?.fail(`default check blocked at ${label}`);
|
|
2197
|
+
for (const remainingStep of normalizedSteps.slice(stepIndex + 1)) options.flow?.skip(`skipped: ${getPipelineStepLabel(remainingStep)}`, { depth: 1 });
|
|
2198
|
+
return false;
|
|
2199
|
+
}
|
|
2200
|
+
pipelineTask?.pass();
|
|
2201
|
+
return true;
|
|
2202
|
+
}
|
|
2143
2203
|
|
|
2144
2204
|
//#endregion
|
|
2145
2205
|
//#region src/cli.ts
|
|
@@ -2176,10 +2236,25 @@ async function main() {
|
|
|
2176
2236
|
cli.option("--config <path>", "Path to limina.config.mjs");
|
|
2177
2237
|
cli.option("--mode <mode>", "Mode passed to limina config functions");
|
|
2178
2238
|
cli.help();
|
|
2179
|
-
cli.command("
|
|
2239
|
+
cli.command("init", "Initialize Limina files for a pnpm workspace").option("--yes", "Accept all init prompts").action(async (flags) => {
|
|
2240
|
+
const flow = createCliFlow();
|
|
2241
|
+
flow.intro("limina init");
|
|
2242
|
+
await runInit({
|
|
2243
|
+
clearScreen: false,
|
|
2244
|
+
cwd: process.cwd(),
|
|
2245
|
+
flow,
|
|
2246
|
+
yes: flags.yes
|
|
2247
|
+
});
|
|
2248
|
+
flow.outro("limina init finished");
|
|
2249
|
+
});
|
|
2250
|
+
cli.command("check [pipeline]", "Run the default check or a configured pipeline").action(async (pipeline, flags) => {
|
|
2180
2251
|
const flow = createCliFlow();
|
|
2181
2252
|
flow.intro("limina check");
|
|
2182
|
-
const
|
|
2253
|
+
const config = await load(flags, "check");
|
|
2254
|
+
const passed = pipeline ? await runPipeline(config, pipeline, {
|
|
2255
|
+
cwd: process.cwd(),
|
|
2256
|
+
flow
|
|
2257
|
+
}) : await runDefaultCheck(config, {
|
|
2183
2258
|
cwd: process.cwd(),
|
|
2184
2259
|
flow
|
|
2185
2260
|
});
|
package/config.d.ts
CHANGED
|
@@ -173,39 +173,14 @@ interface GraphRuleRefDenyEntry {
|
|
|
173
173
|
reason: string;
|
|
174
174
|
}
|
|
175
175
|
/**
|
|
176
|
-
*
|
|
176
|
+
* Dependency denied to projects with a matching Limina label.
|
|
177
177
|
*/
|
|
178
178
|
interface GraphRuleDepDenyEntry {
|
|
179
179
|
/**
|
|
180
|
-
* Target
|
|
180
|
+
* Target package root, package.json imports specifier, or Node builtin name.
|
|
181
181
|
*
|
|
182
|
-
*
|
|
183
|
-
|
|
184
|
-
name: string;
|
|
185
|
-
/**
|
|
186
|
-
* Human-readable explanation shown when the rule fails.
|
|
187
|
-
*/
|
|
188
|
-
reason: string;
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Workspace package dependency denied to projects with a matching Limina label.
|
|
192
|
-
*/
|
|
193
|
-
interface GraphRuleWorkspaceDepDenyEntry {
|
|
194
|
-
/**
|
|
195
|
-
* Target workspace package name.
|
|
196
|
-
*/
|
|
197
|
-
name: string;
|
|
198
|
-
/**
|
|
199
|
-
* Human-readable explanation shown when the rule fails.
|
|
200
|
-
*/
|
|
201
|
-
reason: string;
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Node builtin denied to source files with a matching Limina label.
|
|
205
|
-
*/
|
|
206
|
-
interface GraphRuleNodeBuiltinDenyEntry {
|
|
207
|
-
/**
|
|
208
|
-
* Target Node builtin name, such as `fs`, `node:fs`, or `node:*`.
|
|
182
|
+
* Examples: `@acme/internal`, `zod`, `#internal/*`, `fs`, `node:fs`,
|
|
183
|
+
* or `node:*`.
|
|
209
184
|
*/
|
|
210
185
|
name: string;
|
|
211
186
|
/**
|
|
@@ -222,19 +197,10 @@ interface GraphRuleDenyConfig {
|
|
|
222
197
|
*/
|
|
223
198
|
refs?: GraphRuleRefDenyEntry[];
|
|
224
199
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* @deprecated Use `workspaceDeps` for new configs.
|
|
200
|
+
* Packages, package imports, and Node builtins that matching projects must
|
|
201
|
+
* not reference or import.
|
|
228
202
|
*/
|
|
229
203
|
deps?: GraphRuleDepDenyEntry[];
|
|
230
|
-
/**
|
|
231
|
-
* Node builtins that matching projects must not import.
|
|
232
|
-
*/
|
|
233
|
-
nodeBuiltins?: GraphRuleNodeBuiltinDenyEntry[];
|
|
234
|
-
/**
|
|
235
|
-
* Workspace packages that matching projects must not reference or import.
|
|
236
|
-
*/
|
|
237
|
-
workspaceDeps?: GraphRuleWorkspaceDepDenyEntry[];
|
|
238
204
|
}
|
|
239
205
|
/**
|
|
240
206
|
* Package-level graph governance rule keyed by a label declared in
|
|
@@ -480,4 +446,4 @@ interface LoadConfigOptions {
|
|
|
480
446
|
}
|
|
481
447
|
declare function loadConfig(options?: LoadConfigOptions): Promise<ResolvedLiminaConfig>;
|
|
482
448
|
//#endregion
|
|
483
|
-
export { BuiltinCheckerPreset, BuiltinTaskName, CheckerConfig, CheckerExecutionKind, CheckerPreset, GraphConfig, GraphRule, GraphRuleDenyConfig, GraphRuleDepDenyEntry,
|
|
449
|
+
export { BuiltinCheckerPreset, BuiltinTaskName, CheckerConfig, CheckerExecutionKind, CheckerPreset, GraphConfig, GraphRule, GraphRuleDenyConfig, GraphRuleDepDenyEntry, GraphRuleRefDenyEntry, LiminaCommand, LiminaConfig, LiminaConfigEnv, LiminaConfigExport, LiminaConfigFn, LiminaConfigFnObject, LiminaConfigFnPromise, LoadConfigOptions, PackageAttwCheckConfig, PackageAttwProfile, PackageBoundaryCheckConfig, PackageCheckTarget, PackageCheckTool, PackageCheckToolSelection, PackageChecksConfig, PackagePublintCheckConfig, PathsConfig, PipelineStep, ProofAllowlistEntry, ProofConfig, ResolvedCheckerConfig, ResolvedLiminaConfig, RuntimeEnvironment, SharedLiminaConfig, SourceBoundaryConfig, defineConfig, getActiveCheckerExtensions, getActiveCheckers, loadConfig, validateLiminaConfig };
|