diff --git a/packages/lint/src/lazy-deps.test.ts b/packages/lint/src/lazy-deps.test.ts index d209cddc776..fb1dec22892 100644 --- a/packages/lint/src/lazy-deps.test.ts +++ b/packages/lint/src/lazy-deps.test.ts @@ -1,5 +1,4 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// test-budget: 30 — whole-package import: the in-process case imports the whole @objectstack/lint barrel cold, so the file's floor is vitest's transform of that graph and of every inlined @objectstack/spec dist bundle it reaches, and the two built-dist probes each spawn node once; slowest CI reading 18.0 s (hourly run 38267594064) x 1.25, rounded up // // Boot-path contract: importing @objectstack/lint must NOT load its heavy, // gate-only dependencies. The package sits on the kernel boot path, while each diff --git a/packages/lint/src/runtime-lazy-deps.test.ts b/packages/lint/src/runtime-lazy-deps.test.ts index d8e06f055a4..2cbe3abb70c 100644 --- a/packages/lint/src/runtime-lazy-deps.test.ts +++ b/packages/lint/src/runtime-lazy-deps.test.ts @@ -1,5 +1,4 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// test-budget: 30 — whole-package import: the in-process cases import the @objectstack/lint/runtime entry cold, which reaches the whole gate graph, so the file's floor is vitest's transform of it and of every inlined @objectstack/spec dist bundle it reaches, and the three built-dist probes each spawn node once; slowest CI reading 19.2 s (hourly run 38314183560) x 1.25, rounded up // // The kernel boot-path contract for `@objectstack/lint/runtime` (#4463). // diff --git a/packages/lint/src/vitest-spec-external.test.ts b/packages/lint/src/vitest-spec-external.test.ts new file mode 100644 index 00000000000..061aaec872b --- /dev/null +++ b/packages/lint/src/vitest-spec-external.test.ts @@ -0,0 +1,56 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Positive control for `test.server.deps.external` in ../vitest.config.ts. +// +// That entry hands the linked `@objectstack/spec` dist to Node instead of +// vite's transform. Its pattern is matched against the module's REALPATH, and +// a pattern that matches nothing is silent: the suite stays green and only +// gets slower. So this file asks the one question that tells the two apart — +// is the spec module these sources import the SAME instance Node's own loader +// holds? +// +// - Externalised: vitest's runner loads the dist with a native `import()`, so +// Node's loader holds that module, and a native load of the same file +// returns that very instance — its exports are identical objects. +// - Inlined (the pattern stopped matching): vite evaluates its own copy of the +// dist, and the native load builds a separate one — every export is a +// different object, and the identity assertions below go red. +// +// The native load is `require()` of the ESM file (Node 22's require(esm), +// which shares the ESM loader's module map) through `createRequire`, so vite +// cannot rewrite it; vitest's evaluator gives a `new Function` `import()` no +// dynamic-import callback. The file is the one the package's `exports` map +// names for an ESM import, taken from the linked package root. +import { readFileSync, realpathSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { defineStack } from '@objectstack/spec'; +import { ObjectSchema } from '@objectstack/spec/data'; + +const HERE = fileURLToPath(new URL('.', import.meta.url)); +// The package's own node_modules link — the same one these sources resolve. +const SPEC_ROOT = realpathSync(resolve(HERE, '../node_modules/@objectstack/spec')); + +const nativeRequire = createRequire(import.meta.url); + +function nativeEsmEntry(subpath: '.' | './data'): Record { + const manifest = JSON.parse(readFileSync(join(SPEC_ROOT, 'package.json'), 'utf8')); + const target: string = manifest.exports[subpath].import.default; + return nativeRequire(join(SPEC_ROOT, target)) as Record; +} + +describe('@objectstack/spec is loaded by Node, not inlined by vite', () => { + it('the root entry is the native module instance', () => { + const native = nativeEsmEntry('.'); + expect(typeof defineStack).toBe('function'); + expect(defineStack).toBe(native.defineStack); + }); + + it('a subpath entry is the native module instance', () => { + const native = nativeEsmEntry('./data'); + expect(ObjectSchema).toBeDefined(); + expect(ObjectSchema).toBe(native.ObjectSchema); + }); +}); diff --git a/packages/lint/vitest.config.ts b/packages/lint/vitest.config.ts index 5e0591efc44..4ecbe245f44 100644 --- a/packages/lint/vitest.config.ts +++ b/packages/lint/vitest.config.ts @@ -1,9 +1,11 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// This config exists for exactly one setting; everything else stays on +// This config exists for exactly two settings; everything else stays on // vitest's defaults, deliberately — a key added here re-specifies behaviour // for every test file in the package (packages/cli/vitest.config.ts's header -// records the incident that taught that). +// records the incident that taught that). Both settings are package-wide on +// purpose: each removes a mechanism every file here is exposed to, rather +// than tuning one file. import { defineConfig } from 'vitest/config'; export default defineConfig({ @@ -15,5 +17,30 @@ export default defineConfig({ // Mechanism + measured costs: examples/app-showcase/vitest.config.ts. // Enforced repo-wide by scripts/check-console-intercept-disarm.mjs. disableConsoleIntercept: true, + server: { + deps: { + // Load the linked `@objectstack/spec` dist with Node, not through + // vite's transform. Vitest's default `external` is node_modules-only, + // judged on a module's REALPATH, and a pnpm-linked workspace package's + // realpath is `packages/spec/dist/…` — so without this nearly every + // file here (lint imports spec's root and nine subpaths) inlined about + // 33 MB of built `.mjs`, and the first file to import a lint entry paid + // ~10 s of its cold transform. The import resolves through `exports` + // to `dist/` either way, so this changes only who evaluates that dist, + // never which bytes are under test. + // + // ⚠️ The pattern must match the realpath; a name-shaped one + // (`@objectstack/spec`) matches nothing, silently — see + // packages/cli/vitest.config.ts. The positive control is + // `src/vitest-spec-external.test.ts`, red the moment this stops + // matching. + // + // Cost: an externalised module cannot be `vi.mock`ed. The census taken + // when this landed found zero `vi.mock` / `vi.doMock` / `vi.hoisted` + // sites in this package; a later test that needs to mock spec must + // move this entry, not work around it. + external: [/[\\/]packages[\\/]spec[\\/]dist[\\/]/], + }, + }, }, });