@jterrazz/test 12.1.0 → 13.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.
- package/README.md +16 -2
- package/dist/checker.js +587 -15
- package/dist/index.d.ts +194 -17
- package/dist/index.js +66 -1145
- package/dist/literate.js +1726 -0
- package/dist/match.js +73 -1
- package/dist/oxlint.cjs +168 -13
- package/dist/oxlint.js +168 -13
- package/dist/queue.js +3 -295
- package/dist/spec-document.js +536 -0
- package/dist/vitest.d.ts +41 -0
- package/dist/vitest.js +69 -0
- package/package.json +10 -4
- package/schema/spec.schema.json +179 -0
package/dist/match.js
CHANGED
|
@@ -1,3 +1,75 @@
|
|
|
1
|
+
import { basename, dirname, resolve } from "node:path";
|
|
2
|
+
//#region src/core/specification/shared/fixtures.ts
|
|
3
|
+
/**
|
|
4
|
+
* Fixture path resolution + copy semantics for the `cli` facet's `.fixture()`.
|
|
5
|
+
*
|
|
6
|
+
* A fixture path is one of two shapes:
|
|
7
|
+
*
|
|
8
|
+
* - `$FIXTURES/<rest>` — the shared pool at `<specs-root>/fixtures/<rest>`,
|
|
9
|
+
* where `<specs-root>` is the nearest ancestor directory named `specs`.
|
|
10
|
+
* - `<path>` (no marker) — feature-local, at `<test-dir>/fixtures/<path>`.
|
|
11
|
+
*
|
|
12
|
+
* Any other `$`-prefixed marker is a usage error (listed against the known
|
|
13
|
+
* markers). Copy semantics mirror rsync's trailing-slash rule (see
|
|
14
|
+
* {@link copyPlan}).
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Markers understood in a `.fixture()` path. Extend here + in
|
|
18
|
+
* {@link resolveFixtureSource}. Exported so the lint layer (rule
|
|
19
|
+
* `jterrazz/b2-known-fixture-marker`) validates literals against the same list.
|
|
20
|
+
*/
|
|
21
|
+
const KNOWN_FIXTURE_MARKERS = ["$FIXTURES"];
|
|
22
|
+
/**
|
|
23
|
+
* Walk up from `startDir` to the nearest ancestor directory named `specs`.
|
|
24
|
+
* Throws with guidance if none is found up to the filesystem root — the
|
|
25
|
+
* `$FIXTURES` marker is meaningless without a specs root.
|
|
26
|
+
*/
|
|
27
|
+
function discoverSpecsRoot(startDir) {
|
|
28
|
+
let dir = startDir;
|
|
29
|
+
for (;;) {
|
|
30
|
+
if (basename(dir) === "specs") return dir;
|
|
31
|
+
const parent = dirname(dir);
|
|
32
|
+
if (parent === dir) break;
|
|
33
|
+
dir = parent;
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`.fixture(): the $FIXTURES marker resolves to <specs-root>/fixtures, but no directory named "specs" was found walking up from ${startDir}. Move the specification under a specs/ directory, or use a feature-local fixtures/ path (no $FIXTURES marker).`);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the absolute source path of a fixture, applying marker resolution.
|
|
39
|
+
* The trailing slash (spread semantics) is preserved by callers via
|
|
40
|
+
* {@link copyPlan} — it is irrelevant to source resolution.
|
|
41
|
+
*/
|
|
42
|
+
function resolveFixtureSource(path, testDir) {
|
|
43
|
+
const clean = path.replace(/\/+$/, "");
|
|
44
|
+
if (clean === "$FIXTURES" || path.startsWith("$FIXTURES/")) {
|
|
45
|
+
const rest = clean.slice(9).replace(/^\/+/, "");
|
|
46
|
+
return resolve(discoverSpecsRoot(testDir), "fixtures", rest);
|
|
47
|
+
}
|
|
48
|
+
if (path.startsWith("$")) {
|
|
49
|
+
const marker = clean.split("/")[0];
|
|
50
|
+
throw new Error(`.fixture("${path}"): unknown marker "${marker}". Known markers: ${KNOWN_FIXTURE_MARKERS.join(", ")}. A path without a marker is feature-local (resolved under <test-dir>/fixtures/).`);
|
|
51
|
+
}
|
|
52
|
+
return resolve(testDir, "fixtures", clean);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Compute the source + destination for copying a fixture into the working
|
|
56
|
+
* directory. Copy semantics follow rsync's trailing-slash rule:
|
|
57
|
+
*
|
|
58
|
+
* - `dir/` (trailing slash) — the CONTENTS are spread into the cwd.
|
|
59
|
+
* - `dir` (no slash, a directory) — the directory is copied as `<cwd>/<basename>`.
|
|
60
|
+
* - `file` — copied as `<cwd>/<basename>`.
|
|
61
|
+
*
|
|
62
|
+
* Chained `.fixture()` calls layer in order: a later fixture overwrites files
|
|
63
|
+
* written by an earlier one.
|
|
64
|
+
*/
|
|
65
|
+
function copyPlan(path, testDir, workDir) {
|
|
66
|
+
const src = resolveFixtureSource(path, testDir);
|
|
67
|
+
return {
|
|
68
|
+
dest: /\/+$/.test(path) ? workDir : resolve(workDir, basename(src)),
|
|
69
|
+
src
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
1
73
|
//#region src/core/matching/match.ts
|
|
2
74
|
/** Every kind usable as a `{{token}}` in fixture files (all but ref/regex/includes). */
|
|
3
75
|
const TOKEN_KINDS = [
|
|
@@ -161,4 +233,4 @@ var CaptureScope = class {
|
|
|
161
233
|
}
|
|
162
234
|
};
|
|
163
235
|
//#endregion
|
|
164
|
-
export { match as i, Matcher as n, TOKEN_KINDS as r, CaptureScope as t };
|
|
236
|
+
export { copyPlan as a, match as i, Matcher as n, resolveFixtureSource as o, TOKEN_KINDS as r, CaptureScope as t };
|
package/dist/oxlint.cjs
CHANGED
|
@@ -4,6 +4,7 @@ Object.defineProperties(exports, {
|
|
|
4
4
|
});
|
|
5
5
|
let node_path = require("node:path");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
|
+
let node_url = require("node:url");
|
|
7
8
|
//#region src/lint/fs-cache.ts
|
|
8
9
|
/**
|
|
9
10
|
* Cached filesystem probes for the fs-anchored rules (a9w, c2, c6, c7, i2).
|
|
@@ -305,7 +306,7 @@ const RULE_DOCS = {
|
|
|
305
306
|
},
|
|
306
307
|
"b4-given-then": {
|
|
307
308
|
channel: "statique",
|
|
308
|
-
convention: "Chaque test contient `// Given -` puis `// Then -` (les deux, dans cet ordre) ; Given déclaré après Then est une erreur.",
|
|
309
|
+
convention: "Chaque test contient `// Given -` puis `// Then -` (les deux, dans cet ordre) ; Given déclaré après Then est une erreur. Un document `<cas>.spec.yaml` n’a pas de commentaires : sa narration est sa `description:`.",
|
|
309
310
|
family: "B",
|
|
310
311
|
id: "B4",
|
|
311
312
|
rationale: "La narration Given/Then rend l’intention du test lisible sans lire les assertions."
|
|
@@ -452,7 +453,7 @@ const RULE_DOCS = {
|
|
|
452
453
|
},
|
|
453
454
|
"f1-no-subpath-import": {
|
|
454
455
|
channel: "statique",
|
|
455
|
-
convention: "Tout s’importe depuis `@jterrazz/test` ; un import de `@jterrazz/test/<subpath>` est une erreur, sauf
|
|
456
|
+
convention: "Tout s’importe depuis `@jterrazz/test` ; un import de `@jterrazz/test/<subpath>` est une erreur, sauf les subpaths que la map `exports` du paquet publie — `@jterrazz/test/oxlint` (plugin de lint) et `@jterrazz/test/vitest` (surface de configuration du runner) — exemptés partout. La liste est LUE du manifeste, jamais recopiée dans la règle.",
|
|
456
457
|
family: "F",
|
|
457
458
|
id: "F1",
|
|
458
459
|
rationale: "Un point d’entrée unique garde l’API publique explicite et les subpaths internes invisibles."
|
|
@@ -566,7 +567,7 @@ const RULE_DOCS = {
|
|
|
566
567
|
const CHECKER_PASSES = [
|
|
567
568
|
{
|
|
568
569
|
channel: "checker",
|
|
569
|
-
convention: "Tout `{{token}}` dans une fixture `expected/` appartient au vocabulaire figé ; un token inconnu est une erreur.",
|
|
570
|
+
convention: "Tout `{{token}}` dans une fixture `expected/` — ou dans un flux attendu d’un document `<cas>.spec.yaml` — appartient au vocabulaire figé ; un token inconnu est une erreur.",
|
|
570
571
|
family: "D",
|
|
571
572
|
id: "D4",
|
|
572
573
|
name: "d4-unknown-token",
|
|
@@ -588,6 +589,94 @@ const CHECKER_PASSES = [
|
|
|
588
589
|
name: "d4b-http-first-line",
|
|
589
590
|
rationale: "La ligne d’ouverture distingue une requête d’une réponse — la contraindre attrape les fichiers mal placés."
|
|
590
591
|
},
|
|
592
|
+
{
|
|
593
|
+
channel: "checker",
|
|
594
|
+
convention: "Un document `<cas>.spec.yaml` respecte sa grammaire : clés fermées au niveau du document (`kind`, `description`, `fixture`, `env`, `serve`, `runs`) comme dans un run (`command`, `stdin`, `timeout`, `waitFor`, `exit`, `stdout`, `stderr`, `files`), `description:` et `runs:` obligatoires, `command:` et `exit:` obligatoires dans chaque run, `exit:` entier littéral, `waitFor:` seulement sur le dernier run et jamais avec `stdin:`, chemin `files:` relatif et sous le workdir.",
|
|
595
|
+
family: "D",
|
|
596
|
+
id: "D4b",
|
|
597
|
+
name: "d4b-spec-shape",
|
|
598
|
+
rationale: "La grammaire est lue par le parseur du runner lui-même : le document que le lint accepte est exactement celui que le runner exécute."
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
channel: "checker",
|
|
602
|
+
convention: "Les clés d’un `<cas>.spec.yaml` suivent l’ordre canonique — `kind, description, fixture, env, serve, runs` au niveau du document, `command, stdin, timeout, waitFor, exit, stdout, stderr, files` dans un run. Corrigeable (`--fix`).",
|
|
603
|
+
family: "D",
|
|
604
|
+
id: "D4b",
|
|
605
|
+
name: "d4b-spec-key-order",
|
|
606
|
+
rationale: "Le décor avant la session, l’entrée avant la sortie : un ordre unique rend les documents comparables et supprime les diffs qui ne changent rien."
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
channel: "checker",
|
|
610
|
+
convention: "`stdout`, `stderr`, `stdin` et `files.*.equals` d’un `<cas>.spec.yaml` s’écrivent en scalaire bloc (`|` garde le saut de ligne final, `|-` le supprime), jamais en chaîne entre guillemets avec des `\\n`. Corrigeable (`--fix`).",
|
|
611
|
+
family: "D",
|
|
612
|
+
id: "D4b",
|
|
613
|
+
name: "d4b-spec-block-scalar",
|
|
614
|
+
rationale: "Une sortie attendue doit ressembler à une sortie — une golden sur une ligne est illisible et aucun diff ne peut la montrer."
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
channel: "checker",
|
|
618
|
+
convention: "Un document se nomme `<cas>.spec.yaml` : `<cas>` en kebab-case, sans les mots `test`/`spec`/`cli` que le suffixe porte déjà, et jamais le nom nu de son dossier.",
|
|
619
|
+
family: "C",
|
|
620
|
+
id: "C12",
|
|
621
|
+
name: "c12-spec-file-name",
|
|
622
|
+
rationale: "Le nom du fichier est la première phrase que le lecteur lit ; `rm/rm.spec.yaml` ne dit rien deux fois."
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
channel: "checker",
|
|
626
|
+
convention: "La `description:` d’un document est un titre : une seule ligne, initiale minuscule (identifiant tout en majuscules exempté), sans point final, moins de 100 caractères.",
|
|
627
|
+
family: "J",
|
|
628
|
+
id: "J5",
|
|
629
|
+
name: "j5-spec-description",
|
|
630
|
+
rationale: "La description EST le titre vitest — les règles du titre s’y appliquent, pas celles d’un paragraphe."
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
channel: "checker",
|
|
634
|
+
convention: "Deux `<cas>.spec.yaml` d’un même dossier ne partagent pas leur `description:`.",
|
|
635
|
+
family: "J",
|
|
636
|
+
id: "J4",
|
|
637
|
+
name: "j4-spec-description-unique",
|
|
638
|
+
rationale: "Le rapporteur nomme le titre : deux titres identiques laissent deux fichiers à ouvrir."
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
channel: "checker",
|
|
642
|
+
convention: "Un flux attendu ne contient pas de valeur volatile littérale : origine loopback avec port (`127.0.0.1:8080`, `localhost:3000`), chemin temporaire absolu (`/tmp/`, `/private/tmp/`, `/var/folders/`) ou chemin home (`/Users/…`, `/home/…`). Le message nomme le token à écrire.",
|
|
643
|
+
family: "D",
|
|
644
|
+
id: "D5",
|
|
645
|
+
name: "d5-spec-volatile-literal",
|
|
646
|
+
rationale: "C’est exactement la forme qu’un `TEST_UPDATE` aurait tokenisée : en trouver une signifie qu’un token a été écrasé à la main."
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
channel: "checker",
|
|
650
|
+
convention: "Un horodatage ISO-8601 ou un uuid littéral dans un flux attendu → warning, sauf si la même valeur apparaît dans un `fixture:` ou un `stdin:` du même document (elle est alors fixée, pas volatile).",
|
|
651
|
+
family: "D",
|
|
652
|
+
id: "D5",
|
|
653
|
+
name: "d5w-spec-pinned-value",
|
|
654
|
+
rationale: "Le défaut n’est pas le littéral : c’est le littéral que rien n’a fixé."
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
channel: "checker",
|
|
658
|
+
convention: "Un flux attendu dont tout le contenu est `{{any}}` → warning : le run s’exécute et ne prouve rien.",
|
|
659
|
+
family: "J",
|
|
660
|
+
id: "J3",
|
|
661
|
+
name: "j3w-spec-empty-assertion",
|
|
662
|
+
rationale: "Le miroir de J3 pour les documents — une assertion qui accepte tout est une assertion absente."
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
channel: "checker",
|
|
666
|
+
convention: "Un run dont `exit:` est non nul sans `stdout:` ni `stderr:` → warning : le document ne dit pas pourquoi la commande a refusé.",
|
|
667
|
+
family: "D",
|
|
668
|
+
id: "D11",
|
|
669
|
+
name: "d11w-spec-silent-refusal",
|
|
670
|
+
rationale: "Un refus muet est soit un défaut du produit, soit une golden qui a oublié que les mots partaient sur stderr."
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
channel: "checker",
|
|
674
|
+
convention: "Les mots nus d’`env:` et les noms de `serve:` d’un document sont des clés des objets `env`/`serve` d’un `specification.cli(…)` du plus proche dossier portant des `*.specification.ts` (lecture statique des littéraux ; ignoré sinon).",
|
|
675
|
+
family: "C",
|
|
676
|
+
id: "C8",
|
|
677
|
+
name: "c8-spec-registered-name",
|
|
678
|
+
rationale: "Une faute de frappe (`serve: dashbord`) échoue aujourd’hui à l’exécution, un fichier de test plus tard ; ici c’est une ligne de lint."
|
|
679
|
+
},
|
|
591
680
|
{
|
|
592
681
|
channel: "checker",
|
|
593
682
|
convention: "Un token connu dans un fichier sous `requests/` → warning : les requêtes sont des entrées, jamais matchées.",
|
|
@@ -598,7 +687,7 @@ const CHECKER_PASSES = [
|
|
|
598
687
|
},
|
|
599
688
|
{
|
|
600
689
|
channel: "checker",
|
|
601
|
-
convention: "Aucune fixture morte : tout fichier sous `seeds/`/`requests/`/`fixtures/` et toute entrée de premier niveau de `expected/` doit être référencée ; un dossier de feature sans `*.test.ts` est orphelin (warning si argument non littéral).",
|
|
690
|
+
convention: "Aucune fixture morte : tout fichier sous `seeds/`/`requests/`/`fixtures/` et toute entrée de premier niveau de `expected/` doit être référencée ; un dossier de feature sans `*.test.ts` ni `*.spec.yaml` est orphelin (warning si argument non littéral). Un `<cas>.spec.yaml` référence les fixtures nommées par ses entrées `fixture:`.",
|
|
602
691
|
family: "C",
|
|
603
692
|
id: "C9",
|
|
604
693
|
name: "c9-dead-fixtures",
|
|
@@ -2625,22 +2714,88 @@ const d9wSingleUseRef = {
|
|
|
2625
2714
|
}
|
|
2626
2715
|
};
|
|
2627
2716
|
//#endregion
|
|
2717
|
+
//#region src/lint/package-exports.ts
|
|
2718
|
+
/**
|
|
2719
|
+
* The subpaths this package actually publishes, read from its OWN
|
|
2720
|
+
* `package.json` `exports` map.
|
|
2721
|
+
*
|
|
2722
|
+
* Rule F1 forbids importing `@jterrazz/test/<subpath>` — but a subpath the
|
|
2723
|
+
* package DECLARES is, by definition, part of its public contract
|
|
2724
|
+
* (`/oxlint` for the lint plugin, `/vitest` for the runner-config surface).
|
|
2725
|
+
* Deriving the exemption from the manifest rather than listing it in the rule
|
|
2726
|
+
* is what keeps the two from drifting: a subpath is exempt the moment it is
|
|
2727
|
+
* published, and stops being exempt the moment it is withdrawn — there is
|
|
2728
|
+
* nothing to remember to update.
|
|
2729
|
+
*
|
|
2730
|
+
* The manifest is found by walking up from THIS module, so the answer does not
|
|
2731
|
+
* depend on where the linted file sits: `<pkg>/dist/oxlint.js` in an install,
|
|
2732
|
+
* `<repo>/src/lint/` when the framework lints itself. `import.meta.url` is
|
|
2733
|
+
* safe in both bundle formats — the CJS build shims it to `__filename`.
|
|
2734
|
+
*/
|
|
2735
|
+
const PACKAGE$2 = "@jterrazz/test";
|
|
2736
|
+
/** Manifests are read once per lint process. */
|
|
2737
|
+
let cached = null;
|
|
2738
|
+
/**
|
|
2739
|
+
* Walk up from `startDir` for the `package.json` that declares {@link PACKAGE}.
|
|
2740
|
+
* The FIRST manifest found decides: if it names another package, the walk has
|
|
2741
|
+
* already left this package's tree and there is nothing to find above it.
|
|
2742
|
+
*/
|
|
2743
|
+
function findOwnManifest(startDir) {
|
|
2744
|
+
let dir = startDir;
|
|
2745
|
+
for (;;) {
|
|
2746
|
+
const text = readFileCached((0, node_path.join)(dir, "package.json"));
|
|
2747
|
+
if (text !== null) try {
|
|
2748
|
+
const manifest = JSON.parse(text);
|
|
2749
|
+
return manifest.name === PACKAGE$2 ? manifest : null;
|
|
2750
|
+
} catch {
|
|
2751
|
+
return null;
|
|
2752
|
+
}
|
|
2753
|
+
const parent = (0, node_path.dirname)(dir);
|
|
2754
|
+
if (parent === dir) return null;
|
|
2755
|
+
dir = parent;
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Every subpath specifier the package publishes — `@jterrazz/test/oxlint`,
|
|
2760
|
+
* `@jterrazz/test/vitest`, … The root export (`.`) is not a subpath and is
|
|
2761
|
+
* absent from the list.
|
|
2762
|
+
*
|
|
2763
|
+
* An unreadable manifest yields an EMPTY list: with no contract to read, no
|
|
2764
|
+
* subpath can be vouched for, and F1's plain rule applies.
|
|
2765
|
+
*
|
|
2766
|
+
* @internal Exported for unit tests.
|
|
2767
|
+
*/
|
|
2768
|
+
function declaredSubpaths(startDir) {
|
|
2769
|
+
if (startDir === void 0 && cached !== null) return cached;
|
|
2770
|
+
const exports = findOwnManifest(startDir ?? (0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href)))?.exports;
|
|
2771
|
+
const subpaths = exports !== null && typeof exports === "object" ? Object.keys(exports).filter((key) => key.startsWith("./") && key !== ".").map((key) => `${PACKAGE$2}/${key.slice(2)}`).sort() : [];
|
|
2772
|
+
if (startDir === void 0) cached = subpaths;
|
|
2773
|
+
return subpaths;
|
|
2774
|
+
}
|
|
2775
|
+
//#endregion
|
|
2628
2776
|
//#region src/lint/rules/f1-no-subpath-import.ts
|
|
2629
2777
|
const PACKAGE$1 = "@jterrazz/test";
|
|
2630
|
-
const TOOL_SUBPATH$2 = "@jterrazz/test/oxlint";
|
|
2631
2778
|
/**
|
|
2632
|
-
* CONVENTIONS F1 — everything is imported from `@jterrazz/test`;
|
|
2633
|
-
* not exist.
|
|
2634
|
-
*
|
|
2635
|
-
*
|
|
2779
|
+
* CONVENTIONS F1 — everything a SPEC needs is imported from `@jterrazz/test`;
|
|
2780
|
+
* internal subpaths do not exist.
|
|
2781
|
+
*
|
|
2782
|
+
* The exception is not a list kept here: it is the package's own `exports` map
|
|
2783
|
+
* (`/oxlint`, the tool-facing plugin entry; `/vitest`, the runner-config
|
|
2784
|
+
* surface `vitest.config.ts` imports). A published subpath IS the public
|
|
2785
|
+
* contract, so reading the manifest is what keeps the rule from outlawing an
|
|
2786
|
+
* import the package itself prescribes. Each is exempt from any file — an
|
|
2787
|
+
* oxlint config, a shared preset, a vitest config.
|
|
2636
2788
|
*/
|
|
2637
2789
|
const f1NoSubpathImport = {
|
|
2638
2790
|
create(context) {
|
|
2791
|
+
const allowed = new Set(declaredSubpaths());
|
|
2639
2792
|
return { ...importSourceVisitor(({ node, source }) => {
|
|
2640
|
-
if (!source.startsWith(`${PACKAGE$1}/`)) return;
|
|
2641
|
-
if (source === TOOL_SUBPATH$2) return;
|
|
2793
|
+
if (!source.startsWith(`${PACKAGE$1}/`) || allowed.has(source)) return;
|
|
2642
2794
|
context.report({
|
|
2643
|
-
data: {
|
|
2795
|
+
data: {
|
|
2796
|
+
published: allowed.size === 0 ? "none" : [...allowed].sort().join(", "),
|
|
2797
|
+
source
|
|
2798
|
+
},
|
|
2644
2799
|
messageId: "subpath",
|
|
2645
2800
|
node
|
|
2646
2801
|
});
|
|
@@ -2648,7 +2803,7 @@ const f1NoSubpathImport = {
|
|
|
2648
2803
|
},
|
|
2649
2804
|
meta: {
|
|
2650
2805
|
docs: RULE_DOCS["f1-no-subpath-import"],
|
|
2651
|
-
messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — subpaths do not exist (F1 — see docs/10-linting.md
|
|
2806
|
+
messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — internal subpaths do not exist (F1 — see docs/10-linting.md). Published subpaths, exempt everywhere: {{published}}." },
|
|
2652
2807
|
type: "problem"
|
|
2653
2808
|
}
|
|
2654
2809
|
};
|
package/dist/oxlint.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { basename, dirname, join, resolve } from "node:path";
|
|
2
2
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
//#region src/lint/fs-cache.ts
|
|
4
5
|
/**
|
|
5
6
|
* Cached filesystem probes for the fs-anchored rules (a9w, c2, c6, c7, i2).
|
|
@@ -301,7 +302,7 @@ const RULE_DOCS = {
|
|
|
301
302
|
},
|
|
302
303
|
"b4-given-then": {
|
|
303
304
|
channel: "statique",
|
|
304
|
-
convention: "Chaque test contient `// Given -` puis `// Then -` (les deux, dans cet ordre) ; Given déclaré après Then est une erreur.",
|
|
305
|
+
convention: "Chaque test contient `// Given -` puis `// Then -` (les deux, dans cet ordre) ; Given déclaré après Then est une erreur. Un document `<cas>.spec.yaml` n’a pas de commentaires : sa narration est sa `description:`.",
|
|
305
306
|
family: "B",
|
|
306
307
|
id: "B4",
|
|
307
308
|
rationale: "La narration Given/Then rend l’intention du test lisible sans lire les assertions."
|
|
@@ -448,7 +449,7 @@ const RULE_DOCS = {
|
|
|
448
449
|
},
|
|
449
450
|
"f1-no-subpath-import": {
|
|
450
451
|
channel: "statique",
|
|
451
|
-
convention: "Tout s’importe depuis `@jterrazz/test` ; un import de `@jterrazz/test/<subpath>` est une erreur, sauf
|
|
452
|
+
convention: "Tout s’importe depuis `@jterrazz/test` ; un import de `@jterrazz/test/<subpath>` est une erreur, sauf les subpaths que la map `exports` du paquet publie — `@jterrazz/test/oxlint` (plugin de lint) et `@jterrazz/test/vitest` (surface de configuration du runner) — exemptés partout. La liste est LUE du manifeste, jamais recopiée dans la règle.",
|
|
452
453
|
family: "F",
|
|
453
454
|
id: "F1",
|
|
454
455
|
rationale: "Un point d’entrée unique garde l’API publique explicite et les subpaths internes invisibles."
|
|
@@ -562,7 +563,7 @@ const RULE_DOCS = {
|
|
|
562
563
|
const CHECKER_PASSES = [
|
|
563
564
|
{
|
|
564
565
|
channel: "checker",
|
|
565
|
-
convention: "Tout `{{token}}` dans une fixture `expected/` appartient au vocabulaire figé ; un token inconnu est une erreur.",
|
|
566
|
+
convention: "Tout `{{token}}` dans une fixture `expected/` — ou dans un flux attendu d’un document `<cas>.spec.yaml` — appartient au vocabulaire figé ; un token inconnu est une erreur.",
|
|
566
567
|
family: "D",
|
|
567
568
|
id: "D4",
|
|
568
569
|
name: "d4-unknown-token",
|
|
@@ -584,6 +585,94 @@ const CHECKER_PASSES = [
|
|
|
584
585
|
name: "d4b-http-first-line",
|
|
585
586
|
rationale: "La ligne d’ouverture distingue une requête d’une réponse — la contraindre attrape les fichiers mal placés."
|
|
586
587
|
},
|
|
588
|
+
{
|
|
589
|
+
channel: "checker",
|
|
590
|
+
convention: "Un document `<cas>.spec.yaml` respecte sa grammaire : clés fermées au niveau du document (`kind`, `description`, `fixture`, `env`, `serve`, `runs`) comme dans un run (`command`, `stdin`, `timeout`, `waitFor`, `exit`, `stdout`, `stderr`, `files`), `description:` et `runs:` obligatoires, `command:` et `exit:` obligatoires dans chaque run, `exit:` entier littéral, `waitFor:` seulement sur le dernier run et jamais avec `stdin:`, chemin `files:` relatif et sous le workdir.",
|
|
591
|
+
family: "D",
|
|
592
|
+
id: "D4b",
|
|
593
|
+
name: "d4b-spec-shape",
|
|
594
|
+
rationale: "La grammaire est lue par le parseur du runner lui-même : le document que le lint accepte est exactement celui que le runner exécute."
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
channel: "checker",
|
|
598
|
+
convention: "Les clés d’un `<cas>.spec.yaml` suivent l’ordre canonique — `kind, description, fixture, env, serve, runs` au niveau du document, `command, stdin, timeout, waitFor, exit, stdout, stderr, files` dans un run. Corrigeable (`--fix`).",
|
|
599
|
+
family: "D",
|
|
600
|
+
id: "D4b",
|
|
601
|
+
name: "d4b-spec-key-order",
|
|
602
|
+
rationale: "Le décor avant la session, l’entrée avant la sortie : un ordre unique rend les documents comparables et supprime les diffs qui ne changent rien."
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
channel: "checker",
|
|
606
|
+
convention: "`stdout`, `stderr`, `stdin` et `files.*.equals` d’un `<cas>.spec.yaml` s’écrivent en scalaire bloc (`|` garde le saut de ligne final, `|-` le supprime), jamais en chaîne entre guillemets avec des `\\n`. Corrigeable (`--fix`).",
|
|
607
|
+
family: "D",
|
|
608
|
+
id: "D4b",
|
|
609
|
+
name: "d4b-spec-block-scalar",
|
|
610
|
+
rationale: "Une sortie attendue doit ressembler à une sortie — une golden sur une ligne est illisible et aucun diff ne peut la montrer."
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
channel: "checker",
|
|
614
|
+
convention: "Un document se nomme `<cas>.spec.yaml` : `<cas>` en kebab-case, sans les mots `test`/`spec`/`cli` que le suffixe porte déjà, et jamais le nom nu de son dossier.",
|
|
615
|
+
family: "C",
|
|
616
|
+
id: "C12",
|
|
617
|
+
name: "c12-spec-file-name",
|
|
618
|
+
rationale: "Le nom du fichier est la première phrase que le lecteur lit ; `rm/rm.spec.yaml` ne dit rien deux fois."
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
channel: "checker",
|
|
622
|
+
convention: "La `description:` d’un document est un titre : une seule ligne, initiale minuscule (identifiant tout en majuscules exempté), sans point final, moins de 100 caractères.",
|
|
623
|
+
family: "J",
|
|
624
|
+
id: "J5",
|
|
625
|
+
name: "j5-spec-description",
|
|
626
|
+
rationale: "La description EST le titre vitest — les règles du titre s’y appliquent, pas celles d’un paragraphe."
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
channel: "checker",
|
|
630
|
+
convention: "Deux `<cas>.spec.yaml` d’un même dossier ne partagent pas leur `description:`.",
|
|
631
|
+
family: "J",
|
|
632
|
+
id: "J4",
|
|
633
|
+
name: "j4-spec-description-unique",
|
|
634
|
+
rationale: "Le rapporteur nomme le titre : deux titres identiques laissent deux fichiers à ouvrir."
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
channel: "checker",
|
|
638
|
+
convention: "Un flux attendu ne contient pas de valeur volatile littérale : origine loopback avec port (`127.0.0.1:8080`, `localhost:3000`), chemin temporaire absolu (`/tmp/`, `/private/tmp/`, `/var/folders/`) ou chemin home (`/Users/…`, `/home/…`). Le message nomme le token à écrire.",
|
|
639
|
+
family: "D",
|
|
640
|
+
id: "D5",
|
|
641
|
+
name: "d5-spec-volatile-literal",
|
|
642
|
+
rationale: "C’est exactement la forme qu’un `TEST_UPDATE` aurait tokenisée : en trouver une signifie qu’un token a été écrasé à la main."
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
channel: "checker",
|
|
646
|
+
convention: "Un horodatage ISO-8601 ou un uuid littéral dans un flux attendu → warning, sauf si la même valeur apparaît dans un `fixture:` ou un `stdin:` du même document (elle est alors fixée, pas volatile).",
|
|
647
|
+
family: "D",
|
|
648
|
+
id: "D5",
|
|
649
|
+
name: "d5w-spec-pinned-value",
|
|
650
|
+
rationale: "Le défaut n’est pas le littéral : c’est le littéral que rien n’a fixé."
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
channel: "checker",
|
|
654
|
+
convention: "Un flux attendu dont tout le contenu est `{{any}}` → warning : le run s’exécute et ne prouve rien.",
|
|
655
|
+
family: "J",
|
|
656
|
+
id: "J3",
|
|
657
|
+
name: "j3w-spec-empty-assertion",
|
|
658
|
+
rationale: "Le miroir de J3 pour les documents — une assertion qui accepte tout est une assertion absente."
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
channel: "checker",
|
|
662
|
+
convention: "Un run dont `exit:` est non nul sans `stdout:` ni `stderr:` → warning : le document ne dit pas pourquoi la commande a refusé.",
|
|
663
|
+
family: "D",
|
|
664
|
+
id: "D11",
|
|
665
|
+
name: "d11w-spec-silent-refusal",
|
|
666
|
+
rationale: "Un refus muet est soit un défaut du produit, soit une golden qui a oublié que les mots partaient sur stderr."
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
channel: "checker",
|
|
670
|
+
convention: "Les mots nus d’`env:` et les noms de `serve:` d’un document sont des clés des objets `env`/`serve` d’un `specification.cli(…)` du plus proche dossier portant des `*.specification.ts` (lecture statique des littéraux ; ignoré sinon).",
|
|
671
|
+
family: "C",
|
|
672
|
+
id: "C8",
|
|
673
|
+
name: "c8-spec-registered-name",
|
|
674
|
+
rationale: "Une faute de frappe (`serve: dashbord`) échoue aujourd’hui à l’exécution, un fichier de test plus tard ; ici c’est une ligne de lint."
|
|
675
|
+
},
|
|
587
676
|
{
|
|
588
677
|
channel: "checker",
|
|
589
678
|
convention: "Un token connu dans un fichier sous `requests/` → warning : les requêtes sont des entrées, jamais matchées.",
|
|
@@ -594,7 +683,7 @@ const CHECKER_PASSES = [
|
|
|
594
683
|
},
|
|
595
684
|
{
|
|
596
685
|
channel: "checker",
|
|
597
|
-
convention: "Aucune fixture morte : tout fichier sous `seeds/`/`requests/`/`fixtures/` et toute entrée de premier niveau de `expected/` doit être référencée ; un dossier de feature sans `*.test.ts` est orphelin (warning si argument non littéral).",
|
|
686
|
+
convention: "Aucune fixture morte : tout fichier sous `seeds/`/`requests/`/`fixtures/` et toute entrée de premier niveau de `expected/` doit être référencée ; un dossier de feature sans `*.test.ts` ni `*.spec.yaml` est orphelin (warning si argument non littéral). Un `<cas>.spec.yaml` référence les fixtures nommées par ses entrées `fixture:`.",
|
|
598
687
|
family: "C",
|
|
599
688
|
id: "C9",
|
|
600
689
|
name: "c9-dead-fixtures",
|
|
@@ -2621,22 +2710,88 @@ const d9wSingleUseRef = {
|
|
|
2621
2710
|
}
|
|
2622
2711
|
};
|
|
2623
2712
|
//#endregion
|
|
2713
|
+
//#region src/lint/package-exports.ts
|
|
2714
|
+
/**
|
|
2715
|
+
* The subpaths this package actually publishes, read from its OWN
|
|
2716
|
+
* `package.json` `exports` map.
|
|
2717
|
+
*
|
|
2718
|
+
* Rule F1 forbids importing `@jterrazz/test/<subpath>` — but a subpath the
|
|
2719
|
+
* package DECLARES is, by definition, part of its public contract
|
|
2720
|
+
* (`/oxlint` for the lint plugin, `/vitest` for the runner-config surface).
|
|
2721
|
+
* Deriving the exemption from the manifest rather than listing it in the rule
|
|
2722
|
+
* is what keeps the two from drifting: a subpath is exempt the moment it is
|
|
2723
|
+
* published, and stops being exempt the moment it is withdrawn — there is
|
|
2724
|
+
* nothing to remember to update.
|
|
2725
|
+
*
|
|
2726
|
+
* The manifest is found by walking up from THIS module, so the answer does not
|
|
2727
|
+
* depend on where the linted file sits: `<pkg>/dist/oxlint.js` in an install,
|
|
2728
|
+
* `<repo>/src/lint/` when the framework lints itself. `import.meta.url` is
|
|
2729
|
+
* safe in both bundle formats — the CJS build shims it to `__filename`.
|
|
2730
|
+
*/
|
|
2731
|
+
const PACKAGE$2 = "@jterrazz/test";
|
|
2732
|
+
/** Manifests are read once per lint process. */
|
|
2733
|
+
let cached = null;
|
|
2734
|
+
/**
|
|
2735
|
+
* Walk up from `startDir` for the `package.json` that declares {@link PACKAGE}.
|
|
2736
|
+
* The FIRST manifest found decides: if it names another package, the walk has
|
|
2737
|
+
* already left this package's tree and there is nothing to find above it.
|
|
2738
|
+
*/
|
|
2739
|
+
function findOwnManifest(startDir) {
|
|
2740
|
+
let dir = startDir;
|
|
2741
|
+
for (;;) {
|
|
2742
|
+
const text = readFileCached(join(dir, "package.json"));
|
|
2743
|
+
if (text !== null) try {
|
|
2744
|
+
const manifest = JSON.parse(text);
|
|
2745
|
+
return manifest.name === PACKAGE$2 ? manifest : null;
|
|
2746
|
+
} catch {
|
|
2747
|
+
return null;
|
|
2748
|
+
}
|
|
2749
|
+
const parent = dirname(dir);
|
|
2750
|
+
if (parent === dir) return null;
|
|
2751
|
+
dir = parent;
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
/**
|
|
2755
|
+
* Every subpath specifier the package publishes — `@jterrazz/test/oxlint`,
|
|
2756
|
+
* `@jterrazz/test/vitest`, … The root export (`.`) is not a subpath and is
|
|
2757
|
+
* absent from the list.
|
|
2758
|
+
*
|
|
2759
|
+
* An unreadable manifest yields an EMPTY list: with no contract to read, no
|
|
2760
|
+
* subpath can be vouched for, and F1's plain rule applies.
|
|
2761
|
+
*
|
|
2762
|
+
* @internal Exported for unit tests.
|
|
2763
|
+
*/
|
|
2764
|
+
function declaredSubpaths(startDir) {
|
|
2765
|
+
if (startDir === void 0 && cached !== null) return cached;
|
|
2766
|
+
const exports = findOwnManifest(startDir ?? dirname(fileURLToPath(import.meta.url)))?.exports;
|
|
2767
|
+
const subpaths = exports !== null && typeof exports === "object" ? Object.keys(exports).filter((key) => key.startsWith("./") && key !== ".").map((key) => `${PACKAGE$2}/${key.slice(2)}`).sort() : [];
|
|
2768
|
+
if (startDir === void 0) cached = subpaths;
|
|
2769
|
+
return subpaths;
|
|
2770
|
+
}
|
|
2771
|
+
//#endregion
|
|
2624
2772
|
//#region src/lint/rules/f1-no-subpath-import.ts
|
|
2625
2773
|
const PACKAGE$1 = "@jterrazz/test";
|
|
2626
|
-
const TOOL_SUBPATH$2 = "@jterrazz/test/oxlint";
|
|
2627
2774
|
/**
|
|
2628
|
-
* CONVENTIONS F1 — everything is imported from `@jterrazz/test`;
|
|
2629
|
-
* not exist.
|
|
2630
|
-
*
|
|
2631
|
-
*
|
|
2775
|
+
* CONVENTIONS F1 — everything a SPEC needs is imported from `@jterrazz/test`;
|
|
2776
|
+
* internal subpaths do not exist.
|
|
2777
|
+
*
|
|
2778
|
+
* The exception is not a list kept here: it is the package's own `exports` map
|
|
2779
|
+
* (`/oxlint`, the tool-facing plugin entry; `/vitest`, the runner-config
|
|
2780
|
+
* surface `vitest.config.ts` imports). A published subpath IS the public
|
|
2781
|
+
* contract, so reading the manifest is what keeps the rule from outlawing an
|
|
2782
|
+
* import the package itself prescribes. Each is exempt from any file — an
|
|
2783
|
+
* oxlint config, a shared preset, a vitest config.
|
|
2632
2784
|
*/
|
|
2633
2785
|
const f1NoSubpathImport = {
|
|
2634
2786
|
create(context) {
|
|
2787
|
+
const allowed = new Set(declaredSubpaths());
|
|
2635
2788
|
return { ...importSourceVisitor(({ node, source }) => {
|
|
2636
|
-
if (!source.startsWith(`${PACKAGE$1}/`)) return;
|
|
2637
|
-
if (source === TOOL_SUBPATH$2) return;
|
|
2789
|
+
if (!source.startsWith(`${PACKAGE$1}/`) || allowed.has(source)) return;
|
|
2638
2790
|
context.report({
|
|
2639
|
-
data: {
|
|
2791
|
+
data: {
|
|
2792
|
+
published: allowed.size === 0 ? "none" : [...allowed].sort().join(", "),
|
|
2793
|
+
source
|
|
2794
|
+
},
|
|
2640
2795
|
messageId: "subpath",
|
|
2641
2796
|
node
|
|
2642
2797
|
});
|
|
@@ -2644,7 +2799,7 @@ const f1NoSubpathImport = {
|
|
|
2644
2799
|
},
|
|
2645
2800
|
meta: {
|
|
2646
2801
|
docs: RULE_DOCS["f1-no-subpath-import"],
|
|
2647
|
-
messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — subpaths do not exist (F1 — see docs/10-linting.md
|
|
2802
|
+
messages: { subpath: "Import from \"@jterrazz/test\", not \"{{source}}\" — internal subpaths do not exist (F1 — see docs/10-linting.md). Published subpaths, exempt everywhere: {{published}}." },
|
|
2648
2803
|
type: "problem"
|
|
2649
2804
|
}
|
|
2650
2805
|
};
|