@hublo/sentinel 1.3.0 → 1.4.0-alpha.2
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 +8 -4
- package/dist/bin/sentinel.d.ts +0 -1
- package/dist/bin/sentinel.js +22 -8
- package/dist/chunk-2XLX6PFR.js +132 -0
- package/dist/chunk-3TDUIKVQ.js +178 -0
- package/dist/{chunk-676GBPMS.js → chunk-4UIZJ3TR.js} +3399 -549
- package/dist/chunk-CPCUPK4J.js +70 -0
- package/dist/chunk-NX4GHIHF.js +25 -0
- package/dist/chunk-PWV3BMDA.js +15 -0
- package/dist/chunk-WLFE5RUU.js +264 -0
- package/dist/index.js +2 -1
- package/dist/roles/build/nest/toolchain.d.ts +4 -36
- package/dist/roles/build/nest/toolchain.js +10 -178
- package/dist/roles/build/nest/toolchain.js.map +1 -0
- package/dist/roles/build/toolchain.js.map +1 -0
- package/dist/roles/test/nest/toolchain.d.ts +29 -0
- package/dist/roles/test/nest/toolchain.js +289 -0
- package/dist/roles/test/nest/toolchain.js.map +1 -0
- package/dist/roles/test/react/toolchain.d.ts +62 -0
- package/dist/roles/test/react/toolchain.js +5 -0
- package/dist/roles/test/react/toolchain.js.map +1 -0
- package/dist/roles/test/setup/mock-extended.d.ts +46 -0
- package/dist/roles/test/setup/mock-extended.js +65 -0
- package/dist/roles/test/setup/mock-extended.js.map +1 -0
- package/dist/roles/test/setup/msw-lifecycle.d.ts +16 -0
- package/dist/roles/test/setup/msw-lifecycle.js +12 -0
- package/dist/roles/test/setup/msw-lifecycle.js.map +1 -0
- package/dist/roles/test/setup/msw-server.d.ts +3 -0
- package/dist/roles/test/setup/msw-server.js +10 -0
- package/dist/roles/test/setup/msw-server.js.map +1 -0
- package/dist/roles/test/setup/nest.d.ts +2 -0
- package/dist/roles/test/setup/nest.js +159 -0
- package/dist/roles/test/setup/nest.js.map +1 -0
- package/dist/roles/test/setup/workspace-entry.d.ts +2 -0
- package/dist/roles/test/setup/workspace-entry.js +8 -0
- package/dist/roles/test/setup/workspace-entry.js.map +1 -0
- package/dist/tsconfig-aliases-Ce6axdJ4.d.ts +36 -0
- package/docs/.gitkeep +0 -0
- package/docs/build-adoption.md +521 -0
- package/docs/format-adoption.md +321 -0
- package/docs/lint-adoption.md +290 -0
- package/docs/performance.md +49 -0
- package/docs/test-adoption.md +175 -0
- package/docs/typescript-adoption.md +184 -0
- package/docs/typescript-traces.md +798 -0
- package/docs/using-sentinel.md +180 -0
- package/docs/validating-a-change.md +101 -0
- package/package.json +32 -5
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/roles/build/toolchain.ts"],"sourcesContent":["/**\n * The build toolchain, re-exported so an app imports it from sentinel instead of from the\n * workspace root.\n *\n * This is what makes adoption a REFACTOR rather than a rewrite, and it is the whole point.\n * Today `vite`, `@vitejs/plugin-react`, `@tailwindcss/vite`, `vite-plugin-svgr` and `nitro`\n * sit in the monorepo's root `package.json`, which is what stops any front app from being\n * built or moved on its own. An app adopts by changing WHERE its tools come from, and nothing\n * else: every attribute of its config, every alias, every path stays exactly as it was.\n *\n * The alternative was to read the app's config and rebuild it around `reactApp`. That was\n * designed and rejected, correctly: it means understanding a file that is code rather than\n * configuration, and its failure mode is a build that SUCCEEDS and ships the wrong bundle.\n * Here there is nothing to understand, so there is nothing to lose. The two configs are\n * equivalent by construction, because only the import specifiers changed.\n *\n * `reactApp` remains, and stays optional: an app that wants the shared composition adopts it\n * as a second, separate step, with `--inspect --build` to check the result. Nobody has to do\n * that to unblock the root.\n *\n * TanStack is deliberately NOT here. 48 app source files import `@tanstack/react-start`\n * directly, so it is a framework the app codes against rather than a tool that builds it, and\n * the router plugin generates the app's own route tree. Same for `@rolldown/plugin-babel` and\n * `@locator/babel-jsx`, which the apps declare themselves: verified, neither is at the root,\n * so neither is sentinel's to take.\n */\nexport { default as tailwindcss } from '@tailwindcss/vite'\nexport { default as react } from '@vitejs/plugin-react'\nexport { nitro } from 'nitro/vite'\nexport { default as svgr } from 'vite-plugin-svgr'\n\n/**\n * Vite's own API, re-exported for the same reason.\n *\n * `defineConfig`, `loadEnv` and `mergeConfig` are what the configs in this repo import from\n * `vite`; once the root drops the package, those imports are the ones that stop resolving.\n * `mergeConfig` is here because of a file the build role nearly missed: `vitest.config.ts`.\n * Three of them import from `vite` (career and host-admin take `loadEnv`, `libs/front/ui`\n * takes `mergeConfig`), and they are a SECOND door into the same toolchain. Note that\n * `vitest/config` exports a `mergeConfig` of its own, which is vitest's and stays put: the\n * rewriter matches on the specifier, never on the name. The types travel\n * with them, because a config that imports `PluginOption` from a package it no longer declares\n * fails to typecheck even though it builds.\n */\nexport { defineConfig, loadEnv, mergeConfig } from 'vite'\nexport type {\n Plugin,\n PluginOption,\n SassPreprocessorOptions,\n UserConfig,\n UserConfigExport,\n} from 'vite'\n"],"mappings":";AA0BA,SAAoB,WAAXA,gBAA8B;AACvC,SAAoB,WAAXA,gBAAwB;AACjC,SAAS,aAAa;AACtB,SAAoB,WAAXA,gBAAuB;AAehC,SAAS,cAAc,SAAS,mBAAmB;","names":["default"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ViteUserConfig } from 'vitest/config';
|
|
2
|
+
export { ConfigEnv, TestUserConfig, ViteUserConfig, defineConfig, mergeConfig } from 'vitest/config';
|
|
3
|
+
export { loadEnv } from 'vite';
|
|
4
|
+
export { A as Alias, D as DecoratorMetadataOptions, d as decoratorMetadata, t as tsconfigAliases } from '../../../tsconfig-aliases-Ce6axdJ4.js';
|
|
5
|
+
|
|
6
|
+
interface NestTestOptions {
|
|
7
|
+
/** The module's own directory: where its specs live and what its config is relative to. */
|
|
8
|
+
root: string;
|
|
9
|
+
/** The workspace root, which is where `tsconfig.base.json` and its path aliases are. */
|
|
10
|
+
workspaceRoot: string;
|
|
11
|
+
/**
|
|
12
|
+
* Lower decorators with TypeScript before oxc sees them, for a module that needs it.
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ Written by `--init --test` from the module's own sources, never by hand, because the
|
|
15
|
+
* condition is not a preference: it is whether a decorator sits on an `abstract` class member,
|
|
16
|
+
* the one construct oxc does not reproduce. See `generate-config.ts` for the measurement.
|
|
17
|
+
*
|
|
18
|
+
* Off by default, and that default is the measured one: on
|
|
19
|
+
* `apps/nest/microservices/mission`, 715 of 716 decorated files need nothing, and running the
|
|
20
|
+
* plugin for all of them took the suite from 98s to 249s.
|
|
21
|
+
*/
|
|
22
|
+
lowerDecoratorsWithTypeScript?: boolean;
|
|
23
|
+
/** Merged over the base. For what a module genuinely needs to differ on, nothing else. */
|
|
24
|
+
overrides?: ViteUserConfig;
|
|
25
|
+
}
|
|
26
|
+
/** The config, with the module's own overrides merged over it. */
|
|
27
|
+
declare function nestTestConfig(options: NestTestOptions): ViteUserConfig;
|
|
28
|
+
|
|
29
|
+
export { type NestTestOptions, nestTestConfig };
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decoratorMetadata,
|
|
3
|
+
tsconfigAliases
|
|
4
|
+
} from "../../../chunk-3TDUIKVQ.js";
|
|
5
|
+
import {
|
|
6
|
+
jestExportConditions
|
|
7
|
+
} from "../../../chunk-NX4GHIHF.js";
|
|
8
|
+
|
|
9
|
+
// src/roles/test/nest/toolchain.ts
|
|
10
|
+
import { defineConfig, mergeConfig } from "vitest/config";
|
|
11
|
+
import { loadEnv } from "vite";
|
|
12
|
+
|
|
13
|
+
// src/roles/test/nest/nest-test-config.ts
|
|
14
|
+
import { existsSync } from "fs";
|
|
15
|
+
import { createRequire } from "module";
|
|
16
|
+
import { dirname, join } from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
function siblingFile(relative) {
|
|
19
|
+
try {
|
|
20
|
+
const sibling = fileURLToPath(new URL(relative, import.meta.url));
|
|
21
|
+
return existsSync(sibling) ? sibling : void 0;
|
|
22
|
+
} catch {
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function resolveFrom(specifier) {
|
|
27
|
+
try {
|
|
28
|
+
return fileURLToPath(import.meta.resolve(specifier));
|
|
29
|
+
} catch {
|
|
30
|
+
try {
|
|
31
|
+
return createRequire(import.meta.url).resolve(specifier);
|
|
32
|
+
} catch {
|
|
33
|
+
return void 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function mockExtendedAdapterPath() {
|
|
38
|
+
return siblingFile("../setup/mock-extended.js");
|
|
39
|
+
}
|
|
40
|
+
function vitestMockExtendedPath() {
|
|
41
|
+
try {
|
|
42
|
+
return fileURLToPath(import.meta.resolve("vitest-mock-extended"));
|
|
43
|
+
} catch {
|
|
44
|
+
try {
|
|
45
|
+
return createRequire(import.meta.url).resolve("vitest-mock-extended");
|
|
46
|
+
} catch {
|
|
47
|
+
return "vitest-mock-extended";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function mswAliases() {
|
|
52
|
+
const server = siblingFile("../setup/msw-server.js");
|
|
53
|
+
const own = resolveFrom("msw");
|
|
54
|
+
return [
|
|
55
|
+
...own === void 0 ? [] : [{ find: /^msw$/, replacement: own }],
|
|
56
|
+
...server === void 0 ? [] : [{ find: /^@hublo\/test\/msw\/server$/, replacement: server }]
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
function luxonAlias(root, workspaceRoot) {
|
|
60
|
+
for (const base of [root, workspaceRoot]) {
|
|
61
|
+
const manifest = join(base, "node_modules", "luxon", "package.json");
|
|
62
|
+
if (existsSync(manifest)) return [{ find: /^luxon$/, replacement: dirname(manifest) }];
|
|
63
|
+
}
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
function axiosAlias(root, workspaceRoot) {
|
|
67
|
+
for (const base of [root, workspaceRoot]) {
|
|
68
|
+
const manifest = join(base, "node_modules", "axios", "package.json");
|
|
69
|
+
if (existsSync(manifest)) return [{ find: /^axios$/, replacement: dirname(manifest) }];
|
|
70
|
+
}
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
function prismaRuntimeAlias(workspaceRoot) {
|
|
74
|
+
const prisma = join(workspaceRoot, "node_modules", "@prisma");
|
|
75
|
+
if (!existsSync(prisma)) return [];
|
|
76
|
+
return [
|
|
77
|
+
{
|
|
78
|
+
find: /^@prisma\/([^/]+)\/runtime\/library$/,
|
|
79
|
+
replacement: join(prisma, "$1", "runtime", "library.js")
|
|
80
|
+
}
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
function baseConfig(options) {
|
|
84
|
+
const { root, workspaceRoot } = options;
|
|
85
|
+
return {
|
|
86
|
+
/*
|
|
87
|
+
* NO decorator-metadata plugin, and that is a measured removal rather than an omission.
|
|
88
|
+
*
|
|
89
|
+
* Nest reads constructor parameter types from `emitDecoratorMetadata`, so this preset used to
|
|
90
|
+
* run the BUILD role's SWC plugin to emit it, on the premise that the bundler does not. That
|
|
91
|
+
* premise was true of esbuild and is false of Vite 8, which transforms with oxc: oxc lowers the
|
|
92
|
+
* decorators itself and emits the metadata, provided a tsconfig with `experimentalDecorators`
|
|
93
|
+
* applies to the file.
|
|
94
|
+
*
|
|
95
|
+
* Measured five times before removing it. On `apps/nest/microservices/mission`, with and
|
|
96
|
+
* without the plugin, the output is strictly identical for `@Injectable()` constructors
|
|
97
|
+
* (including a service caught in a two-file import CYCLE, all 8 parameters resolved), for route
|
|
98
|
+
* handler parameters and return types, and for DTO properties; the suite then passes in 98s
|
|
99
|
+
* instead of 249s. Confirmed by a direct `Reflect.getMetadata` probe on
|
|
100
|
+
* `apps/nest/backends-for-frontends/admin` and on `libs/nest/starter`, and by two A/B runs
|
|
101
|
+
* through this very preset: `agency-notification` (100 tests) identical with and without, and
|
|
102
|
+
* `grid-leave` (3335 tests) identical without.
|
|
103
|
+
*
|
|
104
|
+
* One caveat that the same measurement produced, and which belongs to the CONSUMER rather than
|
|
105
|
+
* here: a property typed by an interface or by a type-alias imported with `import type` emits
|
|
106
|
+
* `Object`. That is `emitDecoratorMetadata`'s own behaviour, identical with and without the
|
|
107
|
+
* plugin, not a Vite regression.
|
|
108
|
+
*
|
|
109
|
+
* The BUILD role keeps its plugin. Its context differs, it feeds a shipped artefact, and
|
|
110
|
+
* removing it there needs its own proof.
|
|
111
|
+
*
|
|
112
|
+
* ⚠️ ONE construct escapes oxc, and the exception is why this line is a condition rather than
|
|
113
|
+
* an empty array: a decorator on an `abstract` class member. swc emitted it, oxc erases the
|
|
114
|
+
* member and the decorator with it, silently. There is exactly one such member in this repo,
|
|
115
|
+
* so `lowerDecoratorsWithTypeScript` buys the plugin back for that module alone.
|
|
116
|
+
*/
|
|
117
|
+
/*
|
|
118
|
+
* `jestExportConditions` is UNCONDITIONAL, and it is the one plugin every migrated module gets.
|
|
119
|
+
*
|
|
120
|
+
* jest resolved with `['node', 'require', 'default']`, read off the installed `@nx/jest/preset`
|
|
121
|
+
* rather than off its documentation. `development` was never in that list, so a package
|
|
122
|
+
* shipping a separate development build loaded its PRODUCTION file. Vite's own conditions carry
|
|
123
|
+
* `development|production`, so `@emotion/cache` loads `emotion-cache.development.cjs.js`, whose
|
|
124
|
+
* extra stylis plugin calls `console.error(':first-child is potentially unsafe...')`, and with
|
|
125
|
+
* `jest-fail-on-console` in the setup that console call IS a failure.
|
|
126
|
+
*
|
|
127
|
+
* Measured on `libs/front/components`: 2 of its 3 remaining failures, both green again with
|
|
128
|
+
* this. `apps/front/front-legacy` has 100 over 26 files from the same cause.
|
|
129
|
+
*
|
|
130
|
+
* Unconditional rather than reserved for a jsdom module, because it is not a statement about
|
|
131
|
+
* the DOM: it is what this migration is for, running the suite the way the runner it was
|
|
132
|
+
* written for ran it. It changes nothing for a module with no dual-published dependency, which
|
|
133
|
+
* is why nest and cloud were measured identical without it.
|
|
134
|
+
*/
|
|
135
|
+
plugins: [
|
|
136
|
+
jestExportConditions(),
|
|
137
|
+
...options.lowerDecoratorsWithTypeScript ? [decoratorMetadata({ root })] : []
|
|
138
|
+
],
|
|
139
|
+
/*
|
|
140
|
+
* The proviso in the paragraph above, made unconditional.
|
|
141
|
+
*
|
|
142
|
+
* oxc lowers decorators from the tsconfig that applies to the file, so a file belonging to NO
|
|
143
|
+
* tsconfig `include` is lowered as if it used the STANDARD decorators, and comes out of Vite as
|
|
144
|
+
* invalid JavaScript. Not merely without metadata: the file fails to parse, and every file that
|
|
145
|
+
* imports it disappears with it.
|
|
146
|
+
*
|
|
147
|
+
* Measured on `apps/nest/microservices/mission`, where ONE uncovered helper,
|
|
148
|
+
* `src/app/test/mission.test-wrapper.ts`, took down 158 of 416 test files. Reproduced on a
|
|
149
|
+
* three-file case: covered file fine, uncovered file `SyntaxError: Invalid or unexpected
|
|
150
|
+
* token`, and this option alone turns it into the same output the covered file gets, metadata
|
|
151
|
+
* included.
|
|
152
|
+
*
|
|
153
|
+
* Declared here rather than left to each module's tsconfig `include`, because the alternative
|
|
154
|
+
* is asking 107 teams to find which of their files no tsconfig covers, which is the work this
|
|
155
|
+
* role exists to do for them.
|
|
156
|
+
*
|
|
157
|
+
* Two things the same measurement established, both deliberate:
|
|
158
|
+
*
|
|
159
|
+
* - it OVERRIDES the tsconfig, it is not a default the tsconfig refines. A file under a
|
|
160
|
+
* tsconfig saying `emitDecoratorMetadata: false` gets metadata anyway. Acceptable because
|
|
161
|
+
* this is the NEST preset and a Nest module is legacy decorators by definition: no tsconfig
|
|
162
|
+
* under `apps/nest`, `libs/nest`, `apps/cloud` or `libs/cloud` sets either option to false.
|
|
163
|
+
* - it needs no polyfill. Without `reflect-metadata` loaded, nothing throws, the metadata is
|
|
164
|
+
* simply unreadable, exactly as before.
|
|
165
|
+
*/
|
|
166
|
+
oxc: { decorator: { legacy: true, emitDecoratorMetadata: true } },
|
|
167
|
+
resolve: {
|
|
168
|
+
alias: [
|
|
169
|
+
...axiosAlias(root, workspaceRoot),
|
|
170
|
+
...luxonAlias(root, workspaceRoot),
|
|
171
|
+
...prismaRuntimeAlias(workspaceRoot),
|
|
172
|
+
...mswAliases(),
|
|
173
|
+
/*
|
|
174
|
+
* `jest-mock-extended` loads `@jest/globals`, which refuses to run outside jest. 2491
|
|
175
|
+
* files import it, 104 of them under `libs/` as SHARED helpers, so migrating those helpers
|
|
176
|
+
* breaks every module still on jest and leaving them breaks every module moved to Vitest.
|
|
177
|
+
* Old and new therefore do not cohabit on shared helpers, which would have killed the
|
|
178
|
+
* per-module plan.
|
|
179
|
+
*
|
|
180
|
+
* This one line removes the constraint: an unmigrated helper resolves to the Vitest fork
|
|
181
|
+
* inside an adopted module and keeps resolving to the jest one everywhere else.
|
|
182
|
+
* `vitest-mock-extended@5.1.1` is a fork of the same package and exports the same names.
|
|
183
|
+
*
|
|
184
|
+
* Measured on `mission`: failing suites went from 159 to 10.
|
|
185
|
+
*/
|
|
186
|
+
{
|
|
187
|
+
find: /^jest-mock-extended$/,
|
|
188
|
+
replacement: mockExtendedAdapterPath() ?? vitestMockExtendedPath()
|
|
189
|
+
},
|
|
190
|
+
...tsconfigAliases(workspaceRoot)
|
|
191
|
+
]
|
|
192
|
+
},
|
|
193
|
+
test: {
|
|
194
|
+
globals: true,
|
|
195
|
+
environment: "node",
|
|
196
|
+
root,
|
|
197
|
+
/*
|
|
198
|
+
* What the repo's jest preset actually matched, copied rather than approximated:
|
|
199
|
+
* `**\/?(*.)+(spec|test).[jt]s?(x)`.
|
|
200
|
+
*
|
|
201
|
+
* Both NAMES, because jest ran both: `**\/*.spec.ts` alone read green while missing three
|
|
202
|
+
* `.test.ts` files and 23 tests, with nothing saying so. And all four EXTENSIONS, for the
|
|
203
|
+
* same reason one notch further out. Measured over the repo's 7063 test files:
|
|
204
|
+
*
|
|
205
|
+
* .ts 5844 .tsx 1108 .js 99 .mjs/.cjs 12
|
|
206
|
+
*
|
|
207
|
+
* The `.ts`-only form cost nothing on nest and cloud, which have none of the others, and it
|
|
208
|
+
* cost `libs/front/api` three files and 13 tests on the first front module it met. Half the
|
|
209
|
+
* front's test files are `.tsx`.
|
|
210
|
+
*
|
|
211
|
+
* `.mjs` and `.cjs` are deliberately OUT: jest's `[jt]s?(x)` does not match them either, and
|
|
212
|
+
* Vitest's own default include does. Running a file the reference never ran is as wrong as
|
|
213
|
+
* skipping one it did.
|
|
214
|
+
*/
|
|
215
|
+
include: ["**/*.spec.[jt]s?(x)", "**/*.test.[jt]s?(x)"],
|
|
216
|
+
/*
|
|
217
|
+
* Kept, and it is not a performance knob. Nest registers metadata as an import SIDE EFFECT:
|
|
218
|
+
* a decorator writes into a catalog when its module loads. Sharing a module registry across
|
|
219
|
+
* files lets one suite see what another registered, and the failure appears in whichever
|
|
220
|
+
* file happens to run second.
|
|
221
|
+
*/
|
|
222
|
+
isolate: true,
|
|
223
|
+
/*
|
|
224
|
+
* Concurrency, transposed from what this repo does today rather than chosen.
|
|
225
|
+
*
|
|
226
|
+
* Every jest target inherits `configurations.ci = { ci: true, runInBand: true }` from the
|
|
227
|
+
* `@nx/jest:jest` key in `nx.json`, and CI invokes every test target with
|
|
228
|
+
* `--configuration=ci`. So on CI every suite in this repo runs ONE FILE AT A TIME today. That
|
|
229
|
+
* key belongs to the jest executor and cannot be touched, because the workspace is mixed: it
|
|
230
|
+
* still serves the modules that have not moved.
|
|
231
|
+
*
|
|
232
|
+
* ⚠️ And it is CI-ONLY. A local run omits `--configuration=ci`, so jest runs files in
|
|
233
|
+
* PARALLEL on a developer's machine. A flat `fileParallelism: false` here would make local
|
|
234
|
+
* runs slower than jest, which loses something the module had. Hence the condition rather
|
|
235
|
+
* than the constant: parallel locally, one file at a time on CI, which is jest on both sides.
|
|
236
|
+
*
|
|
237
|
+
* Measured on three files that each hold the clock for 400ms and record their interval:
|
|
238
|
+
*
|
|
239
|
+
* default files overlap, 402ms
|
|
240
|
+
* fileParallelism: false no overlap, 1442ms
|
|
241
|
+
* maxWorkers: 1 no overlap, 1423ms
|
|
242
|
+
*
|
|
243
|
+
* Both candidates give the property that matters. `fileParallelism` is the one that says what
|
|
244
|
+
* the module MEANS ("do not run my files at the same time"); `maxWorkers` is a pool size, and
|
|
245
|
+
* it is what a module asking for a CAP gets instead (three BFFs ask for 4).
|
|
246
|
+
*
|
|
247
|
+
* A module that declared its own concurrency overrides this, in both environments, exactly as
|
|
248
|
+
* it does today.
|
|
249
|
+
*/
|
|
250
|
+
fileParallelism: !process.env.CI
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function asAliasArray(alias) {
|
|
255
|
+
if (alias === void 0) return [];
|
|
256
|
+
if (Array.isArray(alias)) return alias;
|
|
257
|
+
return Object.entries(alias).map(([find, replacement]) => ({
|
|
258
|
+
find,
|
|
259
|
+
replacement
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
function nestTestConfig(options) {
|
|
263
|
+
const base = baseConfig(options);
|
|
264
|
+
if (options.overrides === void 0) return base;
|
|
265
|
+
return {
|
|
266
|
+
...base,
|
|
267
|
+
...options.overrides,
|
|
268
|
+
plugins: [...base.plugins ?? [], ...options.overrides.plugins ?? []],
|
|
269
|
+
resolve: {
|
|
270
|
+
...base.resolve,
|
|
271
|
+
...options.overrides.resolve,
|
|
272
|
+
// The module's own aliases come AFTER sentinel's, so a module that needs to win can.
|
|
273
|
+
alias: [
|
|
274
|
+
...asAliasArray(base.resolve?.alias),
|
|
275
|
+
...asAliasArray(options.overrides.resolve?.alias)
|
|
276
|
+
]
|
|
277
|
+
},
|
|
278
|
+
test: { ...base.test, ...options.overrides.test }
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
export {
|
|
282
|
+
decoratorMetadata,
|
|
283
|
+
defineConfig,
|
|
284
|
+
loadEnv,
|
|
285
|
+
mergeConfig,
|
|
286
|
+
nestTestConfig,
|
|
287
|
+
tsconfigAliases
|
|
288
|
+
};
|
|
289
|
+
//# sourceMappingURL=toolchain.js.map
|
|
@@ -0,0 +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","/**\n * The Vitest config a Nest module gets, as one call instead of forty lines copied 133 times.\n *\n * The React family needed none of this: those modules already had a Vitest config, so adoption\n * moved its imports and changed nothing else. The Jest family has no config at all, so one has to\n * be WRITTEN, and writing the same four decisions into 133 files is how they drift. A module that\n * genuinely differs still can: `overrides` is merged over the base, the same shape `nestService`\n * uses on the build side.\n *\n * Every line below was established by running a real suite, not by reading a guide. `mission` is\n * the reference: 387 specs, the module the build role refused for having its own webpack config,\n * and the one with the only `isolateModules` in the repo.\n */\nimport { existsSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport type { Alias as ViteAlias, AliasOptions } from 'vite'\nimport type { ViteUserConfig } from 'vitest/config'\n\nimport { decoratorMetadata } from '../../build/nest/decorator-metadata.js'\nimport { tsconfigAliases } from '../../build/nest/tsconfig-aliases.js'\nimport type { Alias } from '../../build/nest/tsconfig-aliases.js'\nimport { jestExportConditions } from '../react/jest-export-conditions.js'\n\nexport interface NestTestOptions {\n /** The module's own directory: where its specs live and what its config is relative to. */\n root: string\n /** The workspace root, which is where `tsconfig.base.json` and its path aliases are. */\n workspaceRoot: string\n /**\n * Lower decorators with TypeScript before oxc sees them, for a module that needs it.\n *\n * ⚠️ Written by `--init --test` from the module's own sources, never by hand, because the\n * condition is not a preference: it is whether a decorator sits on an `abstract` class member,\n * the one construct oxc does not reproduce. See `generate-config.ts` for the measurement.\n *\n * Off by default, and that default is the measured one: on\n * `apps/nest/microservices/mission`, 715 of 716 decorated files need nothing, and running the\n * plugin for all of them took the suite from 98s to 249s.\n */\n lowerDecoratorsWithTypeScript?: boolean\n /** Merged over the base. For what a module genuinely needs to differ on, nothing else. */\n overrides?: ViteUserConfig\n}\n\n/** A file shipped beside this module in the build, or nothing when running from source. */\nfunction siblingFile(relative: string): string | undefined {\n try {\n const sibling = fileURLToPath(new URL(relative, import.meta.url))\n return existsSync(sibling) ? sibling : undefined\n } catch {\n return undefined\n }\n}\n\n/** A package resolved from SENTINEL, so the module gets the copy the setup uses. */\nfunction resolveFrom(specifier: string): string | undefined {\n try {\n return fileURLToPath(import.meta.resolve(specifier))\n } catch {\n try {\n return createRequire(import.meta.url).resolve(specifier)\n } catch {\n return undefined\n }\n }\n}\n\n/**\n * sentinel's own adapter around the fork, which the alias points at when it has been built.\n *\n * The adapter only makes a deep mock answer `in` and `getOwnPropertyDescriptor`, which Vitest's\n * `spyOn` asks and jest's did not; `mock-extended.ts` carries the measurement. Resolved as a\n * sibling file rather than by specifier, because it is not a package.\n *\n * Falls back to the fork itself when there is no build beside this module, which is the case while\n * sentinel's own suite runs from `src`. The alias then behaves exactly as it did before the\n * adapter existed, so a missing build degrades instead of throwing.\n */\nfunction mockExtendedAdapterPath(): string | undefined {\n return siblingFile('../setup/mock-extended.js')\n}\n\n/**\n * The ABSOLUTE path of the Vitest fork, resolved from sentinel rather than named.\n *\n * A bare specifier in an alias is resolved by Vite against the IMPORTER, which here is a spec file\n * inside the adopted module. `vitest-mock-extended` is sentinel's dependency, not the module's, so\n * under pnpm's isolated layout the module cannot see it and the alias points at nothing.\n *\n * Measured on `apps/cloud/agency`: three suites died with \"Cannot find package\n * 'jest-mock-extended'\" while the fork sat in the store, installed and unreachable. Same class of\n * defect as the axios alias below, and the same fix: resolve it where it actually is.\n *\n * Falls back to the bare name, which keeps the previous behaviour rather than throwing while a\n * config is being built.\n */\nfunction vitestMockExtendedPath(): string {\n /*\n * `import.meta.resolve` and not `createRequire().resolve`: the latter follows the `require`\n * condition and hands back the package's CJS entry, whose first line requires `vitest`. Vitest\n * refuses that outright (\"Vitest cannot be imported in a CommonJS module using require()\"), so\n * the alias resolved correctly and then failed one step later. Measured on `apps/cloud/agency`.\n */\n try {\n return fileURLToPath(import.meta.resolve('vitest-mock-extended'))\n } catch {\n try {\n return createRequire(import.meta.url).resolve('vitest-mock-extended')\n } catch {\n return 'vitest-mock-extended'\n }\n }\n}\n\n/**\n * The ONE msw copy a module's tests share with the server that listens.\n *\n * sentinel ships msw so that a module stands alone, and the codemod repoints the SPEC files to it.\n * Helpers are not spec files, so they keep `import { rest } from 'msw'` and build their handlers\n * with the WORKSPACE's copy while the server that listens was built with sentinel's. Same version,\n * two physical instances, and a handler registered on one is invisible to the other.\n *\n * Measured on `apps/cloud/shift-offer`, whose `setup-mocks.ts` registers every handler its suites\n * rely on: 19 tests failed on `captured a request without a matching request handler` for a URL the\n * helper had a handler for. The repo has 159 non-spec files importing msw, and 2 importing the\n * workspace's server directly, so this is not one module's habit.\n *\n * Aliased rather than codemodded, for the same reason as `jest-mock-extended` above: it lets an\n * UNMIGRATED shared helper keep working inside a migrated module, which is what makes the\n * per-module plan possible at all. `^msw$` only: `msw/node` stays the package's own, and it is\n * sentinel's setup that imports it.\n */\nfunction mswAliases(): Alias[] {\n const server = siblingFile('../setup/msw-server.js')\n const own = resolveFrom('msw')\n return [\n ...(own === undefined ? [] : [{ find: /^msw$/, replacement: own }]),\n ...(server === undefined ? [] : [{ find: /^@hublo\\/test\\/msw\\/server$/, replacement: server }]),\n ]\n}\n\n/**\n * One PHYSICAL axios, resolved from the MODULE and not from sentinel.\n *\n * pnpm installs axios twice here, same 1.17.0, differing only by a peer. The service client\n * resolves one copy and a provider's `import { AxiosError } from 'axios'` the other, so\n * `if (!(error instanceof AxiosError)) throw error` answers false on a genuine axios error and\n * rethrows it raw. The test then fails on the WRAPPER being absent, naming nothing near the cause.\n *\n * `resolve.dedupe` does NOT fix this, and neither does `deps.inline`: dedupe settles bare\n * specifiers, not two physical paths. The alias has to name the path.\n *\n * The module's OWN `node_modules`, checked directly rather than resolved.\n *\n * `createRequire(...).resolve()` was the first version and it is wrong twice. It walks UP the\n * tree, so a module without axios would be aliased to a PARENT's copy, which is the opposite of\n * pinning its own. And because it consults ambient resolution, the answer depends on where the\n * process runs: it correctly found nothing from a plain CLI run and found something under Vitest,\n * which is how a test of it became a test of its environment.\n *\n * pnpm links a module's direct dependencies into `<module>/node_modules`, so that path is both the\n * right answer and a deterministic one.\n *\n * Measured on `mission`: 378 passing suites to 381 of 387.\n *\n * ## ⚠️ And the workspace root when the module has none of its own\n *\n * The first version stopped there and returned nothing for a module without its own axios, on the\n * reasoning that pinning it to a PARENT's copy is the opposite of pinning its own. That reasoning\n * has an blind spot: a module with no copy of its own does not thereby have no axios PROBLEM. It\n * receives one through its dependencies, and it is still importing it two ways.\n *\n * Measured in `apps/nest/backends-for-frontends/backoffice`, which declares no axios, under this\n * very preset with no alias:\n *\n * the ESM AxiosError === the CJS AxiosError false\n * a CJS error instanceof the CJS class true\n * a CJS error instanceof the ESM class FALSE\n *\n * `@nestjs/axios` is externalised, so Node loads it and it throws the CJS class; the provider does\n * `import { AxiosError } from 'axios'` through Vite and gets the ESM one. Its\n * `if (error instanceof AxiosError) throw new RemoteBackendException(...)` therefore answers false\n * on a genuine axios error and rethrows it raw, and the test fails on the WRAPPER being absent,\n * naming nothing near the cause. Four modules reported it: `admin`, `backoffice`, `mission`,\n * `institution`.\n *\n * So: the module's own copy first, the workspace root second, nearest wins. That is the same shape\n * as `luxonAlias` above, and for the same reason: when a module has nothing of its own, the copy\n * it actually uses is the one to pin. Absent axios everywhere is still not an error.\n */\n/**\n * ONE physical luxon, for the same reason as axios: `Settings` is per-copy state.\n *\n * The setup pins `Settings.defaultZone = 'utc'`, which the root jest setup did for every suite in\n * the repo. It was reaching the copy `require` resolves, the CJS build, while the tests import the\n * one Vite resolves, the ESM build. Two instances, two `Settings`, and the pin landed on the one\n * nobody read.\n *\n * Measured on `libs/cloud/events-notifications`: the suite ran in `Europe/Paris`, so every date\n * came out two hours off and 12 assertions compared timestamps that differed by exactly that.\n *\n * luxon sits at the workspace root here rather than in each module, so both places are checked,\n * nearest first. Absent luxon is not an error: most modules have none.\n */\nfunction luxonAlias(root: string, workspaceRoot: string): Alias[] {\n for (const base of [root, workspaceRoot]) {\n const manifest = join(base, 'node_modules', 'luxon', 'package.json')\n if (existsSync(manifest)) return [{ find: /^luxon$/, replacement: dirname(manifest) }]\n }\n return []\n}\n\nfunction axiosAlias(root: string, workspaceRoot: string): Alias[] {\n for (const base of [root, workspaceRoot]) {\n const manifest = join(base, 'node_modules', 'axios', 'package.json')\n if (existsSync(manifest)) return [{ find: /^axios$/, replacement: dirname(manifest) }]\n }\n return []\n}\n\n/**\n * A generated Prisma client's runtime, resolved to the file the package actually ships.\n *\n * The generated client reaches its own runtime by package name, and the package's `exports` map\n * answers differently depending on which condition asks:\n *\n * \"./runtime/library\": { \"require\": \"./runtime/library.js\", // shipped\n * \"import\": \"./runtime/library.mjs\" } // NOT shipped\n *\n * jest asked as CJS and got the file. Vitest resolves the same specifier under `import`, is sent\n * to a `.mjs` that does not exist, and the spec file does not load at all, so its tests go missing\n * rather than failing. Node itself spells the answer out: \"Did you mean to import\n * .../runtime/library.js?\".\n *\n * Measured on `apps/nest/microservices/client-management` (1 file) and\n * `apps/nest/microservices/institution` (3 files), on two different generated clients. This repo\n * generates 31 of them, all with the same manifest, so this is a property of the generator rather\n * than of a module.\n *\n * Written as a pattern with a back-reference, not one entry per client: the module does not know\n * which clients its dependencies pull in, and enumerating 31 names would go stale the day a\n * thirty-second schema is added.\n *\n * ⚠️ Not a workaround for a mistake of ours. The package declares a target it does not ship, and\n * pointing at the shipped file is what `require` already did. Nothing else changes: the `.js` IS\n * the runtime, in the same package, at the version installed.\n */\nfunction prismaRuntimeAlias(workspaceRoot: string): Alias[] {\n const prisma = join(workspaceRoot, 'node_modules', '@prisma')\n if (!existsSync(prisma)) return []\n\n return [\n {\n find: /^@prisma\\/([^/]+)\\/runtime\\/library$/,\n replacement: join(prisma, '$1', 'runtime', 'library.js'),\n },\n ]\n}\n\n/**\n * The base every Nest module gets. Four decisions, each with its measurement.\n */\nfunction baseConfig(options: NestTestOptions): ViteUserConfig {\n const { root, workspaceRoot } = options\n\n return {\n /*\n * NO decorator-metadata plugin, and that is a measured removal rather than an omission.\n *\n * Nest reads constructor parameter types from `emitDecoratorMetadata`, so this preset used to\n * run the BUILD role's SWC plugin to emit it, on the premise that the bundler does not. That\n * premise was true of esbuild and is false of Vite 8, which transforms with oxc: oxc lowers the\n * decorators itself and emits the metadata, provided a tsconfig with `experimentalDecorators`\n * applies to the file.\n *\n * Measured five times before removing it. On `apps/nest/microservices/mission`, with and\n * without the plugin, the output is strictly identical for `@Injectable()` constructors\n * (including a service caught in a two-file import CYCLE, all 8 parameters resolved), for route\n * handler parameters and return types, and for DTO properties; the suite then passes in 98s\n * instead of 249s. Confirmed by a direct `Reflect.getMetadata` probe on\n * `apps/nest/backends-for-frontends/admin` and on `libs/nest/starter`, and by two A/B runs\n * through this very preset: `agency-notification` (100 tests) identical with and without, and\n * `grid-leave` (3335 tests) identical without.\n *\n * One caveat that the same measurement produced, and which belongs to the CONSUMER rather than\n * here: a property typed by an interface or by a type-alias imported with `import type` emits\n * `Object`. That is `emitDecoratorMetadata`'s own behaviour, identical with and without the\n * plugin, not a Vite regression.\n *\n * The BUILD role keeps its plugin. Its context differs, it feeds a shipped artefact, and\n * removing it there needs its own proof.\n *\n * ⚠️ ONE construct escapes oxc, and the exception is why this line is a condition rather than\n * an empty array: a decorator on an `abstract` class member. swc emitted it, oxc erases the\n * member and the decorator with it, silently. There is exactly one such member in this repo,\n * so `lowerDecoratorsWithTypeScript` buys the plugin back for that module alone.\n */\n /*\n * `jestExportConditions` is UNCONDITIONAL, and it is the one plugin every migrated module gets.\n *\n * jest resolved with `['node', 'require', 'default']`, read off the installed `@nx/jest/preset`\n * rather than off its documentation. `development` was never in that list, so a package\n * shipping a separate development build loaded its PRODUCTION file. Vite's own conditions carry\n * `development|production`, so `@emotion/cache` loads `emotion-cache.development.cjs.js`, whose\n * extra stylis plugin calls `console.error(':first-child is potentially unsafe...')`, and with\n * `jest-fail-on-console` in the setup that console call IS a failure.\n *\n * Measured on `libs/front/components`: 2 of its 3 remaining failures, both green again with\n * this. `apps/front/front-legacy` has 100 over 26 files from the same cause.\n *\n * Unconditional rather than reserved for a jsdom module, because it is not a statement about\n * the DOM: it is what this migration is for, running the suite the way the runner it was\n * written for ran it. It changes nothing for a module with no dual-published dependency, which\n * is why nest and cloud were measured identical without it.\n */\n plugins: [\n jestExportConditions(),\n ...(options.lowerDecoratorsWithTypeScript ? [decoratorMetadata({ root })] : []),\n ],\n\n /*\n * The proviso in the paragraph above, made unconditional.\n *\n * oxc lowers decorators from the tsconfig that applies to the file, so a file belonging to NO\n * tsconfig `include` is lowered as if it used the STANDARD decorators, and comes out of Vite as\n * invalid JavaScript. Not merely without metadata: the file fails to parse, and every file that\n * imports it disappears with it.\n *\n * Measured on `apps/nest/microservices/mission`, where ONE uncovered helper,\n * `src/app/test/mission.test-wrapper.ts`, took down 158 of 416 test files. Reproduced on a\n * three-file case: covered file fine, uncovered file `SyntaxError: Invalid or unexpected\n * token`, and this option alone turns it into the same output the covered file gets, metadata\n * included.\n *\n * Declared here rather than left to each module's tsconfig `include`, because the alternative\n * is asking 107 teams to find which of their files no tsconfig covers, which is the work this\n * role exists to do for them.\n *\n * Two things the same measurement established, both deliberate:\n *\n * - it OVERRIDES the tsconfig, it is not a default the tsconfig refines. A file under a\n * tsconfig saying `emitDecoratorMetadata: false` gets metadata anyway. Acceptable because\n * this is the NEST preset and a Nest module is legacy decorators by definition: no tsconfig\n * under `apps/nest`, `libs/nest`, `apps/cloud` or `libs/cloud` sets either option to false.\n * - it needs no polyfill. Without `reflect-metadata` loaded, nothing throws, the metadata is\n * simply unreadable, exactly as before.\n */\n oxc: { decorator: { legacy: true, emitDecoratorMetadata: true } },\n\n resolve: {\n alias: [\n ...axiosAlias(root, workspaceRoot),\n ...luxonAlias(root, workspaceRoot),\n ...prismaRuntimeAlias(workspaceRoot),\n ...mswAliases(),\n /*\n * `jest-mock-extended` loads `@jest/globals`, which refuses to run outside jest. 2491\n * files import it, 104 of them under `libs/` as SHARED helpers, so migrating those helpers\n * breaks every module still on jest and leaving them breaks every module moved to Vitest.\n * Old and new therefore do not cohabit on shared helpers, which would have killed the\n * per-module plan.\n *\n * This one line removes the constraint: an unmigrated helper resolves to the Vitest fork\n * inside an adopted module and keeps resolving to the jest one everywhere else.\n * `vitest-mock-extended@5.1.1` is a fork of the same package and exports the same names.\n *\n * Measured on `mission`: failing suites went from 159 to 10.\n */\n {\n find: /^jest-mock-extended$/,\n replacement: mockExtendedAdapterPath() ?? vitestMockExtendedPath(),\n },\n ...tsconfigAliases(workspaceRoot),\n ],\n },\n\n test: {\n globals: true,\n environment: 'node',\n root,\n /*\n * What the repo's jest preset actually matched, copied rather than approximated:\n * `**\\/?(*.)+(spec|test).[jt]s?(x)`.\n *\n * Both NAMES, because jest ran both: `**\\/*.spec.ts` alone read green while missing three\n * `.test.ts` files and 23 tests, with nothing saying so. And all four EXTENSIONS, for the\n * same reason one notch further out. Measured over the repo's 7063 test files:\n *\n * .ts 5844 .tsx 1108 .js 99 .mjs/.cjs 12\n *\n * The `.ts`-only form cost nothing on nest and cloud, which have none of the others, and it\n * cost `libs/front/api` three files and 13 tests on the first front module it met. Half the\n * front's test files are `.tsx`.\n *\n * `.mjs` and `.cjs` are deliberately OUT: jest's `[jt]s?(x)` does not match them either, and\n * Vitest's own default include does. Running a file the reference never ran is as wrong as\n * skipping one it did.\n */\n include: ['**/*.spec.[jt]s?(x)', '**/*.test.[jt]s?(x)'],\n /*\n * Kept, and it is not a performance knob. Nest registers metadata as an import SIDE EFFECT:\n * a decorator writes into a catalog when its module loads. Sharing a module registry across\n * files lets one suite see what another registered, and the failure appears in whichever\n * file happens to run second.\n */\n isolate: true,\n /*\n * Concurrency, transposed from what this repo does today rather than chosen.\n *\n * Every jest target inherits `configurations.ci = { ci: true, runInBand: true }` from the\n * `@nx/jest:jest` key in `nx.json`, and CI invokes every test target with\n * `--configuration=ci`. So on CI every suite in this repo runs ONE FILE AT A TIME today. That\n * key belongs to the jest executor and cannot be touched, because the workspace is mixed: it\n * still serves the modules that have not moved.\n *\n * ⚠️ And it is CI-ONLY. A local run omits `--configuration=ci`, so jest runs files in\n * PARALLEL on a developer's machine. A flat `fileParallelism: false` here would make local\n * runs slower than jest, which loses something the module had. Hence the condition rather\n * than the constant: parallel locally, one file at a time on CI, which is jest on both sides.\n *\n * Measured on three files that each hold the clock for 400ms and record their interval:\n *\n * default files overlap, 402ms\n * fileParallelism: false no overlap, 1442ms\n * maxWorkers: 1 no overlap, 1423ms\n *\n * Both candidates give the property that matters. `fileParallelism` is the one that says what\n * the module MEANS (\"do not run my files at the same time\"); `maxWorkers` is a pool size, and\n * it is what a module asking for a CAP gets instead (three BFFs ask for 4).\n *\n * A module that declared its own concurrency overrides this, in both environments, exactly as\n * it does today.\n */\n fileParallelism: !process.env.CI,\n },\n }\n}\n\n/**\n * Vite accepts `alias` as an ARRAY or as an object map, and the two do not combine.\n *\n * The array form is the one that matters here: only it takes a RegExp `find`, which both the axios\n * and the `jest-mock-extended` aliases need. A module overriding with the object form gets its\n * entries converted rather than dropped, because silently losing an override is worse than a shape\n * the caller did not expect.\n */\nfunction asAliasArray(alias: AliasOptions | undefined): ViteAlias[] {\n if (alias === undefined) return []\n if (Array.isArray(alias)) return alias as ViteAlias[]\n return Object.entries(alias as Record<string, string>).map(([find, replacement]) => ({\n find,\n replacement,\n }))\n}\n\n/** The config, with the module's own overrides merged over it. */\nexport function nestTestConfig(options: NestTestOptions): ViteUserConfig {\n const base = baseConfig(options)\n if (options.overrides === undefined) return base\n // Shallow by section rather than a deep merge helper: the three sections a module overrides in\n // practice are `test`, `resolve` and `plugins`, and a deep merge would silently concatenate\n // arrays a module meant to replace.\n return {\n ...base,\n ...options.overrides,\n plugins: [...(base.plugins ?? []), ...(options.overrides.plugins ?? [])],\n resolve: {\n ...base.resolve,\n ...options.overrides.resolve,\n // The module's own aliases come AFTER sentinel's, so a module that needs to win can.\n alias: [\n ...asAliasArray(base.resolve?.alias),\n ...asAliasArray(options.overrides.resolve?.alias),\n ],\n },\n test: { ...base.test, ...options.overrides.test },\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,2 +1,64 @@
|
|
|
1
1
|
export { ConfigEnv, TestUserConfig, ViteUserConfig, defineConfig, mergeConfig } from 'vitest/config';
|
|
2
|
+
import { Plugin } from 'vite';
|
|
2
3
|
export { loadEnv } from 'vite';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve packages the way jest did, for a suite that was written against jest's resolution.
|
|
7
|
+
*
|
|
8
|
+
* ## What jest resolved, measured rather than remembered
|
|
9
|
+
*
|
|
10
|
+
* Every suite in this repo ran through `@nx/jest/preset`, whose
|
|
11
|
+
* `testEnvironmentOptions.customExportConditions` is exactly `['node', 'require', 'default']`
|
|
12
|
+
* (read off the installed preset, not off its documentation). `development` was never in that
|
|
13
|
+
* list, so a package shipping a separate development build behind that condition loaded its
|
|
14
|
+
* PRODUCTION file under jest.
|
|
15
|
+
*
|
|
16
|
+
* Vitest resolves with Vite's conditions instead, and its default carries the
|
|
17
|
+
* `development|production` token. Measured on a bare config, the resolved list is
|
|
18
|
+
* `['node', 'development|production']`. `@emotion/cache` then loads
|
|
19
|
+
* `emotion-cache.development.cjs.js`, whose extra stylis plugin calls
|
|
20
|
+
* `console.error(':first-child is potentially unsafe...')`.
|
|
21
|
+
*
|
|
22
|
+
* With `jest-fail-on-console` in the setup, that console call IS a failure: **100 tests over 26
|
|
23
|
+
* files on `apps/front/front-legacy`**, every one of them green under jest. `libs/front/components`
|
|
24
|
+
* hits the same root cause on one test.
|
|
25
|
+
*
|
|
26
|
+
* ## Why a plugin and not a config line
|
|
27
|
+
*
|
|
28
|
+
* Declaring `ssr.resolve.conditions: ['node']` does nothing: Vite merges config arrays by
|
|
29
|
+
* CONCATENATION, so Vitest's default is appended straight back. The token has to come off the
|
|
30
|
+
* RESOLVED config, which is what `configResolved` is for.
|
|
31
|
+
*
|
|
32
|
+
* This is the same shape as the other thing that cannot be declared: a module cannot reproduce
|
|
33
|
+
* `customExportConditions` either, because the list it asks for is PREFIXED to Vite's defaults
|
|
34
|
+
* rather than substituted for them.
|
|
35
|
+
*
|
|
36
|
+
* ## The three lists are one array
|
|
37
|
+
*
|
|
38
|
+
* Measured on Vite 8: `config.resolve.conditions`, `config.ssr.resolve.conditions` and
|
|
39
|
+
* `config.environments.ssr.resolve.conditions` are the SAME array object, so stripping one strips
|
|
40
|
+
* all three. All three are stripped anyway. Relying on an aliasing that nothing promises is how a
|
|
41
|
+
* silent regression arrives on a Vite upgrade, and the cost of being explicit is two lines.
|
|
42
|
+
*
|
|
43
|
+
* ## ⚠️ What this deliberately hides, said out loud
|
|
44
|
+
*
|
|
45
|
+
* emotion's warning is REAL: `libs/front/components` is consumed by a Next app, and a
|
|
46
|
+
* `:first-child` selector is genuinely unsafe when the markup is rendered server-side. Restoring
|
|
47
|
+
* jest's resolution puts that warning back out of sight.
|
|
48
|
+
*
|
|
49
|
+
* It is hidden here on purpose all the same, because a migration that also turns 100 green tests
|
|
50
|
+
* red cannot be told apart from a migration that broke something. The baseline gate compares test
|
|
51
|
+
* names, and it has no way to know which reds are progress. The selector belongs to the module's
|
|
52
|
+
* owners as its own piece of work, with its own ticket, not as a side effect of changing runner.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Add to `plugins` in a module whose suite was written against jest's resolution.
|
|
57
|
+
*
|
|
58
|
+
* Not applied by the preset for everyone: it is a MIGRATION aid, and a module that was always on
|
|
59
|
+
* Vitest never had jest's resolution to go back to. A module adopts it the day it migrates, and
|
|
60
|
+
* can drop it the day its suite no longer depends on the production build.
|
|
61
|
+
*/
|
|
62
|
+
declare function jestExportConditions(): Plugin;
|
|
63
|
+
|
|
64
|
+
export { jestExportConditions };
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
jestExportConditions
|
|
3
|
+
} from "../../../chunk-NX4GHIHF.js";
|
|
4
|
+
|
|
1
5
|
// src/roles/test/react/toolchain.ts
|
|
2
6
|
import { defineConfig, mergeConfig } from "vitest/config";
|
|
3
7
|
import { loadEnv } from "vite";
|
|
4
8
|
export {
|
|
5
9
|
defineConfig,
|
|
10
|
+
jestExportConditions,
|
|
6
11
|
loadEnv,
|
|
7
12
|
mergeConfig
|
|
8
13
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/react/toolchain.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"],"mappings":";;;;;AAyBA,SAAS,cAAc,mBAAmB;AAQ1C,SAAS,eAAe;","names":[]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as mockExtended from 'vitest-mock-extended';
|
|
2
|
+
export * from 'vitest-mock-extended';
|
|
3
|
+
export { any, anyArray, anyBoolean, anyFunction, anyMap, anyNumber, anyObject, anySet, anyString, anySymbol, arrayIncludes, calledWithFn, captor, isA, isMockObject, mapHas, matches, mockClear, mockFn, mockReset, mocked, mockedFn, notEmpty, notNull, notUndefined, objectContainsKey } from 'vitest-mock-extended';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `jest-mock-extended`'s deep mocks, made answerable to the question Vitest's `spyOn` asks.
|
|
7
|
+
*
|
|
8
|
+
* A deep mock is a Proxy that invents a property the first time something READS it. Nothing exists
|
|
9
|
+
* until then, so the object reports itself empty:
|
|
10
|
+
*
|
|
11
|
+
* const provider = mockDeep<InstitutionProvider>()
|
|
12
|
+
* 'getAdminFirstAndLastNames' in provider // false
|
|
13
|
+
* Object.getOwnPropertyDescriptor(provider, 'getAdmin...') // undefined
|
|
14
|
+
* typeof provider.getAdminFirstAndLastNames // 'function', and now it exists
|
|
15
|
+
*
|
|
16
|
+
* jest's `spyOn` reads the property, so the Proxy created it and the spy worked. Vitest's asks the
|
|
17
|
+
* object whether it HAS the property first, gets no for both questions, and throws
|
|
18
|
+
* `The property "getAdminFirstAndLastNames" is not defined on the object.` The file then reports no
|
|
19
|
+
* test at all, so its names simply go missing rather than failing.
|
|
20
|
+
*
|
|
21
|
+
* Measured on `apps/cloud/shift`: one such spy took 37 of its 108 tests away. The repo has 128 of
|
|
22
|
+
* them across 29 files, mostly in `apps/nest/microservices` and
|
|
23
|
+
* `apps/nest/backends-for-frontends`.
|
|
24
|
+
*
|
|
25
|
+
* ## Why it is fixed here and not in the 128 test files
|
|
26
|
+
*
|
|
27
|
+
* Because it is a difference between two runners, not something 29 suites each got wrong. A
|
|
28
|
+
* codemod rewriting every site would put a migration artefact in front of every reader of those
|
|
29
|
+
* files forever, and teams would carry it. One adapter keeps the test files exactly as they are.
|
|
30
|
+
*
|
|
31
|
+
* The two traps answer by doing what jest's `spyOn` did: read the property once, then answer. The
|
|
32
|
+
* read is the Proxy's own documented way of materialising it, so nothing here reimplements the
|
|
33
|
+
* mock, it only asks the question in the form the underlying object understands.
|
|
34
|
+
*
|
|
35
|
+
* `then` is never materialised. A deep mock that suddenly HAS a `then` is a thenable, and awaiting
|
|
36
|
+
* it, or returning it from an async function, would hang on a promise nothing resolves.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** Reset every deep mock this adapter created, the way jest's registry did. */
|
|
40
|
+
declare function resetDeepMocks(): void;
|
|
41
|
+
/** Clear their calls without touching their implementations. */
|
|
42
|
+
declare function clearDeepMocks(): void;
|
|
43
|
+
declare const mock: typeof mockExtended.mock;
|
|
44
|
+
declare const mockDeep: typeof mockExtended.mockDeep;
|
|
45
|
+
|
|
46
|
+
export { clearDeepMocks, mock, mockDeep, resetDeepMocks };
|