@cplieger/deadset-ts 4.0.1 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/bin/component-mapper.js +5 -0
- package/dist/bin/mapper-connection.js +101 -0
- package/dist/bin/node-host.js +24 -2
- package/dist/src/analysis.js +301 -41
- package/dist/src/component-files.js +612 -0
- package/dist/src/component-layer.js +132 -0
- package/dist/src/config.js +9 -2
- package/dist/src/configuration-files.js +290 -0
- package/dist/src/conformance-record.js +3 -9
- package/dist/src/contextual-uses.js +240 -0
- package/dist/src/convention-rows.js +389 -0
- package/dist/src/conventions.js +565 -0
- package/dist/src/dependencies.js +36 -3
- package/dist/src/destructuring.js +6 -6
- package/dist/src/discover.js +10 -3
- package/dist/src/emit-map.js +72 -0
- package/dist/src/entry-point-gaps.js +81 -35
- package/dist/src/entry-points.js +50 -32
- package/dist/src/exempt-classes.js +6 -1
- package/dist/src/exempt.js +32 -20
- package/dist/src/findings/completion.js +46 -32
- package/dist/src/findings/emitters.js +22 -1
- package/dist/src/findings/interfaces.js +6 -2
- package/dist/src/findings/reads-and-writes.js +53 -9
- package/dist/src/findings/unused-declarations.js +47 -3
- package/dist/src/generated-file.js +24 -0
- package/dist/src/graph.js +13 -5
- package/dist/src/interface-satisfaction.js +2 -68
- package/dist/src/inventory.js +8 -3
- package/dist/src/kinds.js +6 -5
- package/dist/src/manifest.js +37 -11
- package/dist/src/matrix.js +21 -13
- package/dist/src/partial.js +3 -1
- package/dist/src/pnpm-workspace.js +120 -0
- package/dist/src/position.js +17 -2
- package/dist/src/print.js +2 -0
- package/dist/src/query.js +15 -0
- package/dist/src/references.js +404 -36
- package/dist/src/report.js +26 -6
- package/dist/src/reporters/annotations.js +10 -3
- package/dist/src/reporters/template.js +3 -0
- package/dist/src/resolve.js +37 -11
- package/dist/src/roots.js +37 -8
- package/dist/src/run.js +13 -3
- package/dist/src/schema.js +2 -0
- package/dist/src/session.js +141 -34
- package/dist/src/setup-failure.js +23 -0
- package/dist/src/source-files.js +4 -61
- package/dist/src/stores.js +3 -0
- package/dist/src/sweep.js +4 -2
- package/dist/src/template-field.js +38 -6
- package/dist/src/test-support.js +59 -0
- package/dist/src/type-errors.js +252 -0
- package/dist/src/type-query-alias.js +92 -0
- package/dist/src/value-flow.js +74 -0
- package/dist/src/verbs/analyze.js +24 -4
- package/dist/src/verbs/explain.js +4 -1
- package/dist/src/verbs/print-projects.js +17 -56
- package/dist/src/verbs/print-retained.js +7 -2
- package/dist/src/verbs/print-roots.js +7 -2
- package/dist/src/version-range.js +255 -0
- package/dist/src/version.js +1 -1
- package/dist/src/workspace-manifests.js +68 -0
- package/dist/src/workspace-programs.js +342 -0
- package/dist/src/workspace-resolution.js +307 -0
- package/dist/src/workspace-run.js +75 -0
- package/dist/src/workspace.js +358 -0
- package/jsr.json +1 -1
- package/package.json +3 -2
- package/src/analysis.ts +475 -72
- package/src/component-files.ts +718 -0
- package/src/component-layer.ts +160 -0
- package/src/config.ts +11 -2
- package/src/configuration-files.ts +370 -0
- package/src/conformance-record.ts +3 -10
- package/src/contextual-uses.ts +354 -0
- package/src/convention-rows.ts +448 -0
- package/src/conventions.ts +788 -0
- package/src/dependencies.ts +49 -2
- package/src/destructuring.ts +16 -6
- package/src/discover.ts +10 -6
- package/src/emit-map.ts +93 -0
- package/src/entry-point-gaps.ts +92 -38
- package/src/entry-points.ts +79 -36
- package/src/exempt-classes.ts +6 -1
- package/src/exempt.ts +69 -20
- package/src/file-facts.ts +2 -0
- package/src/findings/completion.ts +55 -36
- package/src/findings/emitters.ts +32 -1
- package/src/findings/interfaces.ts +9 -2
- package/src/findings/reads-and-writes.ts +70 -9
- package/src/findings/unused-declarations.ts +60 -3
- package/src/generated-file.ts +27 -0
- package/src/graph.ts +15 -4
- package/src/host.ts +23 -0
- package/src/index.ts +1 -1
- package/src/interface-satisfaction.ts +1 -92
- package/src/inventory.ts +17 -3
- package/src/kinds.ts +7 -5
- package/src/manifest.ts +47 -14
- package/src/matrix.ts +43 -15
- package/src/partial.ts +6 -1
- package/src/pnpm-workspace.ts +135 -0
- package/src/position.ts +23 -2
- package/src/print.ts +2 -0
- package/src/query.ts +26 -0
- package/src/references.ts +472 -40
- package/src/report.ts +56 -6
- package/src/reporters/annotations.ts +12 -3
- package/src/reporters/template.ts +3 -0
- package/src/resolve.ts +65 -11
- package/src/roots.ts +70 -9
- package/src/run.ts +15 -5
- package/src/schema.ts +2 -0
- package/src/session.ts +218 -37
- package/src/setup-failure.ts +39 -0
- package/src/source-files.ts +4 -66
- package/src/stores.ts +9 -0
- package/src/sweep.ts +9 -2
- package/src/template-field.ts +42 -7
- package/src/test-support.ts +77 -0
- package/src/type-errors.ts +340 -0
- package/src/type-query-alias.ts +133 -0
- package/src/value-flow.ts +104 -0
- package/src/verbs/analyze.ts +38 -4
- package/src/verbs/explain.ts +4 -1
- package/src/verbs/print-projects.ts +17 -69
- package/src/verbs/print-retained.ts +7 -2
- package/src/verbs/print-roots.ts +7 -2
- package/src/verbs/verb.ts +6 -3
- package/src/version-range.ts +309 -0
- package/src/version.ts +1 -1
- package/src/workspace-manifests.ts +100 -0
- package/src/workspace-programs.ts +486 -0
- package/src/workspace-resolution.ts +393 -0
- package/src/workspace-run.ts +148 -0
- package/src/workspace.ts +463 -0
package/src/verbs/analyze.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
runAnalysis,
|
|
3
|
+
runBaselineWrite,
|
|
4
|
+
unansweredCounts,
|
|
5
|
+
type ComponentWarning,
|
|
6
|
+
type RunAnalysis,
|
|
7
|
+
type RunProject,
|
|
8
|
+
} from "../analysis.ts";
|
|
2
9
|
import { ConfigError, type Format } from "../config.ts";
|
|
3
10
|
import { CONFORMANCE, DECLARED_GAPS } from "../conformance.ts";
|
|
4
11
|
import { moduleIdentity } from "../consumers.ts";
|
|
@@ -220,17 +227,19 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
220
227
|
let recorded: readonly Recorded[];
|
|
221
228
|
let verdict: number;
|
|
222
229
|
let partial: readonly string[];
|
|
230
|
+
let warnings: readonly ComponentWarning[];
|
|
231
|
+
let unread: readonly string[];
|
|
223
232
|
try {
|
|
224
233
|
const root = runRelative(invoked, targetRoot);
|
|
225
234
|
const mode = { production: true };
|
|
226
235
|
let analysis: RunAnalysis;
|
|
227
236
|
if (documents.baseline === undefined) {
|
|
228
|
-
analysis = runAnalysis(openClient(
|
|
237
|
+
analysis = runAnalysis(openClient(config), host, scoped, config, provenance, mode);
|
|
229
238
|
recorded = [];
|
|
230
239
|
} else {
|
|
231
240
|
const reason = rowReason(analyzerProvenance(version));
|
|
232
241
|
const write = runBaselineWrite(
|
|
233
|
-
openClient(
|
|
242
|
+
openClient(config),
|
|
234
243
|
host,
|
|
235
244
|
scoped,
|
|
236
245
|
config,
|
|
@@ -242,7 +251,16 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
242
251
|
recorded = write.rows;
|
|
243
252
|
}
|
|
244
253
|
const { result, run } = analysis;
|
|
245
|
-
|
|
254
|
+
warnings = run.componentWarnings;
|
|
255
|
+
unread = run.componentsUnread.map(
|
|
256
|
+
({ configFile, reason }) =>
|
|
257
|
+
`${relativePath(resolvePath(invoked, targetRoot), configFile) ?? configFile}: its component files are not read: ${reason}`,
|
|
258
|
+
);
|
|
259
|
+
partial = partialNotes({
|
|
260
|
+
notBuilt: run.notBuilt,
|
|
261
|
+
unanswered: run.unanswered,
|
|
262
|
+
targetRoot: resolvePath(invoked, targetRoot),
|
|
263
|
+
});
|
|
246
264
|
const built = buildReport({
|
|
247
265
|
contractVersion: CONTRACT_VERSION,
|
|
248
266
|
version,
|
|
@@ -258,6 +276,9 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
258
276
|
unavailable: [],
|
|
259
277
|
result,
|
|
260
278
|
testFileRules: run.testFileRules,
|
|
279
|
+
typeErrorSkips: run.typeErrorSkips,
|
|
280
|
+
unanswered: unansweredCounts(analysis),
|
|
281
|
+
conventionsApplied: run.conventionsApplied,
|
|
261
282
|
});
|
|
262
283
|
report = capFindings(sortFindings(built, config.reporters.sort), config.reporters.maxFindings);
|
|
263
284
|
verdict = verdictOf(result, config);
|
|
@@ -293,6 +314,19 @@ export const analyzeVerb: Verb = ({ err, host, inputs, scope, openClient, option
|
|
|
293
314
|
for (const note of partial) {
|
|
294
315
|
err.write(`deadset-ts: ${note}\n`);
|
|
295
316
|
}
|
|
317
|
+
for (const line of unread) {
|
|
318
|
+
err.write(`deadset-ts: ${line}\n`);
|
|
319
|
+
}
|
|
320
|
+
for (const warning of warnings) {
|
|
321
|
+
err.write(
|
|
322
|
+
`deadset-ts: ${warning.path}:${String(warning.line)}: a <script> start tag that begins a line is not read as a block: ${warning.reason}\n`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
for (const skip of report.type_error_skips) {
|
|
326
|
+
err.write(
|
|
327
|
+
`deadset-ts: ${skip.path}:${String(skip.line)}: ${skip.message}: the function or statement holding this type error is not evaluated\n`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
296
330
|
if (report.totals.pending > 0) {
|
|
297
331
|
err.write(
|
|
298
332
|
`deadset-ts: ${counted(report.totals.pending, "pending finding")}: the report names a declared cross-language edge, and no merge has resolved it\n`,
|
package/src/verbs/explain.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { OUTSIDE, swept as judged } from "../graph.ts";
|
|
|
7
7
|
import type { InventorySymbol } from "../inventory.ts";
|
|
8
8
|
import { referenceKey, type Configured } from "../matrix.ts";
|
|
9
9
|
import { partialNotes } from "../partial.ts";
|
|
10
|
+
import { resolvePath } from "../paths.ts";
|
|
10
11
|
import { positionKey } from "../position.ts";
|
|
11
12
|
import type { Reference } from "../references.ts";
|
|
12
13
|
import { resolve } from "../resolve.ts";
|
|
@@ -365,13 +366,15 @@ function explanation(analysis: RunAnalysis, subject: InventorySymbol, cascade: C
|
|
|
365
366
|
export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, option }) => {
|
|
366
367
|
const named = namedSymbol(option);
|
|
367
368
|
const { config, provenance } = resolve(inputs());
|
|
368
|
-
const
|
|
369
|
+
const scoped = scope();
|
|
370
|
+
const analysis = runAnalysis(openClient(config), host, scoped, config, provenance, {
|
|
369
371
|
production: true,
|
|
370
372
|
});
|
|
371
373
|
const { subject, candidates } = subjectOf(analysis.swept.matrix.union.symbols, named);
|
|
372
374
|
const notes = partialNotes({
|
|
373
375
|
notBuilt: analysis.run.notBuilt,
|
|
374
376
|
unanswered: analysis.run.unanswered,
|
|
377
|
+
targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
|
|
375
378
|
});
|
|
376
379
|
if (subject === undefined) {
|
|
377
380
|
err.write(`deadset-ts: ${JSON.stringify(named)} names no one symbol of the target\n`);
|
|
@@ -1,80 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
discoverProjects,
|
|
3
|
-
DiscoveryError,
|
|
4
|
-
isGuess,
|
|
5
|
-
projectErrors,
|
|
6
|
-
renderDiagnostic,
|
|
7
|
-
targetRelative,
|
|
8
|
-
type NotBuilt,
|
|
9
|
-
} from "../discover.ts";
|
|
1
|
+
import { runProjects } from "../analysis.ts";
|
|
10
2
|
import { partialNotes } from "../partial.ts";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { EXIT_CLEAN, EXIT_FAILURE, type Verb } from "./verb.ts";
|
|
3
|
+
import { resolvePath } from "../paths.ts";
|
|
4
|
+
import { resolveUnkinded } from "../resolve.ts";
|
|
5
|
+
import { EXIT_CLEAN, type Verb } from "./verb.ts";
|
|
15
6
|
|
|
16
7
|
/**
|
|
17
|
-
* Writes the name of every project one run analyzes, one per line
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
8
|
+
* Writes the name of every project one run analyzes, one per line: the identifier the
|
|
9
|
+
* build matrix declares for it, or, where discovery derived the project, its
|
|
10
|
+
* configuration file's path below the target root. The projects are read as the
|
|
11
|
+
* analysis reads them, so a project the analysis drops is named on the error stream
|
|
12
|
+
* instead, and one that fails the analysis fails this command the same way.
|
|
22
13
|
*/
|
|
23
14
|
export const printProjectsVerb: Verb = ({ out, err, host, inputs, scope, openClient }) => {
|
|
24
|
-
const
|
|
15
|
+
const config = resolveUnkinded(inputs());
|
|
25
16
|
const scoped = scope();
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
|
|
29
|
-
const root = resolvePath(host.workingDirectory(), scoped.target.path);
|
|
30
|
-
const notBuilt: NotBuilt[] = [...discovered.notBuilt];
|
|
31
|
-
const failures: string[] = [];
|
|
32
|
-
const session = runSession(
|
|
33
|
-
engine,
|
|
34
|
-
discovered.configFiles,
|
|
35
|
-
(project) => {
|
|
36
|
-
const id = ids.get(project.configFile) ?? project.configFile;
|
|
37
|
-
const errors = projectErrors(diagnosticsOf(project));
|
|
38
|
-
const [unloaded] = errors.load;
|
|
39
|
-
if (unloaded !== undefined && isGuess(discovered, project.configFile)) {
|
|
40
|
-
const error = renderDiagnostic(unloaded, (path) => relativePath(root, path) ?? path);
|
|
41
|
-
notBuilt.push({ id, configFile: project.configFile, error: targetRelative(error, root) });
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
failures.push(...[...errors.load, ...errors.check].map((one) => renderDiagnostic(one)));
|
|
45
|
-
return id;
|
|
46
|
-
},
|
|
47
|
-
discovered.derived ? "record" : "refuse",
|
|
48
|
-
);
|
|
49
|
-
for (const configFile of session.unopened) {
|
|
50
|
-
if (!isGuess(discovered, configFile)) {
|
|
51
|
-
throw new DiscoveryError(`${configFile} opened no project`, []);
|
|
52
|
-
}
|
|
53
|
-
notBuilt.push({
|
|
54
|
-
id: ids.get(configFile) ?? configFile,
|
|
55
|
-
configFile,
|
|
56
|
-
error: "the compiler opened no project for the configuration",
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
if (failures.length > 0) {
|
|
60
|
-
for (const line of failures) {
|
|
61
|
-
err.write(`${line}\n`);
|
|
62
|
-
}
|
|
63
|
-
err.write(`deadset-ts: ${String(failures.length)} error(s), so no answer was produced\n`);
|
|
64
|
-
return EXIT_FAILURE;
|
|
65
|
-
}
|
|
66
|
-
const projects = session.projects.filter((id): id is string => id !== undefined);
|
|
67
|
-
if (projects.length === 0 && notBuilt.length > 0) {
|
|
68
|
-
for (const note of partialNotes({ notBuilt, unanswered: [] })) {
|
|
69
|
-
err.write(`deadset-ts: ${note}\n`);
|
|
70
|
-
}
|
|
71
|
-
err.write("deadset-ts: no configuration discovery derived could be built\n");
|
|
72
|
-
return EXIT_FAILURE;
|
|
73
|
-
}
|
|
74
|
-
for (const id of projects) {
|
|
17
|
+
const answer = runProjects(openClient(config), host, scoped, config);
|
|
18
|
+
for (const id of answer.configurations) {
|
|
75
19
|
out.write(`${id}\n`);
|
|
76
20
|
}
|
|
77
|
-
for (const note of partialNotes({
|
|
21
|
+
for (const note of partialNotes({
|
|
22
|
+
notBuilt: answer.notBuilt,
|
|
23
|
+
unanswered: answer.unanswered,
|
|
24
|
+
targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
|
|
25
|
+
})) {
|
|
78
26
|
err.write(`deadset-ts: ${note}\n`);
|
|
79
27
|
}
|
|
80
28
|
return EXIT_CLEAN;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { runSweep } from "../analysis.ts";
|
|
2
2
|
import { retainedLines } from "../exempt.ts";
|
|
3
3
|
import { partialNotes } from "../partial.ts";
|
|
4
|
+
import { resolvePath } from "../paths.ts";
|
|
4
5
|
import { resolve } from "../resolve.ts";
|
|
5
6
|
import { EXIT_CLEAN, type Verb } from "./verb.ts";
|
|
6
7
|
|
|
@@ -14,14 +15,18 @@ import { EXIT_CLEAN, type Verb } from "./verb.ts";
|
|
|
14
15
|
export const printRetainedVerb: Verb = ({ out, err, host, inputs, scope, openClient }) => {
|
|
15
16
|
const { config } = resolve(inputs());
|
|
16
17
|
const scoped = scope();
|
|
17
|
-
const swept = runSweep(openClient(
|
|
18
|
+
const swept = runSweep(openClient(config), host, scoped, config, {
|
|
18
19
|
marked: [],
|
|
19
20
|
mode: { production: true },
|
|
20
21
|
});
|
|
21
22
|
for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
|
|
22
23
|
out.write(`${line}\n`);
|
|
23
24
|
}
|
|
24
|
-
for (const note of partialNotes({
|
|
25
|
+
for (const note of partialNotes({
|
|
26
|
+
notBuilt: swept.notBuilt,
|
|
27
|
+
unanswered: swept.unanswered,
|
|
28
|
+
targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
|
|
29
|
+
})) {
|
|
25
30
|
err.write(`deadset-ts: ${note}\n`);
|
|
26
31
|
}
|
|
27
32
|
return EXIT_CLEAN;
|
package/src/verbs/print-roots.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runRoots, type RunRoot } from "../analysis.ts";
|
|
2
2
|
import { partialNotes } from "../partial.ts";
|
|
3
|
+
import { resolvePath } from "../paths.ts";
|
|
3
4
|
import { resolve } from "../resolve.ts";
|
|
4
5
|
import { EXIT_CLEAN, EXIT_FINDINGS, type Verb } from "./verb.ts";
|
|
5
6
|
|
|
@@ -27,7 +28,7 @@ function rootLine(root: RunRoot, several: boolean): string {
|
|
|
27
28
|
export const printRootsVerb: Verb = ({ out, err, host, inputs, scope, openClient }) => {
|
|
28
29
|
const { config, provenance } = resolve(inputs());
|
|
29
30
|
const scoped = scope();
|
|
30
|
-
const answer = runRoots(openClient(
|
|
31
|
+
const answer = runRoots(openClient(config), host, scoped, config, provenance);
|
|
31
32
|
const several = answer.configurations.length > 1;
|
|
32
33
|
for (const root of answer.roots) {
|
|
33
34
|
out.write(`${rootLine(root, several)}\n`);
|
|
@@ -35,7 +36,11 @@ export const printRootsVerb: Verb = ({ out, err, host, inputs, scope, openClient
|
|
|
35
36
|
for (const finding of answer.findings) {
|
|
36
37
|
err.write(`${finding.code}: roots.patterns names nothing: ${finding.symbol.ref}\n`);
|
|
37
38
|
}
|
|
38
|
-
for (const note of partialNotes({
|
|
39
|
+
for (const note of partialNotes({
|
|
40
|
+
notBuilt: answer.notBuilt,
|
|
41
|
+
unanswered: answer.unanswered,
|
|
42
|
+
targetRoot: resolvePath(host.workingDirectory(), scoped.target.path),
|
|
43
|
+
})) {
|
|
39
44
|
err.write(`deadset-ts: ${note}\n`);
|
|
40
45
|
}
|
|
41
46
|
return answer.findings.length > 0 ? EXIT_FINDINGS : EXIT_CLEAN;
|
package/src/verbs/verb.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Inputs } from "../config.ts";
|
|
1
|
+
import type { Config, Inputs } from "../config.ts";
|
|
2
2
|
import type { Emitters } from "../findings/emitter.ts";
|
|
3
3
|
import type { Host } from "../host.ts";
|
|
4
4
|
import type { Writer } from "../run.ts";
|
|
@@ -24,8 +24,11 @@ export interface Invocation {
|
|
|
24
24
|
readonly inputs: () => Inputs;
|
|
25
25
|
/** The scope the options name: the document `--scope` names, or the target. */
|
|
26
26
|
readonly scope: () => Scope;
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Opens the compiler client for a run under `config`. A client the verb opened is
|
|
29
|
+
* closed if the verb throws.
|
|
30
|
+
*/
|
|
31
|
+
readonly openClient: (config: Config) => Engine;
|
|
29
32
|
/** The emitter of every kind family, which a verb that reports findings runs. */
|
|
30
33
|
readonly emitters: Emitters;
|
|
31
34
|
/** The arguments after the verb's name, as written. */
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a version satisfies a dependency range, under the grammar npm, pnpm and yarn
|
|
3
|
+
* share: `||` alternatives of hyphen ranges or comparator runs, `x` and missing parts
|
|
4
|
+
* standing for any value, and a prerelease admitted only where a comparator names one
|
|
5
|
+
* of the same version. A range this grammar does not read is satisfied by nothing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A version: three numbers and the prerelease identifiers, build metadata dropped. */
|
|
9
|
+
interface Version {
|
|
10
|
+
readonly major: number;
|
|
11
|
+
readonly minor: number;
|
|
12
|
+
readonly patch: number;
|
|
13
|
+
readonly prerelease: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** One comparison a version must pass. */
|
|
17
|
+
interface Comparator {
|
|
18
|
+
readonly operator: "<" | "<=" | ">" | ">=" | "=";
|
|
19
|
+
readonly version: Version;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A version whose parts may be missing or a wildcard, as a range writes one. */
|
|
23
|
+
interface Partial {
|
|
24
|
+
readonly major?: number | undefined;
|
|
25
|
+
readonly minor?: number | undefined;
|
|
26
|
+
readonly patch?: number | undefined;
|
|
27
|
+
readonly prerelease: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const NUMBER = /^(?:0|[1-9]\d*)$/u;
|
|
31
|
+
const IDENTIFIER = /^[0-9A-Za-z-]+$/u;
|
|
32
|
+
const WILDCARD = new Set(["x", "X", "*"]);
|
|
33
|
+
|
|
34
|
+
/** The prerelease identifiers after a hyphen, or `undefined` where one is malformed. */
|
|
35
|
+
function prereleaseOf(text: string | undefined): readonly string[] | undefined {
|
|
36
|
+
if (text === undefined) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const parts = text.split(".");
|
|
40
|
+
return parts.every((part) => IDENTIFIER.test(part)) ? parts : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* One version as a range writes it, a leading `v` or `=` allowed, or `undefined`
|
|
45
|
+
* where it is not one.
|
|
46
|
+
*/
|
|
47
|
+
function partialOf(text: string): Partial | undefined {
|
|
48
|
+
const match =
|
|
49
|
+
/^[v=]?([^.+-]+)(?:\.([^.+-]+)(?:\.([^.+-]+)(?:-([^+]+))?)?)?(?:\+[0-9A-Za-z.-]+)?$/u.exec(
|
|
50
|
+
text,
|
|
51
|
+
);
|
|
52
|
+
if (match === null) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const parts: (number | undefined)[] = [];
|
|
56
|
+
let wild = false;
|
|
57
|
+
for (const part of [match[1], match[2], match[3]]) {
|
|
58
|
+
if (part === undefined || WILDCARD.has(part)) {
|
|
59
|
+
wild = true;
|
|
60
|
+
parts.push(undefined);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (wild || !NUMBER.test(part)) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
parts.push(Number(part));
|
|
67
|
+
}
|
|
68
|
+
const prerelease = prereleaseOf(match[4]);
|
|
69
|
+
if (prerelease === undefined || (prerelease.length > 0 && wild)) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return { major: parts[0], minor: parts[1], patch: parts[2], prerelease };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** One exact version, or `undefined` where the text is not one. */
|
|
76
|
+
function versionOf(text: string): Version | undefined {
|
|
77
|
+
const partial = partialOf(text.trim());
|
|
78
|
+
if (partial?.major === undefined || partial.minor === undefined || partial.patch === undefined) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
major: partial.major,
|
|
83
|
+
minor: partial.minor,
|
|
84
|
+
patch: partial.patch,
|
|
85
|
+
prerelease: partial.prerelease,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function exact(
|
|
90
|
+
major: number,
|
|
91
|
+
minor: number,
|
|
92
|
+
patch: number,
|
|
93
|
+
prerelease: readonly string[] = [],
|
|
94
|
+
): Version {
|
|
95
|
+
return { major, minor, patch, prerelease };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The lowest prerelease of one version, which bounds a range from above. */
|
|
99
|
+
function floorOf(major: number, minor: number, patch: number): Version {
|
|
100
|
+
return exact(major, minor, patch, ["0"]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function compareIdentifiers(a: string, b: string): number {
|
|
104
|
+
const numericA = NUMBER.test(a);
|
|
105
|
+
const numericB = NUMBER.test(b);
|
|
106
|
+
if (numericA && numericB) {
|
|
107
|
+
return Math.sign(Number(a) - Number(b));
|
|
108
|
+
}
|
|
109
|
+
if (numericA !== numericB) {
|
|
110
|
+
return numericA ? -1 : 1;
|
|
111
|
+
}
|
|
112
|
+
return a === b ? 0 : a < b ? -1 : 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Two versions ordered by precedence. */
|
|
116
|
+
function compare(a: Version, b: Version): number {
|
|
117
|
+
const core =
|
|
118
|
+
Math.sign(a.major - b.major) || Math.sign(a.minor - b.minor) || Math.sign(a.patch - b.patch);
|
|
119
|
+
if (core !== 0) {
|
|
120
|
+
return core;
|
|
121
|
+
}
|
|
122
|
+
if (a.prerelease.length === 0 || b.prerelease.length === 0) {
|
|
123
|
+
return Math.sign(b.prerelease.length - a.prerelease.length);
|
|
124
|
+
}
|
|
125
|
+
for (let at = 0; at < Math.max(a.prerelease.length, b.prerelease.length); at += 1) {
|
|
126
|
+
const left = a.prerelease[at];
|
|
127
|
+
const right = b.prerelease[at];
|
|
128
|
+
if (left === undefined || right === undefined) {
|
|
129
|
+
return left === undefined ? -1 : 1;
|
|
130
|
+
}
|
|
131
|
+
const order = compareIdentifiers(left, right);
|
|
132
|
+
if (order !== 0) {
|
|
133
|
+
return order;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function passes(version: Version, comparator: Comparator): boolean {
|
|
140
|
+
const order = compare(version, comparator.version);
|
|
141
|
+
switch (comparator.operator) {
|
|
142
|
+
case "<":
|
|
143
|
+
return order < 0;
|
|
144
|
+
case "<=":
|
|
145
|
+
return order <= 0;
|
|
146
|
+
case ">":
|
|
147
|
+
return order > 0;
|
|
148
|
+
case ">=":
|
|
149
|
+
return order >= 0;
|
|
150
|
+
case "=":
|
|
151
|
+
return order === 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The comparators a version with missing parts stands for: any value of each. */
|
|
156
|
+
function anyOf(partial: Partial): Comparator[] {
|
|
157
|
+
const { major, minor } = partial;
|
|
158
|
+
if (major === undefined) {
|
|
159
|
+
return [{ operator: ">=", version: exact(0, 0, 0) }];
|
|
160
|
+
}
|
|
161
|
+
if (minor === undefined) {
|
|
162
|
+
return [
|
|
163
|
+
{ operator: ">=", version: exact(major, 0, 0) },
|
|
164
|
+
{ operator: "<", version: floorOf(major + 1, 0, 0) },
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
if (partial.patch === undefined) {
|
|
168
|
+
return [
|
|
169
|
+
{ operator: ">=", version: exact(major, minor, 0) },
|
|
170
|
+
{ operator: "<", version: floorOf(major, minor + 1, 0) },
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
return [{ operator: "=", version: exact(major, minor, partial.patch, partial.prerelease) }];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The lowest version a partial version stands for. */
|
|
177
|
+
function lowest(partial: Partial): Version {
|
|
178
|
+
return exact(partial.major ?? 0, partial.minor ?? 0, partial.patch ?? 0, partial.prerelease);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function tilde(partial: Partial): Comparator[] {
|
|
182
|
+
const { major, minor } = partial;
|
|
183
|
+
if (major === undefined || minor === undefined) {
|
|
184
|
+
return anyOf(partial);
|
|
185
|
+
}
|
|
186
|
+
return [
|
|
187
|
+
{ operator: ">=", version: lowest(partial) },
|
|
188
|
+
{ operator: "<", version: floorOf(major, minor + 1, 0) },
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function caret(partial: Partial): Comparator[] {
|
|
193
|
+
const { major, minor, patch } = partial;
|
|
194
|
+
if (major === undefined) {
|
|
195
|
+
return anyOf(partial);
|
|
196
|
+
}
|
|
197
|
+
const from = { operator: ">=" as const, version: lowest(partial) };
|
|
198
|
+
if (major > 0 || minor === undefined) {
|
|
199
|
+
return [from, { operator: "<", version: floorOf(major + 1, 0, 0) }];
|
|
200
|
+
}
|
|
201
|
+
if (minor > 0 || patch === undefined) {
|
|
202
|
+
return [from, { operator: "<", version: floorOf(0, minor + 1, 0) }];
|
|
203
|
+
}
|
|
204
|
+
return [from, { operator: "<", version: floorOf(0, 0, patch + 1) }];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The comparators one primitive comparison stands for, a partial version filled in. */
|
|
208
|
+
function primitive(operator: "<" | "<=" | ">" | ">=", partial: Partial): Comparator[] {
|
|
209
|
+
const { major, minor, patch } = partial;
|
|
210
|
+
if (major === undefined) {
|
|
211
|
+
return operator === "<" || operator === ">"
|
|
212
|
+
? [{ operator: "<", version: floorOf(0, 0, 0) }]
|
|
213
|
+
: [{ operator: ">=", version: exact(0, 0, 0) }];
|
|
214
|
+
}
|
|
215
|
+
if (minor !== undefined && patch !== undefined) {
|
|
216
|
+
return [{ operator, version: lowest(partial) }];
|
|
217
|
+
}
|
|
218
|
+
const next = minor === undefined ? exact(major + 1, 0, 0) : exact(major, minor + 1, 0);
|
|
219
|
+
switch (operator) {
|
|
220
|
+
case ">":
|
|
221
|
+
return [{ operator: ">=", version: next }];
|
|
222
|
+
case ">=":
|
|
223
|
+
return [{ operator: ">=", version: lowest(partial) }];
|
|
224
|
+
case "<":
|
|
225
|
+
return [{ operator: "<", version: floorOf(major, minor ?? 0, 0) }];
|
|
226
|
+
case "<=":
|
|
227
|
+
return [{ operator: "<", version: floorOf(next.major, next.minor, 0) }];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The comparators one written comparator stands for, or `undefined` where it is none. */
|
|
232
|
+
function comparatorsOf(token: string): Comparator[] | undefined {
|
|
233
|
+
const match = /^(<=|>=|<|>|=|~>|~|\^)?(.*)$/u.exec(token);
|
|
234
|
+
const operator = match?.[1] ?? "";
|
|
235
|
+
const partial = partialOf(match?.[2] ?? "");
|
|
236
|
+
if (partial === undefined) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
switch (operator) {
|
|
240
|
+
case "~":
|
|
241
|
+
case "~>":
|
|
242
|
+
return tilde(partial);
|
|
243
|
+
case "^":
|
|
244
|
+
return caret(partial);
|
|
245
|
+
case "<":
|
|
246
|
+
case "<=":
|
|
247
|
+
case ">":
|
|
248
|
+
case ">=":
|
|
249
|
+
return primitive(operator, partial);
|
|
250
|
+
default:
|
|
251
|
+
return anyOf(partial);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The comparators of one alternative, or `undefined` where it does not read. */
|
|
256
|
+
function alternativeOf(text: string): Comparator[] | undefined {
|
|
257
|
+
const spaced = text.trim().replaceAll(/(<=|>=|<|>|=|~>|~|\^)\s+/gu, "$1");
|
|
258
|
+
if (spaced === "") {
|
|
259
|
+
return anyOf({ prerelease: [] });
|
|
260
|
+
}
|
|
261
|
+
const hyphen = /^(\S+)\s+-\s+(\S+)$/u.exec(spaced);
|
|
262
|
+
if (hyphen !== null) {
|
|
263
|
+
const from = partialOf(hyphen[1] ?? "");
|
|
264
|
+
const to = partialOf(hyphen[2] ?? "");
|
|
265
|
+
if (from === undefined || to === undefined) {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
return [...primitive(">=", from), ...(to.major === undefined ? [] : primitive("<=", to))];
|
|
269
|
+
}
|
|
270
|
+
const found: Comparator[] = [];
|
|
271
|
+
for (const token of spaced.split(/\s+/u)) {
|
|
272
|
+
const comparators = comparatorsOf(token);
|
|
273
|
+
if (comparators === undefined) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
found.push(...comparators);
|
|
277
|
+
}
|
|
278
|
+
return found;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Whether one alternative admits one version, its prerelease rule included. */
|
|
282
|
+
function admits(alternative: readonly Comparator[], version: Version): boolean {
|
|
283
|
+
if (!alternative.every((comparator) => passes(version, comparator))) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
if (version.prerelease.length === 0) {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
return alternative.some(
|
|
290
|
+
({ version: bound }) =>
|
|
291
|
+
bound.prerelease.length > 0 &&
|
|
292
|
+
bound.major === version.major &&
|
|
293
|
+
bound.minor === version.minor &&
|
|
294
|
+
bound.patch === version.patch,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Whether `version` satisfies `range`; `false` where either does not read. */
|
|
299
|
+
export function satisfies(version: string, range: string): boolean {
|
|
300
|
+
const parsed = versionOf(version);
|
|
301
|
+
if (parsed === undefined) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
const alternatives = range.split("||").map(alternativeOf);
|
|
305
|
+
return (
|
|
306
|
+
alternatives.every((alternative) => alternative !== undefined) &&
|
|
307
|
+
alternatives.some((alternative) => admits(alternative, parsed))
|
|
308
|
+
);
|
|
309
|
+
}
|
package/src/version.ts
CHANGED