Skip to content
Merged
Show file tree
Hide file tree
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 Mar 31, 2026
b55a2e9
React: add parser tests for manifest RDT
kasperpeulen Mar 31, 2026
6856e79
React: reuse referenced tsconfig selection for docgen
kasperpeulen Mar 31, 2026
4caef7c
React: document tsconfig selection inspiration
kasperpeulen Mar 31, 2026
12512dc
React: fix file-aware argtypes tsconfig resolution
kasperpeulen Apr 1, 2026
0ef17b3
React: fix manifest RDT test typing
kasperpeulen Apr 1, 2026
424318f
React: fix shared tsconfig path test mocks
kasperpeulen Apr 1, 2026
e7f3cc5
React: cache manifest tsconfig lookup
kasperpeulen Apr 1, 2026
e5f45b2
React: polish revived file-aware tsconfig selection
ndelangen Aug 4, 2026
97f16d9
React: add missing .ts extensions in revived RDT tests
ndelangen Aug 4, 2026
a83376c
React: normalize path separators in RDT tsconfig ownership checks
ndelangen Aug 4, 2026
b69dd4c
React: address straightforward AI review nits on tsconfig selection
ndelangen Aug 4, 2026
fbf16b2
React: avoid dirname on importer directories for tsconfig selection
ndelangen Aug 4, 2026
a20bc24
React: cache react-docgen tsconfig path matchers
ndelangen Aug 4, 2026
b77c887
React: parse tsconfigs with TypeScript's JSONC reader
ndelangen Aug 4, 2026
d8215cb
React: avoid importing typescript from common tsconfig helpers
ndelangen Aug 4, 2026
7c38980
React: resolve tsconfig extends for file ownership matching
ndelangen Aug 10, 2026
83a8b8d
Merge branch 'next' into norbert/revive-34415-file-aware-tsconfig
ndelangen Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions code/core/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export * from './utils/remove.ts';
export * from './utils/resolve-path-in-sb-cache.ts';
export * from './utils/symlinks.ts';
export * from './utils/template.ts';
export * from './utils/tsconfig.ts';
export * from './utils/validate-config.ts';
export * from './utils/validate-configuration-files.ts';
export * from './utils/satisfies.ts';
Expand Down
225 changes: 225 additions & 0 deletions code/core/src/common/utils/__tests__/tsconfig.test.ts
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';
Comment thread
ndelangen marked this conversation as resolved.

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;
}
Loading
Loading