@hublo/sentinel 1.4.0-alpha.9 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/bin/sentinel.js +45 -101
- package/dist/{chunk-7PUVK4YM.js → chunk-5VNYQIFD.js} +224 -13
- package/dist/chunk-5VNYQIFD.js.map +1 -0
- package/dist/chunk-DYJ6J43A.js +6419 -0
- package/dist/chunk-ELZHIN6E.js +16 -0
- package/dist/chunk-ELZHIN6E.js.map +1 -0
- package/dist/{chunk-L7WS36XV.js → chunk-GRK2KRFI.js} +221 -4427
- package/dist/chunk-H3GSGAX2.js +11 -0
- package/dist/chunk-H3GSGAX2.js.map +1 -0
- package/dist/{chunk-WLFE5RUU.js → chunk-KMKQDGI6.js} +6 -8
- package/dist/chunk-KMKQDGI6.js.map +1 -0
- package/dist/{chunk-PWV3BMDA.js → chunk-MMKBDO5V.js} +11 -2
- package/dist/chunk-MMKBDO5V.js.map +1 -0
- package/dist/chunk-NUOQXAYR.js +14 -0
- package/dist/chunk-NUOQXAYR.js.map +1 -0
- package/dist/chunk-O7REVMOC.js +38 -0
- package/dist/chunk-O7REVMOC.js.map +1 -0
- package/dist/chunk-QXFCZON7.js +16 -0
- package/dist/chunk-QXFCZON7.js.map +1 -0
- package/dist/{chunk-3TDUIKVQ.js → chunk-SWWQ7X7B.js} +11 -8
- package/dist/{chunk-3TDUIKVQ.js.map → chunk-SWWQ7X7B.js.map} +1 -1
- package/dist/{chunk-CPCUPK4J.js → chunk-Z7L4FGKP.js} +5 -12
- package/dist/chunk-Z7L4FGKP.js.map +1 -0
- package/dist/index.d.ts +28 -5
- package/dist/index.js +7 -3
- package/dist/roles/build/nest/toolchain.js +11 -33
- package/dist/roles/build/nest/toolchain.js.map +1 -1
- package/dist/roles/test/nest/toolchain.js +3 -2
- package/dist/roles/test/nest/toolchain.js.map +1 -1
- package/dist/roles/test/react/toolchain.js +3 -2
- package/dist/roles/test/react/toolchain.js.map +1 -1
- package/dist/roles/test/setup/a11y.d.ts +45 -0
- package/dist/roles/test/setup/a11y.js +72 -0
- package/dist/roles/test/setup/a11y.js.map +1 -0
- package/dist/roles/test/setup/file-boundary-close.d.ts +2 -0
- package/dist/roles/test/setup/file-boundary-close.js +8 -0
- package/dist/roles/test/setup/file-boundary-close.js.map +1 -0
- package/dist/roles/test/setup/file-boundary.d.ts +2 -0
- package/dist/roles/test/setup/file-boundary.js +62 -0
- package/dist/roles/test/setup/file-boundary.js.map +1 -0
- package/dist/roles/test/setup/jest-parity.js +19 -2
- package/dist/roles/test/setup/jest-parity.js.map +1 -1
- package/dist/roles/test/setup/msw-lifecycle.js +2 -1
- package/dist/roles/test/setup/msw-lifecycle.js.map +1 -1
- package/dist/roles/test/setup/msw-server.js +2 -1
- package/dist/roles/test/setup/msw-server.js.map +1 -1
- package/dist/roles/test/setup/w3c.d.ts +75 -0
- package/dist/roles/test/setup/w3c.js +49 -0
- package/dist/roles/test/setup/w3c.js.map +1 -0
- package/dist/roles/test/setup/workspace-entry.js +3 -2
- package/dist/roles/test/setup/workspace-entry.js.map +1 -1
- package/dist/roles/test/shared-test-config.d.ts +13 -0
- package/dist/roles/test/shared-test-config.js +3 -2
- package/dist/validate-57H7GK5B.js +170 -0
- package/docs/test-adoption.md +282 -5
- package/docs/using-sentinel.md +17 -1
- package/docs/validating-a-change.md +35 -2
- package/package.json +25 -1
- package/types/jest-global.d.ts +85 -0
- package/types/mock-extended.d.ts +52 -0
- package/dist/chunk-7PUVK4YM.js.map +0 -1
- package/dist/chunk-CPCUPK4J.js.map +0 -1
- package/dist/chunk-PWV3BMDA.js.map +0 -1
- package/dist/chunk-WLFE5RUU.js.map +0 -1
|
@@ -1,41 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
globToRegExp
|
|
3
|
+
} from "../../../chunk-O7REVMOC.js";
|
|
1
4
|
import {
|
|
2
5
|
decoratorMetadata,
|
|
3
6
|
tsconfigAliases
|
|
4
|
-
} from "../../../chunk-
|
|
7
|
+
} from "../../../chunk-SWWQ7X7B.js";
|
|
8
|
+
import "../../../chunk-QXFCZON7.js";
|
|
5
9
|
|
|
6
10
|
// src/roles/build/nest/nest-service.ts
|
|
7
|
-
import { join as join3 } from "path";
|
|
11
|
+
import { join as join3 } from "node:path";
|
|
8
12
|
import { mergeConfig } from "vite";
|
|
9
13
|
|
|
10
14
|
// src/roles/build/nest/copy-assets.ts
|
|
11
|
-
import { cpSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
12
|
-
import { dirname, join, relative, sep } from "path";
|
|
13
|
-
function globToRegExp(glob) {
|
|
14
|
-
let pattern = "";
|
|
15
|
-
for (let index = 0; index < glob.length; index++) {
|
|
16
|
-
const char = glob[index] ?? "";
|
|
17
|
-
if (char === "*") {
|
|
18
|
-
if (glob[index + 1] === "*") {
|
|
19
|
-
pattern += glob[index + 2] === "/" ? "(?:.*/)?" : ".*";
|
|
20
|
-
index += glob[index + 2] === "/" ? 2 : 1;
|
|
21
|
-
} else {
|
|
22
|
-
pattern += "[^/]*";
|
|
23
|
-
}
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
if (char === "{") {
|
|
27
|
-
const close = glob.indexOf("}", index);
|
|
28
|
-
if (close === -1)
|
|
29
|
-
throw new Error(`sentinel build(nest): unbalanced \`{\` in asset glob ${glob}`);
|
|
30
|
-
const alternatives = glob.slice(index + 1, close).split(",");
|
|
31
|
-
pattern += `(?:${alternatives.map((one) => one.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`;
|
|
32
|
-
index = close;
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
pattern += char.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
36
|
-
}
|
|
37
|
-
return new RegExp(`^${pattern}$`);
|
|
38
|
-
}
|
|
15
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
16
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
39
17
|
function filesUnder(root) {
|
|
40
18
|
const found = [];
|
|
41
19
|
const walk = (dir) => {
|
|
@@ -82,9 +60,9 @@ var copyAssets = (options) => ({
|
|
|
82
60
|
});
|
|
83
61
|
|
|
84
62
|
// src/roles/build/nest/node-manifest.ts
|
|
85
|
-
import { existsSync as existsSync2, writeFileSync } from "fs";
|
|
86
|
-
import { isBuiltin } from "module";
|
|
87
|
-
import { join as join2 } from "path";
|
|
63
|
+
import { existsSync as existsSync2, writeFileSync } from "node:fs";
|
|
64
|
+
import { isBuiltin } from "node:module";
|
|
65
|
+
import { join as join2 } from "node:path";
|
|
88
66
|
var DEFAULT_PROVIDED_BY_IMAGE = ["@prisma"];
|
|
89
67
|
var externalPackages = (bundle) => {
|
|
90
68
|
const emitted = new Set(Object.keys(bundle));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/roles/build/nest/nest-service.ts","../../../../src/roles/build/nest/copy-assets.ts","../../../../src/roles/build/nest/node-manifest.ts","../../../../src/roles/build/nest/toolchain.ts"],"sourcesContent":["/**\n * The whole Vite config for a Nest service, so an adopted module holds two lines.\n *\n * ## Why a shape here, and pieces on the React side\n *\n * It looks inconsistent until you measure the two families. The four React apps each carry a\n * heavy, genuinely different config: one has a plugin wedged between two shared ones, another\n * has two different route-tree footers on the branches of one ternary. Two attempts at a\n * shared shape for them were abandoned, and the rule that survived is that sentinel rewrites\n * their IMPORTS and touches nothing else.\n *\n * The Nest side is the opposite by measurement: 38 services, one shared 25-line webpack config\n * between them, and nothing per-module to preserve. Owning the shape there is not a different\n * philosophy, it is the same one applied to a different fact: sentinel provides the FORM where\n * the form is shared, and the PIECES where it is not.\n *\n * The consequences are the point. An adopted module is readable in review. A correction to the\n * form is one publish instead of 38 diffs. And the day Nest becomes ESM-native, `format`\n * changes here rather than in 38 configs.\n *\n * ## It is a composition, not a block\n *\n * `nestService` assembles the same plugins this module exports individually. A service that\n * needs something else can pass `overrides`, or drop to the pieces entirely. And if the React\n * configs ever converge, a `reactApp()` can be built the same way with no new mechanism.\n */\nimport { join } from 'node:path'\n\nimport { mergeConfig, type UserConfig } from 'vite'\n\nimport { copyAssets } from './copy-assets.js'\nimport { decoratorMetadata } from './decorator-metadata.js'\nimport { nodeManifest } from './node-manifest.js'\nimport type { AssetDeclaration, TransformerDeclaration } from './nx-target.js'\nimport { tsconfigAliases } from './tsconfig-aliases.js'\n\nexport interface NestServiceOptions {\n /** The nx project name. Keys the dependency graph, and names the module in messages. */\n project: string\n /** Absolute path to the module's directory, normally `__dirname`. */\n root: string\n /** Absolute path to the workspace root. */\n workspaceRoot: string\n /** Entry point, relative to `root`. */\n entry?: string\n /**\n * The tsconfig whose emit must be preserved, when the service uses neither conventional name.\n *\n * Relative to `root`. Defaults to `tsconfig.app.json`, then `tsconfig.json`.\n */\n tsconfig?: string\n /** Output directory, relative to `workspaceRoot`. */\n outDir?: string\n /**\n * TypeScript transformers this service compiles with, as its build target declared them.\n *\n * Read off the webpack target by `--init` and written here, never inferred: what a service\n * compiles with is the service's own. Eight services and BFFs run `@nestjs/swagger/plugin`,\n * which writes the `@ApiProperty` decorators their DTOs do not declare by hand. Building\n * without it succeeds and silently costs them most of their published contract (measured on\n * `institution`: 385 insertions, 1378 deletions in the committed OpenAPI document).\n */\n transformers?: readonly TransformerDeclaration[]\n /**\n * Files copied into the output beside the bundle, in the shape `@nx/webpack` declared them.\n *\n * One service here uses it: `planning-period` ships the pug, css and ttf templates it renders\n * printed documents from. Without them the service starts and fails on its first print.\n */\n assets?: readonly AssetDeclaration[]\n /**\n * Anything this service needs that the shape does not give it.\n *\n * Merged with Vite's own `mergeConfig`, never with a merge of our own: plugins concatenate\n * and aliases stack the way Vite does it everywhere else, so there is no second set of\n * semantics to learn or to document.\n */\n overrides?: UserConfig\n}\n\nconst baseConfig = (options: NestServiceOptions): UserConfig => {\n const outDir = join(options.workspaceRoot, options.outDir ?? `dist/${options.project}`)\n const entry = join(options.root, options.entry ?? 'src/main.ts')\n return {\n plugins: [\n decoratorMetadata({\n root: options.root,\n tsconfig: options.tsconfig,\n transformers: options.transformers,\n }),\n ...(options.assets?.length\n ? [copyAssets({ workspaceRoot: options.workspaceRoot, outDir, assets: options.assets })]\n : []),\n nodeManifest({\n project: options.project,\n workspaceRoot: options.workspaceRoot,\n outDir,\n }),\n ],\n resolve: { alias: tsconfigAliases(options.workspaceRoot) },\n ssr: {\n // `importHelpers: true` in the workspace tsconfig makes TypeScript emit `require(\"tslib\")`\n // for `__decorate` and `__metadata`, so every decorated file depends on it. Hoisted into\n // one scope Rollup inlined it and nobody noticed; preserving modules left it external, and\n // the manifest nx generates from the project graph does not list it, because no module\n // DECLARES tslib. The image therefore did not install it and the service died on its first\n // require, only in the image: locally and in CI the workspace root has tslib.\n noExternal: ['tslib'],\n },\n build: {\n // An SSR build targets Node and leaves real packages external, which is what\n // `webpack-node-externals` did: the image installs them from the generated manifest.\n ssr: entry,\n outDir,\n emptyOutDir: true,\n sourcemap: true,\n target: 'node20',\n // The bundle is read by humans when a stack trace points into it, and minifying a\n // server bundle buys nothing: it is never downloaded.\n minify: false,\n rollupOptions: {\n output: {\n format: 'cjs',\n // One output file per source module, instead of hoisting everything into one scope.\n //\n // This is not a preference, it is the only shape that keeps the OpenAPI contract.\n // Merging every module into one scope forces Rollup to rename duplicate class names,\n // and `@nestjs/swagger` keys its schemas on `class.name`, so a renamed class silently\n // becomes a renamed schema in the published API. Measured on `network`, which has two\n // such collisions: single bundle renames `InvalidPermissionError` and\n // `PermissionNotFoundError`; preserving modules renames nothing and reproduces the\n // committed contract byte for byte.\n //\n // The cost is 4.5 MB across 2003 files against 2.9 MB in one, still well under\n // webpack's 6.9 MB for the same service, so nothing regresses against what it replaces.\n preserveModules: true,\n // The image runs `./dist/main.js`, so the entry keeps that name at the root while\n // every other module keeps its own path. Under `preserveModules` a plain string here\n // would be applied to all of them, which numbers them (`main962.js`) and loses the\n // entry.\n entryFileNames: (chunk) => (chunk.facadeModuleId === entry ? 'main.js' : '[name].js'),\n },\n // Nest registers metadata as an IMPORT SIDE EFFECT: a decorator writes into a catalog\n // when its module loads, and nothing references that module afterwards. Rollup may\n // drop such a module; webpack never did.\n //\n // Measured on the first migrated service this changes nothing, so it is insurance\n // rather than a fix, and it is recorded as such rather than credited with the smaller\n // bundle (that comes from barrel re-exports the service does not use).\n treeshake: { moduleSideEffects: true },\n },\n },\n }\n}\n\nexport const nestService = (options: NestServiceOptions): UserConfig =>\n options.overrides === undefined\n ? baseConfig(options)\n : mergeConfig(baseConfig(options), options.overrides)\n","/**\n * The files a service ships beside its bundle, which webpack copied and a bundler does not.\n *\n * `@nx/webpack` takes an `assets` list and copies each match into the output. Nothing in a Vite\n * build does that for a Node target: `publicDir` is a browser concept and is disabled for SSR\n * builds, so an unmigrated `assets` entry is not a smaller output, it is a service that starts\n * and then fails the first time it reaches for a file that is not there.\n *\n * One service in this repo declares it. `planning-period` ships the pug templates, stylesheet and\n * font it renders printed documents from, under `templates/`. Dropping them leaves a build that\n * succeeds, an image that boots, and a printing feature that throws.\n *\n * ## Copied at `closeBundle`, and only what the target declared\n *\n * The declaration is carried through from the webpack target unchanged, globs and all, so this\n * plugin has nothing to decide: it resolves each `input` against the workspace root, matches the\n * glob, and writes under `output` inside the bundle's directory. `closeBundle` because that is\n * when the output directory exists and Vite has finished emptying it.\n *\n * An entry matching nothing FAILS the build. A glob that has quietly stopped matching is the same\n * silent loss as no glob at all, and the point of this file is that this class of loss is loud.\n */\nimport { cpSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs'\nimport { dirname, join, relative, sep } from 'node:path'\n\nimport type { Plugin } from 'vite'\n\nimport type { AssetDeclaration } from './nx-target.js'\n\nexport interface CopyAssetsOptions {\n /** Absolute path to the workspace root, which `input` is relative to. */\n workspaceRoot: string\n /** Absolute path to the build's output directory. */\n outDir: string\n assets: readonly AssetDeclaration[]\n}\n\n/**\n * `@nx/webpack`'s glob dialect, reduced to what it means for a file name.\n *\n * Only the forms the declarations here use are supported, and anything else is rejected at build\n * time rather than silently matching nothing: `**` for any depth, `*` within a segment, and a\n * `{a,b}` alternation. Written out rather than pulled from a glob library because the whole\n * dependency would exist to read one line of configuration.\n */\nfunction globToRegExp(glob: string): RegExp {\n let pattern = ''\n for (let index = 0; index < glob.length; index++) {\n const char = glob[index] ?? ''\n if (char === '*') {\n if (glob[index + 1] === '*') {\n // `**/` matches any number of directories, including none.\n pattern += glob[index + 2] === '/' ? '(?:.*/)?' : '.*'\n index += glob[index + 2] === '/' ? 2 : 1\n } else {\n pattern += '[^/]*'\n }\n continue\n }\n if (char === '{') {\n const close = glob.indexOf('}', index)\n if (close === -1)\n throw new Error(`sentinel build(nest): unbalanced \\`{\\` in asset glob ${glob}`)\n const alternatives = glob.slice(index + 1, close).split(',')\n pattern += `(?:${alternatives.map((one) => one.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')).join('|')})`\n index = close\n continue\n }\n pattern += char.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n }\n return new RegExp(`^${pattern}$`)\n}\n\n/** Every file under `root`, as paths relative to it and with forward slashes. */\nfunction filesUnder(root: string): string[] {\n const found: string[] = []\n const walk = (dir: string): void => {\n for (const entry of readdirSync(dir)) {\n const full = join(dir, entry)\n if (statSync(full).isDirectory()) walk(full)\n else found.push(relative(root, full).split(sep).join('/'))\n }\n }\n walk(root)\n return found\n}\n\n/** What each declaration copies, or the reason it cannot. */\nexport function resolveAssets(options: CopyAssetsOptions): { from: string; to: string }[] {\n const copies: { from: string; to: string }[] = []\n for (const asset of options.assets) {\n const input = join(options.workspaceRoot, asset.input)\n if (!existsSync(input)) {\n throw new Error(\n `sentinel build(nest): the build target declares an asset from \\`${asset.input}\\`, which ` +\n `does not exist. webpack copied it into the output, so the built service would be ` +\n `missing files it reads at runtime.`,\n )\n }\n const matcher = globToRegExp(asset.glob)\n const matched = filesUnder(input).filter((file) => matcher.test(file))\n if (matched.length === 0) {\n throw new Error(\n `sentinel build(nest): the asset glob \\`${asset.glob}\\` under \\`${asset.input}\\` matches ` +\n `no file. An entry that matches nothing is the same silent loss as no entry at all.`,\n )\n }\n for (const file of matched) {\n copies.push({ from: join(input, file), to: join(options.outDir, asset.output, file) })\n }\n }\n return copies\n}\n\nexport const copyAssets = (options: CopyAssetsOptions): Plugin => ({\n name: 'sentinel:copy-assets',\n // After the bundle is written and the output directory has stopped being emptied.\n closeBundle() {\n for (const { from, to } of resolveAssets(options)) {\n mkdirSync(dirname(to), { recursive: true })\n cpSync(from, to)\n }\n },\n})\n","/**\n * The pruned `package.json` and lockfile a Node image installs from.\n *\n * This is the half that made the nx executor look load-bearing. `--generatePackageJson` was an\n * option of `@nx/webpack:webpack`, and the backend image is built on its two outputs:\n *\n * COPY --from=build /app/dist/apps/nest/<service>/package.json /app/package.json\n * COPY --from=build /app/dist/apps/nest/<service>/pnpm-lock.yaml /app/pnpm-lock.yaml\n * RUN pnpm i --prod=true --frozen-lockfile\n *\n * It is not bound to the executor. The executor supplies one thing, the project graph, and\n * every function that does the work is exported by `@nx/js`. So the same two files can be\n * produced from a Vite plugin, and `--frozen-lockfile` accepting them afterwards is the proof\n * that they agree with each other: pnpm refuses the install otherwise.\n *\n * ## Why the graph, and not something simpler\n *\n * A Nest service here declares almost nothing of its own: measured, one dependency in its\n * manifest against 1463 installed in its image. Everything it uses is declared at the\n * workspace root and reaches it through source imports. So the dependency list can only be\n * computed from what the code imports, which is what the graph knows.\n *\n * Deriving it from the bundle's own externals is not equivalent either: measured on the first\n * service, the bundle statically requires 42 packages while the correct manifest holds 65. The\n * 23 missing ones are transitive or required dynamically at runtime (`fastify` behind\n * `@nestjs/platform-fastify`, `jsonwebtoken`, the LaunchDarkly SDK). Shipping the short list\n * fails in production, not in the build.\n */\nimport { existsSync, writeFileSync } from 'node:fs'\nimport { isBuiltin } from 'node:module'\nimport { join } from 'node:path'\n\nimport type { Plugin, Rollup } from 'vite'\n\nexport interface NodeManifestOptions {\n /** The nx project name, which is how the graph is keyed. */\n project: string\n /** Absolute path to the workspace root. */\n workspaceRoot: string\n /** Where the bundle is written; the two files land beside it. */\n outDir: string\n /** Entry file name, written as `main` so `node .` resolves inside the image. */\n entry?: string\n /**\n * Packages the IMAGE provides, which are therefore allowed to be required without being\n * declared in the manifest.\n *\n * The generated Prisma clients are the case this exists for: the Dockerfile copies\n * `node_modules/@prisma` from its own stage, so they resolve at runtime while no module\n * declares them. Everything else that is required and undeclared is a bug, and the check\n * below refuses the build.\n */\n providedByImage?: readonly string[]\n}\n\n/** What the Dockerfile copies in itself, so requiring it without declaring it is legitimate. */\nconst DEFAULT_PROVIDED_BY_IMAGE = ['@prisma'] as const\n\n/**\n * Every real package the emitted output requires, from Rollup's own record rather than a scan\n * of the text.\n *\n * `chunk.imports` mixes three things, and only the third is a dependency:\n *\n * - other EMITTED chunks. Under `preserveModules` these are relative output paths that do not\n * begin with `.`, so `apps/…`, `libs/…` and `_virtual/…` read exactly like package names.\n * They are recognised by being keys of the bundle itself.\n * - Node BUILTINS, which appear both bare (`crypto`) and prefixed (`node:crypto`).\n * - genuine external packages, which is what the manifest has to cover.\n */\nconst externalPackages = (bundle: Rollup.OutputBundle): Set<string> => {\n const emitted = new Set(Object.keys(bundle))\n const packages = new Set<string>()\n\n for (const emittedFile of Object.values(bundle)) {\n // Assets carry no imports, and the bundle holds both. Narrowing on the discriminant is what\n // replaced a cast through `unknown`, which asserted the same thing without checking it.\n if (emittedFile.type !== 'chunk') continue\n\n for (const imported of emittedFile.imports) {\n if (emitted.has(imported)) continue\n if (imported.startsWith('.') || imported.startsWith('/')) continue\n if (isBuiltin(imported)) continue\n\n const parts = imported.split('/')\n const name = imported.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]\n if (name) packages.add(name)\n }\n }\n return packages\n}\n\n/**\n * Writes the manifest and lockfile once the bundle is on disk.\n *\n * `closeBundle` rather than `writeBundle`, so the files land after Vite has finished with the\n * directory and cannot be cleared by `emptyOutDir`.\n *\n * `closeBundle` also runs after a FAILED build, where there is no directory to write into. Left\n * alone this hook then threw `ENOENT` on the manifest, and since it is the last error raised it\n * became the one Vite printed, hiding the failure that actually stopped the build. It cost two\n * diagnoses before being recognised, so the missing directory is now read as what it is: the\n * bundle was never written, and this plugin has nothing to say about why.\n */\nexport const nodeManifest = (options: NodeManifestOptions): Plugin => {\n /*\n * Captured while the bundle still exists in memory, and read back once the manifest is built.\n *\n * `undefined` until `generateBundle` runs, deliberately: an empty SET would make the coverage\n * check below pass by having seen nothing, which is the worst way for a guard to succeed. The\n * two states have to be told apart.\n */\n let required: Set<string> | undefined\n\n return {\n name: 'sentinel:node-manifest',\n apply: 'build',\n\n generateBundle(_outputOptions, bundle) {\n required = externalPackages(bundle)\n },\n\n async closeBundle() {\n // `closeBundle` runs after a FAILED build too. The ENTRY is what says a bundle was really\n // written: `emptyOutDir` leaves the directory in place, so its existence alone would let a\n // failed build still produce a manifest describing nothing.\n const entry = options.entry ?? 'main.js'\n if (!existsSync(join(options.outDir, entry))) return\n\n // Imported here rather than at module scope: a front config importing this file must not\n // pay for loading the nx graph machinery it will never call.\n const { createProjectGraphAsync } = await import('nx/src/devkit-exports.js')\n const { createPackageJson, createLockFile, getLockFileName } = await import('@nx/js')\n\n const graph = await createProjectGraphAsync({ exitOnError: false })\n const manifest = createPackageJson(options.project, graph, {\n root: options.workspaceRoot,\n isProduction: true,\n })\n manifest.main = entry\n\n if (required === undefined) {\n throw new Error(\n `sentinel build(nest): the bundle was written but never inspected, so the manifest ` +\n `could not be checked against what it requires. Refusing to write one that might ` +\n `not install.`,\n )\n }\n assertManifestCovers(required, manifest, options)\n\n writeFileSync(join(options.outDir, 'package.json'), `${JSON.stringify(manifest, null, 2)}\\n`)\n writeFileSync(\n join(options.outDir, getLockFileName('pnpm')),\n createLockFile(manifest, graph, 'pnpm'),\n )\n },\n }\n}\n\n/**\n * Refuse a bundle that requires something the image will not install.\n *\n * The manifest is derived from the project GRAPH, so it lists what modules declare. The bundle\n * requires what the bundler left external. Those two sets agreeing is an assumption, and it was\n * wrong: `tslib` arrives through `importHelpers` in the workspace tsconfig without any module\n * declaring it, so the manifest omitted it and the service died on its first require.\n *\n * It died only in the image. Locally and in CI the workspace root has the package, so the gap is\n * invisible until a container starts, which is the most expensive place to discover it. Checking\n * here turns that into a build failure naming the package.\n */\nconst assertManifestCovers = (\n required: ReadonlySet<string>,\n manifest: { dependencies?: Record<string, string> },\n options: NodeManifestOptions,\n): void => {\n const declared = new Set(Object.keys(manifest.dependencies ?? {}))\n const provided = options.providedByImage ?? DEFAULT_PROVIDED_BY_IMAGE\n const missing = [...required]\n .filter((name) => !declared.has(name))\n .filter((name) => !provided.some((prefix) => name === prefix || name.startsWith(`${prefix}/`)))\n .sort()\n\n if (missing.length === 0) return\n\n throw new Error(\n `sentinel build(nest): the bundle requires ${missing.length} package(s) the generated ` +\n `manifest does not declare, so the image would not install them: ${missing.join(', ')}. ` +\n `Either declare them in the module's package.json, inline them with ` +\n `\\`ssr.noExternal\\`, or list them in \\`providedByImage\\` if the Dockerfile copies them in.`,\n )\n}\n","/**\n * What `@hublo/sentinel/build/nest` gives an adopted service.\n *\n * The sibling `build/react` entry is a re-export surface: the module keeps its own config and\n * only its import lines move. This one carries a shape as well, because 38 Nest services share\n * one config and have nothing of their own to preserve. See `nest-service.ts` for why the two\n * answers differ, and why that is one rule rather than two.\n *\n * Its own entry point, like the React one, so importing it at build time never drags the CLI\n * and its adapters into a Vite config.\n */\nexport { nestService, type NestServiceOptions } from './nest-service.js'\n\n// The pieces the shape is built from, exported individually so a service that needs something\n// the shape does not give it can compose its own without leaving sentinel.\nexport { decoratorMetadata, type DecoratorMetadataOptions } from './decorator-metadata.js'\nexport { nodeManifest, type NodeManifestOptions } from './node-manifest.js'\nexport { tsconfigAliases, type Alias } from './tsconfig-aliases.js'\n\n// Vite's own, so an adopted config imports everything from one place and the module can drop\n// `vite` from its dependencies.\nexport { defineConfig, loadEnv, mergeConfig } from 'vite'\nexport type { Plugin, PluginOption, UserConfig, UserConfigExport } from 'vite'\n"],"mappings":"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","names":["join","existsSync","join","join","mergeConfig"]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/build/nest/nest-service.ts","../../../../src/roles/build/nest/copy-assets.ts","../../../../src/roles/build/nest/node-manifest.ts","../../../../src/roles/build/nest/toolchain.ts"],"sourcesContent":["/**\n * The whole Vite config for a Nest service, so an adopted module holds two lines.\n *\n * ## Why a shape here, and pieces on the React side\n *\n * It looks inconsistent until you measure the two families. The four React apps each carry a\n * heavy, genuinely different config: one has a plugin wedged between two shared ones, another\n * has two different route-tree footers on the branches of one ternary. Two attempts at a\n * shared shape for them were abandoned, and the rule that survived is that sentinel rewrites\n * their IMPORTS and touches nothing else.\n *\n * The Nest side is the opposite by measurement: 38 services, one shared 25-line webpack config\n * between them, and nothing per-module to preserve. Owning the shape there is not a different\n * philosophy, it is the same one applied to a different fact: sentinel provides the FORM where\n * the form is shared, and the PIECES where it is not.\n *\n * The consequences are the point. An adopted module is readable in review. A correction to the\n * form is one publish instead of 38 diffs. And the day Nest becomes ESM-native, `format`\n * changes here rather than in 38 configs.\n *\n * ## It is a composition, not a block\n *\n * `nestService` assembles the same plugins this module exports individually. A service that\n * needs something else can pass `overrides`, or drop to the pieces entirely. And if the React\n * configs ever converge, a `reactApp()` can be built the same way with no new mechanism.\n */\nimport { join } from 'node:path'\n\nimport { mergeConfig, type UserConfig } from 'vite'\n\nimport { copyAssets } from './copy-assets.js'\nimport { decoratorMetadata } from './decorator-metadata.js'\nimport { nodeManifest } from './node-manifest.js'\nimport type { AssetDeclaration, TransformerDeclaration } from './nx-target.js'\nimport { tsconfigAliases } from './tsconfig-aliases.js'\n\nexport interface NestServiceOptions {\n /** The nx project name. Keys the dependency graph, and names the module in messages. */\n project: string\n /** Absolute path to the module's directory, normally `__dirname`. */\n root: string\n /** Absolute path to the workspace root. */\n workspaceRoot: string\n /** Entry point, relative to `root`. */\n entry?: string\n /**\n * The tsconfig whose emit must be preserved, when the service uses neither conventional name.\n *\n * Relative to `root`. Defaults to `tsconfig.app.json`, then `tsconfig.json`.\n */\n tsconfig?: string\n /** Output directory, relative to `workspaceRoot`. */\n outDir?: string\n /**\n * TypeScript transformers this service compiles with, as its build target declared them.\n *\n * Read off the webpack target by `--init` and written here, never inferred: what a service\n * compiles with is the service's own. Eight services and BFFs run `@nestjs/swagger/plugin`,\n * which writes the `@ApiProperty` decorators their DTOs do not declare by hand. Building\n * without it succeeds and silently costs them most of their published contract (measured on\n * `institution`: 385 insertions, 1378 deletions in the committed OpenAPI document).\n */\n transformers?: readonly TransformerDeclaration[]\n /**\n * Files copied into the output beside the bundle, in the shape `@nx/webpack` declared them.\n *\n * One service here uses it: `planning-period` ships the pug, css and ttf templates it renders\n * printed documents from. Without them the service starts and fails on its first print.\n */\n assets?: readonly AssetDeclaration[]\n /**\n * Anything this service needs that the shape does not give it.\n *\n * Merged with Vite's own `mergeConfig`, never with a merge of our own: plugins concatenate\n * and aliases stack the way Vite does it everywhere else, so there is no second set of\n * semantics to learn or to document.\n */\n overrides?: UserConfig\n}\n\nconst baseConfig = (options: NestServiceOptions): UserConfig => {\n const outDir = join(options.workspaceRoot, options.outDir ?? `dist/${options.project}`)\n const entry = join(options.root, options.entry ?? 'src/main.ts')\n return {\n plugins: [\n decoratorMetadata({\n root: options.root,\n tsconfig: options.tsconfig,\n transformers: options.transformers,\n }),\n ...(options.assets?.length\n ? [copyAssets({ workspaceRoot: options.workspaceRoot, outDir, assets: options.assets })]\n : []),\n nodeManifest({\n project: options.project,\n workspaceRoot: options.workspaceRoot,\n outDir,\n }),\n ],\n resolve: { alias: tsconfigAliases(options.workspaceRoot) },\n ssr: {\n // `importHelpers: true` in the workspace tsconfig makes TypeScript emit `require(\"tslib\")`\n // for `__decorate` and `__metadata`, so every decorated file depends on it. Hoisted into\n // one scope Rollup inlined it and nobody noticed; preserving modules left it external, and\n // the manifest nx generates from the project graph does not list it, because no module\n // DECLARES tslib. The image therefore did not install it and the service died on its first\n // require, only in the image: locally and in CI the workspace root has tslib.\n noExternal: ['tslib'],\n },\n build: {\n // An SSR build targets Node and leaves real packages external, which is what\n // `webpack-node-externals` did: the image installs them from the generated manifest.\n ssr: entry,\n outDir,\n emptyOutDir: true,\n sourcemap: true,\n target: 'node20',\n // The bundle is read by humans when a stack trace points into it, and minifying a\n // server bundle buys nothing: it is never downloaded.\n minify: false,\n rollupOptions: {\n output: {\n format: 'cjs',\n // One output file per source module, instead of hoisting everything into one scope.\n //\n // This is not a preference, it is the only shape that keeps the OpenAPI contract.\n // Merging every module into one scope forces Rollup to rename duplicate class names,\n // and `@nestjs/swagger` keys its schemas on `class.name`, so a renamed class silently\n // becomes a renamed schema in the published API. Measured on `network`, which has two\n // such collisions: single bundle renames `InvalidPermissionError` and\n // `PermissionNotFoundError`; preserving modules renames nothing and reproduces the\n // committed contract byte for byte.\n //\n // The cost is 4.5 MB across 2003 files against 2.9 MB in one, still well under\n // webpack's 6.9 MB for the same service, so nothing regresses against what it replaces.\n preserveModules: true,\n // The image runs `./dist/main.js`, so the entry keeps that name at the root while\n // every other module keeps its own path. Under `preserveModules` a plain string here\n // would be applied to all of them, which numbers them (`main962.js`) and loses the\n // entry.\n entryFileNames: (chunk) => (chunk.facadeModuleId === entry ? 'main.js' : '[name].js'),\n },\n // Nest registers metadata as an IMPORT SIDE EFFECT: a decorator writes into a catalog\n // when its module loads, and nothing references that module afterwards. Rollup may\n // drop such a module; webpack never did.\n //\n // Measured on the first migrated service this changes nothing, so it is insurance\n // rather than a fix, and it is recorded as such rather than credited with the smaller\n // bundle (that comes from barrel re-exports the service does not use).\n treeshake: { moduleSideEffects: true },\n },\n },\n }\n}\n\nexport const nestService = (options: NestServiceOptions): UserConfig =>\n options.overrides === undefined\n ? baseConfig(options)\n : mergeConfig(baseConfig(options), options.overrides)\n","/**\n * The files a service ships beside its bundle, which webpack copied and a bundler does not.\n *\n * `@nx/webpack` takes an `assets` list and copies each match into the output. Nothing in a Vite\n * build does that for a Node target: `publicDir` is a browser concept and is disabled for SSR\n * builds, so an unmigrated `assets` entry is not a smaller output, it is a service that starts\n * and then fails the first time it reaches for a file that is not there.\n *\n * One service in this repo declares it. `planning-period` ships the pug templates, stylesheet and\n * font it renders printed documents from, under `templates/`. Dropping them leaves a build that\n * succeeds, an image that boots, and a printing feature that throws.\n *\n * ## Copied at `closeBundle`, and only what the target declared\n *\n * The declaration is carried through from the webpack target unchanged, globs and all, so this\n * plugin has nothing to decide: it resolves each `input` against the workspace root, matches the\n * glob, and writes under `output` inside the bundle's directory. `closeBundle` because that is\n * when the output directory exists and Vite has finished emptying it.\n *\n * An entry matching nothing FAILS the build. A glob that has quietly stopped matching is the same\n * silent loss as no glob at all, and the point of this file is that this class of loss is loud.\n */\nimport { cpSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs'\nimport { dirname, join, relative, sep } from 'node:path'\n\nimport type { Plugin } from 'vite'\n\nimport { globToRegExp } from '../../../shared/glob.js'\n\nimport type { AssetDeclaration } from './nx-target.js'\n\nexport interface CopyAssetsOptions {\n /** Absolute path to the workspace root, which `input` is relative to. */\n workspaceRoot: string\n /** Absolute path to the build's output directory. */\n outDir: string\n assets: readonly AssetDeclaration[]\n}\n\n/** Every file under `root`, as paths relative to it and with forward slashes. */\nfunction filesUnder(root: string): string[] {\n const found: string[] = []\n const walk = (dir: string): void => {\n for (const entry of readdirSync(dir)) {\n const full = join(dir, entry)\n if (statSync(full).isDirectory()) walk(full)\n else found.push(relative(root, full).split(sep).join('/'))\n }\n }\n walk(root)\n return found\n}\n\n/** What each declaration copies, or the reason it cannot. */\nexport function resolveAssets(options: CopyAssetsOptions): { from: string; to: string }[] {\n const copies: { from: string; to: string }[] = []\n for (const asset of options.assets) {\n const input = join(options.workspaceRoot, asset.input)\n if (!existsSync(input)) {\n throw new Error(\n `sentinel build(nest): the build target declares an asset from \\`${asset.input}\\`, which ` +\n `does not exist. webpack copied it into the output, so the built service would be ` +\n `missing files it reads at runtime.`,\n )\n }\n const matcher = globToRegExp(asset.glob)\n const matched = filesUnder(input).filter((file) => matcher.test(file))\n if (matched.length === 0) {\n throw new Error(\n `sentinel build(nest): the asset glob \\`${asset.glob}\\` under \\`${asset.input}\\` matches ` +\n `no file. An entry that matches nothing is the same silent loss as no entry at all.`,\n )\n }\n for (const file of matched) {\n copies.push({ from: join(input, file), to: join(options.outDir, asset.output, file) })\n }\n }\n return copies\n}\n\nexport const copyAssets = (options: CopyAssetsOptions): Plugin => ({\n name: 'sentinel:copy-assets',\n // After the bundle is written and the output directory has stopped being emptied.\n closeBundle() {\n for (const { from, to } of resolveAssets(options)) {\n mkdirSync(dirname(to), { recursive: true })\n cpSync(from, to)\n }\n },\n})\n","/**\n * The pruned `package.json` and lockfile a Node image installs from.\n *\n * This is the half that made the nx executor look load-bearing. `--generatePackageJson` was an\n * option of `@nx/webpack:webpack`, and the backend image is built on its two outputs:\n *\n * COPY --from=build /app/dist/apps/nest/<service>/package.json /app/package.json\n * COPY --from=build /app/dist/apps/nest/<service>/pnpm-lock.yaml /app/pnpm-lock.yaml\n * RUN pnpm i --prod=true --frozen-lockfile\n *\n * It is not bound to the executor. The executor supplies one thing, the project graph, and\n * every function that does the work is exported by `@nx/js`. So the same two files can be\n * produced from a Vite plugin, and `--frozen-lockfile` accepting them afterwards is the proof\n * that they agree with each other: pnpm refuses the install otherwise.\n *\n * ## Why the graph, and not something simpler\n *\n * A Nest service here declares almost nothing of its own: measured, one dependency in its\n * manifest against 1463 installed in its image. Everything it uses is declared at the\n * workspace root and reaches it through source imports. So the dependency list can only be\n * computed from what the code imports, which is what the graph knows.\n *\n * Deriving it from the bundle's own externals is not equivalent either: measured on the first\n * service, the bundle statically requires 42 packages while the correct manifest holds 65. The\n * 23 missing ones are transitive or required dynamically at runtime (`fastify` behind\n * `@nestjs/platform-fastify`, `jsonwebtoken`, the LaunchDarkly SDK). Shipping the short list\n * fails in production, not in the build.\n */\nimport { existsSync, writeFileSync } from 'node:fs'\nimport { isBuiltin } from 'node:module'\nimport { join } from 'node:path'\n\nimport type { Plugin, Rollup } from 'vite'\n\nexport interface NodeManifestOptions {\n /** The nx project name, which is how the graph is keyed. */\n project: string\n /** Absolute path to the workspace root. */\n workspaceRoot: string\n /** Where the bundle is written; the two files land beside it. */\n outDir: string\n /** Entry file name, written as `main` so `node .` resolves inside the image. */\n entry?: string\n /**\n * Packages the IMAGE provides, which are therefore allowed to be required without being\n * declared in the manifest.\n *\n * The generated Prisma clients are the case this exists for: the Dockerfile copies\n * `node_modules/@prisma` from its own stage, so they resolve at runtime while no module\n * declares them. Everything else that is required and undeclared is a bug, and the check\n * below refuses the build.\n */\n providedByImage?: readonly string[]\n}\n\n/** What the Dockerfile copies in itself, so requiring it without declaring it is legitimate. */\nconst DEFAULT_PROVIDED_BY_IMAGE = ['@prisma'] as const\n\n/**\n * Every real package the emitted output requires, from Rollup's own record rather than a scan\n * of the text.\n *\n * `chunk.imports` mixes three things, and only the third is a dependency:\n *\n * - other EMITTED chunks. Under `preserveModules` these are relative output paths that do not\n * begin with `.`, so `apps/…`, `libs/…` and `_virtual/…` read exactly like package names.\n * They are recognised by being keys of the bundle itself.\n * - Node BUILTINS, which appear both bare (`crypto`) and prefixed (`node:crypto`).\n * - genuine external packages, which is what the manifest has to cover.\n */\nconst externalPackages = (bundle: Rollup.OutputBundle): Set<string> => {\n const emitted = new Set(Object.keys(bundle))\n const packages = new Set<string>()\n\n for (const emittedFile of Object.values(bundle)) {\n // Assets carry no imports, and the bundle holds both. Narrowing on the discriminant is what\n // replaced a cast through `unknown`, which asserted the same thing without checking it.\n if (emittedFile.type !== 'chunk') continue\n\n for (const imported of emittedFile.imports) {\n if (emitted.has(imported)) continue\n if (imported.startsWith('.') || imported.startsWith('/')) continue\n if (isBuiltin(imported)) continue\n\n const parts = imported.split('/')\n const name = imported.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]\n if (name) packages.add(name)\n }\n }\n return packages\n}\n\n/**\n * Writes the manifest and lockfile once the bundle is on disk.\n *\n * `closeBundle` rather than `writeBundle`, so the files land after Vite has finished with the\n * directory and cannot be cleared by `emptyOutDir`.\n *\n * `closeBundle` also runs after a FAILED build, where there is no directory to write into. Left\n * alone this hook then threw `ENOENT` on the manifest, and since it is the last error raised it\n * became the one Vite printed, hiding the failure that actually stopped the build. It cost two\n * diagnoses before being recognised, so the missing directory is now read as what it is: the\n * bundle was never written, and this plugin has nothing to say about why.\n */\nexport const nodeManifest = (options: NodeManifestOptions): Plugin => {\n /*\n * Captured while the bundle still exists in memory, and read back once the manifest is built.\n *\n * `undefined` until `generateBundle` runs, deliberately: an empty SET would make the coverage\n * check below pass by having seen nothing, which is the worst way for a guard to succeed. The\n * two states have to be told apart.\n */\n let required: Set<string> | undefined\n\n return {\n name: 'sentinel:node-manifest',\n apply: 'build',\n\n generateBundle(_outputOptions, bundle) {\n required = externalPackages(bundle)\n },\n\n async closeBundle() {\n // `closeBundle` runs after a FAILED build too. The ENTRY is what says a bundle was really\n // written: `emptyOutDir` leaves the directory in place, so its existence alone would let a\n // failed build still produce a manifest describing nothing.\n const entry = options.entry ?? 'main.js'\n if (!existsSync(join(options.outDir, entry))) return\n\n // Imported here rather than at module scope: a front config importing this file must not\n // pay for loading the nx graph machinery it will never call.\n const { createProjectGraphAsync } = await import('nx/src/devkit-exports.js')\n const { createPackageJson, createLockFile, getLockFileName } = await import('@nx/js')\n\n const graph = await createProjectGraphAsync({ exitOnError: false })\n const manifest = createPackageJson(options.project, graph, {\n root: options.workspaceRoot,\n isProduction: true,\n })\n manifest.main = entry\n\n if (required === undefined) {\n throw new Error(\n `sentinel build(nest): the bundle was written but never inspected, so the manifest ` +\n `could not be checked against what it requires. Refusing to write one that might ` +\n `not install.`,\n )\n }\n assertManifestCovers(required, manifest, options)\n\n writeFileSync(join(options.outDir, 'package.json'), `${JSON.stringify(manifest, null, 2)}\\n`)\n writeFileSync(\n join(options.outDir, getLockFileName('pnpm')),\n createLockFile(manifest, graph, 'pnpm'),\n )\n },\n }\n}\n\n/**\n * Refuse a bundle that requires something the image will not install.\n *\n * The manifest is derived from the project GRAPH, so it lists what modules declare. The bundle\n * requires what the bundler left external. Those two sets agreeing is an assumption, and it was\n * wrong: `tslib` arrives through `importHelpers` in the workspace tsconfig without any module\n * declaring it, so the manifest omitted it and the service died on its first require.\n *\n * It died only in the image. Locally and in CI the workspace root has the package, so the gap is\n * invisible until a container starts, which is the most expensive place to discover it. Checking\n * here turns that into a build failure naming the package.\n */\nconst assertManifestCovers = (\n required: ReadonlySet<string>,\n manifest: { dependencies?: Record<string, string> },\n options: NodeManifestOptions,\n): void => {\n const declared = new Set(Object.keys(manifest.dependencies ?? {}))\n const provided = options.providedByImage ?? DEFAULT_PROVIDED_BY_IMAGE\n const missing = [...required]\n .filter((name) => !declared.has(name))\n .filter((name) => !provided.some((prefix) => name === prefix || name.startsWith(`${prefix}/`)))\n .sort()\n\n if (missing.length === 0) return\n\n throw new Error(\n `sentinel build(nest): the bundle requires ${missing.length} package(s) the generated ` +\n `manifest does not declare, so the image would not install them: ${missing.join(', ')}. ` +\n `Either declare them in the module's package.json, inline them with ` +\n `\\`ssr.noExternal\\`, or list them in \\`providedByImage\\` if the Dockerfile copies them in.`,\n )\n}\n","/**\n * What `@hublo/sentinel/build/nest` gives an adopted service.\n *\n * The sibling `build/react` entry is a re-export surface: the module keeps its own config and\n * only its import lines move. This one carries a shape as well, because 38 Nest services share\n * one config and have nothing of their own to preserve. See `nest-service.ts` for why the two\n * answers differ, and why that is one rule rather than two.\n *\n * Its own entry point, like the React one, so importing it at build time never drags the CLI\n * and its adapters into a Vite config.\n */\nexport { nestService, type NestServiceOptions } from './nest-service.js'\n\n// The pieces the shape is built from, exported individually so a service that needs something\n// the shape does not give it can compose its own without leaving sentinel.\nexport { decoratorMetadata, type DecoratorMetadataOptions } from './decorator-metadata.js'\nexport { nodeManifest, type NodeManifestOptions } from './node-manifest.js'\nexport { tsconfigAliases, type Alias } from './tsconfig-aliases.js'\n\n// Vite's own, so an adopted config imports everything from one place and the module can drop\n// `vite` from its dependencies.\nexport { defineConfig, loadEnv, mergeConfig } from 'vite'\nexport type { Plugin, PluginOption, UserConfig, UserConfigExport } from 'vite'\n"],"mappings":"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","names":["join","existsSync","join","join","mergeConfig"]}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
sharedTestConfig
|
|
3
|
-
} from "../../../chunk-
|
|
3
|
+
} from "../../../chunk-5VNYQIFD.js";
|
|
4
4
|
import {
|
|
5
5
|
decoratorMetadata,
|
|
6
6
|
tsconfigAliases
|
|
7
|
-
} from "../../../chunk-
|
|
7
|
+
} from "../../../chunk-SWWQ7X7B.js";
|
|
8
|
+
import "../../../chunk-QXFCZON7.js";
|
|
8
9
|
|
|
9
10
|
// src/roles/test/nest/toolchain.ts
|
|
10
11
|
import { defineConfig, mergeConfig } from "vitest/config";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/roles/test/nest/toolchain.ts","../../../../src/roles/test/nest/nest-test-config.ts"],"sourcesContent":["/**\n * What `@hublo/sentinel/test/nest` gives an adopted module.\n *\n * Its own entry point, like the build ones, so importing it from a config never drags the CLI and\n * its adapters into a test run.\n *\n * It differs from `test/react` in one way that matters, and the difference is not decoration.\n * React modules already had a Vitest config carrying decisions their owners made and justified, so\n * that preset is a pure re-export surface and moves nothing. Nest modules have NO Vitest config:\n * one has to be written, and four of its lines are load-bearing in ways nobody would guess from a\n * migration guide. Those four live in `nestTestConfig` rather than in 133 copies.\n */\n\n/**\n * From `vitest/config`, and NOT from `vite`, which also exports a `defineConfig`.\n *\n * They are different functions: `vitest/config`'s knows the `test` key, vite's does not. A config\n * built with vite's would load and quietly drop its entire test block, which is the kind of\n * failure that reads as \"vitest found no tests\" and sends someone hunting through globs.\n *\n * The BUILD role's nest entry point re-exports vite's, correctly for its purpose. Importing that\n * one here would be the exact mistake this comment exists to prevent.\n */\nexport { defineConfig, mergeConfig } from 'vitest/config'\n\n/**\n * From `vite`, because `vitest/config` does not export it. Two of the six already-Vitest configs\n * build their test env with it, and Nest services read their env the same way.\n *\n * Re-exported rather than claimed: the `vite` package belongs to the build role, and a module\n * adopting only this one keeps its own.\n */\nexport { loadEnv } from 'vite'\n\n/** The whole config, as one call. See `nest-test-config.ts` for why each line is there. */\nexport { nestTestConfig, type NestTestOptions } from './nest-test-config.js'\n\n/**\n * Re-exported so a module that needs to compose rather than override can reach them without\n * importing the BUILD role's entry point from a test config, which would pull vite's\n * `defineConfig` into scope beside vitest's and invite the mistake above.\n */\nexport {\n decoratorMetadata,\n type DecoratorMetadataOptions,\n} from '../../build/nest/decorator-metadata.js'\nexport { tsconfigAliases, type Alias } from '../../build/nest/tsconfig-aliases.js'\n\nexport type { ConfigEnv, TestUserConfig, ViteUserConfig } from 'vitest/config'\n","import type { ViteUserConfig } from 'vitest/config'\n\n/**\n * The Vitest config a NEST module gets: the shared one, plus the three things that are not shared.\n *\n * Everything load-bearing lives in `shared-test-config.ts`, because almost everything IS shared.\n * Measured across this repo before splitting, so the line falls where the code does and not where\n * the names suggest:\n *\n * jest-mock-extended front 21 nest 2542 shared\n * msw front 848 nest 517 shared\n * axios front 692 nest 863 shared\n * @prisma/ front 0 nest 1186 NEST ONLY\n *\n * So the nest side is three things: the generated Prisma client's runtime alias, `isolate` (a Nest\n * module registers metadata as an import side effect, so a shared module registry lets one suite\n * see what another registered), and the decorator plugin for the one module that needs it.\n */\nimport { sharedTestConfig, type SharedTestOptions } from '../shared-test-config.js'\n\nexport type { SharedTestOptions as NestTestOptions }\n\nexport function nestTestConfig(options: SharedTestOptions): ViteUserConfig {\n return sharedTestConfig({ ...options, flavour: 'nest' })\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/nest/toolchain.ts","../../../../src/roles/test/nest/nest-test-config.ts"],"sourcesContent":["/**\n * What `@hublo/sentinel/test/nest` gives an adopted module.\n *\n * Its own entry point, like the build ones, so importing it from a config never drags the CLI and\n * its adapters into a test run.\n *\n * It differs from `test/react` in one way that matters, and the difference is not decoration.\n * React modules already had a Vitest config carrying decisions their owners made and justified, so\n * that preset is a pure re-export surface and moves nothing. Nest modules have NO Vitest config:\n * one has to be written, and four of its lines are load-bearing in ways nobody would guess from a\n * migration guide. Those four live in `nestTestConfig` rather than in 133 copies.\n */\n\n/**\n * From `vitest/config`, and NOT from `vite`, which also exports a `defineConfig`.\n *\n * They are different functions: `vitest/config`'s knows the `test` key, vite's does not. A config\n * built with vite's would load and quietly drop its entire test block, which is the kind of\n * failure that reads as \"vitest found no tests\" and sends someone hunting through globs.\n *\n * The BUILD role's nest entry point re-exports vite's, correctly for its purpose. Importing that\n * one here would be the exact mistake this comment exists to prevent.\n */\nexport { defineConfig, mergeConfig } from 'vitest/config'\n\n/**\n * From `vite`, because `vitest/config` does not export it. Two of the six already-Vitest configs\n * build their test env with it, and Nest services read their env the same way.\n *\n * Re-exported rather than claimed: the `vite` package belongs to the build role, and a module\n * adopting only this one keeps its own.\n */\nexport { loadEnv } from 'vite'\n\n/** The whole config, as one call. See `nest-test-config.ts` for why each line is there. */\nexport { nestTestConfig, type NestTestOptions } from './nest-test-config.js'\n\n/**\n * Re-exported so a module that needs to compose rather than override can reach them without\n * importing the BUILD role's entry point from a test config, which would pull vite's\n * `defineConfig` into scope beside vitest's and invite the mistake above.\n */\nexport {\n decoratorMetadata,\n type DecoratorMetadataOptions,\n} from '../../build/nest/decorator-metadata.js'\nexport { tsconfigAliases, type Alias } from '../../build/nest/tsconfig-aliases.js'\n\nexport type { ConfigEnv, TestUserConfig, ViteUserConfig } from 'vitest/config'\n","import type { ViteUserConfig } from 'vitest/config'\n\n/**\n * The Vitest config a NEST module gets: the shared one, plus the three things that are not shared.\n *\n * Everything load-bearing lives in `shared-test-config.ts`, because almost everything IS shared.\n * Measured across this repo before splitting, so the line falls where the code does and not where\n * the names suggest:\n *\n * jest-mock-extended front 21 nest 2542 shared\n * msw front 848 nest 517 shared\n * axios front 692 nest 863 shared\n * @prisma/ front 0 nest 1186 NEST ONLY\n *\n * So the nest side is three things: the generated Prisma client's runtime alias, `isolate` (a Nest\n * module registers metadata as an import side effect, so a shared module registry lets one suite\n * see what another registered), and the decorator plugin for the one module that needs it.\n */\nimport { sharedTestConfig, type SharedTestOptions } from '../shared-test-config.js'\n\nexport type { SharedTestOptions as NestTestOptions }\n\nexport function nestTestConfig(options: SharedTestOptions): ViteUserConfig {\n return sharedTestConfig({ ...options, flavour: 'nest' })\n}\n"],"mappings":";;;;;;;;;;AAuBA,SAAS,cAAc,mBAAmB;AAS1C,SAAS,eAAe;;;ACVjB,SAAS,eAAe,SAA4C;AACzE,SAAO,iBAAiB,EAAE,GAAG,SAAS,SAAS,OAAO,CAAC;AACzD;","names":[]}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
jestExportConditions,
|
|
3
3
|
sharedTestConfig
|
|
4
|
-
} from "../../../chunk-
|
|
5
|
-
import "../../../chunk-
|
|
4
|
+
} from "../../../chunk-5VNYQIFD.js";
|
|
5
|
+
import "../../../chunk-SWWQ7X7B.js";
|
|
6
|
+
import "../../../chunk-QXFCZON7.js";
|
|
6
7
|
|
|
7
8
|
// src/roles/test/react/toolchain.ts
|
|
8
9
|
import { defineConfig, mergeConfig } from "vitest/config";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/roles/test/react/toolchain.ts","../../../../src/roles/test/react/react-test-config.ts"],"sourcesContent":["/**\n * What `@hublo/sentinel/test/react` gives an adopted module.\n *\n * A re-export surface, not a shape, and that is the measured answer rather than a preference.\n * The six configs already on Vitest fall into two groups: three React apps that converge on one\n * skeleton, and three modules of twelve to twenty lines that share almost nothing. A shared form\n * would serve half of them.\n *\n * More decisive: those configs carry decisions their owners made and justified in comments, a\n * `pool: 'threads'` measured on 1100 test files, an `experimental.fsModuleCache` measured against\n * transform and import time. Moving them is not a tooling migration, so the role moves the\n * imports and leaves every line of judgement where it is.\n *\n * Its own entry point, like the build ones, so importing it from a config never drags the CLI\n * and its adapters into a test run.\n */\n\n/**\n * From `vitest/config`, and NOT from `vite`, which also exports a `defineConfig`.\n *\n * They are different functions: measured, `vitest/config`'s `defineConfig` is not the same object\n * as vite's, and only it knows the `test` key. A config built with vite's would load and quietly\n * drop its entire test block. `mergeConfig` IS the same function in both, so its source does not\n * matter and it is taken from here for consistency.\n */\nexport { defineConfig, mergeConfig } from 'vitest/config'\n\n/**\n * From `vite`, because `vitest/config` does not export it. Measured, not assumed.\n *\n * Two of the six configs build their test env with it. Re-exported rather than claimed: the\n * `vite` package belongs to the build role, and a module adopting only this one keeps its own.\n */\nexport { loadEnv } from 'vite'\n\n/**\n * The names `vitest/config` actually exports, checked against its own declarations rather than\n * guessed: vite's `UserConfig` is re-exported there AS `ViteUserConfig`, and the test-side one is\n * `TestUserConfig`. Importing `UserConfig` from it compiles nowhere.\n */\nexport type { ConfigEnv, TestUserConfig, ViteUserConfig } from 'vitest/config'\n\n/**\n * The one thing this surface ADDS rather than re-exports, and only because a module cannot write\n * it for itself: Vite appends its own export conditions back over anything a config declares, so\n * restoring jest's resolution takes a plugin. Its own file carries the measurement.\n */\nexport { jestExportConditions } from './jest-export-conditions.js'\n\n/**\n * The config a MIGRATED React module gets, written by `--init --test`.\n *\n * Added on 23/09, because until then a front module being migrated received `nestTestConfig` while\n * the report announced `preset=react`. The re-export surface above is for the six modules that\n * already had a Vitest config and keep their own decisions; this is for the ones that had none and\n * need one written.\n */\nexport { reactTestConfig, type ReactTestOptions } from './react-test-config.js'\n","import type { ViteUserConfig } from 'vitest/config'\n\n/**\n * The Vitest config a REACT module gets: the shared one, without the Nest-only parts.\n *\n * ⚠️ This exists because a front module was being migrated with `nestTestConfig`, and the report\n * then said `preset=react` while the file it had just written imported `test/nest`. A tool that\n * contradicts itself in the same run is worse than one that is simply incomplete.\n *\n * What it does NOT carry, each measured rather than assumed: the `@prisma/*` runtime alias (1186\n * files mention it under nest, ZERO under front) and `isolate` (Nest registers metadata as an\n * import side effect; React has no such catalog, and isolation is not free).\n *\n * What it DOES carry is everything else, which is most of it: the workspace tsconfig path aliases,\n * jest's export conditions, the four test-file extensions, the concurrency this repo runs with, and\n * the msw, axios and jest-mock-extended aliases, all three of which the front uses heavily.\n *\n * Deliberately NOT here: a React plugin. Vite's own transform handles JSX in a test run, and the\n * front suites pass without one, so adding it would be paying for something nothing asked for.\n */\nimport { sharedTestConfig, type SharedTestOptions } from '../shared-test-config.js'\n\nexport type { SharedTestOptions as ReactTestOptions }\n\nexport function reactTestConfig(options: SharedTestOptions): ViteUserConfig {\n return sharedTestConfig({ ...options, flavour: 'react' })\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/react/toolchain.ts","../../../../src/roles/test/react/react-test-config.ts"],"sourcesContent":["/**\n * What `@hublo/sentinel/test/react` gives an adopted module.\n *\n * A re-export surface, not a shape, and that is the measured answer rather than a preference.\n * The six configs already on Vitest fall into two groups: three React apps that converge on one\n * skeleton, and three modules of twelve to twenty lines that share almost nothing. A shared form\n * would serve half of them.\n *\n * More decisive: those configs carry decisions their owners made and justified in comments, a\n * `pool: 'threads'` measured on 1100 test files, an `experimental.fsModuleCache` measured against\n * transform and import time. Moving them is not a tooling migration, so the role moves the\n * imports and leaves every line of judgement where it is.\n *\n * Its own entry point, like the build ones, so importing it from a config never drags the CLI\n * and its adapters into a test run.\n */\n\n/**\n * From `vitest/config`, and NOT from `vite`, which also exports a `defineConfig`.\n *\n * They are different functions: measured, `vitest/config`'s `defineConfig` is not the same object\n * as vite's, and only it knows the `test` key. A config built with vite's would load and quietly\n * drop its entire test block. `mergeConfig` IS the same function in both, so its source does not\n * matter and it is taken from here for consistency.\n */\nexport { defineConfig, mergeConfig } from 'vitest/config'\n\n/**\n * From `vite`, because `vitest/config` does not export it. Measured, not assumed.\n *\n * Two of the six configs build their test env with it. Re-exported rather than claimed: the\n * `vite` package belongs to the build role, and a module adopting only this one keeps its own.\n */\nexport { loadEnv } from 'vite'\n\n/**\n * The names `vitest/config` actually exports, checked against its own declarations rather than\n * guessed: vite's `UserConfig` is re-exported there AS `ViteUserConfig`, and the test-side one is\n * `TestUserConfig`. Importing `UserConfig` from it compiles nowhere.\n */\nexport type { ConfigEnv, TestUserConfig, ViteUserConfig } from 'vitest/config'\n\n/**\n * The one thing this surface ADDS rather than re-exports, and only because a module cannot write\n * it for itself: Vite appends its own export conditions back over anything a config declares, so\n * restoring jest's resolution takes a plugin. Its own file carries the measurement.\n */\nexport { jestExportConditions } from './jest-export-conditions.js'\n\n/**\n * The config a MIGRATED React module gets, written by `--init --test`.\n *\n * Added on 23/09, because until then a front module being migrated received `nestTestConfig` while\n * the report announced `preset=react`. The re-export surface above is for the six modules that\n * already had a Vitest config and keep their own decisions; this is for the ones that had none and\n * need one written.\n */\nexport { reactTestConfig, type ReactTestOptions } from './react-test-config.js'\n","import type { ViteUserConfig } from 'vitest/config'\n\n/**\n * The Vitest config a REACT module gets: the shared one, without the Nest-only parts.\n *\n * ⚠️ This exists because a front module was being migrated with `nestTestConfig`, and the report\n * then said `preset=react` while the file it had just written imported `test/nest`. A tool that\n * contradicts itself in the same run is worse than one that is simply incomplete.\n *\n * What it does NOT carry, each measured rather than assumed: the `@prisma/*` runtime alias (1186\n * files mention it under nest, ZERO under front) and `isolate` (Nest registers metadata as an\n * import side effect; React has no such catalog, and isolation is not free).\n *\n * What it DOES carry is everything else, which is most of it: the workspace tsconfig path aliases,\n * jest's export conditions, the four test-file extensions, the concurrency this repo runs with, and\n * the msw, axios and jest-mock-extended aliases, all three of which the front uses heavily.\n *\n * Deliberately NOT here: a React plugin. Vite's own transform handles JSX in a test run, and the\n * front suites pass without one, so adding it would be paying for something nothing asked for.\n */\nimport { sharedTestConfig, type SharedTestOptions } from '../shared-test-config.js'\n\nexport type { SharedTestOptions as ReactTestOptions }\n\nexport function reactTestConfig(options: SharedTestOptions): ViteUserConfig {\n return sharedTestConfig({ ...options, flavour: 'react' })\n}\n"],"mappings":";;;;;;;;AAyBA,SAAS,cAAc,mBAAmB;AAQ1C,SAAS,eAAe;;;ACTjB,SAAS,gBAAgB,SAA4C;AAC1E,SAAO,iBAAiB,EAAE,GAAG,SAAS,SAAS,QAAQ,CAAC;AAC1D;","names":[]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { RunOptions } from 'axe-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `expect(element).toBeAccessible()`, run by axe-core directly.
|
|
5
|
+
*
|
|
6
|
+
* Opt in from a module's config:
|
|
7
|
+
*
|
|
8
|
+
* setupFiles: ['@hublo/sentinel/test/setup/a11y']
|
|
9
|
+
*
|
|
10
|
+
* ## Why axe-core DIRECTLY, and not jest-axe or vitest-axe
|
|
11
|
+
*
|
|
12
|
+
* Both are thin wrappers whose job is to turn axe's result into a matcher, which is the twenty
|
|
13
|
+
* lines below. What they add on top is a dependency that lags axe itself, its own idea of which
|
|
14
|
+
* rules to run, and a second place where a violation can be filtered out. Measured while choosing:
|
|
15
|
+
* the RULE SET decides the outcome far more than the runner does, 12 messages against 70 on the
|
|
16
|
+
* same markup depending only on configuration. So the configuration is the thing worth owning, and
|
|
17
|
+
* a wrapper that owns it for us is a wrapper that decides for the modules.
|
|
18
|
+
*
|
|
19
|
+
* axe-core is also the engine those wrappers call, so this is not a reimplementation, it is the
|
|
20
|
+
* same engine with one fewer layer between the module and its rules.
|
|
21
|
+
*
|
|
22
|
+
* ## What it asserts, and what it deliberately does not
|
|
23
|
+
*
|
|
24
|
+
* It runs axe on the element it is given and fails on any VIOLATION. It says nothing about
|
|
25
|
+
* "incomplete" results, which axe reports when it cannot decide without a real browser: colour
|
|
26
|
+
* contrast against a computed background, for instance. Under jsdom those are not inconclusive by
|
|
27
|
+
* accident, they are inconclusive by construction, and failing on them would teach everyone to
|
|
28
|
+
* disable the matcher.
|
|
29
|
+
*
|
|
30
|
+
* ⚠️ So a green `toBeAccessible()` means "axe found no violation it could decide here", never "this
|
|
31
|
+
* is accessible". The message says so on failure, and the count of incomplete checks is reported
|
|
32
|
+
* alongside, so nobody reads silence as coverage.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
declare module 'vitest' {
|
|
36
|
+
interface Matchers<T = any> {
|
|
37
|
+
/**
|
|
38
|
+
* Run axe-core on this element and fail on any violation.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ A pass means axe found no violation it could DECIDE under jsdom. Checks axe reports as
|
|
41
|
+
* incomplete, such as colour contrast, are counted in the message and never failed.
|
|
42
|
+
*/
|
|
43
|
+
toBeAccessible: (options?: RunOptions) => Promise<T>;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/roles/test/setup/a11y.ts
|
|
2
|
+
import { expect } from "vitest";
|
|
3
|
+
function describeViolation(violation) {
|
|
4
|
+
const nodes = violation.nodes.slice(0, 3).map((node) => ` ${node.target.join(" ")}
|
|
5
|
+
${node.failureSummary ?? ""}`).join("\n");
|
|
6
|
+
const more = violation.nodes.length > 3 ? `
|
|
7
|
+
\u2026and ${violation.nodes.length - 3} more element(s)` : "";
|
|
8
|
+
return ` ${violation.id} (${violation.impact ?? "unknown impact"}): ${violation.help}
|
|
9
|
+
${violation.helpUrl}
|
|
10
|
+
${nodes}${more}`;
|
|
11
|
+
}
|
|
12
|
+
var engine;
|
|
13
|
+
var NODE = { ELEMENT: 1, DOCUMENT: 9, FRAGMENT: 11 };
|
|
14
|
+
function isDomNode(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && "nodeType" in value;
|
|
16
|
+
}
|
|
17
|
+
function nodeTypeOf(value) {
|
|
18
|
+
return isDomNode(value) ? value.nodeType : void 0;
|
|
19
|
+
}
|
|
20
|
+
function toAxeContext(target) {
|
|
21
|
+
if (nodeTypeOf(target) !== NODE.FRAGMENT) return target;
|
|
22
|
+
const elements = Array.from(target.childNodes).filter(
|
|
23
|
+
(node) => node.nodeType === NODE.ELEMENT
|
|
24
|
+
);
|
|
25
|
+
return elements.length > 0 ? { include: elements } : { include: [target] };
|
|
26
|
+
}
|
|
27
|
+
function assertSupportedDom() {
|
|
28
|
+
const agent = typeof navigator === "undefined" ? "" : (navigator.userAgent ?? "").toLowerCase();
|
|
29
|
+
if (agent.includes("happy-dom")) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`toBeAccessible() needs the "jsdom" environment: axe-core does not support happy-dom. Set \`environment: 'jsdom'\` for this suite, or for this file with \`// @vitest-environment jsdom\`.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function run(target, options) {
|
|
36
|
+
engine ??= import("axe-core");
|
|
37
|
+
const loaded = await engine;
|
|
38
|
+
const axe = loaded.default ?? loaded;
|
|
39
|
+
return axe.run(toAxeContext(target), {
|
|
40
|
+
elementRef: false,
|
|
41
|
+
...options
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
expect.extend({
|
|
45
|
+
async toBeAccessible(received, options) {
|
|
46
|
+
assertSupportedDom();
|
|
47
|
+
if (isDomNode(received) && received.isConnected === false) {
|
|
48
|
+
return {
|
|
49
|
+
pass: false,
|
|
50
|
+
message: () => `toBeAccessible() needs a node that is IN the document: axe analyses the page, and this one is detached. Testing Library's \`asFragment()\` returns a detached fragment, so pass \`container\` instead: expect(render(<X />).container).toBeAccessible().`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const type = nodeTypeOf(received);
|
|
54
|
+
if (type !== NODE.ELEMENT && type !== NODE.DOCUMENT && type !== NODE.FRAGMENT) {
|
|
55
|
+
return {
|
|
56
|
+
pass: false,
|
|
57
|
+
message: () => `toBeAccessible() needs an Element, a Document or a DocumentFragment, and received ${typeof received}. From Testing Library, pass the container: expect(render(<X />).container).toBeAccessible(), or asFragment().`
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const results = await run(received, options);
|
|
61
|
+
const incomplete = results.incomplete.length === 0 ? "" : `
|
|
62
|
+
|
|
63
|
+
${results.incomplete.length} check(s) were INCOMPLETE and are not failures: axe cannot decide them without a real browser (contrast against a computed background, for instance). A pass here means no decidable violation, never "this is accessible".`;
|
|
64
|
+
return {
|
|
65
|
+
pass: results.violations.length === 0,
|
|
66
|
+
message: () => results.violations.length === 0 ? `expected accessibility violations, and axe found none.${incomplete}` : `${results.violations.length} accessibility violation(s):
|
|
67
|
+
|
|
68
|
+
${results.violations.map(describeViolation).join("\n\n")}${incomplete}`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
//# sourceMappingURL=a11y.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/setup/a11y.ts"],"sourcesContent":["/**\n * `expect(element).toBeAccessible()`, run by axe-core directly.\n *\n * Opt in from a module's config:\n *\n * setupFiles: ['@hublo/sentinel/test/setup/a11y']\n *\n * ## Why axe-core DIRECTLY, and not jest-axe or vitest-axe\n *\n * Both are thin wrappers whose job is to turn axe's result into a matcher, which is the twenty\n * lines below. What they add on top is a dependency that lags axe itself, its own idea of which\n * rules to run, and a second place where a violation can be filtered out. Measured while choosing:\n * the RULE SET decides the outcome far more than the runner does, 12 messages against 70 on the\n * same markup depending only on configuration. So the configuration is the thing worth owning, and\n * a wrapper that owns it for us is a wrapper that decides for the modules.\n *\n * axe-core is also the engine those wrappers call, so this is not a reimplementation, it is the\n * same engine with one fewer layer between the module and its rules.\n *\n * ## What it asserts, and what it deliberately does not\n *\n * It runs axe on the element it is given and fails on any VIOLATION. It says nothing about\n * \"incomplete\" results, which axe reports when it cannot decide without a real browser: colour\n * contrast against a computed background, for instance. Under jsdom those are not inconclusive by\n * accident, they are inconclusive by construction, and failing on them would teach everyone to\n * disable the matcher.\n *\n * ⚠️ So a green `toBeAccessible()` means \"axe found no violation it could decide here\", never \"this\n * is accessible\". The message says so on failure, and the count of incomplete checks is reported\n * alongside, so nobody reads silence as coverage.\n */\n/// <reference lib=\"dom\" />\nimport type { AxeResults, Result, RunOptions } from 'axe-core'\nimport { expect } from 'vitest'\n\n/** One violation, rendered the way a developer can act on it. */\nfunction describeViolation(violation: Result): string {\n const nodes = violation.nodes\n .slice(0, 3)\n .map((node) => ` ${node.target.join(' ')}\\n ${node.failureSummary ?? ''}`)\n .join('\\n')\n const more =\n violation.nodes.length > 3 ? `\\n …and ${violation.nodes.length - 3} more element(s)` : ''\n return (\n ` ${violation.id} (${violation.impact ?? 'unknown impact'}): ${violation.help}\\n` +\n ` ${violation.helpUrl}\\n${nodes}${more}`\n )\n}\n\n/**\n * ⚠️ The engine is loaded on FIRST USE, not when this file is read.\n *\n * `setupFiles` runs per test FILE under Vitest's isolation, and axe costs 28 ms to import. On\n * `host-admin`, 1460 files, that is 41 seconds added to every run for a matcher almost no file\n * calls. Lazily, a suite that never asserts on accessibility pays nothing at all, and one that\n * does pays once per file that does.\n *\n * The type import above is erased at build time, so it costs nothing either.\n */\nlet engine: Promise<typeof import('axe-core')> | undefined\n\n/** `nodeType`, because that is the one thing every DOM implementation agrees on. */\nconst NODE = { ELEMENT: 1, DOCUMENT: 9, FRAGMENT: 11 } as const\n\n/**\n * ⚠️ `nodeType`, NOT `instanceof Element`, and the difference is not style.\n *\n * `instanceof` compares against the constructor of THIS realm. A DOM built in another one, which\n * is what happens with jsdom inside a Vitest worker, fails the check while being a perfectly good\n * element, and the matcher then refuses the container it was handed. Taken from the OVH manager\n * kit, which hit it first.\n */\nfunction isDomNode(value: unknown): value is Element | Document | DocumentFragment {\n return typeof value === 'object' && value !== null && 'nodeType' in value\n}\n\nfunction nodeTypeOf(value: unknown): number | undefined {\n return isDomNode(value) ? (value as { nodeType: number }).nodeType : undefined\n}\n\n/**\n * The context axe accepts, from whatever Testing Library handed over.\n *\n * ⚠️ `asFragment()` returns a DocumentFragment, which axe cannot take directly: it is turned into\n * the list of its element children, which is the shape axe documents for a partial context.\n */\nfunction toAxeContext(target: Element | Document | DocumentFragment): unknown {\n if (nodeTypeOf(target) !== NODE.FRAGMENT) return target\n const elements = Array.from((target as DocumentFragment).childNodes).filter(\n (node) => (node as { nodeType: number }).nodeType === NODE.ELEMENT,\n )\n return elements.length > 0 ? { include: elements } : { include: [target] }\n}\n\n/**\n * ⚠️ axe does not work under happy-dom, and says nothing useful when it does not.\n *\n * Refused here with a sentence that names the fix, rather than leaving someone to read an\n * incomprehensible axe error. This repo has an open question about moving to happy-dom, which is\n * exactly why the guard is worth having before that question is answered. Taken from the OVH\n * manager kit.\n */\nfunction assertSupportedDom(): void {\n const agent = typeof navigator === 'undefined' ? '' : (navigator.userAgent ?? '').toLowerCase()\n if (agent.includes('happy-dom')) {\n throw new Error(\n `toBeAccessible() needs the \"jsdom\" environment: axe-core does not support happy-dom. ` +\n `Set \\`environment: 'jsdom'\\` for this suite, or for this file with ` +\n `\\`// @vitest-environment jsdom\\`.`,\n )\n }\n}\n\nasync function run(\n target: Element | Document | DocumentFragment,\n options?: RunOptions,\n): Promise<AxeResults> {\n engine ??= import('axe-core')\n const loaded = await engine\n // The package is CJS with a default export under ESM interop, and a namespace otherwise.\n const axe = (loaded as { default?: typeof loaded }).default ?? loaded\n /*\n * `elementRef: false` because the handle axe would attach is useless once the test has torn the\n * DOM down, and it keeps the result serialisable, which is what lets a reporter print it.\n */\n return axe.run(toAxeContext(target) as Parameters<typeof axe.run>[0], {\n elementRef: false,\n ...options,\n })\n}\n\nexpect.extend({\n async toBeAccessible(received: unknown, options?: RunOptions) {\n assertSupportedDom()\n\n /*\n * ⚠️ axe analyses the PAGE, so a node that is not in it has nothing to analyse, and what axe\n * says about that is `No elements found for include in page Context`, which names neither the\n * cause nor the fix. Measured on a detached `DocumentFragment`, which is exactly what\n * Testing Library's `asFragment()` returns.\n */\n if (isDomNode(received) && (received as { isConnected?: boolean }).isConnected === false) {\n return {\n pass: false,\n message: () =>\n `toBeAccessible() needs a node that is IN the document: axe analyses the page, and this ` +\n `one is detached. Testing Library's \\`asFragment()\\` returns a detached fragment, so ` +\n `pass \\`container\\` instead: expect(render(<X />).container).toBeAccessible().`,\n }\n }\n\n const type = nodeTypeOf(received)\n if (type !== NODE.ELEMENT && type !== NODE.DOCUMENT && type !== NODE.FRAGMENT) {\n return {\n pass: false,\n message: () =>\n `toBeAccessible() needs an Element, a Document or a DocumentFragment, and received ` +\n `${typeof received}. From Testing Library, pass the container: ` +\n `expect(render(<X />).container).toBeAccessible(), or asFragment().`,\n }\n }\n\n const results = await run(received as Element | Document | DocumentFragment, options)\n const incomplete =\n results.incomplete.length === 0\n ? ''\n : `\\n\\n${results.incomplete.length} check(s) were INCOMPLETE and are not failures: axe ` +\n `cannot decide them without a real browser (contrast against a computed background, for ` +\n `instance). A pass here means no decidable violation, never \"this is accessible\".`\n\n return {\n pass: results.violations.length === 0,\n message: () =>\n results.violations.length === 0\n ? `expected accessibility violations, and axe found none.${incomplete}`\n : `${results.violations.length} accessibility violation(s):\\n\\n` +\n `${results.violations.map(describeViolation).join('\\n\\n')}${incomplete}`,\n }\n },\n})\n\ndeclare module 'vitest' {\n // `T = any` to match Vitest's own declaration: TypeScript refuses to merge an interface whose\n // type parameters differ, and the message it gives points at this line rather than at the cause.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n interface Matchers<T = any> {\n /**\n * Run axe-core on this element and fail on any violation.\n *\n * ⚠️ A pass means axe found no violation it could DECIDE under jsdom. Checks axe reports as\n * incomplete, such as colour contrast, are counted in the message and never failed.\n */\n toBeAccessible: (options?: RunOptions) => Promise<T>\n }\n}\n"],"mappings":";AAiCA,SAAS,cAAc;AAGvB,SAAS,kBAAkB,WAA2B;AACpD,QAAM,QAAQ,UAAU,MACrB,MAAM,GAAG,CAAC,EACV,IAAI,CAAC,SAAS,SAAS,KAAK,OAAO,KAAK,GAAG,CAAC;AAAA,UAAa,KAAK,kBAAkB,EAAE,EAAE,EACpF,KAAK,IAAI;AACZ,QAAM,OACJ,UAAU,MAAM,SAAS,IAAI;AAAA,kBAAgB,UAAU,MAAM,SAAS,CAAC,qBAAqB;AAC9F,SACE,KAAK,UAAU,EAAE,KAAK,UAAU,UAAU,gBAAgB,MAAM,UAAU,IAAI;AAAA,MACvE,UAAU,OAAO;AAAA,EAAK,KAAK,GAAG,IAAI;AAE7C;AAYA,IAAI;AAGJ,IAAM,OAAO,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG;AAUrD,SAAS,UAAU,OAAgE;AACjF,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,cAAc;AACtE;AAEA,SAAS,WAAW,OAAoC;AACtD,SAAO,UAAU,KAAK,IAAK,MAA+B,WAAW;AACvE;AAQA,SAAS,aAAa,QAAwD;AAC5E,MAAI,WAAW,MAAM,MAAM,KAAK,SAAU,QAAO;AACjD,QAAM,WAAW,MAAM,KAAM,OAA4B,UAAU,EAAE;AAAA,IACnE,CAAC,SAAU,KAA8B,aAAa,KAAK;AAAA,EAC7D;AACA,SAAO,SAAS,SAAS,IAAI,EAAE,SAAS,SAAS,IAAI,EAAE,SAAS,CAAC,MAAM,EAAE;AAC3E;AAUA,SAAS,qBAA2B;AAClC,QAAM,QAAQ,OAAO,cAAc,cAAc,MAAM,UAAU,aAAa,IAAI,YAAY;AAC9F,MAAI,MAAM,SAAS,WAAW,GAAG;AAC/B,UAAM,IAAI;AAAA,MACR;AAAA,IAGF;AAAA,EACF;AACF;AAEA,eAAe,IACb,QACA,SACqB;AACrB,aAAW,OAAO,UAAU;AAC5B,QAAM,SAAS,MAAM;AAErB,QAAM,MAAO,OAAuC,WAAW;AAK/D,SAAO,IAAI,IAAI,aAAa,MAAM,GAAoC;AAAA,IACpE,YAAY;AAAA,IACZ,GAAG;AAAA,EACL,CAAC;AACH;AAEA,OAAO,OAAO;AAAA,EACZ,MAAM,eAAe,UAAmB,SAAsB;AAC5D,uBAAmB;AAQnB,QAAI,UAAU,QAAQ,KAAM,SAAuC,gBAAgB,OAAO;AACxF,aAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS,MACP;AAAA,MAGJ;AAAA,IACF;AAEA,UAAM,OAAO,WAAW,QAAQ;AAChC,QAAI,SAAS,KAAK,WAAW,SAAS,KAAK,YAAY,SAAS,KAAK,UAAU;AAC7E,aAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS,MACP,qFACG,OAAO,QAAQ;AAAA,MAEtB;AAAA,IACF;AAEA,UAAM,UAAU,MAAM,IAAI,UAAmD,OAAO;AACpF,UAAM,aACJ,QAAQ,WAAW,WAAW,IAC1B,KACA;AAAA;AAAA,EAAO,QAAQ,WAAW,MAAM;AAItC,WAAO;AAAA,MACL,MAAM,QAAQ,WAAW,WAAW;AAAA,MACpC,SAAS,MACP,QAAQ,WAAW,WAAW,IAC1B,yDAAyD,UAAU,KACnE,GAAG,QAAQ,WAAW,MAAM;AAAA;AAAA,EACzB,QAAQ,WAAW,IAAI,iBAAiB,EAAE,KAAK,MAAM,CAAC,GAAG,UAAU;AAAA,IAC9E;AAAA,EACF;AACF,CAAC;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/setup/file-boundary-close.ts"],"sourcesContent":["/**\n * The second half of the file boundary, and the reason it is a second entry at all.\n *\n * Placed LAST in `setupFiles`, after the module's own, so that by the time it runs:\n *\n * - every mock a setup file built is already recorded as infrastructure, and anything built from\n * here on belongs to the test file;\n * - every global a setup file installed is already part of the baseline, and anything added from\n * here on belongs to the test file.\n *\n * Neither could be decided from the first entry. Vitest offers no hook between \"the last setup\n * file finished\" and \"the test file is imported\", and a microtask does not help because setup\n * files are awaited in turn. The order of `setupFiles` is the only way to say \"after the others\",\n * so the boundary is opened by `./file-boundary.js` and closed here.\n */\nimport { PHASE } from './file-boundary-state.js'\n\nconst state = globalThis as Record<symbol, unknown>\n\nstate[PHASE] = 'file'\n\n/*\n * ⚠️ NO global sweep here, and that is a retraction.\n *\n * An earlier version snapshotted `Reflect.ownKeys(globalThis)` per file and deleted whatever a\n * file added. It fixed three tests on `front-components` that read a `window.google` left by\n * another file, and it looked principled: a fresh environment had none of those keys either.\n *\n * It is not the same thing. The other four boundaries RESTORE what the runner used to reset. This\n * one DELETED state belonging to whoever created it, and a library that initialises once per\n * worker never gets it back. Measured on `agency`: `@prisma/*` sets `globalThis.DEBUG` with `??=`\n * when its runtime is first imported, the sweep removed it at the end of that file, and 41 suites\n * then died on `Cannot read properties of undefined (reading 'split')`. The same run is green with\n * isolation, so it was ours. Logging every deletion over a `front-components` run had already said\n * as much and I read it as acceptable: 220 deletions, of which 210 were msw's interceptor symbols.\n *\n * Removing it costs ONE test on `front-components`, and that number is measured, not predicted:\n * an earlier version of this comment claimed 1418 green before the run had happened, and the run\n * said 1417. The survivor is `useFeatureFlags`, and it is not a global a test wrote. It is msw:\n * the sweep was deleting `Symbol(setup-server)` and the three interceptor symbols beside it 210\n * times a run, which forced msw to rebuild per file and hid the fact that its registry carries\n * state from one file to the next. Narrowing the sweep to string keys loses the same single test,\n * which is what pointed at msw in the first place.\n *\n * So the msw leak is real and open, and it belongs to msw's lifecycle rather than to a blanket\n * sweep of the global object. A test that writes to `globalThis` and does not clean up stays the\n * module's to fix, and `--validate` names it.\n */\n"],"mappings":";;;;;AAiBA,IAAM,QAAQ;AAEd,MAAM,KAAK,IAAI;","names":[]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CURRENT_FILE_MOCKS,
|
|
3
|
+
PHASE,
|
|
4
|
+
SHARED_WORKER
|
|
5
|
+
} from "../../../chunk-H3GSGAX2.js";
|
|
6
|
+
import {
|
|
7
|
+
alreadyInstalled
|
|
8
|
+
} from "../../../chunk-NUOQXAYR.js";
|
|
9
|
+
import {
|
|
10
|
+
fromModule
|
|
11
|
+
} from "../../../chunk-ELZHIN6E.js";
|
|
12
|
+
|
|
13
|
+
// src/roles/test/setup/file-boundary.ts
|
|
14
|
+
import { afterAll, afterEach, vi } from "vitest";
|
|
15
|
+
var state = globalThis;
|
|
16
|
+
state[SHARED_WORKER] = true;
|
|
17
|
+
var body = globalThis.document?.body;
|
|
18
|
+
if (body !== void 0) {
|
|
19
|
+
body.innerHTML = "";
|
|
20
|
+
body.removeAttribute("style");
|
|
21
|
+
}
|
|
22
|
+
var testingLibrary = fromModule("@testing-library/react");
|
|
23
|
+
if (typeof testingLibrary?.cleanup === "function") afterEach(testingLibrary.cleanup);
|
|
24
|
+
var mocksInThisFile = /* @__PURE__ */ new Set();
|
|
25
|
+
state[CURRENT_FILE_MOCKS] = mocksInThisFile;
|
|
26
|
+
state[PHASE] = "infrastructure";
|
|
27
|
+
var api = vi;
|
|
28
|
+
if (!alreadyInstalled(vi, "file-boundary")) {
|
|
29
|
+
for (const factory of ["fn", "spyOn"]) {
|
|
30
|
+
const create = api[factory].bind(api);
|
|
31
|
+
api[factory] = (...args) => {
|
|
32
|
+
const mock = create(...args);
|
|
33
|
+
register(mock);
|
|
34
|
+
return mock;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
for (const [name, method] of [
|
|
38
|
+
["clearAllMocks", "mockClear"],
|
|
39
|
+
["resetAllMocks", "mockReset"],
|
|
40
|
+
["restoreAllMocks", "mockRestore"]
|
|
41
|
+
]) {
|
|
42
|
+
api[name] = () => {
|
|
43
|
+
for (const mock of currentFileMocks()) mock[method]?.();
|
|
44
|
+
return api;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
afterAll(() => {
|
|
49
|
+
for (const mock of mocksInThisFile) mock.mockRestore?.();
|
|
50
|
+
mocksInThisFile.clear();
|
|
51
|
+
vi.useRealTimers();
|
|
52
|
+
vi.unstubAllGlobals();
|
|
53
|
+
vi.unstubAllEnvs();
|
|
54
|
+
});
|
|
55
|
+
function register(mock) {
|
|
56
|
+
if (state[PHASE] === "infrastructure") return;
|
|
57
|
+
state[CURRENT_FILE_MOCKS]?.add(mock);
|
|
58
|
+
}
|
|
59
|
+
function currentFileMocks() {
|
|
60
|
+
return state[CURRENT_FILE_MOCKS] ?? /* @__PURE__ */ new Set();
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=file-boundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/setup/file-boundary.ts"],"sourcesContent":["/**\n * The end of a test file, given back its meaning when Vitest is told not to isolate.\n *\n * ## What isolation was paying for\n *\n * With `isolate: true`, which is Vitest's default, every test file gets its own module registry\n * and its own environment. A file that patches a prototype, leaves fake timers installed, writes\n * `window.google` or empties `document.body` cannot reach the next file, because the next file\n * does not share the things it touched. Nobody writes that down, and no test asserts it: it is\n * bought silently, and it is the single most expensive thing a Vitest run does. Measured on\n * `front-components`, 230 files and 1418 tests, on the same machine and in the same session:\n *\n * jest, files in parallel 15.9 s\n * Vitest, isolated (today) 44.6 s\n * Vitest, this boundary, not isolated 13.0 s\n *\n * So the runner is not slower than jest. Re-evaluating the import graph once per file is, and that\n * is exactly what isolation asks for.\n *\n * ## Why a setup file is the only place this can live\n *\n * Measured: Vitest re-executes each `setupFiles` entry for EVERY test file, even with isolation\n * off, while an ordinary imported module is evaluated once per worker. So a hook registered here\n * re-arms per file, and `afterAll` here IS the end-of-file boundary. A hook registered at the\n * import time of a normal module arms once and never again, which is the whole bug behind\n * Testing Library's cleanup below.\n *\n * ## The five things a file must give back\n *\n * Each line was found by bisecting a real failure on `front-components`, never by guessing:\n *\n * | given back | what leaked without it |\n * | ------------------------------ | ---------------------------------------------------------- |\n * | Testing Library's `cleanup` | it registers at IMPORT time, so it armed for one file only |\n * | an empty `document` | a test writing `document.body.innerHTML`, and focus with it |\n * | spies | `vi.spyOn(Element.prototype, 'scrollIntoView')`, left on |\n * | real timers | a file installing fake timers and never restoring them |\n * | the file's own mock register | `vi.resetAllMocks()` reaching a shared fixture |\n *\n * The global object is given back by `./file-boundary-close.js`, which also closes the\n * infrastructure phase this file opens. The two are one mechanism in two entries because the\n * closing one has to run AFTER the module's own setup files, and `setupFiles` order is the only\n * way to say that.\n *\n * ## What this deliberately does NOT do\n *\n * Not a blanket `vi.restoreAllMocks()` at the boundary. Measured: it also resets the\n * `vi.fn().mockImplementation(...)` that a shared fixture exports at module scope, and 30 tests\n * then fail with `observe is not a function`. Isolation RE-CREATED that fixture, it did not\n * restore it, and those are different actions.\n *\n * Not a substitute for isolating a file that calls `vi.mock`. A module already evaluated in the\n * worker cannot be un-evaluated, so a mock declared by a later file cannot reach it. Those files\n * are routed to an isolated project instead; measured on `front-components`, 13 of 230.\n */\nimport { afterAll, afterEach, vi } from 'vitest'\n\nimport { CURRENT_FILE_MOCKS, PHASE, SHARED_WORKER } from './file-boundary-state.js'\nimport { alreadyInstalled } from './install-once.js'\nimport { fromModule } from './module-copy.js'\n\nconst state = globalThis as Record<symbol, unknown>\n\n// Anything whose lifecycle is the worker's rather than the file's reads this.\nstate[SHARED_WORKER] = true\n\n/* ── A document each file starts from ──────────────────────────────────────────────────────────\n *\n * Testing Library's cleanup removes the containers Testing Library rendered, and nothing else. A\n * test that writes `document.body.innerHTML` itself, or focuses an element, used to be forgiven by\n * the next file getting a new document.\n *\n * Reset at the START of the file rather than at its end, on purpose: a file that crashes never\n * runs its `afterAll`, and the next file should not inherit the wreckage of a file that failed.\n */\n// Reached through `globalThis` rather than the `document` global: sentinel's build emits types\n// without the DOM lib, and this same file is loaded by node-environment suites that have no\n// document at all.\nconst body = (globalThis as { document?: { body?: DocumentBody } }).document?.body\nif (body !== undefined) {\n body.innerHTML = ''\n body.removeAttribute('style')\n}\n\n/* ── Testing Library's cleanup, re-armed ───────────────────────────────────────────────────────\n *\n * `@testing-library/react` registers `afterEach(cleanup)` when it is imported, guarded by\n * `RTL_SKIP_AUTO_CLEANUP`. Without isolation that import happens once per worker, so the hook\n * belongs to whichever file pulled the module in first and every later file renders into a\n * document nobody clears. Measured: 342 of 1418 tests fail, almost all of them\n * `Found multiple elements`.\n *\n * Loaded defensively, the way `./workspace.js` loads `dotenv-flow`: a module that does not render\n * React has no reason to have Testing Library installed, and asking for it would make this file\n * unusable there.\n */\nconst testingLibrary = fromModule('@testing-library/react') as { cleanup?: () => void } | undefined\nif (typeof testingLibrary?.cleanup === 'function') afterEach(testingLibrary.cleanup)\n\n/* ── The mock register, given back its file ────────────────────────────────────────────────────\n *\n * Measured on plain Vitest 4, with no sentinel in the picture, against a\n * `vi.fn().mockImplementation(...)`:\n *\n * vi.clearAllMocks() implementation kept\n * vi.resetAllMocks() implementation DESTROYED\n * vi.restoreAllMocks() implementation kept\n *\n * Under jest that destruction could not outlive the file, because the next file re-evaluated the\n * module and rebuilt the mock. Without isolation it reaches every later file in the worker, so one\n * `vi.resetAllMocks()` in one hook leaves a shared fixture returning `undefined` for the rest of\n * the run. Bisected on `front-components`: `useDebounce.test.ts` does exactly that, and ten tests\n * in `Tabs.test.tsx` die of it.\n *\n * Two registers, therefore. What a setup file builds is infrastructure and is never reset; what\n * the test file builds is the file's own and behaves exactly as it always did. The phase is opened\n * here and closed by `./file-boundary-close.js`, last in `setupFiles`.\n */\nconst mocksInThisFile = new Set<MockLike>()\n\nstate[CURRENT_FILE_MOCKS] = mocksInThisFile\nstate[PHASE] = 'infrastructure'\n\nconst api = vi as unknown as MockApi\n\nif (!alreadyInstalled(vi, 'file-boundary')) {\n for (const factory of ['fn', 'spyOn'] as const) {\n const create = api[factory].bind(api)\n api[factory] = (...args: never[]) => {\n const mock = create(...args)\n register(mock)\n return mock\n }\n }\n\n for (const [name, method] of [\n ['clearAllMocks', 'mockClear'],\n ['resetAllMocks', 'mockReset'],\n ['restoreAllMocks', 'mockRestore'],\n ] as const) {\n api[name] = () => {\n for (const mock of currentFileMocks()) mock[method]?.()\n return api\n }\n }\n}\n\nafterAll(() => {\n /*\n * A spy patches something SHARED: a prototype, a global, another module's export. Isolation undid\n * that by throwing the registry away, so restoring at the boundary is the transposition, not an\n * improvement. Restoring the file's plain `vi.fn()` mocks too is harmless, since the file that\n * made them is over.\n */\n for (const mock of mocksInThisFile) mock.mockRestore?.()\n mocksInThisFile.clear()\n\n vi.useRealTimers()\n vi.unstubAllGlobals()\n vi.unstubAllEnvs()\n})\n\n/** Only what this file does to the document. The DOM lib is not in sentinel's build. */\ninterface DocumentBody {\n innerHTML: string\n removeAttribute: (name: string) => void\n}\n\n/** The part of `vi` this file replaces. Vitest's own overloaded types are not needed to say it. */\ninterface MockApi {\n fn: (...args: never[]) => MockLike\n spyOn: (...args: never[]) => MockLike\n clearAllMocks: () => unknown\n resetAllMocks: () => unknown\n restoreAllMocks: () => unknown\n}\n\n/** Only the part of a mock this file touches; Vitest's own generic types are not needed to say it. */\ninterface MockLike {\n mockClear?: () => unknown\n mockReset?: () => unknown\n mockRestore?: () => unknown\n}\n\n/**\n * Remembers a mock only while the TEST FILE is the one building them.\n *\n * ⚠️ An earlier version kept a second set for the mocks a setup file builds, so the global reset\n * APIs could skip them. It retained them instead: a setup file is re-executed per test file, so a\n * `vi.fn()` written at its module scope is a NEW mock every time, and `front-components` has\n * several (`matchMedia` among them). 230 files later the set held hundreds of mocks, each holding\n * its `mock.calls`, and through them whatever was passed in, DOM nodes included.\n *\n * Nothing ever read that set. Skipping the registration says the same thing and keeps nothing.\n */\nfunction register(mock: MockLike): void {\n if (state[PHASE] === 'infrastructure') return\n ;(state[CURRENT_FILE_MOCKS] as Set<MockLike> | undefined)?.add(mock)\n}\n\nfunction currentFileMocks(): Set<MockLike> {\n return (state[CURRENT_FILE_MOCKS] as Set<MockLike> | undefined) ?? new Set()\n}\n"],"mappings":";;;;;;;;;;;;;AAuDA,SAAS,UAAU,WAAW,UAAU;AAMxC,IAAM,QAAQ;AAGd,MAAM,aAAa,IAAI;AAcvB,IAAM,OAAQ,WAAsD,UAAU;AAC9E,IAAI,SAAS,QAAW;AACtB,OAAK,YAAY;AACjB,OAAK,gBAAgB,OAAO;AAC9B;AAcA,IAAM,iBAAiB,WAAW,wBAAwB;AAC1D,IAAI,OAAO,gBAAgB,YAAY,WAAY,WAAU,eAAe,OAAO;AAqBnF,IAAM,kBAAkB,oBAAI,IAAc;AAE1C,MAAM,kBAAkB,IAAI;AAC5B,MAAM,KAAK,IAAI;AAEf,IAAM,MAAM;AAEZ,IAAI,CAAC,iBAAiB,IAAI,eAAe,GAAG;AAC1C,aAAW,WAAW,CAAC,MAAM,OAAO,GAAY;AAC9C,UAAM,SAAS,IAAI,OAAO,EAAE,KAAK,GAAG;AACpC,QAAI,OAAO,IAAI,IAAI,SAAkB;AACnC,YAAM,OAAO,OAAO,GAAG,IAAI;AAC3B,eAAS,IAAI;AACb,aAAO;AAAA,IACT;AAAA,EACF;AAEA,aAAW,CAAC,MAAM,MAAM,KAAK;AAAA,IAC3B,CAAC,iBAAiB,WAAW;AAAA,IAC7B,CAAC,iBAAiB,WAAW;AAAA,IAC7B,CAAC,mBAAmB,aAAa;AAAA,EACnC,GAAY;AACV,QAAI,IAAI,IAAI,MAAM;AAChB,iBAAW,QAAQ,iBAAiB,EAAG,MAAK,MAAM,IAAI;AACtD,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEA,SAAS,MAAM;AAOb,aAAW,QAAQ,gBAAiB,MAAK,cAAc;AACvD,kBAAgB,MAAM;AAEtB,KAAG,cAAc;AACjB,KAAG,iBAAiB;AACpB,KAAG,cAAc;AACnB,CAAC;AAmCD,SAAS,SAAS,MAAsB;AACtC,MAAI,MAAM,KAAK,MAAM,iBAAkB;AACtC,EAAC,MAAM,kBAAkB,GAAiC,IAAI,IAAI;AACrE;AAEA,SAAS,mBAAkC;AACzC,SAAQ,MAAM,kBAAkB,KAAmC,oBAAI,IAAI;AAC7E;","names":[]}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
alreadyInstalled
|
|
3
|
+
} from "../../../chunk-NUOQXAYR.js";
|
|
1
4
|
import {
|
|
2
5
|
installWorkspaceSetup
|
|
3
|
-
} from "../../../chunk-
|
|
6
|
+
} from "../../../chunk-Z7L4FGKP.js";
|
|
7
|
+
import "../../../chunk-ELZHIN6E.js";
|
|
4
8
|
import {
|
|
5
9
|
clearDeepMocks,
|
|
6
10
|
resetDeepMocks
|
|
7
11
|
} from "../../../chunk-2XLX6PFR.js";
|
|
8
|
-
import "../../../chunk-
|
|
12
|
+
import "../../../chunk-KMKQDGI6.js";
|
|
9
13
|
|
|
10
14
|
// src/roles/test/setup/jest-parity.ts
|
|
11
15
|
import { expect, vi } from "vitest";
|
|
@@ -21,6 +25,7 @@ function asConstructable(implementation) {
|
|
|
21
25
|
};
|
|
22
26
|
}
|
|
23
27
|
function installJestConstructorSemantics(vi2) {
|
|
28
|
+
if (alreadyInstalled(vi2, "jest-constructor-semantics")) return;
|
|
24
29
|
const patch = (mock) => {
|
|
25
30
|
const { mockImplementation, mockImplementationOnce } = mock;
|
|
26
31
|
mock.mockImplementation = function(implementation) {
|
|
@@ -48,6 +53,7 @@ function installJestConstructorSemantics(vi2) {
|
|
|
48
53
|
|
|
49
54
|
// src/roles/test/setup/deep-mock-reset.ts
|
|
50
55
|
function installDeepMockReset(vi2) {
|
|
56
|
+
if (alreadyInstalled(vi2, "deep-mock-reset")) return;
|
|
51
57
|
const { resetAllMocks, clearAllMocks } = vi2;
|
|
52
58
|
vi2.resetAllMocks = function extended() {
|
|
53
59
|
const answer = resetAllMocks.call(this);
|
|
@@ -67,6 +73,7 @@ function errorsCompareByMessage(left, right) {
|
|
|
67
73
|
return void 0;
|
|
68
74
|
}
|
|
69
75
|
function installJestErrorEquality(expect2) {
|
|
76
|
+
if (alreadyInstalled(expect2, "jest-error-equality")) return;
|
|
70
77
|
expect2.addEqualityTesters([errorsCompareByMessage]);
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -90,6 +97,7 @@ function withoutJestOnlyOptions(options) {
|
|
|
90
97
|
return { ...rest, toFake: base.filter((timer) => !doNotFake.includes(timer)) };
|
|
91
98
|
}
|
|
92
99
|
function installJestFakeTimerOptions(vi2) {
|
|
100
|
+
if (alreadyInstalled(vi2, "jest-fake-timer-options")) return;
|
|
93
101
|
const inherited = vi2.useFakeTimers.bind(vi2);
|
|
94
102
|
vi2.useFakeTimers = (options) => inherited(withoutJestOnlyOptions(options));
|
|
95
103
|
}
|
|
@@ -115,6 +123,7 @@ function resetLikeJest(mock) {
|
|
|
115
123
|
return mock;
|
|
116
124
|
}
|
|
117
125
|
function installJestMockReset(vi2) {
|
|
126
|
+
if (alreadyInstalled(vi2, "jest-mock-reset")) return;
|
|
118
127
|
for (const name of ["fn", "spyOn"]) {
|
|
119
128
|
const factory = vi2[name].bind(vi2);
|
|
120
129
|
vi2[name] = ((...args) => resetLikeJest(factory(...args)));
|
|
@@ -147,6 +156,13 @@ function installJestRejectedFunction() {
|
|
|
147
156
|
}
|
|
148
157
|
}
|
|
149
158
|
|
|
159
|
+
// src/roles/test/setup/worker-id.ts
|
|
160
|
+
function installWorkerId(env) {
|
|
161
|
+
if (env.JEST_WORKER_ID !== void 0) return;
|
|
162
|
+
if (env.VITEST_POOL_ID === void 0) return;
|
|
163
|
+
env.JEST_WORKER_ID = env.VITEST_POOL_ID;
|
|
164
|
+
}
|
|
165
|
+
|
|
150
166
|
// src/roles/test/setup/jest-parity.ts
|
|
151
167
|
installJestErrorEquality(expect);
|
|
152
168
|
installJestMockReset(vi);
|
|
@@ -155,5 +171,6 @@ installDeepMockReset(vi);
|
|
|
155
171
|
installJestFakeTimerOptions(vi);
|
|
156
172
|
installJestRejectedFunction();
|
|
157
173
|
installJestGlobal(vi);
|
|
174
|
+
installWorkerId(process.env);
|
|
158
175
|
await installWorkspaceSetup();
|
|
159
176
|
//# sourceMappingURL=jest-parity.js.map
|