diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f52274cddd..2c6036968f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: - uses: actions/cache/restore@v4 with: path: .cache/eslint - key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} restore-keys: | ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- @@ -173,7 +173,7 @@ jobs: - uses: actions/cache/restore@v4 with: path: .cache/eslint - key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} restore-keys: | ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- @@ -237,7 +237,7 @@ jobs: - uses: actions/cache@v4 with: path: .cache/eslint - key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} restore-keys: | ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- @@ -309,7 +309,7 @@ jobs: - uses: actions/cache@v4 with: path: .cache/eslint - key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} restore-keys: | ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- @@ -525,7 +525,7 @@ jobs: if: matrix.platform == 'linux' with: path: .cache/eslint - key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} restore-keys: | ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- diff --git a/eslint.config.mjs b/eslint.config.mjs index 334d3799af..eec63181df 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -86,7 +86,8 @@ export default tseslint.config( languageOptions: { parserOptions: { // Same shared project service as the src block: test files resolve - // through the root tsconfig (its include covers every tests/ tree). + // through the root solution to tsconfig.host.json (its include covers + // every host tests/ tree). projectService: true, tsconfigRootDir: import.meta.dirname, }, diff --git a/tsconfig.base.json b/tsconfig.base.json index 28f7e2d0f3..2018a1c747 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,4 +1,7 @@ { + // Doubles as the resolution facade for vite-tsconfig-paths (vitest configs + // point here). NEVER add include/files to this file: it would leak into + // every extending package project and narrow the facade's match-all scope. "compilerOptions": { "target": "es2024", "module": "esnext", @@ -87,8 +90,9 @@ // One wildcard maps every @deepseek-ai/dsh- to its source. Package // dir names are unique across groups, so first-on-disk-wins resolution is // unambiguous; adding a package under an existing group needs no edit - // here. The build graph's project references (tsconfig.build.json) stay - // explicit — TS project references have no wildcard form. + // here. The aggregates' project references (tsconfig.host.json / + // tsconfig.client.json) stay explicit — TS project references have no + // wildcard form. "@deepseek-ai/dsh-host-apiproxy": ["./packages/host/apiproxy/src"], "@deepseek-ai/dsh-host-apiproxy/client": ["./packages/host/apiproxy/src/fetch/client.ts"], "@deepseek-ai/dsh-host-apiproxy/*": ["./packages/host/apiproxy/src/*"], diff --git a/tsconfig.vitest.json b/tsconfig.vitest.json deleted file mode 100644 index 9cb8a888c6..0000000000 --- a/tsconfig.vitest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - // Path-map scope for vite-tsconfig-paths in vitest only (never a tsc -b target): - // widens include to package src and .tsx so bare workspace imports resolve to source. - "extends": "./tsconfig.base.json", - "include": [ - "examples/*/src/**/*.ts", - "examples/*/start.ts", - "examples/*/tests/**/*.ts", - "packages/*/*/src/**/*.ts", - "packages/*/*/src/**/*.tsx", - "packages/*/*/tests/**/*.ts", - "packages/*/*/tests/**/*.tsx", - "scripts/**/*.ts" - ] -} diff --git a/vitest.config.ts b/vitest.config.ts index 408f100130..1177782a8a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,12 @@ import tsconfigPaths from 'vite-tsconfig-paths' import { defineConfig } from 'vitest/config' +// Resolution facade shared by every plugin instance below: tsconfig.base.json +// has no include, which vite-tsconfig-paths treats as match-all, so its paths +// map applies to every test file. paths must win over package exports so built +// lib/ never loads a second module-singleton copy. +const pathsPlugin = (): ReturnType => tsconfigPaths({ projects: ['./tsconfig.base.json'] }) + const windowsUnsupportedPackages = process.platform === 'win32' ? [ 'packages/bash/*', @@ -40,12 +46,7 @@ const processBoundTests = [ ] export default defineConfig({ - // Native path resolution reads each package's nearest tsconfig, but only the root defines - // workspace paths. Keep this plugin pinned to the root map so bare package imports resolve - // to source — with built lib/ present, manifest-exports fallthrough would load a second - // copy of module singletons. tsconfig.vitest.json widens include to .tsx specs (the root - // include stops at .ts for tsc -b; the plugin scopes applicability by include). - plugins: [tsconfigPaths({ projects: ['./tsconfig.vitest.json'] })], + plugins: [pathsPlugin()], test: { setupFiles: ['./scripts/test-invariants.ts'], // .tsx: client component specs (jsdom via per-file @vitest-environment pragma). @@ -55,7 +56,7 @@ export default defineConfig({ // for lower startup/IPC overhead; only explicit process-bound suites fork. projects: [ { - plugins: [tsconfigPaths({ projects: ['./tsconfig.vitest.json'] })], + plugins: [pathsPlugin()], test: { name: 'thread-safe', pool: 'threads', @@ -68,7 +69,7 @@ export default defineConfig({ }, }, { - plugins: [tsconfigPaths({ projects: ['./tsconfig.vitest.json'] })], + plugins: [pathsPlugin()], test: { name: 'process-bound', pool: 'forks', diff --git a/vitest.e2e.config.ts b/vitest.e2e.config.ts index 97be1395b3..2e2221b6e8 100644 --- a/vitest.e2e.config.ts +++ b/vitest.e2e.config.ts @@ -29,13 +29,13 @@ const e2eMaxWorkers = positiveIntFromEnv('DSH_E2E_MAX_WORKERS', DEFAULT_E2E_MAX_ export default defineConfig({ // Same resolution note as vitest.config.ts: bare workspace names resolve - // through the vitest-scoped tsconfig paths map (its include spans package - // src, so client-package sources get mapping too — the root tsconfig - // excludes packages/client, which would drop /client subpath imports onto - // package exports and load browser dist bundles into node). Built-artifact - // e2e suites are unaffected: their built-ness lives in subprocesses and - // createRequire lookups, which bypass vite resolution entirely. - plugins: [tsconfigPaths({ projects: ['./tsconfig.vitest.json'] })], + // through the tsconfig.base.json paths facade (no include = match-all, so + // client-package sources get mapping too — dropping /client subpath imports + // onto package exports would load browser dist bundles into node). + // Built-artifact e2e suites are unaffected: their built-ness lives in + // subprocesses and createRequire lookups, which bypass vite resolution + // entirely. + plugins: [tsconfigPaths({ projects: ['./tsconfig.base.json'] })], test: { setupFiles: ['./scripts/test-invariants.ts'], include: ['packages/*/*/tests/**/*.e2e.ts', 'examples/*/tests/**/*.e2e.ts'], diff --git a/vitest.snapshot.config.ts b/vitest.snapshot.config.ts index 142528e604..24764cf6c7 100644 --- a/vitest.snapshot.config.ts +++ b/vitest.snapshot.config.ts @@ -36,8 +36,9 @@ if (process.env.DSH_SNAPSHOT === 'record') { export default defineConfig({ // Same resolution note as vitest.config.ts: bare workspace names resolve - // through the root tsconfig paths map; the native option cannot do this. - plugins: [tsconfigPaths({ projects: ['./tsconfig.json'] })], + // through the tsconfig.base.json paths facade; the native option cannot do + // this (the root tsconfig is a solution file with no paths). + plugins: [tsconfigPaths({ projects: ['./tsconfig.base.json'] })], test: { setupFiles: ['./scripts/test-invariants.ts'], include: [ diff --git a/vitest.web.config.ts b/vitest.web.config.ts index 765fb2e4a9..de220c9f12 100644 --- a/vitest.web.config.ts +++ b/vitest.web.config.ts @@ -1,4 +1,4 @@ -import { fileURLToPath } from 'node:url' +import tsconfigPaths from 'vite-tsconfig-paths' import { defineConfig } from 'vitest/config' // Web smoke lane (GUI, gate-exempt — not part of the CI sequence yet): built @@ -13,13 +13,10 @@ try { } export default defineConfig({ - resolve: { - alias: { - // apps/web/tests is outside the root tsconfig include, so the shared - // tsconfig-paths plugin never maps it; alias the one bare import to source. - '@deepseek-ai/dsh-host-webserver': fileURLToPath(new URL('packages/host/webserver/src/index.ts', import.meta.url)), - }, - }, + // Same resolution note as vitest.config.ts: the tsconfig.base.json paths + // facade has no include (match-all), so apps/web/tests resolves bare + // workspace imports to source like every other lane. + plugins: [tsconfigPaths({ projects: ['./tsconfig.base.json'] })], test: { include: ['apps/web/tests/**/*.e2e.ts'], // Browser boot + real-model turns are slow; files share one browser, run serial.