@jterrazz/test 17.0.0 → 18.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/dist/browser/index.js +1 -2
- package/dist/browser/services.js +1 -2
- package/dist/browser/stubs.js +1 -2
- package/dist/{shared-index.browser.js → browser-shared-index.browser.js} +82 -40
- package/dist/checker.js +15 -824
- package/dist/in-place.global-setup.d.ts +1 -0
- package/dist/in-place.global-setup.js +1 -1
- package/dist/index.d.ts +2 -14
- package/dist/index.js +83 -204
- package/dist/mobile-session.global-setup.d.ts +1 -0
- package/dist/oxlint.cjs +2 -2
- package/dist/oxlint.js +67 -2321
- package/dist/route-coverage.global-setup.d.ts +1 -0
- package/dist/route-coverage.global-setup.js +1 -2
- package/dist/services.global-setup.d.ts +1 -1
- package/dist/services.global-setup.js +1 -2
- package/dist/services.js +1 -1
- package/dist/shared-appium.adapter.js +14 -14
- package/dist/shared-artifacts.js +7 -5
- package/dist/shared-derived-projects.js +23 -8
- package/dist/shared-in-place.js +1 -1
- package/dist/shared-ledger.js +1 -1
- package/dist/shared-listened-port.js +19 -0
- package/dist/shared-literate-plugin.js +2 -1
- package/dist/shared-pause.js +12 -1
- package/dist/shared-playwright.adapter.js +17 -19
- package/dist/shared-process-alive.js +13 -0
- package/dist/shared-record-key.js +13 -0
- package/dist/shared-serve.js +180 -69
- package/dist/shared-services.global-setup.js +4 -9
- package/dist/shared-session-ledger.js +10 -6
- package/dist/shared-spec-document.js +2 -64
- package/dist/shared-spec-grammar.js +66 -0
- package/dist/shared-specs-modules.js +1033 -0
- package/dist/{shared-stream-body.js → shared-stub-reply.js} +40 -3
- package/dist/shared-stub-server.js +30 -78
- package/dist/shared-temp-root.global-setup.js +3 -10
- package/dist/shared-testcontainers.adapter.js +111 -8
- package/dist/temp-root.global-setup.js +1 -2
- package/dist/vitest.d.ts +17 -22
- package/dist/vitest.js +37 -53
- package/docs/concepts/architecture.md +20 -20
- package/docs/concepts/conventions.md +2 -2
- package/docs/guides/cli-specs.md +2 -0
- package/docs/guides/develop.md +20 -17
- package/docs/guides/mobile-specs.md +3 -3
- package/docs/guides/release.md +4 -3
- package/docs/guides/website-specs.md +1 -1
- package/docs/reference/linting.md +2 -2
- package/docs/reference/services.md +13 -9
- package/docs/reference/stubs.md +4 -2
- package/docs/start/set-up-a-project.md +31 -17
- package/package.json +10 -10
package/dist/checker.js
CHANGED
|
@@ -1,214 +1,21 @@
|
|
|
1
|
-
import { n as parsedJson
|
|
1
|
+
import { n as parsedJson } from "./shared-json-shape.js";
|
|
2
2
|
import { t as CATALOGUE_CHAPTER } from "./shared-chapter.js";
|
|
3
3
|
import { i as findUnknownTokens, n as findKnownTokens, r as findMalformedRefs, t as TOKEN_KINDS } from "./shared-tokens.js";
|
|
4
|
-
import { a as
|
|
5
|
-
import {
|
|
4
|
+
import { a as RUN_KEYS, c as isRelativePath, o as SPEC_EXTENSION, s as SpecSyntaxError, t as DOCUMENT_KEYS } from "./shared-spec-grammar.js";
|
|
5
|
+
import { i as literalEnd, n as argumentsOf, o as openersAt, r as closingOf, s as skipString, t as argumentSpans } from "./shared-source-scan.js";
|
|
6
|
+
import { a as scalarString, i as readSpecFile, n as documentPaths, o as dedent, r as parseSpecDocument, t as assertedStreams } from "./shared-spec-document.js";
|
|
6
7
|
import { c as nodeItems, d as renderYamlSource, l as nodePairs, o as isScalar, r as isMap, s as isSeq, t as Scalar } from "./shared-yaml-document.js";
|
|
7
8
|
import { n as reachesRunner, t as SPECIFICATIONS } from "./shared-specifications.js";
|
|
8
9
|
import { i as testSettingsAtSync, n as listsTestPreset, t as TEST_SECTION } from "./shared-test-section.js";
|
|
9
10
|
import { i as writeBaseline } from "./shared-test-baseline.js";
|
|
10
11
|
import { t as consumerRows } from "./shared-manifest.js";
|
|
11
|
-
import {
|
|
12
|
-
import { BASELINE, GROUND_EXPECTED, GROUND_FIXTURES, GROUND_FOLDERS, GROUND_REQUESTS, GROUND_SEEDS, MEMBER_DEPTH, OXLINT_CONFIG_NAMES, OXLINT_SUPPRESSIONS, PRUNED,
|
|
12
|
+
import { $ as identifierName, A as removeIfEmpty, B as ambiguousSegment, C as lineAt, D as lowestCommonFolder, E as listReaders, F as literalPathVisitor, G as SUBJECT_SPECIFICATIONS, H as subjectFolder, K as doorsOf$1, M as specsRootOfPool, N as resolveSpecifier, O as readSource, P as parseProgram, R as visitProgram, S as isWithin, T as listFiles, U as subjectName, W as subjectsOf, X as childList, Y as child, _ as findPools, a as isRunnerName, at as nodeStart, b as isDir, c as modulesUnder, d as refusedReservedAbove, dt as isNodeTestFileName, et as isNode, f as renamedSpecialOf, ft as isTestFileName, g as decodeText, h as columnAt, ht as projectRootOf, i as isRetiredShared, j as reservedFolderOf, k as readText, l as placeOf, lt as stringValue, m as DISK_CALL, mt as packageRootOf, n as importsThroughAlias, o as isTestOrSpecName, p as setupFolderOf, q as declaredCommands, r as isModuleName, s as lowestPlaceOf, t as declaresTests, u as readersOf, ut as walk$1, v as findSpecsRoots, vt as specsAnchor, w as listDirs, x as isReferrer, y as isConventionalSubdir, yt as SETTINGS_FILE } from "./shared-specs-modules.js";
|
|
13
|
+
import { BASELINE, GROUND_EXPECTED, GROUND_FIXTURES, GROUND_FOLDERS, GROUND_REQUESTS, GROUND_SEEDS, MEMBER_DEPTH, OXLINT_CONFIG_NAMES, OXLINT_SUPPRESSIONS, PRUNED, SETUP_FOLDER, SPECIAL_FOLDERS, TESTS_FOLDER, blankComments, countsOf, defineCheck, forgetTree, holdsDirectives, isGroundFolder, keepsSection, labelOf, moveKeepingHistory, quotedMask, readAt, readManifest, refusal, rowNamed, sectionOf, suppressionDirectives, treeEntries, treeKind, treeNames, workspaceMembers, workspacePatterns } from "@jterrazz/typescript/rules";
|
|
13
14
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
14
15
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
15
16
|
import { spawnSync } from "node:child_process";
|
|
16
17
|
import { createHash } from "node:crypto";
|
|
17
18
|
import { runInNewContext } from "node:vm";
|
|
18
|
-
//#region lint/kit/project/settings-file.ts
|
|
19
|
-
const SETTINGS_FILE = "jterrazz.config.ts";
|
|
20
|
-
//#endregion
|
|
21
|
-
//#region lint/kit/project/specs-anchor.ts
|
|
22
|
-
/** The NEAREST `specs` ancestor, never above the project root: a checkout living under `~/specs/` must not read as one whole specs tree. */
|
|
23
|
-
function specsAnchor(filename) {
|
|
24
|
-
const relative = [basename(filename)];
|
|
25
|
-
let directory = dirname(filename);
|
|
26
|
-
for (;;) {
|
|
27
|
-
if (basename(directory) === "specs") return {
|
|
28
|
-
directory,
|
|
29
|
-
relative
|
|
30
|
-
};
|
|
31
|
-
if (isProjectRoot(directory)) return;
|
|
32
|
-
const parent = dirname(directory);
|
|
33
|
-
if (parent === directory) return;
|
|
34
|
-
relative.unshift(basename(directory));
|
|
35
|
-
directory = parent;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
new Set(GROUND_FOLDERS);
|
|
39
|
-
/** For the passes that walk a tree and hold only a name: one vocabulary with `roleOf`. */
|
|
40
|
-
function isTestFileName(name) {
|
|
41
|
-
return name.endsWith(".test.ts") || name.endsWith(".test.tsx") || name.endsWith(".spec.ts");
|
|
42
|
-
}
|
|
43
|
-
/** A `.test.tsx` renders and is not one. */
|
|
44
|
-
function isNodeTestFileName(name) {
|
|
45
|
-
return name.endsWith(".test.ts") || name.endsWith(".spec.ts");
|
|
46
|
-
}
|
|
47
|
-
function packageRootOf(directory) {
|
|
48
|
-
return nearestRoot(directory, holdsManifest);
|
|
49
|
-
}
|
|
50
|
-
const projectRoots = /* @__PURE__ */ new Map();
|
|
51
|
-
/** Remembered per folder, and forgotten for one directory once its own tree listing changes: a `package.json` written or removed there is what the memo must see. */
|
|
52
|
-
function projectRootOf(directory) {
|
|
53
|
-
if (!folderChanged(directory) && projectRoots.has(directory)) return projectRoots.get(directory);
|
|
54
|
-
const root = nearestRoot(directory, isProjectRoot);
|
|
55
|
-
projectRoots.set(directory, root);
|
|
56
|
-
return root;
|
|
57
|
-
}
|
|
58
|
-
//#endregion
|
|
59
|
-
//#region lint/kit/project/tree.ts
|
|
60
|
-
function isPool(dir) {
|
|
61
|
-
return isGroundFolder(basename(dir)) && basename(dirname(dir)) === "specs";
|
|
62
|
-
}
|
|
63
|
-
function specsRootOfPool(pool) {
|
|
64
|
-
return dirname(pool);
|
|
65
|
-
}
|
|
66
|
-
function isPruned(dir) {
|
|
67
|
-
return PRUNED.has(basename(dir)) || isGroundFolder(basename(dir));
|
|
68
|
-
}
|
|
69
|
-
function listFiles(dir, predicate) {
|
|
70
|
-
const out = [];
|
|
71
|
-
const visit = (current) => {
|
|
72
|
-
let entries;
|
|
73
|
-
try {
|
|
74
|
-
entries = treeEntries(current);
|
|
75
|
-
} catch {
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
for (const entry of entries) {
|
|
79
|
-
const path = join(current, entry.name);
|
|
80
|
-
if (entry.isDirectory()) {
|
|
81
|
-
if (!isPruned(path)) visit(path);
|
|
82
|
-
} else if (predicate(path)) out.push(path);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
visit(dir);
|
|
86
|
-
return out;
|
|
87
|
-
}
|
|
88
|
-
function listDirs(dir) {
|
|
89
|
-
const out = [];
|
|
90
|
-
const visit = (current) => {
|
|
91
|
-
out.push(current);
|
|
92
|
-
let entries;
|
|
93
|
-
try {
|
|
94
|
-
entries = treeEntries(current);
|
|
95
|
-
} catch {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
for (const entry of entries) {
|
|
99
|
-
const path = join(current, entry.name);
|
|
100
|
-
if (entry.isDirectory() && !isPruned(path)) visit(path);
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
visit(dir);
|
|
104
|
-
return out;
|
|
105
|
-
}
|
|
106
|
-
function findSpecsRoots(dir) {
|
|
107
|
-
const roots = [];
|
|
108
|
-
const visit = (current) => {
|
|
109
|
-
if (basename(current) === "specs") {
|
|
110
|
-
roots.push(current);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
let entries;
|
|
114
|
-
try {
|
|
115
|
-
entries = treeEntries(current);
|
|
116
|
-
} catch {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
for (const entry of entries) if (entry.isDirectory() && !PRUNED.has(entry.name)) visit(join(current, entry.name));
|
|
120
|
-
};
|
|
121
|
-
visit(dir);
|
|
122
|
-
return roots;
|
|
123
|
-
}
|
|
124
|
-
function findPools(dir) {
|
|
125
|
-
const pools = [];
|
|
126
|
-
const visit = (current) => {
|
|
127
|
-
let entries;
|
|
128
|
-
try {
|
|
129
|
-
entries = treeEntries(current);
|
|
130
|
-
} catch {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
for (const entry of entries) {
|
|
134
|
-
if (!entry.isDirectory() || PRUNED.has(entry.name)) continue;
|
|
135
|
-
const path = join(current, entry.name);
|
|
136
|
-
if (isPool(path)) pools.push(path);
|
|
137
|
-
else if (!isGroundFolder(entry.name)) visit(path);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
visit(dir);
|
|
141
|
-
return pools;
|
|
142
|
-
}
|
|
143
|
-
function readText(path) {
|
|
144
|
-
try {
|
|
145
|
-
return readFileSync(path, "utf8");
|
|
146
|
-
} catch {
|
|
147
|
-
return "";
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
function readSource(path) {
|
|
151
|
-
return blankComments(readText(path));
|
|
152
|
-
}
|
|
153
|
-
/** A document counts: a rendered unit's ARIA golden sits in the same `__expected__/` as a spec's. */
|
|
154
|
-
function isReferrer(name) {
|
|
155
|
-
return isTestFileName(name) || name.endsWith(".spec.yaml");
|
|
156
|
-
}
|
|
157
|
-
function isDir(path) {
|
|
158
|
-
return treeKind(path) === "directory";
|
|
159
|
-
}
|
|
160
|
-
function columnAt(text, index) {
|
|
161
|
-
return index - text.lastIndexOf("\n", index - 1);
|
|
162
|
-
}
|
|
163
|
-
function lineAt(text, index) {
|
|
164
|
-
return text.slice(0, index).split("\n").length;
|
|
165
|
-
}
|
|
166
|
-
const DISK_CALL = /(?<![.\w])(?<kind>file|directory)\(\s*(?<quote>['"`])(?<path>(?:[^'"`\\]|\\.)*)\k<quote>/gu;
|
|
167
|
-
/** A path that is no folder holds no tests and lists no children: it is none. */
|
|
168
|
-
function isConventionalSubdir(path) {
|
|
169
|
-
if (listFiles(path, isTestFileName).length > 0) return false;
|
|
170
|
-
let children;
|
|
171
|
-
try {
|
|
172
|
-
children = treeEntries(path);
|
|
173
|
-
} catch {
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
return !children.some((child) => child.isDirectory() && isGroundFolder(child.name));
|
|
177
|
-
}
|
|
178
|
-
function listReaders(specsRoot) {
|
|
179
|
-
return listFiles(specsRoot, (path) => path.endsWith(".ts") || path.endsWith(".spec.yaml"));
|
|
180
|
-
}
|
|
181
|
-
function removeIfEmpty(folder) {
|
|
182
|
-
if (existsSync(folder) && readdirSync(folder).length === 0) rmSync(folder, { recursive: true });
|
|
183
|
-
}
|
|
184
|
-
function isWithin(directory, path) {
|
|
185
|
-
const rest = relative(directory, path);
|
|
186
|
-
return rest !== "" && !rest.startsWith("..") && !isAbsolute(rest);
|
|
187
|
-
}
|
|
188
|
-
function lowestCommonFolder(folders) {
|
|
189
|
-
const [first, ...others] = folders;
|
|
190
|
-
if (first === void 0) return;
|
|
191
|
-
let common = first;
|
|
192
|
-
while (!others.every((folder) => folder === common || isWithin(common, folder))) {
|
|
193
|
-
const parent = dirname(common);
|
|
194
|
-
if (parent === common) return;
|
|
195
|
-
common = parent;
|
|
196
|
-
}
|
|
197
|
-
return common;
|
|
198
|
-
}
|
|
199
|
-
function reservedFolderOf(owner, reserved) {
|
|
200
|
-
return join(owner, reserved);
|
|
201
|
-
}
|
|
202
|
-
function decodeText(path) {
|
|
203
|
-
let text;
|
|
204
|
-
try {
|
|
205
|
-
text = readFileSync(path, "utf8");
|
|
206
|
-
} catch {
|
|
207
|
-
return null;
|
|
208
|
-
}
|
|
209
|
-
return text.includes("\0") || text.includes("�") ? null : text;
|
|
210
|
-
}
|
|
211
|
-
//#endregion
|
|
212
19
|
//#region lint/kit/project/document-model.ts
|
|
213
20
|
function finding$1(rule, line, { fix, found }) {
|
|
214
21
|
return {
|
|
@@ -469,362 +276,6 @@ function isTestsFolderGround(path) {
|
|
|
469
276
|
return segments.some((segment, index) => segment === TESTS_FOLDER && isGroundFolder(segments[index + 1] ?? ""));
|
|
470
277
|
}
|
|
471
278
|
//#endregion
|
|
472
|
-
//#region lint/kit/project/ast.ts
|
|
473
|
-
function fieldOf(value, key) {
|
|
474
|
-
return typeof value === "object" && value !== null ? Reflect.get(value, key) : void 0;
|
|
475
|
-
}
|
|
476
|
-
function stringValue(node) {
|
|
477
|
-
if (node === void 0) return;
|
|
478
|
-
if (node.type === "Literal" && typeof node.value === "string") return node.value;
|
|
479
|
-
if (node.type !== "TemplateLiteral" || childList(node, "expressions").length > 0) return;
|
|
480
|
-
const [text] = childList(node, "quasis");
|
|
481
|
-
return cookedText(text);
|
|
482
|
-
}
|
|
483
|
-
function cookedText(quasi) {
|
|
484
|
-
const cooked = fieldOf(quasi?.value, "cooked");
|
|
485
|
-
return typeof cooked === "string" ? cooked : void 0;
|
|
486
|
-
}
|
|
487
|
-
function memberPropertyName(node) {
|
|
488
|
-
if (node?.type !== "MemberExpression" || node.computed === true) return;
|
|
489
|
-
const property = child(node, "property");
|
|
490
|
-
return property?.type === "Identifier" ? stringField(property, "name") ?? "" : void 0;
|
|
491
|
-
}
|
|
492
|
-
function walk$1(root, visit) {
|
|
493
|
-
visit(root);
|
|
494
|
-
for (const key of Object.keys(root)) {
|
|
495
|
-
if (key === "parent") continue;
|
|
496
|
-
const value = root[key];
|
|
497
|
-
if (Array.isArray(value)) {
|
|
498
|
-
for (const item of value) if (isNode(item)) walk$1(item, visit);
|
|
499
|
-
} else if (isNode(value)) walk$1(value, visit);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
function nodeStart(node) {
|
|
503
|
-
return typeof node?.start === "number" ? node.start : -1;
|
|
504
|
-
}
|
|
505
|
-
function isNode(value) {
|
|
506
|
-
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
507
|
-
}
|
|
508
|
-
function child(node, key) {
|
|
509
|
-
const value = node?.[key];
|
|
510
|
-
return isNode(value) ? value : void 0;
|
|
511
|
-
}
|
|
512
|
-
function childList(node, key) {
|
|
513
|
-
const value = node?.[key];
|
|
514
|
-
return Array.isArray(value) ? value.filter((item) => isNode(item)) : [];
|
|
515
|
-
}
|
|
516
|
-
function stringField(node, key) {
|
|
517
|
-
const value = node?.[key];
|
|
518
|
-
return typeof value === "string" ? value : void 0;
|
|
519
|
-
}
|
|
520
|
-
function identifierName(node) {
|
|
521
|
-
if (node?.type !== "Identifier") return;
|
|
522
|
-
const { name } = node;
|
|
523
|
-
return typeof name === "string" ? name : void 0;
|
|
524
|
-
}
|
|
525
|
-
//#endregion
|
|
526
|
-
//#region lint/kit/project/module-parser.ts
|
|
527
|
-
let parse;
|
|
528
|
-
function parsedModule(filename, source) {
|
|
529
|
-
if (parse === void 0) parse = createRequire(import.meta.url)("oxc-parser").parseSync;
|
|
530
|
-
return parse(filename, source);
|
|
531
|
-
}
|
|
532
|
-
function parseProgram(filename, source) {
|
|
533
|
-
const { errors, program } = parsedModule(filename, source);
|
|
534
|
-
return errors.length === 0 ? program : void 0;
|
|
535
|
-
}
|
|
536
|
-
//#endregion
|
|
537
|
-
//#region lint/kit/project/specifiers.ts
|
|
538
|
-
const EXTENSIONS = [
|
|
539
|
-
".ts",
|
|
540
|
-
".tsx",
|
|
541
|
-
".mts",
|
|
542
|
-
".cts",
|
|
543
|
-
".js",
|
|
544
|
-
".jsx",
|
|
545
|
-
".mjs",
|
|
546
|
-
".cjs",
|
|
547
|
-
".json"
|
|
548
|
-
];
|
|
549
|
-
const COMPILED = {
|
|
550
|
-
".cjs": [".cts"],
|
|
551
|
-
".js": [".ts", ".tsx"],
|
|
552
|
-
".jsx": [".tsx"],
|
|
553
|
-
".mjs": [".mts"]
|
|
554
|
-
};
|
|
555
|
-
function onDisk(path) {
|
|
556
|
-
if (isFile(path)) return path;
|
|
557
|
-
const extension = /\.[cm]?jsx?$/u.exec(path)?.[0] ?? "";
|
|
558
|
-
return [
|
|
559
|
-
...(COMPILED[extension] ?? []).map((swap) => path.slice(0, -extension.length) + swap),
|
|
560
|
-
...EXTENSIONS.map((added) => path + added),
|
|
561
|
-
...EXTENSIONS.map((added) => join(path, `index${added}`))
|
|
562
|
-
].find((candidate) => isFile(candidate)) ?? path;
|
|
563
|
-
}
|
|
564
|
-
function readJsonc(path) {
|
|
565
|
-
const text = readFileCached(path);
|
|
566
|
-
if (text === null) return;
|
|
567
|
-
return parsedJson(blankComments(text).replaceAll(/,(?=\s*[\]}])/gu, ""), null);
|
|
568
|
-
}
|
|
569
|
-
function recordOf(value) {
|
|
570
|
-
return isRecord(value) ? value : void 0;
|
|
571
|
-
}
|
|
572
|
-
function targetsOf(value) {
|
|
573
|
-
if (typeof value === "string") return [value];
|
|
574
|
-
if (Array.isArray(value)) return value.flatMap((entry) => targetsOf(entry));
|
|
575
|
-
return Object.values(recordOf(value) ?? {}).flatMap((entry) => targetsOf(entry));
|
|
576
|
-
}
|
|
577
|
-
function substitution(key, specifier) {
|
|
578
|
-
const star = key.indexOf("*");
|
|
579
|
-
if (star === -1) return key === specifier ? "" : void 0;
|
|
580
|
-
const head = key.slice(0, star);
|
|
581
|
-
const tail = key.slice(star + 1);
|
|
582
|
-
return specifier.startsWith(head) && specifier.endsWith(tail) && specifier.length >= head.length + tail.length ? specifier.slice(head.length, specifier.length - tail.length) : void 0;
|
|
583
|
-
}
|
|
584
|
-
function mapped(map, specifier, base) {
|
|
585
|
-
for (const [key, value] of Object.entries(map)) {
|
|
586
|
-
const star = substitution(key, specifier);
|
|
587
|
-
if (star !== void 0) return targetsOf(value).map((target) => resolve(base, target.replace("*", star)));
|
|
588
|
-
}
|
|
589
|
-
return [];
|
|
590
|
-
}
|
|
591
|
-
function tsconfigOf(directory) {
|
|
592
|
-
const root = projectRootOf(directory);
|
|
593
|
-
let current = directory;
|
|
594
|
-
for (;;) {
|
|
595
|
-
const candidate = join(current, "tsconfig.json");
|
|
596
|
-
if (isFile(candidate)) return candidate;
|
|
597
|
-
const parent = dirname(current);
|
|
598
|
-
if (current === root || parent === current) return;
|
|
599
|
-
current = parent;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
function configCandidates(base) {
|
|
603
|
-
return [
|
|
604
|
-
base,
|
|
605
|
-
`${base}.json`,
|
|
606
|
-
join(base, "tsconfig.json")
|
|
607
|
-
];
|
|
608
|
-
}
|
|
609
|
-
function extendedConfig(from, entry) {
|
|
610
|
-
const directory = dirname(from);
|
|
611
|
-
if (entry.startsWith(".") || entry.startsWith("/")) return configCandidates(resolve(directory, entry)).find((path) => isFile(path));
|
|
612
|
-
for (let current = directory;; current = dirname(current)) {
|
|
613
|
-
const found = configCandidates(join(current, "node_modules", entry)).find((path) => isFile(path));
|
|
614
|
-
if (found !== void 0 || dirname(current) === current) return found;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
function resolutionOf(tsconfig, seen) {
|
|
618
|
-
if (seen.has(tsconfig)) return {};
|
|
619
|
-
seen.add(tsconfig);
|
|
620
|
-
const config = recordOf(readJsonc(tsconfig));
|
|
621
|
-
const bases = targetsOf(config?.extends);
|
|
622
|
-
const resolution = {};
|
|
623
|
-
for (const entry of bases) {
|
|
624
|
-
const base = extendedConfig(tsconfig, entry);
|
|
625
|
-
if (base !== void 0) Object.assign(resolution, resolutionOf(base, seen));
|
|
626
|
-
}
|
|
627
|
-
const options = recordOf(config?.compilerOptions);
|
|
628
|
-
const directory = dirname(tsconfig);
|
|
629
|
-
if (typeof options?.baseUrl === "string") resolution.baseUrl = resolve(directory, options.baseUrl);
|
|
630
|
-
const paths = recordOf(options?.paths);
|
|
631
|
-
if (paths !== void 0) resolution.paths = {
|
|
632
|
-
base: directory,
|
|
633
|
-
paths
|
|
634
|
-
};
|
|
635
|
-
return resolution;
|
|
636
|
-
}
|
|
637
|
-
function throughPaths(directory, specifier) {
|
|
638
|
-
const tsconfig = tsconfigOf(directory);
|
|
639
|
-
if (tsconfig === void 0) return [];
|
|
640
|
-
const { baseUrl, paths } = resolutionOf(tsconfig, /* @__PURE__ */ new Set());
|
|
641
|
-
if (paths === void 0) return [];
|
|
642
|
-
return mapped(paths.paths, specifier, baseUrl ?? paths.base);
|
|
643
|
-
}
|
|
644
|
-
function throughImports(directory, specifier) {
|
|
645
|
-
const root = packageRootOf(directory);
|
|
646
|
-
if (root === void 0) return [];
|
|
647
|
-
const imports = recordOf(recordOf(readJsonc(join(root, "package.json")))?.imports);
|
|
648
|
-
return imports === void 0 ? [] : mapped(imports, specifier, root);
|
|
649
|
-
}
|
|
650
|
-
function resolveSpecifier(from, specifier) {
|
|
651
|
-
const directory = dirname(from);
|
|
652
|
-
if (specifier === "." || specifier === ".." || /^\.\.?\//u.test(specifier)) return [onDisk(resolve(directory, specifier))];
|
|
653
|
-
if (specifier.startsWith("/") || specifier.startsWith("node:")) return [];
|
|
654
|
-
return (specifier.startsWith("#") ? throughImports(directory, specifier) : throughPaths(directory, specifier)).map((target) => onDisk(target));
|
|
655
|
-
}
|
|
656
|
-
//#endregion
|
|
657
|
-
//#region lint/kit/project/import-sources.ts
|
|
658
|
-
function attributeText(node) {
|
|
659
|
-
return stringValue(node) ?? (node?.type === "Identifier" ? String(node.name) : void 0);
|
|
660
|
-
}
|
|
661
|
-
function isJsonImport(declaration) {
|
|
662
|
-
return childList(declaration, "attributes").some((attribute) => attributeText(child(attribute, "key")) === "type" && stringValue(child(attribute, "value")) === "json");
|
|
663
|
-
}
|
|
664
|
-
function importSourceVisitor(onSource) {
|
|
665
|
-
const fromSourceField = (node) => {
|
|
666
|
-
const source = child(node, "source");
|
|
667
|
-
const value = stringValue(source);
|
|
668
|
-
if (value !== void 0) onSource({
|
|
669
|
-
json: isJsonImport(node),
|
|
670
|
-
node: source ?? node,
|
|
671
|
-
source: value
|
|
672
|
-
});
|
|
673
|
-
};
|
|
674
|
-
return {
|
|
675
|
-
ExportAllDeclaration: fromSourceField,
|
|
676
|
-
ExportNamedDeclaration: fromSourceField,
|
|
677
|
-
ImportDeclaration: fromSourceField,
|
|
678
|
-
ImportExpression: fromSourceField
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
//#endregion
|
|
682
|
-
//#region lint/kit/project/path-references.ts
|
|
683
|
-
const PATH_BUILDERS = /* @__PURE__ */ new Set(["join", "resolve"]);
|
|
684
|
-
function isMetaMember(node, name) {
|
|
685
|
-
return child(node, "object")?.type === "MetaProperty" && memberPropertyName(node) === name;
|
|
686
|
-
}
|
|
687
|
-
function isOwnDirectory(node) {
|
|
688
|
-
return identifierName(node) === "__dirname" || isMetaMember(node, "dirname");
|
|
689
|
-
}
|
|
690
|
-
function calleeName(node) {
|
|
691
|
-
const callee = child(node, "callee");
|
|
692
|
-
return identifierName(callee) ?? memberPropertyName(callee);
|
|
693
|
-
}
|
|
694
|
-
function argumentsOf(node) {
|
|
695
|
-
return childList(node, "arguments");
|
|
696
|
-
}
|
|
697
|
-
function literalArguments(node) {
|
|
698
|
-
return argumentsOf(node).map((target) => {
|
|
699
|
-
const literal = stringValue(target);
|
|
700
|
-
return literal === void 0 ? void 0 : {
|
|
701
|
-
literal,
|
|
702
|
-
target
|
|
703
|
-
};
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
function literalTail(pieces) {
|
|
707
|
-
const tail = [];
|
|
708
|
-
for (const piece of pieces) {
|
|
709
|
-
const value = stringValue(piece);
|
|
710
|
-
if (value === void 0) break;
|
|
711
|
-
tail.push(value);
|
|
712
|
-
}
|
|
713
|
-
return tail;
|
|
714
|
-
}
|
|
715
|
-
function urlLiteral(node) {
|
|
716
|
-
const [first] = literalArguments(node);
|
|
717
|
-
const [, base] = argumentsOf(node);
|
|
718
|
-
return identifierName(child(node, "callee")) === "URL" && isMetaMember(base, "url") ? first : void 0;
|
|
719
|
-
}
|
|
720
|
-
/** A piece that is not a literal leaves the call out. */
|
|
721
|
-
function literalPathVisitor(onPath) {
|
|
722
|
-
return {
|
|
723
|
-
CallExpression(node) {
|
|
724
|
-
if (!PATH_BUILDERS.has(calleeName(node))) return;
|
|
725
|
-
const [anchor, ...rest] = argumentsOf(node);
|
|
726
|
-
const pieces = literalTail(rest);
|
|
727
|
-
const [head] = rest;
|
|
728
|
-
if (isOwnDirectory(anchor) && head !== void 0 && pieces.length === rest.length) onPath({
|
|
729
|
-
node: head,
|
|
730
|
-
pieces
|
|
731
|
-
});
|
|
732
|
-
},
|
|
733
|
-
NewExpression(node) {
|
|
734
|
-
const url = urlLiteral(node);
|
|
735
|
-
if (url !== void 0) onPath({
|
|
736
|
-
node: url.target,
|
|
737
|
-
pieces: [url.literal]
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
function visitProgram(program, visitor) {
|
|
743
|
-
walk$1(program, (node) => {
|
|
744
|
-
visitor[node.type]?.(node);
|
|
745
|
-
});
|
|
746
|
-
}
|
|
747
|
-
//#endregion
|
|
748
|
-
//#region lint/kit/project/specs-modules.ts
|
|
749
|
-
const MODULE = /\.(?:[cm]?[jt]sx?)$/u;
|
|
750
|
-
const TEST_OR_SPEC = /\.(?:test|test-d|spec)\.[cm]?[jt]sx?$/u;
|
|
751
|
-
const RUNNER = /\.specification\.[cm]?[jt]sx?$/u;
|
|
752
|
-
function isModuleName(name) {
|
|
753
|
-
return MODULE.test(name) && !name.endsWith(".d.ts");
|
|
754
|
-
}
|
|
755
|
-
function isTestOrSpecName(name) {
|
|
756
|
-
return TEST_OR_SPEC.test(name);
|
|
757
|
-
}
|
|
758
|
-
function isRunnerName(name) {
|
|
759
|
-
return RUNNER.test(name);
|
|
760
|
-
}
|
|
761
|
-
function modulesUnder(specsRoot) {
|
|
762
|
-
return listFiles(specsRoot, (path) => isModuleName(basename(path)));
|
|
763
|
-
}
|
|
764
|
-
function setupFolderOf(specsRoot, file) {
|
|
765
|
-
for (let folder = dirname(file); isWithin(specsRoot, folder); folder = dirname(folder)) if (basename(folder) === SETUP_FOLDER) return folder;
|
|
766
|
-
}
|
|
767
|
-
function placeOf(specsRoot, reader) {
|
|
768
|
-
const setup = setupFolderOf(specsRoot, reader);
|
|
769
|
-
return setup === void 0 ? dirname(reader) : dirname(setup);
|
|
770
|
-
}
|
|
771
|
-
function importsOf$2(file) {
|
|
772
|
-
const program = parseProgram(file, readText(file));
|
|
773
|
-
if (!isNode(program)) return [];
|
|
774
|
-
const imported = [];
|
|
775
|
-
visitProgram(program, importSourceVisitor(({ source }) => {
|
|
776
|
-
imported.push({
|
|
777
|
-
relative: isRelativePath(source),
|
|
778
|
-
targets: resolveSpecifier(file, source)
|
|
779
|
-
});
|
|
780
|
-
}));
|
|
781
|
-
return imported;
|
|
782
|
-
}
|
|
783
|
-
function importedModulesOf(file) {
|
|
784
|
-
return importsOf$2(file).flatMap(({ targets }) => targets);
|
|
785
|
-
}
|
|
786
|
-
function importsThroughAlias(file, module) {
|
|
787
|
-
return importsOf$2(file).some(({ relative, targets }) => !relative && targets.includes(module));
|
|
788
|
-
}
|
|
789
|
-
function readersOf(modules) {
|
|
790
|
-
const known = new Set(modules);
|
|
791
|
-
const readers = /* @__PURE__ */ new Map();
|
|
792
|
-
for (const file of modules) for (const target of new Set(importedModulesOf(file))) if (target !== file && known.has(target)) readers.set(target, [...readers.get(target) ?? [], file]);
|
|
793
|
-
return readers;
|
|
794
|
-
}
|
|
795
|
-
function lowestPlaceOf(specsRoot, readers) {
|
|
796
|
-
return lowestCommonFolder(readers.map((reader) => placeOf(specsRoot, reader)));
|
|
797
|
-
}
|
|
798
|
-
function declaresTests(name) {
|
|
799
|
-
return isTestOrSpecName(name) || name.endsWith(".spec.yaml");
|
|
800
|
-
}
|
|
801
|
-
function holdsASpec(folder) {
|
|
802
|
-
let entries;
|
|
803
|
-
try {
|
|
804
|
-
entries = treeEntries(folder);
|
|
805
|
-
} catch {
|
|
806
|
-
return false;
|
|
807
|
-
}
|
|
808
|
-
return entries.some((entry) => entry.isDirectory() ? !PRUNED.has(entry.name) && !isSpecialFolder(entry.name) && holdsASpec(join(folder, entry.name)) : declaresTests(entry.name));
|
|
809
|
-
}
|
|
810
|
-
/** The special folder a bare earlier spelling names (a `setup/` at the specs root or holding no spec), or `undefined`; a `_` name is refused before it is asked. */
|
|
811
|
-
function renamedSpecialOf(specsRoot, folder) {
|
|
812
|
-
const renamed = RENAMED_FOLDERS[basename(folder)];
|
|
813
|
-
if (renamed === void 0) return;
|
|
814
|
-
return dirname(folder) === specsRoot || !holdsASpec(folder) ? renamed : void 0;
|
|
815
|
-
}
|
|
816
|
-
/** A bare `shared/` at the specs root or holding no spec: what specifications share sits at the specs root now; a `_` name is refused before it is asked. */
|
|
817
|
-
function isRetiredShared(specsRoot, folder) {
|
|
818
|
-
return RETIRED_SHARED_FOLDERS.includes(basename(folder)) && (dirname(folder) === specsRoot || !holdsASpec(folder));
|
|
819
|
-
}
|
|
820
|
-
function isRefusedReserved(specsRoot, folder) {
|
|
821
|
-
const name = basename(folder);
|
|
822
|
-
return name.startsWith("_") && !isSpecialFolder(name) || renamedSpecialOf(specsRoot, folder) !== void 0 || isRetiredShared(specsRoot, folder);
|
|
823
|
-
}
|
|
824
|
-
function refusedReservedAbove(specsRoot, file) {
|
|
825
|
-
for (let folder = dirname(file); isWithin(specsRoot, folder); folder = dirname(folder)) if (isRefusedReserved(specsRoot, folder)) return folder;
|
|
826
|
-
}
|
|
827
|
-
//#endregion
|
|
828
279
|
//#region lint/consumer/checks/adapter-named-for-its-port.ts
|
|
829
280
|
const ADAPTER_SUFFIX = ".adapter.ts";
|
|
830
281
|
const WORD_START = /(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/gu;
|
|
@@ -843,7 +294,8 @@ function portsImported(specsRoot, file, program) {
|
|
|
843
294
|
};
|
|
844
295
|
for (const statement of childList(program, "body")) {
|
|
845
296
|
if (statement.type !== "ImportDeclaration") continue;
|
|
846
|
-
const
|
|
297
|
+
const source = String(stringValue(child(statement, "source")));
|
|
298
|
+
const targets = resolveSpecifier(file, source);
|
|
847
299
|
if (targets.length === 0 || targets.some((target) => isWithin(specsRoot, target))) continue;
|
|
848
300
|
for (const specifier of childList(statement, "specifiers")) {
|
|
849
301
|
const local = String(identifierName(child(specifier, "local")));
|
|
@@ -960,7 +412,7 @@ function argumentsAt(text, open) {
|
|
|
960
412
|
function runnerBindings(text) {
|
|
961
413
|
return [...text.matchAll(RUNNER_DECLARATION)].flatMap((match) => {
|
|
962
414
|
const { names = "" } = match.groups ?? {};
|
|
963
|
-
return argumentsOf
|
|
415
|
+
return argumentsOf(names).flatMap((part) => {
|
|
964
416
|
const [key = "", local = key] = part.split(":").map((token) => token.trim());
|
|
965
417
|
return NON_RUNNER_BINDINGS.has(key) ? [] : [{
|
|
966
418
|
key,
|
|
@@ -976,7 +428,7 @@ function sqlDatabaseCount(text) {
|
|
|
976
428
|
const close = closingOf(text, open);
|
|
977
429
|
if (close === text.length) return null;
|
|
978
430
|
let count = 0;
|
|
979
|
-
for (const pair of argumentsOf
|
|
431
|
+
for (const pair of argumentsOf(text.slice(open + 1, close))) {
|
|
980
432
|
const service = SERVICE_CALL.exec(pair)?.groups?.callee;
|
|
981
433
|
if (service === void 0) return null;
|
|
982
434
|
count += SQL_SERVICES.has(service) ? 1 : 0;
|
|
@@ -1003,7 +455,7 @@ function namedImports(text) {
|
|
|
1003
455
|
const bindings = [];
|
|
1004
456
|
for (const match of text.matchAll(NAMED_IMPORT$1)) {
|
|
1005
457
|
const { names = "", source = "" } = match.groups ?? {};
|
|
1006
|
-
for (const part of argumentsOf
|
|
458
|
+
for (const part of argumentsOf(names)) {
|
|
1007
459
|
const [name = "", local = name] = part.split(RENAMED);
|
|
1008
460
|
bindings.push({
|
|
1009
461
|
local,
|
|
@@ -2006,7 +1458,8 @@ function misplacedGround(rootDir) {
|
|
|
2006
1458
|
const readOutside = new Set([...pathsModuleTestsName(dirname(specsRoot)), ...pathsMemberConfigsName(dirname(specsRoot))].map((path) => entryOf(specsRoot, path)).filter((found) => found !== void 0).map(({ entry, ground }) => join(ground, entry)));
|
|
2007
1459
|
for (const [key, { entry, ground, leaves, referrers }] of usageUnder(specsRoot)) {
|
|
2008
1460
|
const lowest = lowestCommonFolder([...leaves]) ?? specsRoot;
|
|
2009
|
-
|
|
1461
|
+
const owner = dirname(ground);
|
|
1462
|
+
if (!isWithin(owner, lowest) || readOutside.has(key) || treeKind(key) === void 0) continue;
|
|
2010
1463
|
misplaced.push({
|
|
2011
1464
|
entry,
|
|
2012
1465
|
ground,
|
|
@@ -2914,115 +2367,6 @@ function noVolatileLiteralsInGround(file, text) {
|
|
|
2914
2367
|
});
|
|
2915
2368
|
}
|
|
2916
2369
|
//#endregion
|
|
2917
|
-
//#region lint/kit/project/command-words.ts
|
|
2918
|
-
const COMMAND_WORD = /^[a-z][a-z0-9-]*$/u;
|
|
2919
|
-
/** Without the runner's declared list, the first word alone: a positional argument is never read as a subcommand. */
|
|
2920
|
-
function commandWords(line, commands) {
|
|
2921
|
-
const words = line.trim().split(/\s+/u);
|
|
2922
|
-
if (commands === void 0) {
|
|
2923
|
-
const [first = ""] = words;
|
|
2924
|
-
return COMMAND_WORD.test(first) ? [first] : [];
|
|
2925
|
-
}
|
|
2926
|
-
const named = [];
|
|
2927
|
-
for (const word of words) {
|
|
2928
|
-
if (!commands.includes(word)) break;
|
|
2929
|
-
named.push(word);
|
|
2930
|
-
}
|
|
2931
|
-
return named;
|
|
2932
|
-
}
|
|
2933
|
-
const CLI_CONSTRUCTOR = /\bspecification\s*\.\s*cli\s*\(/u;
|
|
2934
|
-
function wordsOf(text, open) {
|
|
2935
|
-
return argumentsOf$1(text.slice(open + 1, closingOf(text, open))).flatMap((entry) => {
|
|
2936
|
-
const word = literalOf(entry);
|
|
2937
|
-
return word === void 0 ? [] : [word];
|
|
2938
|
-
});
|
|
2939
|
-
}
|
|
2940
|
-
function constantWords(text, name) {
|
|
2941
|
-
const escaped = name.replaceAll("$", String.raw`\$`);
|
|
2942
|
-
const pattern = String.raw`\bconst\s+${escaped}\s*(?::[^=]+)?=\s*\[`;
|
|
2943
|
-
const binding = new RegExp(pattern, "u").exec(text);
|
|
2944
|
-
return binding === null ? void 0 : wordsOf(text, binding.index + binding[0].length - 1);
|
|
2945
|
-
}
|
|
2946
|
-
/** Written as an array or bound to a `const` of the same file (`commands: COMMANDS`, `{ commands }`). */
|
|
2947
|
-
function declaredCommands(door) {
|
|
2948
|
-
const text = blankComments(door);
|
|
2949
|
-
const call = CLI_CONSTRUCTOR.exec(text);
|
|
2950
|
-
if (call === null) return;
|
|
2951
|
-
const open = call.index + call[0].length - 1;
|
|
2952
|
-
const [, options = ""] = argumentsOf$1(text.slice(open + 1, closingOf(text, open)));
|
|
2953
|
-
const list = /[{,\s]commands\s*:\s*\[/u.exec(options);
|
|
2954
|
-
if (list !== null) return wordsOf(options, list.index + list[0].length - 1);
|
|
2955
|
-
const named = /[{,\s]commands\s*:\s*(?<name>[A-Za-z_$][\w$]*)/u.exec(options)?.groups?.name ?? (/[{,\s]commands\s*(?=[,}])/u.test(options) ? "commands" : void 0);
|
|
2956
|
-
return named === void 0 ? void 0 : constantWords(text, named);
|
|
2957
|
-
}
|
|
2958
|
-
//#endregion
|
|
2959
|
-
//#region lint/kit/project/spec-chains.ts
|
|
2960
|
-
const SUBJECT_SPECIFICATIONS = [
|
|
2961
|
-
"api",
|
|
2962
|
-
"cli",
|
|
2963
|
-
"job",
|
|
2964
|
-
"mobile",
|
|
2965
|
-
"website"
|
|
2966
|
-
];
|
|
2967
|
-
function chainAt(text, handle, start) {
|
|
2968
|
-
const calls = [];
|
|
2969
|
-
let index = start + handle.length;
|
|
2970
|
-
for (;;) {
|
|
2971
|
-
const member = /^\s*\.\s*(?<name>[A-Za-z_$][\w$]*)\s*\(/u.exec(text.slice(index));
|
|
2972
|
-
if (member === null) return {
|
|
2973
|
-
calls,
|
|
2974
|
-
end: index,
|
|
2975
|
-
handle,
|
|
2976
|
-
index: start
|
|
2977
|
-
};
|
|
2978
|
-
const { name = "" } = member.groups ?? {};
|
|
2979
|
-
const open = index + member[0].length - 1;
|
|
2980
|
-
const close = closingOf(text, open);
|
|
2981
|
-
calls.push({
|
|
2982
|
-
args: text.slice(open + 1, close),
|
|
2983
|
-
name
|
|
2984
|
-
});
|
|
2985
|
-
index = close + 1;
|
|
2986
|
-
}
|
|
2987
|
-
}
|
|
2988
|
-
const SPECIFICATION_DOOR = /import\s*\{(?<names>[^}]*)\}\s*from\s*['"](?<from>[^'"]*\.specification(?:\.[jt]s)?)['"]/gu;
|
|
2989
|
-
const VIEW = /const\s+(?<view>[A-Za-z_$][\w$]*)\s*=\s*(?<from>[A-Za-z_$][\w$]*)\s*\.\s*with\s*\(/gu;
|
|
2990
|
-
function importedDoors(text, wanted) {
|
|
2991
|
-
const doors = /* @__PURE__ */ new Map();
|
|
2992
|
-
for (const found of text.matchAll(SPECIFICATION_DOOR)) {
|
|
2993
|
-
const { from = "", names = "" } = found.groups ?? {};
|
|
2994
|
-
for (const name of names.split(",")) {
|
|
2995
|
-
const [imported = "", local = imported] = name.trim().split(/\s+as\s+/u);
|
|
2996
|
-
if (wanted(imported)) doors.set(local, from);
|
|
2997
|
-
}
|
|
2998
|
-
}
|
|
2999
|
-
return doors;
|
|
3000
|
-
}
|
|
3001
|
-
function doorsWanted(text, wanted) {
|
|
3002
|
-
const doors = importedDoors(text, wanted);
|
|
3003
|
-
const views = [...text.matchAll(VIEW)];
|
|
3004
|
-
for (let size = -1; size !== doors.size;) {
|
|
3005
|
-
({size} = doors);
|
|
3006
|
-
for (const found of views) {
|
|
3007
|
-
const { from = "", view = "" } = found.groups ?? {};
|
|
3008
|
-
const door = doors.get(from);
|
|
3009
|
-
if (door !== void 0) doors.set(view, door);
|
|
3010
|
-
}
|
|
3011
|
-
}
|
|
3012
|
-
return doors;
|
|
3013
|
-
}
|
|
3014
|
-
function handlesOf(text) {
|
|
3015
|
-
return [...doorsWanted(text, (imported) => SUBJECT_SPECIFICATIONS.includes(imported)).keys()];
|
|
3016
|
-
}
|
|
3017
|
-
function chainsOf(text, handles) {
|
|
3018
|
-
if (handles.length === 0) return [];
|
|
3019
|
-
const pattern = new RegExp(String.raw`(?<![.\w$])(?:${handles.join("|")})(?=\s*\.)`, "gu");
|
|
3020
|
-
return [...text.matchAll(pattern)].map((found) => chainAt(text, found[0], found.index));
|
|
3021
|
-
}
|
|
3022
|
-
function doorsOf$1(text, specification) {
|
|
3023
|
-
return Object.fromEntries(doorsWanted(text, (imported) => imported === specification));
|
|
3024
|
-
}
|
|
3025
|
-
//#endregion
|
|
3026
2370
|
//#region lint/kit/project/specimen.ts
|
|
3027
2371
|
const INTO_FIXTURES = new RegExp(String.raw`(?:^|[/'"\x60\s])${GROUND_FIXTURES}/(?<name>[\w.-]+)(?<inside>/[\w.-])?`, "u");
|
|
3028
2372
|
const SPECIFICATION_FROM = /\bfrom\s*['"](?<from>[^'"]*\.specification(?:\.[jt]s)?)['"]/gu;
|
|
@@ -3112,160 +2456,6 @@ function specimenOf(file, text, specification) {
|
|
|
3112
2456
|
}
|
|
3113
2457
|
}
|
|
3114
2458
|
//#endregion
|
|
3115
|
-
//#region lint/kit/project/subject.ts
|
|
3116
|
-
const NO_READING = Object.freeze({
|
|
3117
|
-
collections: [],
|
|
3118
|
-
methodless: []
|
|
3119
|
-
});
|
|
3120
|
-
const LITERAL_SEGMENT = /^[a-z][a-z._-]*$/u;
|
|
3121
|
-
function segmentsOf(path) {
|
|
3122
|
-
return (path.split(/[?#]/u)[0] ?? "").split("/").filter((segment) => segment !== "");
|
|
3123
|
-
}
|
|
3124
|
-
function isLiteral(segment) {
|
|
3125
|
-
return LITERAL_SEGMENT.test(segment) && !segment.includes("\0");
|
|
3126
|
-
}
|
|
3127
|
-
/** Every segment that is a value (a hole, a number, an id, a symbol) is written `:id`. */
|
|
3128
|
-
function pathTemplate(path) {
|
|
3129
|
-
return `/${segmentsOf(path).map((segment) => isLiteral(segment) ? segment : ":id").join("/")}`;
|
|
3130
|
-
}
|
|
3131
|
-
function isCollection(word, collections) {
|
|
3132
|
-
return collections.includes(word) || word.endsWith("s") && !/(?:ss|us|is)$/u.test(word);
|
|
3133
|
-
}
|
|
3134
|
-
function actionOf(method, resource, item, collections) {
|
|
3135
|
-
if (method === "GET") return !item && isCollection(resource, collections) ? "list" : "get";
|
|
3136
|
-
if (method === "POST") return "create";
|
|
3137
|
-
if (method === "PUT" || method === "PATCH") return "update";
|
|
3138
|
-
return method.toLowerCase();
|
|
3139
|
-
}
|
|
3140
|
-
function followsACollection(segments, index, collections) {
|
|
3141
|
-
return isCollection(segments[index - 1] ?? "", collections);
|
|
3142
|
-
}
|
|
3143
|
-
/** `btc` in `/indicators/btc/chart`. The last segment is read as the route's: after a collection it names a sub-route far more often than an item. */
|
|
3144
|
-
function ambiguousSegment(subject, reading = NO_READING) {
|
|
3145
|
-
if (subject.specification !== "api" || subject.template === true) return;
|
|
3146
|
-
const segments = segmentsOf(subject.path);
|
|
3147
|
-
const { collections } = reading;
|
|
3148
|
-
return segments.find((segment, index) => index < segments.length - 1 && isLiteral(segment) && followsACollection(segments, index, collections));
|
|
3149
|
-
}
|
|
3150
|
-
function routeFolder(method, path, collections) {
|
|
3151
|
-
const segments = segmentsOf(pathTemplate(path));
|
|
3152
|
-
const literals = segments.filter((segment) => segment !== ":id");
|
|
3153
|
-
if (literals.length === 0) return;
|
|
3154
|
-
if (method === "ALL" || literals.length > 1) return literals.join("/");
|
|
3155
|
-
const [resource = ""] = literals;
|
|
3156
|
-
return `${resource}/${actionOf(method, resource, segments.at(-1) === ":id", collections)}`;
|
|
3157
|
-
}
|
|
3158
|
-
function underBase(path, basePath) {
|
|
3159
|
-
if (basePath === void 0) return path;
|
|
3160
|
-
if (path === basePath) return "/";
|
|
3161
|
-
return path.startsWith(`${basePath}/`) ? path.slice(basePath.length) : path;
|
|
3162
|
-
}
|
|
3163
|
-
/** Nothing for a subject with no standard name: the root route, a cli run with no command word, a page, a journey. */
|
|
3164
|
-
function subjectFolder(subject, reading = NO_READING) {
|
|
3165
|
-
if (subject.specification === "api") {
|
|
3166
|
-
const mounted = {
|
|
3167
|
-
...subject,
|
|
3168
|
-
path: underBase(subject.path, reading.basePath)
|
|
3169
|
-
};
|
|
3170
|
-
return ambiguousSegment(mounted, reading) === void 0 ? routeFolder(subject.method, mounted.path, reading.collections) : void 0;
|
|
3171
|
-
}
|
|
3172
|
-
if (subject.specification === "cli") return subject.command.length === 0 ? void 0 : subject.command.join("/");
|
|
3173
|
-
return subject.specification === "job" ? subject.job : void 0;
|
|
3174
|
-
}
|
|
3175
|
-
/** `GET /events/:id`, `trigger digest`, ... */
|
|
3176
|
-
function subjectName(subject) {
|
|
3177
|
-
if (subject.specification === "api") return `${subject.method} ${pathTemplate(subject.path)}`;
|
|
3178
|
-
if (subject.specification === "cli") return subject.command.length === 0 ? "exec (no command)" : `exec ${subject.command.join(" ")}`;
|
|
3179
|
-
if (subject.specification === "job") return `trigger ${subject.job}`;
|
|
3180
|
-
return subject.specification === "mobile" ? `open ${subject.journey}` : `visit ${subject.page}`;
|
|
3181
|
-
}
|
|
3182
|
-
const HTTP_VERBS = {
|
|
3183
|
-
delete: "DELETE",
|
|
3184
|
-
get: "GET",
|
|
3185
|
-
patch: "PATCH",
|
|
3186
|
-
post: "POST",
|
|
3187
|
-
put: "PUT"
|
|
3188
|
-
};
|
|
3189
|
-
const REQUEST_LINE = /^\s*(?<method>DELETE|GET|HEAD|OPTIONS|PATCH|POST|PUT)\s+(?<path>\S+)/u;
|
|
3190
|
-
const REQUEST_FILE = /^file\(\s*(?<quote>['"])(?<path>[^'"]*)\k<quote>\s*\)$/u;
|
|
3191
|
-
function requestLineOf(text) {
|
|
3192
|
-
const { method = "", path = "/" } = REQUEST_LINE.exec(text)?.groups ?? {};
|
|
3193
|
-
const verb = HTTP_VERBS[method.toLowerCase()];
|
|
3194
|
-
return verb === void 0 ? [] : [{
|
|
3195
|
-
specification: "api",
|
|
3196
|
-
method: verb,
|
|
3197
|
-
path,
|
|
3198
|
-
template: true
|
|
3199
|
-
}];
|
|
3200
|
-
}
|
|
3201
|
-
const pageOf = (first) => {
|
|
3202
|
-
const literal = literalOf(first);
|
|
3203
|
-
return literal?.startsWith("/") === true ? [{
|
|
3204
|
-
specification: "website",
|
|
3205
|
-
page: pathTemplate(literal)
|
|
3206
|
-
}] : [];
|
|
3207
|
-
};
|
|
3208
|
-
const TERMINALS = {
|
|
3209
|
-
exec: (first, _readRequest, commands) => {
|
|
3210
|
-
const line = first?.startsWith("[") === true ? literalOf(argumentsOf$1(first.slice(1, -1)).at(-1)) : literalOf(first);
|
|
3211
|
-
return line === void 0 ? [] : [{
|
|
3212
|
-
command: commandWords(line, commands),
|
|
3213
|
-
specification: "cli"
|
|
3214
|
-
}];
|
|
3215
|
-
},
|
|
3216
|
-
fetch: pageOf,
|
|
3217
|
-
open: (first) => [{
|
|
3218
|
-
specification: "mobile",
|
|
3219
|
-
journey: literalOf(first) ?? "(launch)"
|
|
3220
|
-
}],
|
|
3221
|
-
request: (first, readRequest) => {
|
|
3222
|
-
const path = REQUEST_FILE.exec(first ?? "")?.groups?.path;
|
|
3223
|
-
return path === void 0 ? [] : requestLineOf(readRequest(path) ?? "");
|
|
3224
|
-
},
|
|
3225
|
-
trigger: (first) => {
|
|
3226
|
-
const job = literalOf(first);
|
|
3227
|
-
return job === void 0 ? [] : [{
|
|
3228
|
-
specification: "job",
|
|
3229
|
-
job
|
|
3230
|
-
}];
|
|
3231
|
-
},
|
|
3232
|
-
visit: pageOf
|
|
3233
|
-
};
|
|
3234
|
-
function routeOf(method, args) {
|
|
3235
|
-
const path = literalOf(args[0]);
|
|
3236
|
-
if (path?.startsWith("/") !== true) return [];
|
|
3237
|
-
const options = args[method === "POST" || method === "PUT" ? 2 : 1] ?? "";
|
|
3238
|
-
return [{
|
|
3239
|
-
specification: "api",
|
|
3240
|
-
method,
|
|
3241
|
-
path,
|
|
3242
|
-
template: /\/:[A-Za-z_$]/u.test(path) || /(?<![\w$])params(?![\w$])/u.test(options)
|
|
3243
|
-
}];
|
|
3244
|
-
}
|
|
3245
|
-
function declaredMethod(subject, methodless) {
|
|
3246
|
-
if (subject.specification !== "api") return subject;
|
|
3247
|
-
const template = pathTemplate(subject.path);
|
|
3248
|
-
return methodless.some((path) => pathTemplate(path) === template) ? {
|
|
3249
|
-
...subject,
|
|
3250
|
-
method: "ALL"
|
|
3251
|
-
} : subject;
|
|
3252
|
-
}
|
|
3253
|
-
function subjectsOfChain(chain, readRequest = () => "", reading = NO_READING) {
|
|
3254
|
-
const terminal = chain.calls.at(-1);
|
|
3255
|
-
if (terminal === void 0) return [];
|
|
3256
|
-
const args = argumentsOf$1(terminal.args);
|
|
3257
|
-
const [first] = args;
|
|
3258
|
-
const method = HTTP_VERBS[terminal.name];
|
|
3259
|
-
return (method === void 0 ? TERMINALS[terminal.name]?.(first, readRequest, reading.commands?.[chain.handle]) ?? [] : routeOf(method, args)).map((subject) => declaredMethod(subject, reading.methodless));
|
|
3260
|
-
}
|
|
3261
|
-
function subjectsOf(source, readRequest, reading = NO_READING) {
|
|
3262
|
-
const text = blankComments(source);
|
|
3263
|
-
return chainsOf(text, handlesOf(text)).flatMap((chain) => subjectsOfChain(chain, readRequest, reading).map((subject) => ({
|
|
3264
|
-
index: chain.index,
|
|
3265
|
-
subject
|
|
3266
|
-
})));
|
|
3267
|
-
}
|
|
3268
|
-
//#endregion
|
|
3269
2459
|
//#region lint/kit/project/spec-subjects.ts
|
|
3270
2460
|
const TEST_CALL = String.raw`(?<![.\w$])(?:test|it)(?:\.\w+(?:\([^()]*\))?)*\s*\(`;
|
|
3271
2461
|
function holdsTestCall(text) {
|
|
@@ -4085,7 +3275,8 @@ function subjectOf(specsRoot, file) {
|
|
|
4085
3275
|
}
|
|
4086
3276
|
function setupTargetOf(specsRoot, file, readers) {
|
|
4087
3277
|
const subject = subjectOf(specsRoot, file);
|
|
4088
|
-
const
|
|
3278
|
+
const owner = lowestPlaceOf(specsRoot, readers) ?? subject;
|
|
3279
|
+
const home = reservedFolderOf(owner, SETUP_FOLDER);
|
|
4089
3280
|
return relative(specsRoot, join(home, relative(subject, file)));
|
|
4090
3281
|
}
|
|
4091
3282
|
function codeOutsideSetup(rootDir, specsRoot) {
|