@jterrazz/test 17.0.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/browser/index.js +1 -2
  2. package/dist/browser/services.js +1 -2
  3. package/dist/browser/stubs.js +1 -2
  4. package/dist/{shared-index.browser.js → browser-shared-index.browser.js} +82 -40
  5. package/dist/checker.js +15 -824
  6. package/dist/in-place.global-setup.d.ts +1 -0
  7. package/dist/in-place.global-setup.js +1 -1
  8. package/dist/index.d.ts +2 -14
  9. package/dist/index.js +83 -204
  10. package/dist/mobile-session.global-setup.d.ts +1 -0
  11. package/dist/oxlint.cjs +2 -2
  12. package/dist/oxlint.js +67 -2321
  13. package/dist/route-coverage.global-setup.d.ts +1 -0
  14. package/dist/route-coverage.global-setup.js +1 -2
  15. package/dist/services.global-setup.d.ts +1 -1
  16. package/dist/services.global-setup.js +1 -2
  17. package/dist/services.js +1 -1
  18. package/dist/shared-appium.adapter.js +14 -14
  19. package/dist/shared-artifacts.js +7 -5
  20. package/dist/shared-derived-projects.js +23 -8
  21. package/dist/shared-in-place.js +1 -1
  22. package/dist/shared-ledger.js +1 -1
  23. package/dist/shared-listened-port.js +19 -0
  24. package/dist/shared-literate-plugin.js +2 -1
  25. package/dist/shared-pause.js +12 -1
  26. package/dist/shared-playwright.adapter.js +17 -19
  27. package/dist/shared-process-alive.js +13 -0
  28. package/dist/shared-record-key.js +13 -0
  29. package/dist/shared-serve.js +107 -65
  30. package/dist/shared-services.global-setup.js +4 -9
  31. package/dist/shared-session-ledger.js +10 -6
  32. package/dist/shared-spec-document.js +2 -64
  33. package/dist/shared-spec-grammar.js +66 -0
  34. package/dist/shared-specs-modules.js +1033 -0
  35. package/dist/{shared-stream-body.js → shared-stub-reply.js} +40 -3
  36. package/dist/shared-stub-server.js +30 -78
  37. package/dist/shared-temp-root.global-setup.js +3 -10
  38. package/dist/shared-testcontainers.adapter.js +111 -8
  39. package/dist/temp-root.global-setup.js +1 -2
  40. package/dist/vitest.d.ts +17 -22
  41. package/dist/vitest.js +37 -53
  42. package/docs/concepts/architecture.md +20 -20
  43. package/docs/concepts/conventions.md +2 -2
  44. package/docs/guides/cli-specs.md +2 -0
  45. package/docs/guides/develop.md +20 -17
  46. package/docs/guides/mobile-specs.md +3 -3
  47. package/docs/guides/release.md +4 -3
  48. package/docs/guides/website-specs.md +1 -1
  49. package/docs/reference/linting.md +2 -2
  50. package/docs/reference/services.md +9 -9
  51. package/docs/reference/stubs.md +4 -2
  52. package/docs/start/set-up-a-project.md +31 -17
  53. package/package.json +10 -10
package/dist/checker.js CHANGED
@@ -1,214 +1,21 @@
1
- import { n as parsedJson, t as isRecord } from "./shared-json-shape.js";
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 scalarString, c as SPEC_EXTENSION, d as isRelativePath, i as readSpecFile, l as SpecSyntaxError, n as documentPaths, o as DOCUMENT_KEYS, r as parseSpecDocument, s as RUN_KEYS, t as assertedStreams, u as dedent } from "./shared-spec-document.js";
5
- import { a as literalOf, i as literalEnd, n as argumentsOf$1, o as openersAt, r as closingOf, s as skipString, t as argumentSpans } from "./shared-source-scan.js";
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 { createRequire } from "node:module";
12
- import { BASELINE, GROUND_EXPECTED, GROUND_FIXTURES, GROUND_FOLDERS, GROUND_REQUESTS, GROUND_SEEDS, MEMBER_DEPTH, OXLINT_CONFIG_NAMES, OXLINT_SUPPRESSIONS, PRUNED, RENAMED_FOLDERS, RETIRED_SHARED_FOLDERS, SETUP_FOLDER, SPECIAL_FOLDERS, TESTS_FOLDER, blankComments, countsOf, defineCheck, folderChanged, forgetTree, holdsDirectives, holdsManifest, isFile, isGroundFolder, isProjectRoot, isSpecialFolder, keepsSection, labelOf, moveKeepingHistory, nearestRoot, quotedMask, readAt, readFileCached, readManifest, refusal, rowNamed, sectionOf, suppressionDirectives, treeEntries, treeKind, treeNames, workspaceMembers, workspacePatterns } from "@jterrazz/typescript/rules";
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 targets = resolveSpecifier(file, String(stringValue(child(statement, "source"))));
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$1(names).flatMap((part) => {
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$1(text.slice(open + 1, close))) {
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$1(names)) {
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
- if (!isWithin(dirname(ground), lowest) || readOutside.has(key) || treeKind(key) === void 0) continue;
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 home = reservedFolderOf(lowestPlaceOf(specsRoot, readers) ?? subject, SETUP_FOLDER);
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) {