diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a79fbb0f..016ec1f9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,6 +115,32 @@ jobs: - name: Run type-check run: pnpm type-check + # The four repo-root `vitest.setup.*` files were in ZERO tsc programs + # (objectui#3515) — the same shape as `scripts/` before objectui#3494, one + # directory over. They live outside every workspace package, so + # `pnpm type-check` above cannot reach them; the root `tsconfig.json` + # includes only packages/examples/apps; and no consumer IMPORTS them (every + # one names them as a Vitest `setupFiles` runtime path string), so no gated + # program pulls them in transitively either. + # + # Placed after `pnpm type-check`, NOT next to `pnpm type-check:scripts` + # above, and the difference is load-bearing. `tsconfig.scripts.json`'s + # program imports no workspace package, so it can run straight after the + # install; this one must, because `vitest.setup.dom.tsx` side-effect-imports + # @object-ui/components, /fields, /plugin-dashboard and /plugin-grid for + # their registrations. `tsconfig.vitest-setup.json` resolves those through + # each package's own `exports.types`, i.e. its built declarations — which + # exist only after the `^build` that `pnpm type-check` depends on. + # `scripts/__tests__/vitest-setup-type-check.test.ts` pins that ordering. + # + # A direct step, deliberately: this is not a turbo task, so no cache key + # decides its verdict. `pnpm type-check` is cached by turbo and its `inputs` + # would have to hash every root file a program reads (the objectui#3513 + # lesson); running this one directly sidesteps that class of half-armed + # gate entirely, exactly as `type-check:scripts` does. + - name: Type-check repo-root vitest setup files + run: pnpm type-check:vitest-setup + # PRs run the suite split across 4 runners. The suite is dominated by fixed # per-file cost rather than by the assertions: a PR run reported 495s wall # clock of which only 178s was `tests` — the rest was `setup` (831s diff --git a/content/docs/guide/ci-cd-pipeline.md b/content/docs/guide/ci-cd-pipeline.md index 65e9043f4..1912f150c 100644 --- a/content/docs/guide/ci-cd-pipeline.md +++ b/content/docs/guide/ci-cd-pipeline.md @@ -66,7 +66,7 @@ been deleted three months earlier — [#3451](https://github.com/objectstack-ai/ | Job key | Appears as | What it runs | When | |---|---|---|---| | `changeset-check` | Changeset Fixed Group Check | `scripts/check-changeset-fixed.mjs` — every workspace package must be in the changeset `fixed` group or explicitly ignored. It checks group *membership*; it does **not** check whether the PR added a changeset. | Every run | -| `type-check` | Type Check | `scripts/check-type-check-coverage.mjs`, then `pnpm check:spec-symbols`, then `pnpm type-check:scripts`, then `pnpm type-check`. The coverage guard runs first because turbo silently skips packages that have no `type-check` script, so a package without one would otherwise read as passing (#2911). `pnpm type-check:scripts` (`tsconfig.scripts.json`) covers `scripts/**/*.ts`, which `pnpm type-check` cannot reach at all — `scripts/` has no package.json, so turbo never walks it, and the coverage guard decides coverage per *package*. Until [#3494](https://github.com/objectstack-ai/objectui/issues/3494) that left the pin tests in `scripts/__tests__/` — including the one pinning this very page — compiled by nothing. | Every run | +| `type-check` | Type Check | `scripts/check-type-check-coverage.mjs`, then `pnpm check:spec-symbols`, then `pnpm type-check:scripts`, then `pnpm type-check`, then `pnpm type-check:vitest-setup`. The coverage guard runs first because turbo silently skips packages that have no `type-check` script, so a package without one would otherwise read as passing (#2911). `pnpm type-check:scripts` (`tsconfig.scripts.json`) covers `scripts/**/*.ts`, which `pnpm type-check` cannot reach at all — `scripts/` has no package.json, so turbo never walks it, and the coverage guard decides coverage per *package*. Until [#3494](https://github.com/objectstack-ai/objectui/issues/3494) that left the pin tests in `scripts/__tests__/` — including the one pinning this very page — compiled by nothing. `pnpm type-check:vitest-setup` (`tsconfig.vitest-setup.json`) closes the same gap for the four repo-root `vitest.setup.*` files, uncovered until [#3515](https://github.com/objectstack-ai/objectui/issues/3515); it runs *last*, after `pnpm type-check`, because `vitest.setup.dom.tsx` side-effect-imports four `@object-ui/*` packages and resolves them through the declarations that turbo's `^build` produces. | Every run | | `test` | Test (shard N/4) | `pnpm test --shard=N/4` across a 4-runner matrix with `fail-fast: false`, so every shard reports its own failures. No coverage instrumentation — v8 adds 40–100% overhead. | **Pull requests only** | | `test-coverage` | Test (coverage) | One unsharded `pnpm test:coverage`, uploaded to Codecov. Nothing blocks on it, which is why it is not sharded. | **Push only** | | `e2e` | Build & E2E | Builds the console with `vite build` (`VITE_BASE_PATH=/console/`), verifies the artifact, then `pnpm test:e2e --project=chromium`. Uploads the Playwright report on failure. | Every run | diff --git a/package.json b/package.json index 519af9834..50ec1fe5c 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "type-check": "turbo run type-check", "type-check:coverage": "node scripts/check-type-check-coverage.mjs", "type-check:scripts": "tsc -p tsconfig.scripts.json", + "type-check:vitest-setup": "tsc -p tsconfig.vitest-setup.json", "check:spec-symbols": "node scripts/check-spec-symbol-derivation.mjs", "check:control-bytes": "node scripts/check-control-bytes.mjs", "cli": "node packages/cli/dist/cli.js", diff --git a/scripts/__tests__/vitest-setup-type-check.test.ts b/scripts/__tests__/vitest-setup-type-check.test.ts new file mode 100644 index 000000000..0fa0c2f55 --- /dev/null +++ b/scripts/__tests__/vitest-setup-type-check.test.ts @@ -0,0 +1,304 @@ +import { describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import ts from 'typescript'; + +/** + * objectui#3515 — the four repo-ROOT `vitest.setup.*` files were in zero tsc + * programs. + * + * They live at the repo root, outside every workspace package, so `pnpm + * type-check` (`turbo run type-check`, driven by package.json `scripts`) + * structurally cannot reach them. The root `tsconfig.json` includes only + * `packages`/`examples`/`apps`; `tsconfig.scripts.json` only `scripts/**`. And + * nothing IMPORTS them — every consumer names them as a Vitest `setupFiles` + * runtime path string, so no gated program has a type edge to them either. The + * only program that did contain them, the root `tsconfig.node.json`, has no + * `include` (it therefore compiles the entire repository, currently ~21.6k + * errors) and no script runs it: not a gate. + * + * That matters because `vitest.setup.dom.tsx` decides which package graphs get + * registered before the `dom-heavy` project runs. Drift there does not fail as a + * type error — it fails as a confusing "component not registered" in whichever + * test happened to depend on the registration. + * + * `tsconfig.vitest-setup.json` closes it. This file is what stops it reopening, + * and like its sibling `scripts-type-check.test.ts` it asserts BEHAVIOUR — which + * files the project actually resolves, whether CI actually runs it, and in the + * right place — rather than the config's spelling. A test that only checked the + * spelling would be satisfied by a config that compiles nothing, which is the + * exact failure mode it exists to prevent. + */ +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); +const configPath = path.join(repoRoot, 'tsconfig.vitest-setup.json'); +const ciWorkflowPath = path.join(repoRoot, '.github/workflows/ci.yml'); + +/** The root script CI invokes, and the one a contributor can run locally. */ +const SCRIPT_NAME = 'type-check:vitest-setup'; + +/** + * The packages `vitest.setup.dom.tsx` side-effect-imports for their + * ComponentRegistry registrations. Resolving these is the reason this project + * needs a built workspace, and therefore the reason its CI step sits where it + * does. + */ +const SIDE_EFFECT_PACKAGES = [ + '@object-ui/components', + '@object-ui/fields', + '@object-ui/plugin-dashboard', + '@object-ui/plugin-grid', +] as const; + +/** + * The project as TypeScript itself resolves it: the same parse `tsc -p` does, so + * `fileNames` is the real program root set rather than a re-implementation of + * TypeScript's glob semantics. + */ +function parsedProject(): ts.ParsedCommandLine { + const read = ts.readConfigFile(configPath, ts.sys.readFile); + expect( + read.error && ts.flattenDiagnosticMessageText(read.error.messageText, ' '), + 'tsconfig.vitest-setup.json must parse as JSON with comments', + ).toBeFalsy(); + + return ts.parseJsonConfigFileContent(read.config, ts.sys, repoRoot, undefined, configPath); +} + +/** Every `vitest.setup*` TypeScript source sitting at the repo root, repo-relative. */ +function rootSetupFilesOnDisk(): string[] { + return fs + .readdirSync(repoRoot, { withFileTypes: true }) + .filter((e) => e.isFile() && /^vitest\.setup.*\.tsx?$/.test(e.name) && !e.name.endsWith('.d.ts')) + .map((e) => e.name) + .sort(); +} + +describe('tsconfig.vitest-setup.json — the project itself (objectui#3515)', () => { + /** + * A tsconfig that fails to parse does not fail loudly: TypeScript falls back to + * defaults, and with no `include` surviving, `tsc -p` cheerfully compiles the + * ENTIRE repository — which reads as "this config is broken" rather than "this + * config is unparseable". `tsconfig.scripts.json` hit exactly that while being + * written (a `**` followed by `/` inside a block comment terminates it), which + * is why both configs comment with `//`. + */ + it('parses with no diagnostics at all', () => { + const messages = parsedProject().errors.map((d) => + ts.flattenDiagnosticMessageText(d.messageText, ' '), + ); + expect(messages, 'tsconfig.vitest-setup.json emitted config diagnostics').toEqual([]); + }); + + it('is a checking project, not an emitting one', () => { + const { options } = parsedProject(); + + expect(options.noEmit, 'tsconfig.vitest-setup.json must set "noEmit": true').toBe(true); + expect(options.strict, 'tsconfig.vitest-setup.json must set "strict": true').toBe(true); + + // `noEmit` is only legal because the project is standalone. If someone makes + // it `composite` or gives it `references`, TS6310 fires and the two + // assertions above become mutually unsatisfiable — fail on the cause, not on + // the symptom. + expect(options.composite, 'a composite project may not set "noEmit" (TS6310)').toBeFalsy(); + expect(parsedProject().projectReferences ?? [], 'this project must stay standalone').toEqual([]); + }); + + it('checks the files against a DOM and can read .tsx', () => { + const { options } = parsedProject(); + + // Both were measured load-bearing, so pin them rather than trusting the + // comment. Without DOM in `lib`, `vitest.setup.base.ts` alone produces 14 + // errors (`Storage`, `document`, `Element`); these files exist to polyfill a + // DOM, so they have to be checked against one. + expect( + (options.lib ?? []).map((l) => l.toLowerCase()), + '"lib" must include DOM — the setup files declare Storage/document/Element', + ).toContain('lib.dom.d.ts'); + + // Without `jsx`, `vitest.setup.tsx` fails with TS6142: importing a module + // that resolves to a `.tsx` file requires `--jsx`, whether or not that file + // contains any JSX (none of them currently does). + expect(options.jsx, '"jsx" must be set — three of the four files are .tsx').toBeDefined(); + }); + + /** + * `allowJs` is the option objectui#3494 measured into `tsconfig.scripts.json` + * and objectui#3513 documented the cost of: it makes a `@ts-expect-error` over + * a plain-JS import unused, and therefore an error of its own (TS2578). This + * project sidesteps it because none of its files import `.js`/`.mjs` — pin + * that premise, so turning `allowJs` on later has to be a decision rather than + * an accident. + */ + it('needs no allowJs, because nothing in its program imports plain JS', () => { + const { options } = parsedProject(); + expect(options.allowJs, 'tsconfig.vitest-setup.json must not enable allowJs').toBeFalsy(); + + const jsImports = parsedProject() + .fileNames.flatMap((f) => [...fs.readFileSync(f, 'utf8').matchAll(/from\s+'([^']+\.m?js)'/g)]) + .map((m) => m[1]); + expect( + jsImports, + 'a setup file now imports plain JS. Enabling `allowJs` is the fix, but read ' + + 'apps/console/tsconfig.node.json first — it turns any @ts-expect-error over such an ' + + 'import into an unused-directive error (TS2578).', + ).toEqual([]); + }); +}); + +describe('tsconfig.vitest-setup.json — coverage of the root setup files (objectui#3515)', () => { + it('resolves every root vitest.setup* source, with none left out', () => { + const onDisk = rootSetupFilesOnDisk(); + + // Non-vacuity first. Every other assertion here is satisfied by a project + // that resolves nothing, and "compiles nothing, exits 0" is exactly what this + // gate exists to make impossible. + expect( + onDisk.length, + 'no vitest.setup* sources found at the repo root — the scan is broken', + ).toBeGreaterThanOrEqual(4); + + const inProject = new Set( + parsedProject().fileNames.map((f) => path.relative(repoRoot, f).split(path.sep).join('/')), + ); + const uncovered = onDisk.filter((f) => !inProject.has(f)); + + expect( + uncovered, + 'These repo-root setup files are not in tsconfig.vitest-setup.json’s program, so nothing ' + + 'type-checks them:\n' + + uncovered.map((f) => ` - ${f}`).join('\n') + + '\n\nWiden the "include" globs in tsconfig.vitest-setup.json. A root setup file that no ' + + 'tsc invocation reads is objectui#3515 all over again — and drift in one surfaces as a ' + + 'confusing "component not registered" test failure, not as a type error.', + ).toEqual([]); + }); + + it('really does cover all four, by name', () => { + // The forward assertion above is a set difference, which stays green if the + // directory scan ever stops finding anything. Name them outright. + const inProject = new Set( + parsedProject().fileNames.map((f) => path.relative(repoRoot, f).split(path.sep).join('/')), + ); + for (const file of [ + 'vitest.setup.base.ts', + 'vitest.setup.dom.tsx', + 'vitest.setup.dom-light.tsx', + 'vitest.setup.tsx', + ]) { + expect(inProject, `${file} must be type-checked by tsconfig.vitest-setup.json`).toContain(file); + } + }); + + /** + * The program must stay SMALL. Mapping the four side-effect packages at their + * `src/` instead — mirroring vitest.config.mts's `resolve.alias` — was measured + * and rejected: it drags 318–563 foreign source files in and re-checks packages + * that already have their own gates under a different option set (24 of the + * residual errors are implicit-anys that `packages/components/tsconfig.json` + * makes legal with `noImplicitAny: false`). Mapping to the package directory + * lands on published declarations instead, which `skipLibCheck` skips. + */ + it('checks only the root setup files, not other packages’ sources', () => { + // Built as a real PROGRAM, not read off `fileNames`. `fileNames` is only the + // project's ROOT set — the files the `include` globs matched — so whatever + // `paths` pulls in never appears there. An earlier draft of this test asserted + // over `fileNames` and stayed green when `paths` was repointed at `src/`: + // green because nothing was produced, which is the shape it exists to catch. + const { fileNames, options } = parsedProject(); + const foreign = ts + .createProgram(fileNames, options) + .getSourceFiles() + .filter((sf) => !sf.isDeclarationFile) + .map((sf) => path.relative(repoRoot, sf.fileName).split(path.sep).join('/')) + .filter((f) => !f.startsWith('vitest.setup') && !f.startsWith('node_modules/')); + // The node_modules exclusion is not a loophole: it drops exactly one file, + // `@reduxjs/toolkit`'s vendored `dist/uncheckedindexed.ts`, which a dependency + // declaration references. It is third-party, unactionable from here, and — as + // a non-declaration file — the one thing in this program `skipLibCheck` does + // not cover. The subject of this assertion is WORKSPACE sources. + + expect( + foreign, + 'tsconfig.vitest-setup.json is now checking non-declaration files outside its subject:\n' + + foreign.map((f) => ` - ${f}`).join('\n') + + '\n\nIts "paths" must resolve the @object-ui/* side-effect imports to each package ' + + 'DIRECTORY (i.e. its published `exports.types`), not to `src/`. Pointing at sources ' + + 're-checks packages that already have their own gates, under this project’s stricter ' + + 'option set — the "green in one project, red in the other" hazard tsconfig.scripts.json ' + + 'documents.', + ).toEqual([]); + }); +}); + +describe('the gate is actually wired up (objectui#3515)', () => { + const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) as { + scripts?: Record; + }; + const ciWorkflow = fs.readFileSync(ciWorkflowPath, 'utf8'); + + /** The `type-check:` job body, from its key up to the next job at the same indent. */ + function typeCheckJob(): string { + const start = ciWorkflow.search(/^ {2}type-check:[ \t]*$/m); + expect(start, 'ci.yml must still have a `type-check:` job').toBeGreaterThan(-1); + const rest = ciWorkflow.slice(start + 1); + const next = rest.search(/^ {2}[a-z0-9][a-z0-9-]*:[ \t]*$/m); + return next === -1 ? rest : rest.slice(0, next); + } + + it('is runnable locally, not CI-only', () => { + // `pnpm type-check` is `turbo run type-check` and cannot reach files outside + // a workspace package, so without a named root script this project would only + // ever run on a CI runner — and a gate nobody can reproduce locally is a gate + // people learn to ignore. + expect(pkg.scripts?.[SCRIPT_NAME], `package.json must define "${SCRIPT_NAME}"`).toBeDefined(); + expect(pkg.scripts?.[SCRIPT_NAME]).toContain('tsconfig.vitest-setup.json'); + }); + + it('runs in ci.yml’s type-check job', () => { + expect( + typeCheckJob(), + `ci.yml’s type-check job must run \`pnpm ${SCRIPT_NAME}\`. Without it, ` + + 'tsconfig.vitest-setup.json is a file that looks like a gate while no CI job reads it — ' + + 'the same shape as the unchecked setup files it was added to fix.', + ).toContain(`pnpm ${SCRIPT_NAME}`); + }); + + /** + * The placement premise, and the one real difference from + * `tsconfig.scripts.json`: that project's pin test asserts its program imports + * NO workspace package, so it may run in the cheap half of the job. This one + * asserts the opposite, because `vitest.setup.dom.tsx` genuinely does — and the + * declarations it resolves them through only exist after turbo's `type-check` + * task has run its `^build`. + */ + it('runs AFTER the build, because its program needs built declarations', () => { + const job = typeCheckJob(); + const build = job.indexOf('run: pnpm type-check\n'); + const check = job.indexOf(`pnpm ${SCRIPT_NAME}`); + + expect(build, 'the type-check job must still run `pnpm type-check`').toBeGreaterThan(-1); + expect(check, 'the type-check job must run the vitest-setup type-check').toBeGreaterThan(-1); + expect( + check, + `\`pnpm ${SCRIPT_NAME}\` must come after \`pnpm type-check\`, whose turbo task dependsOn ` + + '`^build`. Its program resolves @object-ui/* through those packages’ built declarations; ' + + 'run earlier it fails with four TS2882s that look nothing like their real cause.', + ).toBeGreaterThan(build); + }); + + it('still has a real reason to run after the build', () => { + // If the side-effect imports ever disappear, this project stops needing the + // build and should move up into the cheap half of the job. Pin the premise so + // the placement cannot quietly become cargo cult. + const domSetup = fs.readFileSync(path.join(repoRoot, 'vitest.setup.dom.tsx'), 'utf8'); + const imported = SIDE_EFFECT_PACKAGES.filter((p) => domSetup.includes(`import '${p}'`)); + + expect( + imported, + 'vitest.setup.dom.tsx no longer side-effect-imports all four registration packages. If it ' + + 'imports none of them, this project needs no built workspace and its ci.yml step belongs ' + + 'next to `pnpm type-check:scripts` instead.', + ).toEqual([...SIDE_EFFECT_PACKAGES]); + }); +}); diff --git a/tsconfig.vitest-setup.json b/tsconfig.vitest-setup.json new file mode 100644 index 000000000..c9c599331 --- /dev/null +++ b/tsconfig.vitest-setup.json @@ -0,0 +1,155 @@ +// Type-checks the repo-ROOT Vitest setup files: +// +// vitest.setup.base.ts vitest.setup.dom.tsx +// vitest.setup.dom-light.tsx vitest.setup.tsx +// +// objectui#3515: nothing compiled them. They sit at the repo root, outside every +// workspace package (`pnpm-workspace.yaml`: packages/*, examples/*, apps/*, +// docs), so `turbo run type-check` — which is driven by package.json `scripts` — +// structurally cannot reach them; the root `tsconfig.json` includes only +// packages/examples/apps; `tsconfig.scripts.json` includes only `scripts/**`. +// Nor are they pulled in transitively: every consumer names them as a Vitest +// `setupFiles` RUNTIME PATH STRING (`setupFiles: ['../../vitest.setup.tsx']`), +// never as an import, so no gated program has a type edge to them. The one +// program that does contain them, the root `tsconfig.node.json`, has no +// `include` (so it defaults to the whole repository) and no script anywhere runs +// it — it is not a gate. +// +// That is objectui#3494's shape one directory over and objectui#3476's one +// directory up: a checked-in `.ts`/`.tsx` file that reads as ordinary maintained +// source while no `tsc` invocation has ever looked at it. It matters here +// because `vitest.setup.dom.tsx` decides which package graphs are registered +// before the `dom-heavy` project runs, and drift there does not surface as a +// type error — it surfaces as a confusing "component not registered" in whatever +// test happened to depend on the registration. +// +// Run it with `pnpm type-check:vitest-setup`; ci.yml's `type-check` job runs the +// same command. `scripts/__tests__/vitest-setup-type-check.test.ts` pins the +// behaviour below — which files the project really resolves, and that CI really +// runs it — rather than the spelling of this file, because a config that +// compiles nothing satisfies every spelling assertion. +// +// Comments here are `//`, not `/* */`, for the reason recorded in +// `tsconfig.scripts.json`: a glob containing `**` immediately followed by `/` +// closes a block comment early, and a tsconfig that silently fails to parse +// falls back to compiling the entire repository. +{ + "compilerOptions": { + // Node 22 (`engines.node: ">=22"`) runs Vitest, and Vitest hands these files + // to esbuild, so ES2022 is both available and what actually executes. + "target": "ES2022", + + // DOM is load-bearing, not decoration — measured. Dropping it from `lib` + // costs 14 errors in `vitest.setup.base.ts` alone: `Storage` (the + // `createMemoryStorage(): Storage` return type), `document`, + // `Element`/`HTMLCanvasElement` in the maplibre mock. These files exist to + // polyfill a DOM, so they must be checked against one. + "lib": ["ES2022", "DOM", "DOM.Iterable"], + + "module": "ESNext", + "moduleResolution": "bundler", + + // Also load-bearing, and not for the obvious reason: none of the three + // `.tsx` files currently contains any JSX. Without `jsx`, `vitest.setup.tsx` + // fails with TS6142 — importing a module that resolves to a `.tsx` file + // requires `--jsx` whether or not that file uses JSX. `react-jsx` matches + // what Vitest/esbuild transform them with. + "jsx": "react-jsx", + + "strict": true, + + // `esModuleInterop` + `allowSyntheticDefaultImports` are set for RUNTIME + // FIDELITY, not to silence anything (measured: 0 errors either way today). + // Vitest loads these files through esbuild, which already behaves this way, + // so a stricter setting here would produce false reds on a default import + // the runtime accepts. The point of this gate is to predict what the test + // run does, not to be stricter than it. + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + + // Vitest transpiles per file with no cross-file type information, so + // constructs `isolatedModules` rejects (a `const enum`, a non-`type` + // re-export of a type) break at RUNTIME here rather than being caught. + // Produces no error today; it is a constraint the loader genuinely imposes. + "isolatedModules": true, + + // Measured: without it, 12 errors, every one inside a third-party `.d.ts` + // under node_modules (`@ai-sdk/provider-utils`, `@objectstack/client` — all + // `Cannot find name 'Buffer'`, because this project deliberately does not + // pull in `@types/node`; the four files use no Node globals). This does NOT + // weaken the gate on its own subject: `skipLibCheck` skips declaration + // files, and all four subject files are `.ts`/`.tsx`, checked in full. + "skipLibCheck": true, + + // No `allowJs`, and that is a measured choice rather than an omission. None + // of the four files imports a `.js`/`.mjs` module and none carries a + // `@ts-expect-error`, so `allowJs` would buy nothing — while walking + // straight into the TS2578 interaction documented in + // `apps/console/tsconfig.node.json` (objectui#3513) and paid for in + // `tsconfig.scripts.json`: turning `allowJs` on makes a `@ts-expect-error` + // over a plain-JS import unused, and therefore an error of its own. + + // Legal only because this project is standalone: TS6310 forbids `noEmit` on + // a project that is `composite` or referenced by another. Keeping it + // standalone is also what keeps stray `.js`/`.d.ts` from landing next to the + // sources — a large part of why `apps/console`'s node project went unwired + // for so long (objectui#3305). + "noEmit": true, + + // `vitest.setup.dom.tsx` side-effect-imports four workspace packages for + // their ComponentRegistry registrations. Resolving them needed measuring; + // all three routes were run against this exact file set: + // + // Plain node resolution -> IMPOSSIBLE. The root package.json declares no + // `@object-ui/*` dependency, so pnpm creates no root + // `node_modules/@object-ui/` links at all; `--traceResolution` shows the + // lookup walking to `/node_modules` and giving up. Four TS2882. Building + // the workspace does not help, because there is nothing to resolve + // THROUGH. (Adding four root devDependencies would fix that, at the cost + // of changing the install graph to serve a type-check.) + // + // Mapping them to `src/`, mirroring vitest.config.mts's `resolve.alias` -> + // 484 errors and 318 foreign source files with these four entries alone; + // mirroring the full 42-entry alias map still leaves 30 errors across + // 563 foreign files. The residue cannot be fixed from here: 24 of the 30 + // are TS7006/TS7031 implicit-any inside `packages/components/src`, which + // is green in its own gate only because + // `packages/components/tsconfig.json` sets `noImplicitAny: false`. + // Matching that would mean turning `noImplicitAny` off for the four + // files this project exists to check — and re-checking 563 files that + // already have their own gates, under a different option set, is exactly + // the "green in one project, red in the other" hazard + // `tsconfig.scripts.json` documents. + // + // Mapping them to the PACKAGE DIRECTORY, below -> 0 errors, 4.5s, and the + // only workspace files this program checks are the four subject files. + // What the mapping drags in is declarations, which `skipLibCheck` skips — + // with exactly one measured exception, `@reduxjs/toolkit`'s vendored + // `dist/uncheckedindexed.ts`, which a dependency declaration references + // and which is therefore checked (cleanly today). TypeScript reads each + // package's own package.json and follows its `exports.types`, so the + // mapping tracks the package's declared entry point instead of hardcoding + // `dist/index.d.ts`. + // + // The consequence of the chosen route is a real dependency and is wired for + // accordingly: this project needs those four packages BUILT. Hence the + // ci.yml step sits after `pnpm type-check` (turbo's `type-check` task + // `dependsOn: ["^build"]`, so the declarations exist by then) — the exact + // opposite of `tsconfig.scripts.json`, whose pin test asserts its program + // imports no workspace package. Ours pins the ordering instead. + "paths": { + "@object-ui/components": ["./packages/components"], + "@object-ui/fields": ["./packages/fields"], + "@object-ui/plugin-dashboard": ["./packages/plugin-dashboard"], + "@object-ui/plugin-grid": ["./packages/plugin-grid"] + } + }, + + // By glob, not by file list — a list is a thing to forget, and the whole point + // of objectui#3515 is that a root `vitest.setup.*` file must not be able to + // land outside every program again. `vitest-setup-type-check.test.ts` pins + // that these globs really do reach every `vitest.setup*` source on disk at the + // repo root. + "include": ["./vitest.setup*.ts", "./vitest.setup*.tsx"] +}