Repository navigation
React: Fix RDT tsconfig selection for Vite project references #35743
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
6452a18
React: fix manifest react-docgen-typescript tsconfig resolution
kasperpeulen b55a2e9
React: add parser tests for manifest RDT
kasperpeulen 6856e79
React: reuse referenced tsconfig selection for docgen
kasperpeulen 4caef7c
React: document tsconfig selection inspiration
kasperpeulen 12512dc
React: fix file-aware argtypes tsconfig resolution
kasperpeulen 0ef17b3
React: fix manifest RDT test typing
kasperpeulen 424318f
React: fix shared tsconfig path test mocks
kasperpeulen e7f3cc5
React: cache manifest tsconfig lookup
kasperpeulen e5f45b2
React: polish revived file-aware tsconfig selection
ndelangen 97f16d9
React: add missing .ts extensions in revived RDT tests
ndelangen a83376c
React: normalize path separators in RDT tsconfig ownership checks
ndelangen b69dd4c
React: address straightforward AI review nits on tsconfig selection
ndelangen fbf16b2
React: avoid dirname on importer directories for tsconfig selection
ndelangen a20bc24
React: cache react-docgen tsconfig path matchers
ndelangen b77c887
React: parse tsconfigs with TypeScript's JSONC reader
ndelangen d8215cb
React: avoid importing typescript from common tsconfig helpers
ndelangen 7c38980
React: resolve tsconfig extends for file ownership matching
ndelangen 83a8b8d
Merge branch 'next' into norbert/revive-34415-file-aware-tsconfig
ndelangen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { dirname, join } from 'node:path'; | ||
|
|
||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| vi.mock('../paths.ts', { spy: true }); | ||
|
|
||
| import { findTsconfigPathForFile, findTsconfigPathForPath } from '../tsconfig.ts'; | ||
| import * as paths from '../paths.ts'; | ||
|
|
||
| const tempDirs: string[] = []; | ||
|
|
||
| beforeEach(() => { | ||
| vi.mocked(paths.getProjectRoot).mockReset(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.restoreAllMocks(); | ||
|
|
||
| for (const dir of tempDirs.splice(0)) { | ||
| rmSync(dir, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
|
|
||
| describe('findTsconfigPathForFile', () => { | ||
| it('uses the referenced app tsconfig for Vite-style project references', () => { | ||
| const dir = createTempProject({ | ||
| 'tsconfig.json': JSON.stringify({ | ||
| files: [], | ||
| references: [{ path: './tsconfig.app.json' }, { path: './tsconfig.node.json' }], | ||
| }), | ||
| 'tsconfig.app.json': JSON.stringify({ | ||
| compilerOptions: { | ||
| baseUrl: '.', | ||
| paths: { | ||
| '@ui/*': ['src/*'], | ||
| }, | ||
| }, | ||
| include: ['src'], | ||
| }), | ||
| 'tsconfig.node.json': JSON.stringify({ | ||
| include: ['vite.config.ts'], | ||
| }), | ||
| 'src/Button.tsx': 'export const Button = () => null;', | ||
| }); | ||
|
|
||
| expect(findTsconfigPathForFile(dir, join(dir, 'src/Button.tsx'))).toBe( | ||
| join(dir, 'tsconfig.app.json') | ||
| ); | ||
| }); | ||
|
|
||
| it('keeps reference order for same-directory sibling tsconfigs that both match the file', () => { | ||
| const dir = createTempProject({ | ||
| 'tsconfig.json': JSON.stringify({ | ||
| files: [], | ||
| references: [{ path: './tsconfig.app.json' }, { path: './tsconfig.node.json' }], | ||
| }), | ||
| 'tsconfig.app.json': JSON.stringify({ | ||
| compilerOptions: { | ||
| baseUrl: '.', | ||
| }, | ||
| include: ['src'], | ||
| }), | ||
| 'tsconfig.node.json': JSON.stringify({ | ||
| compilerOptions: { | ||
| module: 'ESNext', | ||
| }, | ||
| }), | ||
| 'src/Button.tsx': 'export const Button = () => null;', | ||
| }); | ||
|
|
||
| expect(findTsconfigPathForFile(dir, join(dir, 'src/Button.tsx'))).toBe( | ||
| join(dir, 'tsconfig.app.json') | ||
| ); | ||
| }); | ||
|
|
||
| it('falls back to the nearest discovered tsconfig when no reference matches the file', () => { | ||
| const dir = createTempProject({ | ||
| 'tsconfig.json': JSON.stringify({ | ||
| compilerOptions: { | ||
| baseUrl: '.', | ||
| }, | ||
| }), | ||
| 'src/Button.tsx': 'export const Button = () => null;', | ||
| }); | ||
|
|
||
| expect(findTsconfigPathForFile(dir, join(dir, 'src/Button.tsx'))).toBe( | ||
| join(dir, 'tsconfig.json') | ||
| ); | ||
| }); | ||
|
|
||
| it('reads referenced tsconfigs that use JSONC trailing commas', () => { | ||
| const dir = createTempProject({ | ||
| 'tsconfig.json': `{ | ||
| "files": [], | ||
| "references": [ | ||
| { "path": "./tsconfig.app.json" }, | ||
| ], | ||
| }`, | ||
| 'tsconfig.app.json': `{ | ||
| "compilerOptions": { | ||
| "baseUrl": ".", | ||
| }, | ||
| "include": ["src"], | ||
| }`, | ||
| 'src/Button.tsx': 'export const Button = () => null;', | ||
| }); | ||
|
|
||
| expect(findTsconfigPathForFile(dir, join(dir, 'src/Button.tsx'))).toBe( | ||
| join(dir, 'tsconfig.app.json') | ||
| ); | ||
| }); | ||
|
|
||
| it('uses include globs inherited via extends from a nested autogen config', () => { | ||
| const dir = createTempProject({ | ||
| 'tsconfig.json': JSON.stringify({ | ||
| extends: './generated/autogen.tsconfig.json', | ||
| }), | ||
| 'generated/autogen.tsconfig.json': JSON.stringify({ | ||
| include: ['../**/*.tsx'], | ||
| }), | ||
| 'src/Button.tsx': 'export const Button = () => null;', | ||
| }); | ||
|
|
||
| expect(findTsconfigPathForFile(dir, join(dir, 'src/Button.tsx'))).toBe( | ||
| join(dir, 'tsconfig.json') | ||
| ); | ||
| }); | ||
|
|
||
| it('does not treat a referenced project as owning files outside its extended include', () => { | ||
| const dir = createTempProject({ | ||
| 'tsconfig.json': JSON.stringify({ | ||
| files: [], | ||
| references: [{ path: './tsconfig.app.json' }], | ||
| }), | ||
| 'tsconfig.app.json': JSON.stringify({ | ||
| extends: './generated/autogen.tsconfig.json', | ||
| compilerOptions: { | ||
| baseUrl: '.', | ||
| }, | ||
| }), | ||
| 'generated/autogen.tsconfig.json': JSON.stringify({ | ||
| include: ['../src/components/**/*.tsx'], | ||
| }), | ||
| 'src/components/Button.tsx': 'export const Button = () => null;', | ||
| 'src/utils/helper.ts': 'export const helper = () => null;', | ||
| }); | ||
|
|
||
| expect(findTsconfigPathForFile(dir, join(dir, 'src/components/Button.tsx'))).toBe( | ||
| join(dir, 'tsconfig.app.json') | ||
| ); | ||
| // Without extends resolution, the app config would default to **/* and incorrectly own helper.ts. | ||
| expect(findTsconfigPathForFile(dir, join(dir, 'src/utils/helper.ts'))).toBe( | ||
| join(dir, 'tsconfig.json') | ||
| ); | ||
| }); | ||
|
|
||
| it('lets a leaf include override an extended include', () => { | ||
| const dir = createTempProject({ | ||
| 'tsconfig.json': JSON.stringify({ | ||
| files: [], | ||
| references: [{ path: './tsconfig.app.json' }], | ||
| }), | ||
| 'tsconfig.app.json': JSON.stringify({ | ||
| extends: './tsconfig.base.json', | ||
| include: ['src/components'], | ||
| }), | ||
| 'tsconfig.base.json': JSON.stringify({ | ||
| include: ['src'], | ||
| }), | ||
| 'src/components/Button.tsx': 'export const Button = () => null;', | ||
| 'src/utils/helper.ts': 'export const helper = () => null;', | ||
| }); | ||
|
|
||
| expect(findTsconfigPathForFile(dir, join(dir, 'src/components/Button.tsx'))).toBe( | ||
| join(dir, 'tsconfig.app.json') | ||
| ); | ||
| expect(findTsconfigPathForFile(dir, join(dir, 'src/utils/helper.ts'))).toBe( | ||
| join(dir, 'tsconfig.json') | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('findTsconfigPathForPath', () => { | ||
| it('does not dirname directories when resolving from an importer basedir', () => { | ||
| const dir = createTempProject({ | ||
| 'tsconfig.json': JSON.stringify({ | ||
| compilerOptions: { | ||
| baseUrl: '.', | ||
| }, | ||
| }), | ||
| 'src/Button.tsx': 'export const Button = () => null;', | ||
| 'nested/tsconfig.json': JSON.stringify({ | ||
| compilerOptions: { | ||
| baseUrl: '.', | ||
| }, | ||
| }), | ||
| 'nested/src/Button.tsx': 'export const Button = () => null;', | ||
| }); | ||
|
|
||
| // Directory input must search from that directory, not its parent. | ||
| expect(findTsconfigPathForPath(join(dir, 'nested/src'))).toBe( | ||
| join(dir, 'nested/tsconfig.json') | ||
| ); | ||
| // File input still uses file-aware ownership. | ||
| expect(findTsconfigPathForPath(join(dir, 'nested/src/Button.tsx'))).toBe( | ||
| join(dir, 'nested/tsconfig.json') | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| function createTempProject(files: Record<string, string>) { | ||
| const dir = mkdtempSync(join(tmpdir(), 'storybook-tsconfig-')); | ||
| tempDirs.push(dir); | ||
|
|
||
| for (const [relativePath, content] of Object.entries(files)) { | ||
| const fullPath = join(dir, relativePath); | ||
| mkdirSync(dirname(fullPath), { recursive: true }); | ||
| writeFileSync(fullPath, content, 'utf-8'); | ||
| } | ||
|
|
||
| vi.mocked(paths.getProjectRoot).mockReturnValue(dir); | ||
| return dir; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.