@jterrazz/test 15.3.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/README.md +120 -427
- package/dist/appium.adapter.js +71 -15
- package/dist/artifacts.js +25 -1735
- package/dist/browser/index.js +304 -233
- package/dist/checker.d.ts +1 -1
- package/dist/checker.js +1103 -97
- package/dist/fixtures.js +110 -0
- package/dist/index.d.ts +956 -971
- package/dist/index.js +2159 -2281
- package/dist/literate.js +1747 -0
- package/dist/match.js +625 -84
- package/dist/oxlint.cjs +3585 -1480
- package/dist/oxlint.d.cts +60 -48
- package/dist/oxlint.d.ts +60 -48
- package/dist/oxlint.js +3585 -1480
- package/dist/playwright.adapter.js +51 -8
- package/dist/{intercept.js → server.js} +2 -2
- package/dist/stream.js +6 -6
- package/dist/{ambiguity.js → substring-warning.js} +106 -7
- package/dist/vitest.d.ts +67 -48
- package/dist/vitest.js +229 -93
- 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 +32 -11
- package/dist/spec-document.js +0 -651
package/dist/vitest.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { i as GROUND_EXPECTED } from "./fixtures.js";
|
|
2
|
+
import { i as SCREENSHOTS_DIR, n as COVERAGE_DIR, o as VITEST_ARTIFACTS_DIR, r as REPORTER_OUTPUT_FILES, t as ATTACHMENTS_DIR } from "./artifacts.js";
|
|
3
|
+
import { n as specDescription } from "./literate.js";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { configDefaults, mergeConfig } from "vitest/config";
|
|
8
|
-
//#region src/
|
|
9
|
+
//#region src/runner/literate-plugin.ts
|
|
9
10
|
/**
|
|
10
11
|
* The `literate()` vite plugin — door one of the spec-document format.
|
|
11
12
|
*
|
|
@@ -70,7 +71,7 @@ function literate(options) {
|
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
73
|
//#endregion
|
|
73
|
-
//#region src/
|
|
74
|
+
//#region src/runner/preset.ts
|
|
74
75
|
/**
|
|
75
76
|
* `defineSpecConfig()` — the shared vitest config every repository of the
|
|
76
77
|
* ecosystem starts from.
|
|
@@ -116,6 +117,27 @@ const HOOK_TIMEOUT_MS = 3e4;
|
|
|
116
117
|
*/
|
|
117
118
|
const EXCLUDE = [...configDefaults.exclude, "**/_fixtures/**"];
|
|
118
119
|
/**
|
|
120
|
+
* What a test may never carry into the next one, and the one policy the
|
|
121
|
+
* framework states rather than lints.
|
|
122
|
+
*
|
|
123
|
+
* `retry: 0` is the policy: a flaky test is fixed or deleted, never re-rolled
|
|
124
|
+
* until it passes. A retry turns a real defect into a slow one and hides the
|
|
125
|
+
* day it becomes permanent.
|
|
126
|
+
*
|
|
127
|
+
* The three restores are the other half of the same idea. `vi.spyOn`,
|
|
128
|
+
* `vi.stubGlobal` and `vi.stubEnv` each own a teardown, and a test that forgets
|
|
129
|
+
* one leaks into whatever runs next — a failure that appears in a FILE that did
|
|
130
|
+
* nothing wrong and disappears when that file is run alone. Turning them on
|
|
131
|
+
* here is what lets `clock.at()` and `vi.stubEnv()` be written without an
|
|
132
|
+
* `afterEach`, which is the shape rule J6w asks for.
|
|
133
|
+
*/
|
|
134
|
+
const HYGIENE = {
|
|
135
|
+
restoreMocks: true,
|
|
136
|
+
retry: 0,
|
|
137
|
+
unstubEnvs: true,
|
|
138
|
+
unstubGlobals: true
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
119
141
|
* Defaults a vitest PROJECT accepts. `coverage` and `reporters` are root-only
|
|
120
142
|
* options (vitest omits them from a project's config), so they are absent here
|
|
121
143
|
* on purpose — setting them would be silently dropped.
|
|
@@ -130,24 +152,122 @@ function projectDefaults() {
|
|
|
130
152
|
return {
|
|
131
153
|
cacheDir: VITEST_ARTIFACTS_DIR,
|
|
132
154
|
test: {
|
|
155
|
+
attachmentsDir: ATTACHMENTS_DIR,
|
|
133
156
|
exclude: EXCLUDE,
|
|
134
157
|
hookTimeout: HOOK_TIMEOUT_MS,
|
|
158
|
+
...HYGIENE,
|
|
135
159
|
testTimeout: TEST_TIMEOUT_MS
|
|
136
160
|
}
|
|
137
161
|
};
|
|
138
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Coverage, when `--coverage` asks for it.
|
|
165
|
+
*
|
|
166
|
+
* `v8` because it is the runtime's own counter: no instrumentation pass, no
|
|
167
|
+
* second transform of every file, and the numbers do not move when the bundler
|
|
168
|
+
* does. The provider is an OPTIONAL peer (`@vitest/coverage-v8`), so a project
|
|
169
|
+
* that never asks for coverage never installs it.
|
|
170
|
+
*
|
|
171
|
+
* `json-summary` is not a taste: it is the machine-readable report the ratchet
|
|
172
|
+
* reads. `text` is for the human running the command, and `html` for the one
|
|
173
|
+
* chasing a line. All three land under `.artifacts/vitest/coverage/`, the one
|
|
174
|
+
* directory the toolchain's page owns.
|
|
175
|
+
*
|
|
176
|
+
* What is EXCLUDED is the shape of the answer, not a way to raise the number:
|
|
177
|
+
* a type-only module has nothing to execute (v8 reports it as 0/0 or, worse,
|
|
178
|
+
* as uncovered lines that cannot be covered), and the CLI entries are proven
|
|
179
|
+
* by running the binary, not by importing it.
|
|
180
|
+
*/
|
|
181
|
+
const COVERAGE = {
|
|
182
|
+
exclude: [
|
|
183
|
+
"**/node_modules/**",
|
|
184
|
+
"specs/**",
|
|
185
|
+
"**/_fixtures/**",
|
|
186
|
+
"**/*.d.ts",
|
|
187
|
+
"**/*.test-d.ts",
|
|
188
|
+
"**/*.fixtures.ts",
|
|
189
|
+
"dist/**",
|
|
190
|
+
".artifacts/**",
|
|
191
|
+
"**/*.config.ts",
|
|
192
|
+
"**/*.config.mts"
|
|
193
|
+
],
|
|
194
|
+
provider: "v8",
|
|
195
|
+
reporter: [
|
|
196
|
+
"text",
|
|
197
|
+
"html",
|
|
198
|
+
"json-summary"
|
|
199
|
+
],
|
|
200
|
+
reportsDirectory: COVERAGE_DIR
|
|
201
|
+
};
|
|
139
202
|
/** Defaults for the root config — the project ones, plus what only a root carries. */
|
|
140
203
|
function rootDefaults() {
|
|
141
204
|
return {
|
|
142
205
|
cacheDir: VITEST_ARTIFACTS_DIR,
|
|
143
206
|
test: {
|
|
144
|
-
|
|
207
|
+
attachmentsDir: ATTACHMENTS_DIR,
|
|
208
|
+
coverage: COVERAGE,
|
|
145
209
|
exclude: EXCLUDE,
|
|
146
210
|
hookTimeout: HOOK_TIMEOUT_MS,
|
|
211
|
+
...HYGIENE,
|
|
212
|
+
outputFile: REPORTER_OUTPUT_FILES,
|
|
147
213
|
testTimeout: TEST_TIMEOUT_MS
|
|
148
214
|
}
|
|
149
215
|
};
|
|
150
216
|
}
|
|
217
|
+
/** A glob list with its repeats taken out, or nothing where there is no list. */
|
|
218
|
+
function eachOnce(globs) {
|
|
219
|
+
return globs === void 0 ? void 0 : [...new Set(globs)];
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The glob lists vite CONCATENATES rather than overrides, each glob kept once.
|
|
223
|
+
*
|
|
224
|
+
* A project built ON the defaults and then merged INTO them by
|
|
225
|
+
* `defineSpecConfig` carried every default twice. Glob matching is idempotent,
|
|
226
|
+
* so nothing collected differently — but the no-tests banner is the one place a
|
|
227
|
+
* consumer ever reads that list, and it read as three merges of the same three
|
|
228
|
+
* globs.
|
|
229
|
+
*/
|
|
230
|
+
function statedOnce(test) {
|
|
231
|
+
const exclude = eachOnce(test.exclude);
|
|
232
|
+
const include = eachOnce(test.include);
|
|
233
|
+
return {
|
|
234
|
+
...test,
|
|
235
|
+
...exclude === void 0 ? {} : { exclude },
|
|
236
|
+
...include === void 0 ? {} : { include }
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Build a project ON the defaults — the ONE way a project is built here.
|
|
241
|
+
*
|
|
242
|
+
* @internal
|
|
243
|
+
*/
|
|
244
|
+
function onProjectDefaults(stated) {
|
|
245
|
+
const merged = mergeConfig(projectDefaults(), stated);
|
|
246
|
+
return merged.test === void 0 ? merged : {
|
|
247
|
+
...merged,
|
|
248
|
+
test: statedOnce(merged.test)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Is every project an inline object — one this preset merged its defaults into?
|
|
253
|
+
* A project named by a glob or handed as a promise carries a config this call
|
|
254
|
+
* never saw, and the root's exclusions are the only ones it gets from here.
|
|
255
|
+
*/
|
|
256
|
+
function allInline(projects) {
|
|
257
|
+
return projects.every((project) => typeof project === "object" && project !== null && !("then" in project));
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The root's exclusions, beside projects that already carry them.
|
|
261
|
+
*
|
|
262
|
+
* With `projects`, the root collects nothing itself: its `exclude` only travels
|
|
263
|
+
* INTO each project, where vitest CONCATENATES it with the project's own — so
|
|
264
|
+
* every preset glob reached the banner twice. The preset's own are dropped
|
|
265
|
+
* here, since each inline project states them in full; what the CONSUMER stated
|
|
266
|
+
* at the root stays, because no project holds a copy of that.
|
|
267
|
+
*/
|
|
268
|
+
function rootExcludeBeside(projects, exclude) {
|
|
269
|
+
return allInline(projects) ? exclude.filter((glob) => !EXCLUDE.includes(glob)) : exclude;
|
|
270
|
+
}
|
|
151
271
|
/**
|
|
152
272
|
* A project inherits NOTHING from the root `test` block — vitest resolves each
|
|
153
273
|
* project as its own config — so the defaults are merged into every inline one.
|
|
@@ -156,24 +276,71 @@ function rootDefaults() {
|
|
|
156
276
|
*/
|
|
157
277
|
function withProjectDefaults(project) {
|
|
158
278
|
if (typeof project !== "object" || project === null || "then" in project) return project;
|
|
159
|
-
return
|
|
279
|
+
return onProjectDefaults(project);
|
|
160
280
|
}
|
|
161
281
|
function defineSpecConfig(config = {}) {
|
|
162
282
|
const { literate: literateOptions, ...userConfig } = config;
|
|
163
283
|
const stated = literateOptions ? mergeConfig({ plugins: [literate(literateOptions)] }, userConfig) : userConfig;
|
|
164
284
|
const merged = mergeConfig(rootDefaults(), stated);
|
|
165
|
-
const
|
|
166
|
-
if (
|
|
285
|
+
const root = merged.test === void 0 ? void 0 : statedOnce(merged.test);
|
|
286
|
+
if (root === void 0) return merged;
|
|
287
|
+
const { projects } = root;
|
|
288
|
+
if (projects === void 0) return {
|
|
289
|
+
...merged,
|
|
290
|
+
test: root
|
|
291
|
+
};
|
|
292
|
+
const inline = projects.map(withProjectDefaults);
|
|
167
293
|
return {
|
|
168
294
|
...merged,
|
|
169
295
|
test: {
|
|
170
|
-
...
|
|
171
|
-
|
|
296
|
+
...root,
|
|
297
|
+
...root.exclude === void 0 ? {} : { exclude: rootExcludeBeside(inline, root.exclude) },
|
|
298
|
+
projects: inline
|
|
172
299
|
}
|
|
173
300
|
};
|
|
174
301
|
}
|
|
175
302
|
//#endregion
|
|
176
|
-
//#region src/
|
|
303
|
+
//#region src/runner/facet-project.ts
|
|
304
|
+
/** The globs a node facet collects, and the ones a caller stated instead. */
|
|
305
|
+
function facetProject(facet, options) {
|
|
306
|
+
return onProjectDefaults({ test: {
|
|
307
|
+
...options.exclude === void 0 ? {} : { exclude: options.exclude },
|
|
308
|
+
include: options.include ?? [`specs/${facet}/**/*.spec.ts`],
|
|
309
|
+
...options.serial === true ? { fileParallelism: false } : {},
|
|
310
|
+
name: facet,
|
|
311
|
+
sequence: { groupOrder: 0 },
|
|
312
|
+
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
313
|
+
} });
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region src/facets/api/api.project.ts
|
|
317
|
+
/** `api()` — the app met through HTTP. */
|
|
318
|
+
function api(options = {}) {
|
|
319
|
+
return facetProject("api", options);
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/facets/cli/cli.project.ts
|
|
323
|
+
/** The literate defaults — the path convention `cli()` assumes when nothing says otherwise. */
|
|
324
|
+
const CLI_SPECIFICATION = "./specs/cli/cli.specification.ts";
|
|
325
|
+
const CLI_DOCUMENTS = ["specs/cli/**/*.spec.yaml"];
|
|
326
|
+
/**
|
|
327
|
+
* A cli project collects the facet's `.test.ts` files AND its documents: a
|
|
328
|
+
* `<case>.spec.yaml` is a test file of its own (docs/07 § the literate door),
|
|
329
|
+
* so the plugin's glob has to join the include of the project that collects
|
|
330
|
+
* them — which is this one, never the root config.
|
|
331
|
+
*/
|
|
332
|
+
function cli(options = {}) {
|
|
333
|
+
const { literate: literateOptions, ...common } = options;
|
|
334
|
+
const project = facetProject("cli", common);
|
|
335
|
+
if (literateOptions === false) return project;
|
|
336
|
+
const documents = literateOptions?.include ?? CLI_DOCUMENTS;
|
|
337
|
+
return mergeConfig(project, { plugins: [literate({
|
|
338
|
+
include: documents,
|
|
339
|
+
specification: literateOptions?.specification ?? CLI_SPECIFICATION
|
|
340
|
+
})] });
|
|
341
|
+
}
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region src/seams/vitest-browser/commands.ts
|
|
177
344
|
/** Where a golden sits: `_expected/<name>` beside the test that asked for it. */
|
|
178
345
|
function goldenPath(testPath, name) {
|
|
179
346
|
if (testPath === void 0) throw new Error("golden: the command carries no test path — a golden is read from a test file.");
|
|
@@ -205,7 +372,7 @@ const COMPONENT_COMMANDS = {
|
|
|
205
372
|
goldenWrite
|
|
206
373
|
};
|
|
207
374
|
//#endregion
|
|
208
|
-
//#region src/
|
|
375
|
+
//#region src/facets/component/component.project.ts
|
|
209
376
|
/** The pipeline keys a consumer's Vite config contributes, and nothing else. */
|
|
210
377
|
const PIPELINE_KEYS = [
|
|
211
378
|
"assetsInclude",
|
|
@@ -459,39 +626,6 @@ function componentGlobs(options) {
|
|
|
459
626
|
};
|
|
460
627
|
}
|
|
461
628
|
/**
|
|
462
|
-
* A module test runs beside the module it covers: no DOM, no browser, no
|
|
463
|
-
* pipeline. `.test.tsx` is excluded because a rendered thing is a component
|
|
464
|
-
* test — a project's dependency scan reads every file its globs match, and a
|
|
465
|
-
* `.tsx` collected here would be optimised for a page that never opens.
|
|
466
|
-
*/
|
|
467
|
-
function unit(options = {}) {
|
|
468
|
-
const fromRoots = options.roots?.map((root) => `${root.replace(/\/+$/u, "")}/**/*.test.ts`);
|
|
469
|
-
return mergeConfig(projectDefaults(), { test: {
|
|
470
|
-
exclude: options.exclude ?? ["specs/**", "**/*.test.tsx"],
|
|
471
|
-
include: options.include ?? fromRoots ?? ["**/*.test.ts"],
|
|
472
|
-
...options.serial === true ? { fileParallelism: false } : {},
|
|
473
|
-
name: "unit",
|
|
474
|
-
sequence: { groupOrder: 0 },
|
|
475
|
-
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
476
|
-
} });
|
|
477
|
-
}
|
|
478
|
-
/**
|
|
479
|
-
* A website spec meets the assembled product through a served page, in the
|
|
480
|
-
* Chromium playwright drives. It opens a browser, so it is scheduled before the
|
|
481
|
-
* component project and never beside it: two Chromiums must not share a slot on
|
|
482
|
-
* a 2-vCPU runner.
|
|
483
|
-
*/
|
|
484
|
-
function website(options = {}) {
|
|
485
|
-
return mergeConfig(projectDefaults(), { test: {
|
|
486
|
-
...options.exclude === void 0 ? {} : { exclude: options.exclude },
|
|
487
|
-
...options.serial === true ? { fileParallelism: false } : {},
|
|
488
|
-
include: options.include ?? ["specs/website/**/*.test.ts"],
|
|
489
|
-
name: "website",
|
|
490
|
-
sequence: { groupOrder: 1 },
|
|
491
|
-
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
492
|
-
} });
|
|
493
|
-
}
|
|
494
|
-
/**
|
|
495
629
|
* A component test renders in a real Chromium — the browser the website facet
|
|
496
630
|
* already drives through the same `playwright` peer.
|
|
497
631
|
*
|
|
@@ -505,23 +639,23 @@ async function component(options = {}) {
|
|
|
505
639
|
const { playwright } = await import("@vitest/browser-playwright");
|
|
506
640
|
const pipeline = options.vite === void 0 ? {} : await reroot(options.vite, root);
|
|
507
641
|
const consumerPlugins = Array.isArray(pipeline.plugins) ? pipeline.plugins : [];
|
|
508
|
-
|
|
642
|
+
return onProjectDefaults({
|
|
509
643
|
...pipeline,
|
|
510
644
|
...jsxPipeline(pipeline),
|
|
511
645
|
optimizeDeps: { include: SEAM_DEPENDENCIES.filter(resolves) },
|
|
512
646
|
plugins: [mswWorkerPlugin(), ...consumerPlugins],
|
|
513
647
|
test: {
|
|
514
|
-
attachmentsDir: `${VITEST_ARTIFACTS_DIR}/attachments`,
|
|
515
648
|
browser: {
|
|
516
649
|
commands: COMPONENT_COMMANDS,
|
|
517
650
|
enabled: true,
|
|
651
|
+
expect: { toMatchScreenshot: { screenshotDirectory: SCREENSHOTS_DIR } },
|
|
518
652
|
headless: true,
|
|
519
653
|
instances: [{ browser: "chromium" }],
|
|
520
654
|
provider: playwright({ contextOptions: {
|
|
521
655
|
locale: options.locale ?? "en-US",
|
|
522
656
|
timezoneId: options.timezone ?? "UTC"
|
|
523
657
|
} }),
|
|
524
|
-
screenshotDirectory:
|
|
658
|
+
screenshotDirectory: SCREENSHOTS_DIR,
|
|
525
659
|
viewport: viewportOf(options)
|
|
526
660
|
},
|
|
527
661
|
...componentGlobs(options),
|
|
@@ -532,42 +666,22 @@ async function component(options = {}) {
|
|
|
532
666
|
sequence: { groupOrder: 2 },
|
|
533
667
|
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
534
668
|
}
|
|
535
|
-
};
|
|
536
|
-
return mergeConfig(projectDefaults(), project);
|
|
537
|
-
}
|
|
538
|
-
/**
|
|
539
|
-
* The node facets' canonical projects.
|
|
540
|
-
*
|
|
541
|
-
* Each one collects the facet's own tree, carries the preset's budgets and
|
|
542
|
-
* artefact directory, and runs in group 0 — the browser facets follow, so two
|
|
543
|
-
* Chromiums never share a slot on a two-vCPU runner. `{ include, exclude,
|
|
544
|
-
* timeout, serial }` is what a repository states when its tree, its budget or
|
|
545
|
-
* its parallelism differs;
|
|
546
|
-
* everything else is the framework's.
|
|
547
|
-
*/
|
|
548
|
-
/** The globs a node facet collects, and the ones a caller stated instead. */
|
|
549
|
-
function facetProject(facet, options) {
|
|
550
|
-
return mergeConfig(projectDefaults(), { test: {
|
|
551
|
-
...options.exclude === void 0 ? {} : { exclude: options.exclude },
|
|
552
|
-
include: options.include ?? [`specs/${facet}/**/*.test.ts`],
|
|
553
|
-
...options.serial === true ? { fileParallelism: false } : {},
|
|
554
|
-
name: facet,
|
|
555
|
-
sequence: { groupOrder: 0 },
|
|
556
|
-
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
557
|
-
} });
|
|
669
|
+
});
|
|
558
670
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
671
|
+
//#endregion
|
|
672
|
+
//#region src/facets/integration/integration.project.ts
|
|
673
|
+
/** `integration()` — a module against real services, or against a golden. */
|
|
674
|
+
function integration(options = {}) {
|
|
675
|
+
return facetProject("integration", options);
|
|
562
676
|
}
|
|
677
|
+
//#endregion
|
|
678
|
+
//#region src/facets/jobs/jobs.project.ts
|
|
563
679
|
/** `jobs()` — what a name triggers in-process. */
|
|
564
680
|
function jobs(options = {}) {
|
|
565
681
|
return facetProject("jobs", options);
|
|
566
682
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
return facetProject("integration", options);
|
|
570
|
-
}
|
|
683
|
+
//#endregion
|
|
684
|
+
//#region src/facets/mobile/mobile.project.ts
|
|
571
685
|
/**
|
|
572
686
|
* `mobile()` — the app on a simulator. A simulator is the one thing that
|
|
573
687
|
* cannot share a machine with itself, so it runs last and alone (group 3).
|
|
@@ -575,23 +689,45 @@ function integration(options = {}) {
|
|
|
575
689
|
function mobile(options = {}) {
|
|
576
690
|
return mergeConfig(facetProject("mobile", options), { test: { sequence: { groupOrder: 3 } } });
|
|
577
691
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const CLI_DOCUMENTS = ["specs/cli/**/*.spec.yaml"];
|
|
692
|
+
//#endregion
|
|
693
|
+
//#region src/facets/website/website.project.ts
|
|
581
694
|
/**
|
|
582
|
-
* A
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
695
|
+
* A website spec meets the assembled product through a served page, in the
|
|
696
|
+
* Chromium playwright drives. It opens a browser, so it is scheduled before the
|
|
697
|
+
* component project and never beside it: two Chromiums must not share a slot on
|
|
698
|
+
* a 2-vCPU runner.
|
|
586
699
|
*/
|
|
587
|
-
function
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
700
|
+
function website(options = {}) {
|
|
701
|
+
return onProjectDefaults({ test: {
|
|
702
|
+
...options.exclude === void 0 ? {} : { exclude: options.exclude },
|
|
703
|
+
...options.serial === true ? { fileParallelism: false } : {},
|
|
704
|
+
include: options.include ?? ["specs/website/**/*.spec.ts"],
|
|
705
|
+
name: "website",
|
|
706
|
+
sequence: { groupOrder: 1 },
|
|
707
|
+
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
708
|
+
} });
|
|
709
|
+
}
|
|
710
|
+
//#endregion
|
|
711
|
+
//#region src/runner/projects.ts
|
|
712
|
+
/**
|
|
713
|
+
* A module test runs beside the module it covers: no DOM, no browser, no
|
|
714
|
+
* pipeline. Its suffix is `.test.ts` and its place is OUTSIDE `specs/`, where
|
|
715
|
+
* the assembled product is specified in `.spec.ts` — the two exclusions say
|
|
716
|
+
* exactly that. `.test.tsx` is the third exclusion because a rendered thing is
|
|
717
|
+
* a component test: a project's dependency scan reads every file its globs
|
|
718
|
+
* match, and a `.tsx` collected here would be optimised for a page that never
|
|
719
|
+
* opens.
|
|
720
|
+
*/
|
|
721
|
+
function unit(options = {}) {
|
|
722
|
+
const fromRoots = options.roots?.map((root) => `${root.replace(/\/+$/u, "")}/**/*.test.ts`);
|
|
723
|
+
return onProjectDefaults({ test: {
|
|
724
|
+
exclude: options.exclude ?? ["specs/**", "**/*.test.tsx"],
|
|
725
|
+
include: options.include ?? fromRoots ?? ["**/*.test.ts"],
|
|
726
|
+
...options.serial === true ? { fileParallelism: false } : {},
|
|
727
|
+
name: "unit",
|
|
728
|
+
sequence: { groupOrder: 0 },
|
|
729
|
+
...options.timeout === void 0 ? {} : { testTimeout: options.timeout }
|
|
730
|
+
} });
|
|
595
731
|
}
|
|
596
732
|
//#endregion
|
|
597
733
|
export { api, cli, component, defineSpecConfig, integration, jobs, literate, mobile, unit, website };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# 01 — Architecture
|
|
2
|
+
|
|
3
|
+
What this package IS: one runner model behind six constructors, one chain that needs none, five source trees with declared edges, and seven channels through which its conventions are enforced — each row of the catalogue naming exactly one. The chapters that follow explain how to USE each facet; this one draws the lines they sit inside.
|
|
4
|
+
|
|
5
|
+
| The shape | Held below |
|
|
6
|
+
| -------------------------- | -------------------------------------------------------------------------------- |
|
|
7
|
+
| The runner model | [The runner model](#the-runner-model) — constructor, handle, chain, result |
|
|
8
|
+
| The source trees | [The five trees](#the-five-trees) — `model`, `facets`, `seams`, `runner`, `lint` |
|
|
9
|
+
| How a convention is held | [The seven enforcement channels](#the-seven-enforcement-channels) |
|
|
10
|
+
| What ships out of the tree | [What the tree publishes](#what-the-tree-publishes) |
|
|
11
|
+
|
|
12
|
+
The rules themselves — what each family says, what a reviewer must judge — are the constitution, [18 — Conventions](18-conventions.md); their normative sentences are the generated catalogue, [19 — Linting](19-linting.md).
|
|
13
|
+
|
|
14
|
+
## The runner model
|
|
15
|
+
|
|
16
|
+
Every facet is the same four steps. A **constructor** takes options and returns a **handle**; the handle opens a **chain** of zero or more setups closed by exactly one terminal action; the action executes and resolves to a **typed result**; the result is asserted through vitest's `expect()`. Nothing else is public — there is no imperative escape hatch, because a spec that can do anything proves nothing in particular.
|
|
17
|
+
|
|
18
|
+
Six constructors exist and the list is closed (`src/facets/specification.ts`):
|
|
19
|
+
|
|
20
|
+
| Constructor | Handle destructures to | Subject under test |
|
|
21
|
+
| ----------------------------- | ---------------------------------- | ------------------------------------------------ |
|
|
22
|
+
| `specification.api()` | `{ api, cleanup, docker }` | An HTTP API, built and served in this process |
|
|
23
|
+
| `specification.jobs()` | `{ jobs, cleanup }` | A background pipeline, triggered by name |
|
|
24
|
+
| `specification.cli()` | `{ cli, cleanup, docker }` | A command binary in a fresh temp directory |
|
|
25
|
+
| `specification.integration()` | `{ integration, cleanup, docker }` | A module against real services, or a golden |
|
|
26
|
+
| `specification.website()` | `{ website, cleanup, url }` | A rendered page — fetched, or driven in chromium |
|
|
27
|
+
| `specification.mobile()` | `{ mobile, cleanup, udid }` | A native screen on an iOS simulator |
|
|
28
|
+
|
|
29
|
+
The asymmetry in that column is the model, not an oversight: `jobs` never spawns a container, so it is handed no `docker`; `website` and `mobile` drive a browser and a simulator rather than an orchestrated stack, so they carry neither.
|
|
30
|
+
|
|
31
|
+
**One facet has no constructor**, and the same reason explains it: a rendered component starts nothing. `component` ([07 — Component specs](07-component.md)) is a chain the package exports directly — `component.intercept(…).render(<X />)` — so there is no handle to destructure and no `cleanup` to pass to `afterAll`. What every render of a project shares (the providers, the Vite pipeline, the viewport) is the PROJECT's, stated once by `component()` in `vitest.config.ts`.
|
|
32
|
+
|
|
33
|
+
A runner is created **once per suite**, in a `*.specification.ts` file, and imported by the test files beside it. That split is what makes the container lifecycle affordable — one Postgres per suite, not one per test — and it is why `afterAll(cleanup)` belongs in the specification file and nowhere else.
|
|
34
|
+
|
|
35
|
+
### The seam under the chain
|
|
36
|
+
|
|
37
|
+
`SpecificationBuilder` (`src/model/chain/builder.ts`) holds the chain for every facet; each facet contributes its own setups and its own terminal actions on top. The infrastructure a chain needs is reached through **ports** — `src/model/ports/` declares eight of them (`browser`, `cli`, `container`, `database`, `device`, `isolation`, `server`, `service`) — and an integration implements one. So the chain knows "a database exists"; it never knows Postgres.
|
|
38
|
+
|
|
39
|
+
Every seam is opened lazily rather than imported, and each one is an optional peer loaded by the one module that owns it: `playwright` for `.visit()`, `appium`/`webdriverio` for `.open()`, `@vitest/browser-playwright` + `vitest-browser-react` + `react` for `component.render()`, and — since 16.0 — `better-sqlite3`, `pg`, `redis` and `testcontainers` for the services a record declares. A project that tests no page, no component and no database installs none of them; `src/seams/peer.ts` owns the one message a missing peer produces, which names the facet that asked, the command, and under pnpm the `onlyBuiltDependencies` line a native binding needs.
|
|
40
|
+
|
|
41
|
+
## The five trees
|
|
42
|
+
|
|
43
|
+
The source tree is five trees with declared, one-directional edges. The map is not the linter's to assume — `i1-layer-boundaries` ships inert — so this package declares its own as `FRAMEWORK_LAYERS` in `oxlint.config.ts`, and that declaration is the enforced statement of what follows.
|
|
44
|
+
|
|
45
|
+
| Tree | May import | Holds |
|
|
46
|
+
| --------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| `model/` | itself, the docker/process/yaml seams, the msw seam through two named lazy imports, `runner/update`, and each facet's `*.result` module | What every facet is made of: the chain and the orchestrator, the results and their accessors, the element vocabulary, the contracts and `intercept()`, the `{{token}}` engine, the goldens, the clock, the doubles, the ports |
|
|
48
|
+
| `facets/` | `model/`, itself, the runner's project shape, and the one seam each constructor opens | Seven folders, the SAME four files in each: `<facet>.specification.ts` (the constructor), `<facet>.chain.ts`, `<facet>.result.ts`, `<facet>.project.ts` (the vitest helper), plus what is unique to the facet |
|
|
49
|
+
| `seams/` | its OWN external dependency, plus `model/` and the facet vocabulary an adapter projects into | One folder per dependency: `postgres`, `redis`, `sqlite`, `testcontainers`, `docker`, `hono`, `playwright`, `vitest-browser`, `appium`, `msw`, `openai`, `anthropic`, `yaml`, `process` |
|
|
50
|
+
| `runner/` | `vitest`, `vite`, `@vitest/browser-playwright`, `model/`, `facets/` | The CONFIG side of the runner coupling: the preset, update mode, the `literate()` plugin, the shape a node facet's project takes, and the project index |
|
|
51
|
+
| `lint/` | itself, plus a short list of PURE `model/` modules | The tool-facing channel: the oxlint plugin, the conventions checker, the catalogue manifest and generator |
|
|
52
|
+
|
|
53
|
+
**Every facet folder carries the same four files, whether or not it owns all four.** The setups and terminal actions of the six node facets are ONE builder, because a second copy of "declare a contract, seed a table, run the thing" is how two facets drift — so `api.chain.ts` and its five siblings are a named re-export of that builder rather than an implementation, and `jobs.result.ts` re-exports the shape the builder states. The shape is uniform so that a reader who has opened one facet has opened all seven; the re-export says, in the folder itself, which of the four a facet does not own.
|
|
54
|
+
|
|
55
|
+
Three of those edges carry their reason in the declaration itself. `model/` reaches `seams/docker`, `seams/process` and the yaml parser because none of the three has a dependency of its own to leak, and it reaches each facet's `*.result` module because the ONE builder constructs all seven results — a value edge, stated rather than hidden. `lint/` reaches exactly the pure modules the runner also uses — the token list, the ground names, the root walk, the `<case>.spec.yaml` parser — so that the file the lint accepts is the file the runner runs, from ONE parser. And the runner is named in exactly TWO places outside a test, one per side of it: `runner/` for the config side — the preset, the project shape, the plugin — and `seams/vitest-browser/` for the page side, where the locators, the visitor, the golden commands and the page's own `vitest` primitives live. Swapping the runner would be a rewrite of those two folders and of nothing else; `model/` states what a render IS and never which runner performs it, which is why the layer map exempts that seam from the prod-import ban (F2) by name.
|
|
56
|
+
|
|
57
|
+
The table's May-import column is a reading of `FRAMEWORK_LAYERS`, never a second statement of it: the declaration in `oxlint.config.ts` is what the linter enforces and what a change edits.
|
|
58
|
+
|
|
59
|
+
There are TWO composition roots and neither names a layer. `src/index.ts` is node's: it wires the container integrations into the registry seam and re-exports the public surface. `src/browser/index.ts` is the page's, and `src/surface.ts` is what they agree on — see [What the tree publishes](#what-the-tree-publishes).
|
|
60
|
+
|
|
61
|
+
## The seven enforcement channels
|
|
62
|
+
|
|
63
|
+
A convention this package states is held by a **channel**, and every row of the catalogue names exactly one of seven. The guiding aim is that most enforcement is programmatic, not review-borne.
|
|
64
|
+
|
|
65
|
+
- **statique** — the `jterrazz/*` oxlint plugin: one file per rule under `src/lint/rules/model/` or `src/lint/rules/<facet>/` — the tree the rule reaches, which is also the `facet` field of its manifest row — AST analysis, one diagnostic per file.
|
|
66
|
+
- **upstream** — a convention an oxlint vitest-plugin rule already holds, set as an OPTION from the `testing` fragment rather than duplicated as a rule of ours ([ADR-005](decisions/005-the-doubles-ladder-is-closed.md)).
|
|
67
|
+
- **checker** — passes of the `jterrazz-test-check` binary reading what an AST cannot: the `{{token}}` grammar of `_requests/` and `_expected/` fixtures, the `<case>.spec.yaml` document family, cross-file analyses that cross a `*.specification.ts` with its tests, and the member pass that judges a package rather than a tree.
|
|
68
|
+
- **runtime** — the framework refuses incorrect usage as it executes, where static analysis abstains (a non-literal argument) or cannot reach (the network, a container lifecycle).
|
|
69
|
+
- **type** — what the compiler refuses with no rule at all, proven by `src/type-channel.test-d.ts`.
|
|
70
|
+
- **meta** — what the framework's own suite holds about itself: the catalogue's freshness, its completeness, the reach of its rules — [03 — Testing](03-testing.md) owns the channel.
|
|
71
|
+
- **process** — the judgement no single channel settles: asset-driven grouping, golden-file discipline, retro-propagation.
|
|
72
|
+
|
|
73
|
+
The channel a rule sits on is not prose here: it is a field of `src/lint/manifest.ts`, the single source of truth from which the catalogue in [19 — Linting](19-linting.md) is generated. A rule and its normative sentence live together in the code, so the two cannot drift.
|
|
74
|
+
|
|
75
|
+
## What the tree publishes
|
|
76
|
+
|
|
77
|
+
The bundle is built by `tsdown` into `dist/` and its shape follows how each entry is consumed. `index` and `vitest`, plus the `checker`/`catalog` CLIs, are ESM-only — vitest is ESM-only and the CLIs are invoked as `node dist/*.js`. The `oxlint` plugin ships dual, because oxlint loads it from a consumer project that may itself be CommonJS.
|
|
78
|
+
|
|
79
|
+
**There are FOUR entries and no more**: `@jterrazz/test` for everything a spec uses, `@jterrazz/test/vitest` for what `vitest.config.ts` needs, `@jterrazz/test/oxlint` for the lint plugin, `@jterrazz/test/schema` for an editor validating a `<case>.spec.yaml`. That is rule F1 rather than a convention of taste, and the exemption is derived from the manifest's own `exports` map (`src/lint/package-exports.ts`), so a subpath is exempt the moment it is published and stops being exempt the moment it is withdrawn.
|
|
80
|
+
|
|
81
|
+
**The root entry has two runtimes.** A component test renders in a real browser, and the node entry cannot load there: it wires `pg`, `better-sqlite3`, `testcontainers` and `node:fs` at import. So `exports["."]` carries a `browser` condition resolving to `dist/browser/index.js` — the same public surface, built for a page, where every node-only name (the six constructors, the services, the docker accessors, the results that walk a disk) is a stub that throws where it was called, naming the facet and saying it runs under node. The split is the RUNTIME's and never the facets': one specifier, so no spec has to remember which entry it may import.
|
|
82
|
+
|
|
83
|
+
`types` is stated once, and it is the node build's — so the two runtimes are ONE type surface and a name cannot exist on one side only. What both agree on lives in `src/surface.ts`, and the `package-exports` meta-test compares the two builds' exported names on every run.
|
|
84
|
+
|
|
85
|
+
Two committed projections leave the code and land in the corpus: the API reference under `docs/reference/` (typedoc, through `typescript docs`) and the rule catalogue spliced into [19 — Linting](19-linting.md) and `skills/jterrazz-test/references/rules.md`. A third, `schema/spec.schema.json`, is generated from the document grammar's own constants. Both `docs/` and `schema/` ship in the tarball — the reference included, since the corpus links into it — so a consumer reads the manual where the messages send them, inside the install ([04 — Operating](04-operating.md#what-leaves-the-tree)). All three are regenerated by one gesture and sync-checked — see [02 — Developing](02-developing.md).
|
|
86
|
+
|
|
87
|
+
## Pitfalls
|
|
88
|
+
|
|
89
|
+
- **Reaching for a dependency from `model/`.** A new external package belongs in a folder of `seams/` that imports it and nothing else; `model/` importing it directly is the one boundary this package cannot afford to blur, and `i1-layer-boundaries` refuses it.
|
|
90
|
+
- **Adding a seventh constructor.** The six are the closed vocabulary the conventions, the linter and the documentation are all shaped around. A new SUBJECT to specify is a design decision, and it earns a record in [`decisions/`](decisions/) before it earns a constructor — as `integration` did in [ADR-004](decisions/004-a-module-against-real-services-or-a-golden-is-the-integration-facet.md).
|
|
91
|
+
- **Writing a rule's normative sentence into a chapter.** It belongs in `src/lint/manifest.ts` beside the implementation; a chapter that restates it is the second copy the whole design exists to prevent.
|
|
92
|
+
|
|
93
|
+
## Related
|
|
94
|
+
|
|
95
|
+
[02 — Developing](02-developing.md) · [03 — Testing](03-testing.md) · [18 — Conventions](18-conventions.md) · [19 — Linting](19-linting.md)
|