@jterrazz/test 16.0.0 → 16.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/appium.adapter.js +59 -4
- package/dist/browser/index.js +88 -40
- package/dist/checker.js +272 -63
- package/dist/index.d.ts +33 -4
- package/dist/index.js +47 -24
- package/dist/oxlint.cjs +100 -27
- package/dist/oxlint.js +100 -27
- package/dist/playwright.adapter.js +14 -65
- package/dist/{ambiguity.js → substring-warning.js} +99 -1
- package/dist/vitest.js +71 -11
- package/docs/01-architecture.md +95 -0
- package/docs/02-developing.md +351 -0
- package/docs/03-testing.md +495 -0
- package/docs/04-operating.md +101 -0
- package/docs/05-module-tests.md +152 -0
- package/docs/06-integration.md +180 -0
- package/docs/07-component.md +246 -0
- package/docs/08-website.md +404 -0
- package/docs/09-mobile.md +270 -0
- package/docs/10-api.md +264 -0
- package/docs/11-jobs.md +157 -0
- package/docs/12-cli.md +677 -0
- package/docs/13-elements.md +242 -0
- package/docs/14-assertions.md +314 -0
- package/docs/15-tokens.md +253 -0
- package/docs/16-contracts.md +438 -0
- package/docs/17-services.md +188 -0
- package/docs/18-conventions.md +305 -0
- package/docs/19-linting.md +510 -0
- package/docs/README.md +36 -0
- package/docs/decisions/001-v9-conventions-on-three-channels.md +50 -0
- package/docs/decisions/002-the-statique-channel-holds-only-what-upstream-does-not.md +49 -0
- package/docs/decisions/003-a-rendered-component-is-a-facet-on-vitest-browser-mode.md +84 -0
- package/docs/decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md +81 -0
- package/docs/decisions/005-the-doubles-ladder-is-closed.md +86 -0
- package/docs/decisions/006-the-catalogue-reaches-a-file-by-its-role.md +245 -0
- package/docs/decisions/007-compose-mode-leaves-the-package.md +53 -0
- package/docs/decisions/008-vitest-5-node-24-and-the-native-seams-as-optional-peers.md +75 -0
- package/docs/decisions/_template.md +22 -0
- package/docs/reference/classes/Matcher.md +120 -0
- package/docs/reference/classes/ProcessHandle.md +368 -0
- package/docs/reference/functions/defineContract.md +39 -0
- package/docs/reference/functions/defineContracts.md +36 -0
- package/docs/reference/functions/disabled.md +28 -0
- package/docs/reference/functions/enabled.md +20 -0
- package/docs/reference/functions/focused.md +26 -0
- package/docs/reference/functions/mockOf.md +79 -0
- package/docs/reference/functions/postgres.md +31 -0
- package/docs/reference/functions/process.md +33 -0
- package/docs/reference/functions/redis.md +28 -0
- package/docs/reference/functions/required.md +41 -0
- package/docs/reference/functions/selected.md +26 -0
- package/docs/reference/functions/sqlite.md +33 -0
- package/docs/reference/functions/testId.md +20 -0
- package/docs/reference/functions/text.md +40 -0
- package/docs/reference/functions/valued.md +28 -0
- package/docs/reference/functions/waitUntil.md +46 -0
- package/docs/reference/functions/within.md +29 -0
- package/docs/reference/index.md +196 -0
- package/docs/reference/interfaces/BaseResult.md +87 -0
- package/docs/reference/interfaces/CallResult.md +160 -0
- package/docs/reference/interfaces/CaptureScope.md +82 -0
- package/docs/reference/interfaces/CliResult.md +253 -0
- package/docs/reference/interfaces/ContainerAccessor.md +166 -0
- package/docs/reference/interfaces/DirectoryAccessor.md +33 -0
- package/docs/reference/interfaces/FetchResult.md +180 -0
- package/docs/reference/interfaces/FilesystemAccessor.md +46 -0
- package/docs/reference/interfaces/HttpResult.md +126 -0
- package/docs/reference/interfaces/JsonAccessor.md +46 -0
- package/docs/reference/interfaces/PageResult.md +356 -0
- package/docs/reference/interfaces/RenderResult.md +72 -0
- package/docs/reference/interfaces/RenderedText.md +110 -0
- package/docs/reference/interfaces/ResponseAccessor.md +47 -0
- package/docs/reference/interfaces/ScreenResult.md +128 -0
- package/docs/reference/interfaces/TableAccessor.md +26 -0
- package/docs/reference/interfaces/TextAccessor.md +96 -0
- package/docs/reference/type-aliases/ApiHandle.md +68 -0
- package/docs/reference/type-aliases/ApiSpecification.md +213 -0
- package/docs/reference/type-aliases/ApiSpecificationOptions.md +68 -0
- package/docs/reference/type-aliases/BrowserConsoleMessage.md +30 -0
- package/docs/reference/type-aliases/BrowserLinkElement.md +50 -0
- package/docs/reference/type-aliases/BrowserMetaElement.md +40 -0
- package/docs/reference/type-aliases/BrowserOpenOptions.md +87 -0
- package/docs/reference/type-aliases/BrowserPage.md +132 -0
- package/docs/reference/type-aliases/BrowserPort.md +51 -0
- package/docs/reference/type-aliases/CliEnv.md +11 -0
- package/docs/reference/type-aliases/CliHandle.md +80 -0
- package/docs/reference/type-aliases/CliInput.md +36 -0
- package/docs/reference/type-aliases/CliOutput.md +46 -0
- package/docs/reference/type-aliases/CliPort.md +58 -0
- package/docs/reference/type-aliases/CliSpecification.md +144 -0
- package/docs/reference/type-aliases/CliSpecificationOptions.md +134 -0
- package/docs/reference/type-aliases/ComponentChain.md +156 -0
- package/docs/reference/type-aliases/ComponentScenario.md +20 -0
- package/docs/reference/type-aliases/ComponentUi.md +47 -0
- package/docs/reference/type-aliases/ComponentVisitor.md +10 -0
- package/docs/reference/type-aliases/ContainerPort.md +113 -0
- package/docs/reference/type-aliases/Contract.md +68 -0
- package/docs/reference/type-aliases/ContractInput.md +13 -0
- package/docs/reference/type-aliases/ContractRequest.md +95 -0
- package/docs/reference/type-aliases/ContractResponder.md +23 -0
- package/docs/reference/type-aliases/ContractResponse.md +78 -0
- package/docs/reference/type-aliases/ContractResponseValue.md +13 -0
- package/docs/reference/type-aliases/Contracts.md +48 -0
- package/docs/reference/type-aliases/DatabaseKeys.md +16 -0
- package/docs/reference/type-aliases/DatabasePort.md +72 -0
- package/docs/reference/type-aliases/DeviceOpenOptions.md +46 -0
- package/docs/reference/type-aliases/DevicePort.md +51 -0
- package/docs/reference/type-aliases/DeviceScreen.md +32 -0
- package/docs/reference/type-aliases/DeviceTimeouts.md +39 -0
- package/docs/reference/type-aliases/DockerSpecConfig.md +43 -0
- package/docs/reference/type-aliases/DomMount.md +21 -0
- package/docs/reference/type-aliases/ElementKind.md +27 -0
- package/docs/reference/type-aliases/ElementMatch.md +74 -0
- package/docs/reference/type-aliases/ElementOptions.md +29 -0
- package/docs/reference/type-aliases/ElementRef.md +128 -0
- package/docs/reference/type-aliases/ExecOptions.md +37 -0
- package/docs/reference/type-aliases/FileAccessor.md +54 -0
- package/docs/reference/type-aliases/HonoApp.md +31 -0
- package/docs/reference/type-aliases/HttpContractFilter.md +49 -0
- package/docs/reference/type-aliases/HttpResponseInit.md +46 -0
- package/docs/reference/type-aliases/HttpStreamInit.md +58 -0
- package/docs/reference/type-aliases/IntegrationHandle.md +46 -0
- package/docs/reference/type-aliases/IntegrationSpecification.md +111 -0
- package/docs/reference/type-aliases/IntegrationSpecificationOptions.md +42 -0
- package/docs/reference/type-aliases/Intercept.md +19 -0
- package/docs/reference/type-aliases/InterceptOptions.md +28 -0
- package/docs/reference/type-aliases/InterceptScope.md +11 -0
- package/docs/reference/type-aliases/IsolationStrategy.md +75 -0
- package/docs/reference/type-aliases/JobHandle.md +34 -0
- package/docs/reference/type-aliases/JobsHandle.md +57 -0
- package/docs/reference/type-aliases/JobsResult.md +16 -0
- package/docs/reference/type-aliases/JobsSpecification.md +94 -0
- package/docs/reference/type-aliases/JobsSpecificationOptions.md +56 -0
- package/docs/reference/type-aliases/LandmarkKind.md +20 -0
- package/docs/reference/type-aliases/LiterateRunFlags.md +27 -0
- package/docs/reference/type-aliases/LiterateServeRegistration.md +70 -0
- package/docs/reference/type-aliases/MatchFixtureOptions.md +27 -0
- package/docs/reference/type-aliases/MatchableRequest.md +61 -0
- package/docs/reference/type-aliases/MatcherKind.md +34 -0
- package/docs/reference/type-aliases/MobileBackendOptions.md +28 -0
- package/docs/reference/type-aliases/MobileElementKind.md +14 -0
- package/docs/reference/type-aliases/MobileElementMatch.md +60 -0
- package/docs/reference/type-aliases/MobileElementRef.md +20 -0
- package/docs/reference/type-aliases/MobileHandle.md +65 -0
- package/docs/reference/type-aliases/MobileScenario.md +20 -0
- package/docs/reference/type-aliases/MobileSpecification.md +51 -0
- package/docs/reference/type-aliases/MobileSpecificationOptions.md +126 -0
- package/docs/reference/type-aliases/MobileVisitor.md +81 -0
- package/docs/reference/type-aliases/MockOfOptions.md +26 -0
- package/docs/reference/type-aliases/MockPort.md +10 -0
- package/docs/reference/type-aliases/PinnedClock.md +10 -0
- package/docs/reference/type-aliases/PostgresOptions.md +32 -0
- package/docs/reference/type-aliases/ProcessOptions.md +112 -0
- package/docs/reference/type-aliases/RedisOptions.md +20 -0
- package/docs/reference/type-aliases/RenderSubject.md +12 -0
- package/docs/reference/type-aliases/ScreenNode.md +67 -0
- package/docs/reference/type-aliases/ServerPort.md +36 -0
- package/docs/reference/type-aliases/ServerResponse.md +46 -0
- package/docs/reference/type-aliases/ServerSpec.md +21 -0
- package/docs/reference/type-aliases/ServiceHandle.md +216 -0
- package/docs/reference/type-aliases/ServiceRecord.md +12 -0
- package/docs/reference/type-aliases/SpecDocument.md +84 -0
- package/docs/reference/type-aliases/SpecEnvToken.md +21 -0
- package/docs/reference/type-aliases/SpecFileAssertion.md +27 -0
- package/docs/reference/type-aliases/SpecFixture.md +30 -0
- package/docs/reference/type-aliases/SpecKind.md +8 -0
- package/docs/reference/type-aliases/SpecRun.md +108 -0
- package/docs/reference/type-aliases/SpecServeEntry.md +40 -0
- package/docs/reference/type-aliases/SpecStream.md +32 -0
- package/docs/reference/type-aliases/SpecificationConfig.md +290 -0
- package/docs/reference/type-aliases/SqliteOptions.md +36 -0
- package/docs/reference/type-aliases/SseEvent.md +58 -0
- package/docs/reference/type-aliases/StreamBody.md +61 -0
- package/docs/reference/type-aliases/TextFilter.md +22 -0
- package/docs/reference/type-aliases/VisitScenario.md +20 -0
- package/docs/reference/type-aliases/Visitor.md +215 -0
- package/docs/reference/type-aliases/WaitUntilOptions.md +46 -0
- package/docs/reference/type-aliases/WebsiteBackendOptions.md +37 -0
- package/docs/reference/type-aliases/WebsiteHandle.md +51 -0
- package/docs/reference/type-aliases/WebsiteSpecification.md +118 -0
- package/docs/reference/type-aliases/WebsiteSpecificationOptions.md +40 -0
- package/docs/reference/variables/anthropic.md +19 -0
- package/docs/reference/variables/banner.md +10 -0
- package/docs/reference/variables/button.md +10 -0
- package/docs/reference/variables/clock.md +16 -0
- package/docs/reference/variables/complementary.md +10 -0
- package/docs/reference/variables/component.md +10 -0
- package/docs/reference/variables/content.md +10 -0
- package/docs/reference/variables/contentinfo.md +10 -0
- package/docs/reference/variables/dialog.md +10 -0
- package/docs/reference/variables/field.md +10 -0
- package/docs/reference/variables/form.md +10 -0
- package/docs/reference/variables/heading.md +10 -0
- package/docs/reference/variables/http.md +40 -0
- package/docs/reference/variables/intercept.md +11 -0
- package/docs/reference/variables/link.md +10 -0
- package/docs/reference/variables/listitem.md +10 -0
- package/docs/reference/variables/main.md +10 -0
- package/docs/reference/variables/match.md +8 -0
- package/docs/reference/variables/navigation.md +10 -0
- package/docs/reference/variables/openai.md +21 -0
- package/docs/reference/variables/option.md +17 -0
- package/docs/reference/variables/region.md +10 -0
- package/docs/reference/variables/row.md +10 -0
- package/docs/reference/variables/search.md +10 -0
- package/docs/reference/variables/specification.md +8 -0
- package/docs/reference/variables/status.md +10 -0
- package/docs/reference/variables/table.md +10 -0
- package/package.json +2 -1
package/dist/checker.js
CHANGED
|
@@ -688,7 +688,7 @@ function checkPoolFixtures(rootDir) {
|
|
|
688
688
|
violations.push({
|
|
689
689
|
file: relEntry,
|
|
690
690
|
line: 1,
|
|
691
|
-
message: `${relEntry}: dead pool fixture — no spec under ${relative(rootDir, specsRoot)} references $FIXTURES/${entry.name} (C9 — see docs/19-linting.md)`,
|
|
691
|
+
message: `${relEntry}: dead pool fixture — no spec under ${relative(rootDir, specsRoot) || basename(specsRoot)} references $FIXTURES/${entry.name} (C9 — see docs/19-linting.md)`,
|
|
692
692
|
rule: "c9",
|
|
693
693
|
severity: "error"
|
|
694
694
|
});
|
|
@@ -939,6 +939,13 @@ const SKIPPED$2 = /* @__PURE__ */ new Set([
|
|
|
939
939
|
GROUND_FIXTURES,
|
|
940
940
|
"node_modules"
|
|
941
941
|
]);
|
|
942
|
+
/**
|
|
943
|
+
* Does this facet-relative path cross ground? A LEADING underscore names it —
|
|
944
|
+
* the four ground directories and nothing else carry one (C13).
|
|
945
|
+
*/
|
|
946
|
+
function isUnderGround(relativePath) {
|
|
947
|
+
return relativePath.split(/[/\\]/u).some((segment) => segment.startsWith("_"));
|
|
948
|
+
}
|
|
942
949
|
/** `readdirSync` that answers `[]` for anything it cannot read. */
|
|
943
950
|
function entriesOf(dir) {
|
|
944
951
|
try {
|
|
@@ -1045,6 +1052,7 @@ function checkModuleTestUnderFacet(specsRoot) {
|
|
|
1045
1052
|
if (specificationsIn(path).length === 0) continue;
|
|
1046
1053
|
for (const file of walkFiles(path)) {
|
|
1047
1054
|
if (!isTestFileName(file.slice(file.lastIndexOf("/") + 1))) continue;
|
|
1055
|
+
if (isUnderGround(relative(path, file))) continue;
|
|
1048
1056
|
if (reachesRunner(textOf$1(file) ?? "")) continue;
|
|
1049
1057
|
const rel = relative(specsRoot, file);
|
|
1050
1058
|
violations.push({
|
|
@@ -1142,6 +1150,14 @@ function checkGroundOwnedByOne(specsRoot) {
|
|
|
1142
1150
|
* (the package's own `specs/lint/`, a repository's consistency suites) is a
|
|
1143
1151
|
* repository suite: it covers a tree rather than the assembled product, so
|
|
1144
1152
|
* it keeps `.test.ts` and C1's declared depth is what judges its shape.
|
|
1153
|
+
*
|
|
1154
|
+
* A tree may also be ONE facet's whole, with no `specs/<facet>/` level: the
|
|
1155
|
+
* runner sits at the root of `specs/` and the folders below it are domains (the
|
|
1156
|
+
* `depth: 'mirror'` shape, where the tree follows a command tree instead of a
|
|
1157
|
+
* facet/domain one). Such a tree carries a `*.specification.ts` at its root,
|
|
1158
|
+
* and that is what says so — every `.test.ts` below it specifies the assembled
|
|
1159
|
+
* product, whatever folder it sits in. Without this clause the whole shape was
|
|
1160
|
+
* invisible to C12 and its specs had to be renamed by hand.
|
|
1145
1161
|
*/
|
|
1146
1162
|
/** The six facet folders — a first-level name under `specs/` that IS a constructor. */
|
|
1147
1163
|
const FACETS = /* @__PURE__ */ new Set([
|
|
@@ -1229,23 +1245,39 @@ function checkSpecOutsideSpecs(memberDir) {
|
|
|
1229
1245
|
function checkTestUnderFacet(specsRoot) {
|
|
1230
1246
|
return movesUnderFacet(specsRoot).map(({ from }) => {
|
|
1231
1247
|
const rel = relative(specsRoot, from);
|
|
1248
|
+
const first = rel.split(/[/\\]/u)[0] ?? "";
|
|
1232
1249
|
return {
|
|
1233
1250
|
file: rel,
|
|
1234
1251
|
line: 1,
|
|
1235
|
-
message: `${rel}:1: a \`.test.ts\` under
|
|
1252
|
+
message: `${rel}:1: a \`.test.ts\` under \`${FACETS.has(first) ? `specs/${first}/` : "specs/"}\` specifies the assembled product — rename it \`.spec.ts\` (fixable: \`jterrazz-test-check <root> --fix\`) (C12 — see docs/19-linting.md)`,
|
|
1236
1253
|
rule: "c12-spec-file-name",
|
|
1237
1254
|
severity: "error"
|
|
1238
1255
|
};
|
|
1239
1256
|
});
|
|
1240
1257
|
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Is this tree ONE facet's whole, rooted at `specs/`?
|
|
1260
|
+
*
|
|
1261
|
+
* A `*.specification.ts` at the ROOT of the tree is the statement: the runner
|
|
1262
|
+
* is the tree's, so every folder below it is a domain of that one facet and no
|
|
1263
|
+
* first level is a repository suite.
|
|
1264
|
+
*/
|
|
1265
|
+
function isRootedFacetTree(specsRoot) {
|
|
1266
|
+
try {
|
|
1267
|
+
return readdirSync(specsRoot, { withFileTypes: true }).some((entry) => entry.isFile() && entry.name.includes(".specification."));
|
|
1268
|
+
} catch {
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1241
1272
|
/** The renames clause two asks for, as absolute from/to pairs. */
|
|
1242
1273
|
function movesUnderFacet(specsRoot) {
|
|
1243
1274
|
const moves = [];
|
|
1275
|
+
const rooted = isRootedFacetTree(specsRoot);
|
|
1244
1276
|
for (const path of walk(specsRoot)) {
|
|
1245
1277
|
if (!path.endsWith(".test.ts")) continue;
|
|
1246
1278
|
const parts = relative(specsRoot, path).split(/[/\\]/u);
|
|
1247
1279
|
const facet = parts[0];
|
|
1248
|
-
if (parts.length < 2 || facet === void 0 || !FACETS.has(facet)) continue;
|
|
1280
|
+
if (!rooted && (parts.length < 2 || facet === void 0 || !FACETS.has(facet))) continue;
|
|
1249
1281
|
if (parts.some((part) => part.startsWith("_"))) continue;
|
|
1250
1282
|
if (!reachesRunner(textOf(path))) continue;
|
|
1251
1283
|
moves.push({
|
|
@@ -1273,6 +1305,16 @@ function movesUnderFacet(specsRoot) {
|
|
|
1273
1305
|
* it for one member, and a path-less `--format json` run at the project root
|
|
1274
1306
|
* runs it for every member the root manifest declares.
|
|
1275
1307
|
*/
|
|
1308
|
+
/** How deep a member may sit below the root — the toolchain's bound, to the segment. */
|
|
1309
|
+
const MEMBER_DEPTH = 6;
|
|
1310
|
+
/** Directories the member walk never enters. */
|
|
1311
|
+
const SKIPPED = /* @__PURE__ */ new Set([
|
|
1312
|
+
".git",
|
|
1313
|
+
".artifacts",
|
|
1314
|
+
"coverage",
|
|
1315
|
+
"dist",
|
|
1316
|
+
"node_modules"
|
|
1317
|
+
]);
|
|
1276
1318
|
/** The two simulated DOMs — the same pair the statique E5b refuses. */
|
|
1277
1319
|
const SIMULATED_DOM = /\benvironment\s*:\s*['"](?<dom>happy-dom|jsdom)['"]/u;
|
|
1278
1320
|
/**
|
|
@@ -1310,16 +1352,80 @@ const RETIRED_SCOPES = ["@testing-library/"];
|
|
|
1310
1352
|
* React Native renders under jest until the react-native-web answer lands, and
|
|
1311
1353
|
* where jest is the sanctioned runner `@testing-library/react-native` is the
|
|
1312
1354
|
* only vocabulary there is: the rule would be asking for a facet that does not
|
|
1313
|
-
* reach that runtime yet. The allowance
|
|
1314
|
-
*
|
|
1315
|
-
*
|
|
1355
|
+
* reach that runtime yet. The allowance lapses the day jest stops running the
|
|
1356
|
+
* member's tests, and it has TWO sources, because jest is not always the
|
|
1357
|
+
* member's own: in a workspace where one app owns the only working RN
|
|
1358
|
+
* toolchain, the library declares the vocabulary and the app runs the tests
|
|
1359
|
+
* that use it.
|
|
1316
1360
|
*/
|
|
1317
1361
|
const JEST_ONLY_SEAM = "@testing-library/react-native";
|
|
1318
|
-
/** Does
|
|
1319
|
-
function
|
|
1362
|
+
/** Does the member's OWN manifest say jest runs here? */
|
|
1363
|
+
function declaresJest(manifest) {
|
|
1320
1364
|
const declares = (name) => name in (manifest.dependencies ?? {}) || name in (manifest.devDependencies ?? {});
|
|
1321
1365
|
return declares("jest") || declares("jest-expo") || manifest.jest !== void 0 || /(?:^|[\s/])jest(?:\s|$)/u.test(manifest.scripts?.test ?? "");
|
|
1322
1366
|
}
|
|
1367
|
+
/** The config names jest answers to, whatever a project writes it in. */
|
|
1368
|
+
const JEST_CONFIG = /^jest\.config\.[cm]?[jt]s$/u;
|
|
1369
|
+
/** The keys of a jest config that name WHERE its tests are. */
|
|
1370
|
+
const JEST_REACH = /\b(?:roots|testMatch|testPathDirs|testPathIgnorePatterns)\s*:\s*\[(?<paths>[^\]]*)\]/gu;
|
|
1371
|
+
/** One quoted entry of such a list. */
|
|
1372
|
+
const JEST_PATH = /['"`](?<path>[^'"`\n]*)['"`]/gu;
|
|
1373
|
+
/** Every `jest.config.*` under `rootDir`, by absolute path — read once per root. */
|
|
1374
|
+
const jestConfigs = /* @__PURE__ */ new Map();
|
|
1375
|
+
/** Find them, bounded by the same skips and depth the member walk uses. */
|
|
1376
|
+
function findJestConfigs(rootDir) {
|
|
1377
|
+
const cached = jestConfigs.get(rootDir);
|
|
1378
|
+
if (cached !== void 0) return cached;
|
|
1379
|
+
const found = [];
|
|
1380
|
+
const walk = (dir, depth) => {
|
|
1381
|
+
if (depth > MEMBER_DEPTH) return;
|
|
1382
|
+
let entries;
|
|
1383
|
+
try {
|
|
1384
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1385
|
+
} catch {
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
for (const entry of entries) {
|
|
1389
|
+
const path = join(dir, entry.name);
|
|
1390
|
+
if (entry.isDirectory()) {
|
|
1391
|
+
if (!entry.name.startsWith(".") && !SKIPPED.has(entry.name)) walk(path, depth + 1);
|
|
1392
|
+
} else if (JEST_CONFIG.test(entry.name)) found.push(path);
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
walk(rootDir, 0);
|
|
1396
|
+
jestConfigs.set(rootDir, found);
|
|
1397
|
+
return found;
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Does a jest config ELSEWHERE in the workspace collect this member's tests?
|
|
1401
|
+
*
|
|
1402
|
+
* jterrazz-design is the shape: the render tests are co-located in
|
|
1403
|
+
* `packages/native/src`, and `apps/playground` — which owns the only working
|
|
1404
|
+
* React Native toolchain — is what runs them, rooting into the library from its
|
|
1405
|
+
* own config. The library's manifest says `vitest --run`, so reading the
|
|
1406
|
+
* allowance from the member alone answered "no jest here" and F8 errored on
|
|
1407
|
+
* exactly the layout the exemption was written for.
|
|
1408
|
+
*
|
|
1409
|
+
* `<rootDir>` is jest's own token for the directory its config sits in, so a
|
|
1410
|
+
* path is resolved against that, and the member is reached when the result
|
|
1411
|
+
* lands inside it.
|
|
1412
|
+
*/
|
|
1413
|
+
function jestReachesMember(memberDir, rootDir) {
|
|
1414
|
+
return findJestConfigs(rootDir).some((config) => {
|
|
1415
|
+
const from = dirname(config);
|
|
1416
|
+
let text;
|
|
1417
|
+
try {
|
|
1418
|
+
text = readFileSync(config, "utf8");
|
|
1419
|
+
} catch {
|
|
1420
|
+
return false;
|
|
1421
|
+
}
|
|
1422
|
+
return [...text.matchAll(JEST_REACH)].some((list) => [...(list.groups?.paths ?? "").matchAll(JEST_PATH)].some((entry) => {
|
|
1423
|
+
const stated = (entry.groups?.path ?? "").replace("<rootDir>", from);
|
|
1424
|
+
const absolute = resolve(from, stated);
|
|
1425
|
+
return absolute === memberDir || absolute.startsWith(`${memberDir}/`);
|
|
1426
|
+
}));
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1323
1429
|
/** Where a member states what it collects, whatever extension it writes it in. */
|
|
1324
1430
|
const CONFIG_NAMES$1 = [
|
|
1325
1431
|
"vitest.config.ts",
|
|
@@ -1329,14 +1435,6 @@ const CONFIG_NAMES$1 = [
|
|
|
1329
1435
|
"vitest.config.mjs",
|
|
1330
1436
|
"vitest.config.cjs"
|
|
1331
1437
|
];
|
|
1332
|
-
/** Directories the member walk never enters. */
|
|
1333
|
-
const SKIPPED = /* @__PURE__ */ new Set([
|
|
1334
|
-
".git",
|
|
1335
|
-
".artifacts",
|
|
1336
|
-
"coverage",
|
|
1337
|
-
"dist",
|
|
1338
|
-
"node_modules"
|
|
1339
|
-
]);
|
|
1340
1438
|
/** A member's manifest, or null when it has none (it is then not a member). */
|
|
1341
1439
|
function readManifest(dir) {
|
|
1342
1440
|
const path = join(dir, "package.json");
|
|
@@ -1394,9 +1492,8 @@ function holdsTests(dir) {
|
|
|
1394
1492
|
*/
|
|
1395
1493
|
const DELEGATING_TEST = /--workspaces|(?:^|\s)-r(?:\s|$)|--recursive|turbo\s+run|nx\s+run-many|lerna\s+run/u;
|
|
1396
1494
|
/** Every seam name this member declares that it should not, and why it should not. */
|
|
1397
|
-
function declaredSeams(manifest) {
|
|
1495
|
+
function declaredSeams(manifest, jestRuns) {
|
|
1398
1496
|
if (manifest.name === FRAMEWORK) return [];
|
|
1399
|
-
const jestRuns = runsJest(manifest);
|
|
1400
1497
|
const isRetired = (name) => {
|
|
1401
1498
|
if (name === JEST_ONLY_SEAM && jestRuns) return false;
|
|
1402
1499
|
return RETIRED.has(name) || RETIRED_SCOPES.some((scope) => name.startsWith(scope));
|
|
@@ -1452,8 +1549,9 @@ function simulatedDom(config, rootDir) {
|
|
|
1452
1549
|
* The two are not the same sentence: removing a transitive changes nothing a
|
|
1453
1550
|
* test can see, while dropping a retired seam means writing its facet.
|
|
1454
1551
|
*/
|
|
1455
|
-
function seamDependencies(
|
|
1456
|
-
|
|
1552
|
+
function seamDependencies(member) {
|
|
1553
|
+
const { label, manifest, memberDir, rootDir, subject } = member;
|
|
1554
|
+
return declaredSeams(manifest, declaresJest(manifest) || jestReachesMember(memberDir, rootDir)).map((seam) => ({
|
|
1457
1555
|
file: join(label, "package.json"),
|
|
1458
1556
|
line: lineOfKey(memberDir, seam.name),
|
|
1459
1557
|
message: seam.kind === "carried" ? `${subject}: \`${seam.name}\` is a transitive of \`@jterrazz/test\` — remove the declaration (F8 — see docs/19-linting.md#f8-no-seam-dependency)` : `${subject}: \`${seam.name}\` is a seam \`@jterrazz/test\` replaced — the facet is \`component()\`, \`website()\`, \`clock\` or \`intercept()\`; remove the declaration (F8 — see docs/19-linting.md#f8-no-seam-dependency)`,
|
|
@@ -1488,7 +1586,13 @@ function checkMember(memberDir, rootDir) {
|
|
|
1488
1586
|
return [
|
|
1489
1587
|
...config === null ? configPresent(manifest, memberDir, label, subject) : [],
|
|
1490
1588
|
...config === null ? [] : simulatedDom(config, rootDir),
|
|
1491
|
-
...seamDependencies(
|
|
1589
|
+
...seamDependencies({
|
|
1590
|
+
label,
|
|
1591
|
+
manifest,
|
|
1592
|
+
memberDir,
|
|
1593
|
+
rootDir,
|
|
1594
|
+
subject
|
|
1595
|
+
}),
|
|
1492
1596
|
...anchoredToRoot(checkSpecOutsideSpecs(memberDir), label)
|
|
1493
1597
|
];
|
|
1494
1598
|
}
|
|
@@ -1509,8 +1613,6 @@ function toPattern(glob) {
|
|
|
1509
1613
|
const escaped = glob.replaceAll(/[.+^${}()|[\]\\]/gu, String.raw`\$&`).replaceAll("**", " ").replaceAll("*", "[^/]*").replaceAll(" ", ".*");
|
|
1510
1614
|
return new RegExp(`^${escaped}$`, "u");
|
|
1511
1615
|
}
|
|
1512
|
-
/** How deep a member may sit below the root — the toolchain's bound, to the segment. */
|
|
1513
|
-
const MEMBER_DEPTH = 6;
|
|
1514
1616
|
/**
|
|
1515
1617
|
* Every workspace member declared by the root manifest, plus the root itself —
|
|
1516
1618
|
* a single-package repository IS its own member, and the pass has to reach it.
|
|
@@ -1573,7 +1675,8 @@ const SPECS_DEPTH = 6;
|
|
|
1573
1675
|
*/
|
|
1574
1676
|
function discoverSpecRoots(rootDir) {
|
|
1575
1677
|
const roots = /* @__PURE__ */ new Set();
|
|
1576
|
-
|
|
1678
|
+
const members = discoverMembers(rootDir);
|
|
1679
|
+
for (const member of members.length === 0 ? [rootDir] : members) {
|
|
1577
1680
|
if (basename(member) === "specs") roots.add(member);
|
|
1578
1681
|
const walk = (dir, depth) => {
|
|
1579
1682
|
if (depth > SPECS_DEPTH) return;
|
|
@@ -2219,7 +2322,7 @@ const CLOCK_READ = /'now'|\bCURRENT_(?:TIMESTAMP|DATE|TIME)\b|\bNOW\s*\(\s*\)/iu
|
|
|
2219
2322
|
const SQL_LINE_COMMENT = /--[^\n]*/gu;
|
|
2220
2323
|
const SQL_BLOCK_COMMENT = /\/\*[\s\S]*?\*\//gu;
|
|
2221
2324
|
/** The file with its comments blanked out, line for line so a finding still points. */
|
|
2222
|
-
function withoutComments(text) {
|
|
2325
|
+
function withoutComments$1(text) {
|
|
2223
2326
|
return text.replaceAll(SQL_BLOCK_COMMENT, (block) => block.replaceAll(/[^\n]/gu, " ")).replaceAll(SQL_LINE_COMMENT, "");
|
|
2224
2327
|
}
|
|
2225
2328
|
/**
|
|
@@ -2394,7 +2497,7 @@ function checkSeedClockReads(path, rel, name) {
|
|
|
2394
2497
|
const text = decodeText(path);
|
|
2395
2498
|
if (text === null) return [];
|
|
2396
2499
|
const violations = [];
|
|
2397
|
-
for (const [index, line] of withoutComments(text).split("\n").entries()) {
|
|
2500
|
+
for (const [index, line] of withoutComments$1(text).split("\n").entries()) {
|
|
2398
2501
|
const found = CLOCK_READ.exec(line);
|
|
2399
2502
|
if (found === null) continue;
|
|
2400
2503
|
violations.push({
|
|
@@ -2532,23 +2635,33 @@ function formatViolations(violations) {
|
|
|
2532
2635
|
/**
|
|
2533
2636
|
* CLI entry for the conventions checker (bundled as `dist/checker.js`).
|
|
2534
2637
|
*
|
|
2535
|
-
* node dist/checker.js [
|
|
2638
|
+
* node dist/checker.js [dir] [--fix] # a specs tree, or a project root
|
|
2536
2639
|
* node dist/checker.js --format json # every root, every member
|
|
2537
2640
|
* node dist/checker.js --member <dir> # one workspace member
|
|
2538
2641
|
*
|
|
2539
|
-
* Three runs, one reporter.
|
|
2540
|
-
*
|
|
2541
|
-
*
|
|
2642
|
+
* Three runs, one reporter. A PATH is read for what it IS. Name a specs tree
|
|
2643
|
+
* — a directory with a `specs` segment in its path — and it walks that tree
|
|
2644
|
+
* with every tree pass: the token/HTTP grammar (D4 / D4b / D10), the
|
|
2645
|
+
* `<case>.spec.yaml` document conventions, the cross-file passes (C9 dead
|
|
2542
2646
|
* fixtures, C14/C15 fixture placement, B5 await-using inference, A7 database
|
|
2543
|
-
* property)
|
|
2544
|
-
*
|
|
2545
|
-
*
|
|
2647
|
+
* property) and the facet passes (C12, C18, C20, C21w), which read the first
|
|
2648
|
+
* level of the tree and are the reason the anchor has to be right. Name
|
|
2649
|
+
* anything else and it is a PROJECT ROOT: the run is the path-less one,
|
|
2650
|
+
* anchored there. With `--member <dir>` it runs the member pass (E3
|
|
2651
|
+
* config-present, E5b no simulated DOM in a config, F8 no seam dependency)
|
|
2652
|
+
* over one workspace member, whether or not that member has a `specs/` root.
|
|
2546
2653
|
*
|
|
2547
2654
|
* With NO path it runs both over the whole project: every `specs/` root it
|
|
2548
2655
|
* discovers from the root manifest's workspaces, and every member. That is the
|
|
2549
2656
|
* run `@jterrazz/typescript`'s ratchet rests on, so the contract is exactly
|
|
2550
2657
|
* "a path-less run reports what the per-root and per-member runs report".
|
|
2551
2658
|
*
|
|
2659
|
+
* A NAMED root that turns out to hold no `specs/` tree at all is refused, loud
|
|
2660
|
+
* and non-zero. Pointed at a project root, the facet passes used to key on its
|
|
2661
|
+
* first level, find no facet folder there and report nothing — `<project>` and
|
|
2662
|
+
* `<project>/specs` disagreed on the same tree, and the run that reported
|
|
2663
|
+
* nothing was the one a CI line was likelier to be written with.
|
|
2664
|
+
*
|
|
2552
2665
|
* `--format json` prints `[{ code, file, line, severity, message }]` on stdout
|
|
2553
2666
|
* and nothing else, where `code` is `jterrazz-check(<id>)` — the namespace the
|
|
2554
2667
|
* ratchet records under. Exit 1 on any ERROR-level violation either way;
|
|
@@ -2561,10 +2674,11 @@ function formatViolations(violations) {
|
|
|
2561
2674
|
* fixes everything exits 0. With no path it applies them to every `specs/` root
|
|
2562
2675
|
* it discovers: `--fix` means the same thing whichever run the operator typed.
|
|
2563
2676
|
*
|
|
2564
|
-
* After a C12 rename it NAMES
|
|
2565
|
-
*
|
|
2566
|
-
*
|
|
2567
|
-
*
|
|
2677
|
+
* After a C12 rename it NAMES the entries of the owning member's configs that
|
|
2678
|
+
* the rename BROKE — a glob whose prefix covers a file it moved, or a literal
|
|
2679
|
+
* naming one — and nothing else. An entry the rename left behind collects
|
|
2680
|
+
* nothing, the run stays green with fewer files, and nothing else in the
|
|
2681
|
+
* toolchain would have said a word.
|
|
2568
2682
|
*
|
|
2569
2683
|
* C12's rename goes through `git mv` where the tree is a git working tree, so
|
|
2570
2684
|
* the history follows the file across a migration that touches hundreds of
|
|
@@ -2661,7 +2775,10 @@ function fixSpecSuffixes(root) {
|
|
|
2661
2775
|
for (const { from, to } of movesUnderFacet(root)) {
|
|
2662
2776
|
if (to === void 0 || existsSync(to)) continue;
|
|
2663
2777
|
renameKeepingHistory(from, to);
|
|
2664
|
-
renamed.push(
|
|
2778
|
+
renamed.push({
|
|
2779
|
+
from,
|
|
2780
|
+
to
|
|
2781
|
+
});
|
|
2665
2782
|
}
|
|
2666
2783
|
return renamed;
|
|
2667
2784
|
}
|
|
@@ -2674,53 +2791,145 @@ const CONFIG_NAMES = [
|
|
|
2674
2791
|
"vitest.config.mjs",
|
|
2675
2792
|
"vitest.config.cjs"
|
|
2676
2793
|
];
|
|
2677
|
-
/**
|
|
2678
|
-
const
|
|
2794
|
+
/** What a config COLLECTS: the two keys whose entries name test files. */
|
|
2795
|
+
const COLLECTION = /\b(?:include|exclude)\s*:\s*\[(?<entries>[^\]]*)\]/gu;
|
|
2796
|
+
/** One quoted entry of such a list. */
|
|
2797
|
+
const ENTRY = /['"`](?<entry>[^'"`\n]*)['"`]/gu;
|
|
2798
|
+
/** The quotes a config's entries are written in. */
|
|
2799
|
+
const QUOTES = /* @__PURE__ */ new Set([
|
|
2800
|
+
"'",
|
|
2801
|
+
"\"",
|
|
2802
|
+
"`"
|
|
2803
|
+
]);
|
|
2804
|
+
/** The index of the newline ending the `//` comment that starts at `index`. */
|
|
2805
|
+
function endOfLineComment(text, index) {
|
|
2806
|
+
const newline = text.indexOf("\n", index);
|
|
2807
|
+
return newline === -1 ? text.length : newline;
|
|
2808
|
+
}
|
|
2809
|
+
/** The index just past the block comment that starts at `index`. */
|
|
2810
|
+
function endOfBlockComment(text, index) {
|
|
2811
|
+
const close = text.indexOf("*/", index + 2);
|
|
2812
|
+
return close === -1 ? text.length : close + 2;
|
|
2813
|
+
}
|
|
2679
2814
|
/**
|
|
2680
|
-
* The
|
|
2815
|
+
* The config, with what is not code taken out.
|
|
2816
|
+
*
|
|
2817
|
+
* A notice that read a COMMENT as an include told signews-mobile to follow a
|
|
2818
|
+
* glob no project collected on. The scan is character by character because a
|
|
2819
|
+
* regex cannot tell the two apart: `specs/api/**` + `/*.test.ts` opens a block
|
|
2820
|
+
* comment to any pattern that does not know it is inside a string, and a `://`
|
|
2821
|
+
* opens a line comment to one that does not either.
|
|
2822
|
+
*/
|
|
2823
|
+
function withoutComments(text) {
|
|
2824
|
+
let out = "";
|
|
2825
|
+
let quote = null;
|
|
2826
|
+
let index = 0;
|
|
2827
|
+
while (index < text.length) {
|
|
2828
|
+
const char = text[index] ?? "";
|
|
2829
|
+
const pair = text.slice(index, index + 2);
|
|
2830
|
+
if (quote !== null) {
|
|
2831
|
+
const escaped = char === "\\";
|
|
2832
|
+
out += escaped ? pair : char;
|
|
2833
|
+
quote = char === quote ? null : quote;
|
|
2834
|
+
index += escaped ? 2 : 1;
|
|
2835
|
+
} else if (pair === "//") {
|
|
2836
|
+
out += "\n";
|
|
2837
|
+
index = endOfLineComment(text, index);
|
|
2838
|
+
} else if (pair === "/*") index = endOfBlockComment(text, index);
|
|
2839
|
+
else {
|
|
2840
|
+
quote = QUOTES.has(char) ? char : null;
|
|
2841
|
+
out += char;
|
|
2842
|
+
index += 1;
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
return out;
|
|
2846
|
+
}
|
|
2847
|
+
/**
|
|
2848
|
+
* Does this entry of a config sitting in `dir` reach `file`?
|
|
2849
|
+
*
|
|
2850
|
+
* A glob reaches what its literal PREFIX — everything before the first `*` —
|
|
2851
|
+
* contains; an entry with no `*` reaches the one file it names. Both are
|
|
2852
|
+
* resolved against the config's own directory, which is what vitest does.
|
|
2853
|
+
*/
|
|
2854
|
+
function reaches(dir, entry, file) {
|
|
2855
|
+
const star = entry.indexOf("*");
|
|
2856
|
+
if (star === -1) return resolve(dir, entry) === file;
|
|
2857
|
+
const prefix = resolve(dir, entry.slice(0, star));
|
|
2858
|
+
return file === prefix || file.startsWith(`${prefix}/`);
|
|
2859
|
+
}
|
|
2860
|
+
/** Every entry of every `include`/`exclude` list a config states. */
|
|
2861
|
+
function collectedEntries(text) {
|
|
2862
|
+
const entries = [];
|
|
2863
|
+
for (const list of text.matchAll(COLLECTION)) for (const match of (list.groups?.entries ?? "").matchAll(ENTRY)) entries.push(match.groups?.entry ?? "");
|
|
2864
|
+
return entries;
|
|
2865
|
+
}
|
|
2866
|
+
/** What one broken entry says, glob or literal. */
|
|
2867
|
+
function noticeFor(where, entry) {
|
|
2868
|
+
return entry.includes("*") ? `${where}: ${entry} still names \`.test.ts\` — the files it collected are \`.spec.ts\` now, and the project collects nothing until the glob follows (C12)` : `${where}: ${entry} names a file this run renamed — it is \`${entry.slice(0, -8)}.spec.ts\` now, and the entry collects nothing until it follows (C12)`;
|
|
2869
|
+
}
|
|
2870
|
+
/** The entries of ONE config the renames broke. */
|
|
2871
|
+
function staleEntriesIn(path, owner, renamed) {
|
|
2872
|
+
const directory = dirname(path);
|
|
2873
|
+
return collectedEntries(withoutComments(readFileSync(path, "utf8"))).filter((entry) => entry.endsWith(".test.ts")).filter((entry) => renamed.some(({ from }) => reaches(directory, entry, from))).map((entry) => noticeFor(relative(owner, path), entry));
|
|
2874
|
+
}
|
|
2875
|
+
/**
|
|
2876
|
+
* The entries a rename leaves behind.
|
|
2681
2877
|
*
|
|
2682
2878
|
* A consumer that names its suffix in an `include` loses the whole suite the
|
|
2683
2879
|
* moment the mover renames it: vitest collects fewer files, exits 0, and the
|
|
2684
2880
|
* only trace is a number nobody compares. The mover owns that consequence —
|
|
2685
|
-
* it cannot rewrite a config it does not parse, so it NAMES
|
|
2686
|
-
*
|
|
2687
|
-
*
|
|
2688
|
-
|
|
2689
|
-
|
|
2881
|
+
* it cannot rewrite a config it does not parse, so it NAMES what it broke, and
|
|
2882
|
+
* the author updates it in the same commit as the rename.
|
|
2883
|
+
*
|
|
2884
|
+
* What it names is exactly what it broke: an entry of an `include`/`exclude`
|
|
2885
|
+
* list, never a comment, and only where the entry reaches a file this run
|
|
2886
|
+
* actually moved. A glob over `src/` collecting module tests the mover never
|
|
2887
|
+
* touched is not a stale glob, and four repositories of the wave were told it
|
|
2888
|
+
* was — one of them would have emptied its unit project by following the
|
|
2889
|
+
* notice.
|
|
2890
|
+
*/
|
|
2891
|
+
function staleIncludes(root, renamed) {
|
|
2892
|
+
if (renamed.length === 0) return [];
|
|
2690
2893
|
const owner = packageRootOf(root) ?? dirname(root);
|
|
2691
|
-
|
|
2692
|
-
for (const directory of /* @__PURE__ */ new Set([
|
|
2894
|
+
return [.../* @__PURE__ */ new Set([
|
|
2693
2895
|
root,
|
|
2694
2896
|
owner,
|
|
2695
2897
|
dirname(root)
|
|
2696
|
-
]))
|
|
2697
|
-
const path = join(directory, name);
|
|
2698
|
-
if (!existsSync(path)) continue;
|
|
2699
|
-
const text = readFileSync(path, "utf8");
|
|
2700
|
-
for (const match of text.matchAll(TEST_GLOB)) {
|
|
2701
|
-
const glob = match.groups?.glob ?? "";
|
|
2702
|
-
if (glob !== "") found.push(`${relative(owner, path)}: ${glob}`);
|
|
2703
|
-
}
|
|
2704
|
-
}
|
|
2705
|
-
return found;
|
|
2898
|
+
])].flatMap((directory) => CONFIG_NAMES.map((name) => join(directory, name)).filter((path) => existsSync(path))).flatMap((path) => staleEntriesIn(path, owner, renamed));
|
|
2706
2899
|
}
|
|
2707
2900
|
/** Apply every rewritable pass over one tree, printing what it did. */
|
|
2708
2901
|
function applyFixes(root) {
|
|
2709
2902
|
const renamed = fixSpecSuffixes(root);
|
|
2710
|
-
for (const
|
|
2711
|
-
for (const
|
|
2903
|
+
for (const { from, to } of renamed) console.log(`conventions checker: renamed ${relative(root, from)} → ${relative(root, to)} (C12)`);
|
|
2904
|
+
for (const stale of staleIncludes(root, renamed)) console.log(`conventions checker: ${stale}`);
|
|
2712
2905
|
const written = fixSpecFiles(root);
|
|
2713
2906
|
if (written.length > 0) console.log(`conventions checker: rewrote ${written.length} spec document(s)`);
|
|
2714
2907
|
for (const move of fixPoolFixtures(root)) console.log(`conventions checker: moved ${move} (C14) — stage the rename`);
|
|
2715
2908
|
}
|
|
2716
|
-
|
|
2909
|
+
/**
|
|
2910
|
+
* Is this directory a specs tree?
|
|
2911
|
+
*
|
|
2912
|
+
* The `specs` SEGMENT is the anchor every other pass reads (`specsAnchor`), so
|
|
2913
|
+
* the CLI reads it too: `specs`, `apps/web/specs` and `specs/api` are trees —
|
|
2914
|
+
* a subtree of one is still inside it — and everything else is a project root
|
|
2915
|
+
* holding zero or more of them.
|
|
2916
|
+
*/
|
|
2917
|
+
function isSpecsTree(dir) {
|
|
2918
|
+
return dir.split(/[/\\]/u).includes("specs");
|
|
2919
|
+
}
|
|
2920
|
+
if (positional !== void 0 && isSpecsTree(requireDirectory(positional, "directory"))) {
|
|
2717
2921
|
const root = requireDirectory(positional, "directory");
|
|
2718
2922
|
if (fix) applyFixes(root);
|
|
2719
2923
|
report(runAllChecks(root), `under ${root}`, TREE_PASSES);
|
|
2720
2924
|
}
|
|
2721
|
-
const root = resolve(".");
|
|
2925
|
+
const root = positional === void 0 ? resolve(".") : requireDirectory(positional, "directory");
|
|
2926
|
+
const specsRoots = discoverSpecRoots(root);
|
|
2927
|
+
if (positional !== void 0 && specsRoots.length === 0) {
|
|
2928
|
+
console.error(`conventions checker: no \`specs/\` tree under ${root} — name a specs tree, or a project root that holds one`);
|
|
2929
|
+
process.exit(1);
|
|
2930
|
+
}
|
|
2722
2931
|
const found = [];
|
|
2723
|
-
for (const specsRoot of
|
|
2932
|
+
for (const specsRoot of specsRoots) {
|
|
2724
2933
|
const prefix = relative(root, specsRoot);
|
|
2725
2934
|
if (fix) applyFixes(specsRoot);
|
|
2726
2935
|
for (const violation of runAllChecks(specsRoot)) found.push({
|
package/dist/index.d.ts
CHANGED
|
@@ -707,8 +707,25 @@ type CallResultOptions = BaseResultOptions & {
|
|
|
707
707
|
* What `result.value` is, decided by what the call returned: a string reads as
|
|
708
708
|
* a stream, anything else as JSON. `.call<T>()` already knows T, so the test
|
|
709
709
|
* reads one field (`result.value.value.ok`) without narrowing a union first.
|
|
710
|
+
*
|
|
711
|
+
* Three answers, because a type has three ways of relating to `string`:
|
|
712
|
+
*
|
|
713
|
+
* - EXACTLY a string — a `TextAccessor`, and nothing to narrow;
|
|
714
|
+
* - it MAY be a string (`string | undefined`, `string | number`, `any`) — the
|
|
715
|
+
* UNION, which the test narrows. The type used to read the static type only
|
|
716
|
+
* while the accessor was built from the RUNTIME value, so a subject typed
|
|
717
|
+
* `string | undefined` was handed back as a `JsonAccessor`: `.value.value`
|
|
718
|
+
* compiled, the runtime had built a `TextAccessor`, and the read silently
|
|
719
|
+
* answered `undefined`;
|
|
720
|
+
* - never a string — a `JsonAccessor<Returned>`.
|
|
721
|
+
*
|
|
722
|
+
* `Returned` there is what the value CLAIMS, not what survived the crossing:
|
|
723
|
+
* the accessor is built from `JSON.stringify`, so a `Map`, a `Date` or a class
|
|
724
|
+
* instance arrives as the plain JSON it serialises to (`{}`, an ISO string, an
|
|
725
|
+
* object of its own fields). A subject whose answer is one of those is
|
|
726
|
+
* projected to plain data inside the call, where the test can see it happen.
|
|
710
727
|
*/
|
|
711
|
-
type CallValue<Returned> = Returned extends string ? TextAccessor : JsonAccessor<Returned
|
|
728
|
+
type CallValue<Returned> = [Returned] extends [string] ? TextAccessor : [Extract<Returned, string>] extends [never] ? JsonAccessor<Returned> : JsonAccessor<Exclude<Returned, string>> | TextAccessor;
|
|
712
729
|
/**
|
|
713
730
|
* The result of an in-process call — the integration facet's whole surface.
|
|
714
731
|
*
|
|
@@ -741,10 +758,14 @@ declare class CallResult<Returned = unknown> extends BaseResult {
|
|
|
741
758
|
/**
|
|
742
759
|
* What the call RETURNED. A string reads as a stream (`<name>.txt`),
|
|
743
760
|
* anything else as JSON (`<name>.json`) — the same two subjects every
|
|
744
|
-
* other facet's results are built from
|
|
761
|
+
* other facet's results are built from, chosen on the value in hand and
|
|
762
|
+
* typed by {@link CallValue}, which answers for the three ways a type can
|
|
763
|
+
* relate to `string`.
|
|
745
764
|
*
|
|
746
|
-
* A call that threw
|
|
747
|
-
*
|
|
765
|
+
* A call that threw has no value to choose an accessor on, so `value` is
|
|
766
|
+
* the JSON reading of `null` whatever the declared type promises: what a
|
|
767
|
+
* refusal produced is {@link CallResult.error}, and that is where a spec
|
|
768
|
+
* reads it.
|
|
748
769
|
*/
|
|
749
770
|
get value(): CallValue<Returned>;
|
|
750
771
|
}
|
|
@@ -2883,6 +2904,14 @@ type WebsiteSpecificationOptions<Services extends ServiceRecord = ServiceRecord>
|
|
|
2883
2904
|
* API it was started beside.
|
|
2884
2905
|
*/
|
|
2885
2906
|
services?: Services;
|
|
2907
|
+
/**
|
|
2908
|
+
* Escape hatch: normaliser applied to every compared reading of a result
|
|
2909
|
+
* — the head projection and the json-ld among them — before the
|
|
2910
|
+
* comparison (CONVENTIONS D6). Does NOT mutate the raw `.text` accessor.
|
|
2911
|
+
* Prefer `{{token}}` placeholders in fixtures; this is for what a token
|
|
2912
|
+
* cannot name, a meta a FRAMEWORK owns and the page never asked for.
|
|
2913
|
+
*/
|
|
2914
|
+
transform?: ((text: string) => string) | undefined;
|
|
2886
2915
|
} & ({
|
|
2887
2916
|
/**
|
|
2888
2917
|
* Declared stub backend: started BEFORE the server command, its
|