From 0354718e1dea09c239bf66a8affed235790af1da Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Wed, 19 Nov 2025 12:30:13 +0100 Subject: [PATCH 1/7] React: Add isPackage flag to component imports for better package identification --- .../componentManifest/getComponentImports.ts | 30 +++++++-- .../componentManifest/valid-package-name.ts | 67 ------------------- 2 files changed, 25 insertions(+), 72 deletions(-) delete mode 100644 code/renderers/react/src/componentManifest/valid-package-name.ts diff --git a/code/renderers/react/src/componentManifest/getComponentImports.ts b/code/renderers/react/src/componentManifest/getComponentImports.ts index 784a56315959..5d5449d6eedf 100644 --- a/code/renderers/react/src/componentManifest/getComponentImports.ts +++ b/code/renderers/react/src/componentManifest/getComponentImports.ts @@ -6,7 +6,6 @@ import { logger } from 'storybook/internal/node-logger'; import { getImportTag, getReactDocgen, matchPath } from './reactDocgen'; import { cachedResolveImport } from './utils'; -import { stripSubpath, validPackageName } from './valid-package-name'; // Public component metadata type used across passes export type ComponentRef = { @@ -17,6 +16,7 @@ export type ComponentRef = { importName?: string; namespace?: string; path?: string; + isPackage: boolean; reactDocgen?: ReturnType; }; @@ -192,6 +192,7 @@ export const getComponents = ({ }) .map((component) => { let path; + let isPackage = false; try { if (component.importId && storyFilePath) { path = cachedResolveImport(matchPath(component.importId, dirname(storyFilePath)), { @@ -201,17 +202,32 @@ export const getComponents = ({ } catch (e) { logger.debug(e); } + + try { + if (component.importId && !component.importId.startsWith('.') && storyFilePath) { + const realPath = cachedResolveImport(component.importId, { + basedir: dirname(storyFilePath), + }); + if (realPath.includes('node_modules')) { + isPackage = true; + } + } + } catch {} + + const componentWithPackage = { ...component, isPackage }; + if (path) { - const reactDocgen = getReactDocgen(path, component); + const reactDocgen = getReactDocgen(path, componentWithPackage); return { - ...component, + ...componentWithPackage, path, + isPackage, reactDocgen, importOverride: reactDocgen.type === 'success' ? getImportTag(reactDocgen.data) : undefined, }; } - return component; + return componentWithPackage; }) .sort((a, b) => a.componentName.localeCompare(b.componentName)); @@ -257,6 +273,8 @@ export const getImports = ({ order: number; }; + const isRelative = (id: string) => id.startsWith('.') || id === '.'; + const withSource = components .filter((c) => Boolean(c.importId)) .map((c, idx) => { @@ -280,7 +298,9 @@ export const getImports = ({ const rewritten = overrideSource !== undefined ? overrideSource - : packageName && !validPackageName(stripSubpath(importId)) + : // only rewrite to the package name it the import id is not already a valid package + // tsconfig paths such as ~/components/Button and components/Button are not seen as packages + packageName && !c.isPackage ? packageName : importId; return { c, src: t.stringLiteral(rewritten), key: rewritten, ord: idx }; diff --git a/code/renderers/react/src/componentManifest/valid-package-name.ts b/code/renderers/react/src/componentManifest/valid-package-name.ts deleted file mode 100644 index 5999402c85f1..000000000000 --- a/code/renderers/react/src/componentManifest/valid-package-name.ts +++ /dev/null @@ -1,67 +0,0 @@ -// inspired by https://github.com/npm/validate-npm-package-name/blob/main/lib/index.js -const scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$'); - -export function stripSubpath(name: string): string { - const parts = name.split('/'); - - if (name.startsWith('@')) { - // @scope/pkg/... - if (parts.length >= 3) { - return `${parts[0]}/${parts[1]}`; - } - return name; - } - - // react/..., lodash/..., etc - return parts[0]; -} - -export function validPackageName(name: string) { - if (!name.length) { - return false; - } - - if (name.startsWith('.')) { - return false; - } - - if (name.match(/^_/)) { - return false; - } - - if (name.trim() !== name) { - return false; - } - - if (name.length > 214) { - return false; - } - - // mIxeD CaSe nAMEs - if (name.toLowerCase() !== name) { - return false; - } - - if (/[~'!()*]/.test(name.split('/').slice(-1)[0])) { - return false; - } - - if (encodeURIComponent(name) !== name) { - const nameMatch = name.match(scopedPackagePattern); - if (nameMatch) { - const org = nameMatch[1]; - const pkg = nameMatch[2]; - - if (pkg.startsWith('.')) { - return false; - } - - if (encodeURIComponent(org) === org && encodeURIComponent(pkg) === pkg) { - return true; - } - } - return false; - } - - return true; -} From a3b196a6f3a4a353c0b413eb97001e5f5ec8f18e Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Wed, 19 Nov 2025 12:36:16 +0100 Subject: [PATCH 2/7] Update code/renderers/react/src/componentManifest/getComponentImports.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../react/src/componentManifest/getComponentImports.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/code/renderers/react/src/componentManifest/getComponentImports.ts b/code/renderers/react/src/componentManifest/getComponentImports.ts index 5d5449d6eedf..f22fadd25eef 100644 --- a/code/renderers/react/src/componentManifest/getComponentImports.ts +++ b/code/renderers/react/src/componentManifest/getComponentImports.ts @@ -218,14 +218,17 @@ export const getComponents = ({ if (path) { const reactDocgen = getReactDocgen(path, componentWithPackage); + return { return { ...componentWithPackage, path, - isPackage, reactDocgen, importOverride: reactDocgen.type === 'success' ? getImportTag(reactDocgen.data) : undefined, }; + importOverride: + reactDocgen.type === 'success' ? getImportTag(reactDocgen.data) : undefined, + }; } return componentWithPackage; }) From 41112e743d5fb14c7f77143d6881eed0abe144dc Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Wed, 19 Nov 2025 12:42:18 +0100 Subject: [PATCH 3/7] improve logic --- .../src/componentManifest/getComponentImports.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/code/renderers/react/src/componentManifest/getComponentImports.ts b/code/renderers/react/src/componentManifest/getComponentImports.ts index f22fadd25eef..5853dba1b358 100644 --- a/code/renderers/react/src/componentManifest/getComponentImports.ts +++ b/code/renderers/react/src/componentManifest/getComponentImports.ts @@ -205,12 +205,9 @@ export const getComponents = ({ try { if (component.importId && !component.importId.startsWith('.') && storyFilePath) { - const realPath = cachedResolveImport(component.importId, { - basedir: dirname(storyFilePath), - }); - if (realPath.includes('node_modules')) { - isPackage = true; - } + // throws when it can not be resolved + cachedResolveImport(component.importId, { basedir: dirname(storyFilePath) }); + isPackage = true; } } catch {} @@ -218,7 +215,6 @@ export const getComponents = ({ if (path) { const reactDocgen = getReactDocgen(path, componentWithPackage); - return { return { ...componentWithPackage, path, @@ -226,9 +222,6 @@ export const getComponents = ({ importOverride: reactDocgen.type === 'success' ? getImportTag(reactDocgen.data) : undefined, }; - importOverride: - reactDocgen.type === 'success' ? getImportTag(reactDocgen.data) : undefined, - }; } return componentWithPackage; }) From dee069b0e505e936c2cae097059083666a5b9ae8 Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Wed, 19 Nov 2025 13:16:02 +0100 Subject: [PATCH 4/7] Fix tests --- .../react/src/componentManifest/fixtures.ts | 6 +- .../getComponentImports.test.ts | 231 +++++++++--------- code/renderers/react/vitest.setup.ts | 12 +- 3 files changed, 127 insertions(+), 122 deletions(-) diff --git a/code/renderers/react/src/componentManifest/fixtures.ts b/code/renderers/react/src/componentManifest/fixtures.ts index 075557eb84d1..e7597601f306 100644 --- a/code/renderers/react/src/componentManifest/fixtures.ts +++ b/code/renderers/react/src/componentManifest/fixtures.ts @@ -31,7 +31,7 @@ export const fsMocks = { /** * Primary UI component for user interaction - * @import import { Button } from '@design-system/components/Button'; + * @import import { Button } from '@design-system/components/override'; */ export const Button = ({ primary = false, @@ -60,7 +60,6 @@ export const fsMocks = { /** * Description from meta and very long. * @summary Component summary - * @import import { Header } from '@design-system/components/Header'; */ const meta = { component: Header, @@ -85,9 +84,6 @@ export const fsMocks = { onCreateAccount?: () => void; } - /** - * @import import { Header } from '@design-system/components/Header'; - */ export default ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => (
diff --git a/code/renderers/react/src/componentManifest/getComponentImports.test.ts b/code/renderers/react/src/componentManifest/getComponentImports.test.ts index 83c07a4b71cd..b1602043bcde 100644 --- a/code/renderers/react/src/componentManifest/getComponentImports.test.ts +++ b/code/renderers/react/src/componentManifest/getComponentImports.test.ts @@ -13,17 +13,20 @@ beforeEach(() => { vol.fromJSON(fsMocks, '/app'); }); -const getImports = (code: string, packageName?: string, storyFilePath?: string) => - getComponentData({ +const getImports = (code: string, packageName?: string, storyFilePath?: string) => { + storyFilePath ??= '/app/src/stories/Button.stories.tsx'; + const { components, imports } = getComponentData({ csf: loadCsf(code, { makeTitle: (t?: string) => t ?? 'title' }).parse(), packageName, storyFilePath, }); + return { components: components.map(({ reactDocgen, ...rest }) => rest), imports }; +}; test('Get imports from multiple components', () => { const code = dedent` import type { Meta } from '@storybook/react'; - import { ButtonGroup } from '@design-system/button-group'; + import { ButtonGroup } from './button-group'; import { Button } from '@design-system/button'; const meta: Meta = { @@ -43,18 +46,22 @@ test('Get imports from multiple components', () => { "componentName": "Button", "importId": "@design-system/button", "importName": "Button", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Button", + "path": "./src/stories/Button.tsx", }, { "componentName": "ButtonGroup", - "importId": "@design-system/button-group", + "importId": "./button-group", "importName": "ButtonGroup", + "isPackage": false, "localImportName": "ButtonGroup", }, ], "imports": [ - "import { Button } from "@design-system/button";", - "import { ButtonGroup } from "@design-system/button-group";", + "import { Button } from "@design-system/components/override";", + "import { ButtonGroup } from "./button-group";", ], } ` @@ -63,7 +70,7 @@ test('Get imports from multiple components', () => { test('Namespace import with member usage', () => { const code = dedent` - import * as Accordion from '@ds/accordion'; + import * as Accordion from './accordion'; const meta = {}; export default meta; @@ -75,14 +82,15 @@ test('Namespace import with member usage', () => { "components": [ { "componentName": "Accordion.Root", - "importId": "@ds/accordion", + "importId": "./accordion", "importName": "Root", + "isPackage": false, "localImportName": "Accordion", "namespace": "Accordion", }, ], "imports": [ - "import * as Accordion from "@ds/accordion";", + "import * as Accordion from "./accordion";", ], } ` @@ -91,7 +99,7 @@ test('Namespace import with member usage', () => { test('Named import used as namespace object', () => { const code = dedent` - import { Accordion } from '@ds/accordion'; + import { Accordion } from './accordion'; const meta = {}; export default meta; @@ -103,13 +111,14 @@ test('Named import used as namespace object', () => { "components": [ { "componentName": "Accordion.Root", - "importId": "@ds/accordion", + "importId": "./accordion", "importName": "Accordion", + "isPackage": false, "localImportName": "Accordion", }, ], "imports": [ - "import { Accordion } from "@ds/accordion";", + "import { Accordion } from "./accordion";", ], } ` @@ -132,11 +141,14 @@ test('Default import', () => { "componentName": "Button", "importId": "@ds/button", "importName": "default", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Button", + "path": "./src/stories/Button.tsx", }, ], "imports": [ - "import Button from "@ds/button";", + "import { Button } from "@design-system/components/override";", ], } ` @@ -145,7 +157,8 @@ test('Default import', () => { test('Alias named import and meta.component inclusion', () => { const code = dedent` - import DefaultComponent, { Button as Btn, Other } from '@ds/button'; + import DefaultComponent, { Button as Btn } from '@ds/button'; + import { Other } from './other'; const meta = { component: Btn }; export default meta; @@ -159,17 +172,22 @@ test('Alias named import and meta.component inclusion', () => { "componentName": "Btn", "importId": "@ds/button", "importName": "Button", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Btn", + "path": "./src/stories/Button.tsx", }, { "componentName": "Other", - "importId": "@ds/button", + "importId": "./other", "importName": "Other", + "isPackage": false, "localImportName": "Other", }, ], "imports": [ - "import { Button as Btn, Other } from "@ds/button";", + "import { Button as Btn } from "@design-system/components/override";", + "import { Other } from "./other";", ], } ` @@ -192,11 +210,14 @@ test('Strip unused specifiers from the same import statement', () => { "componentName": "Btn", "importId": "@ds/button", "importName": "Button", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Btn", + "path": "./src/stories/Button.tsx", }, ], "imports": [ - "import { Button as Btn } from "@ds/button";", + "import { Button as Btn } from "@design-system/components/override";", ], } ` @@ -205,7 +226,7 @@ test('Strip unused specifiers from the same import statement', () => { test('Meta component with member and star import', () => { const code = dedent` - import * as Accordion from '@ds/accordion'; + import * as Accordion from './accordion'; const meta = { component: Accordion.Root }; export default meta; @@ -216,14 +237,15 @@ test('Meta component with member and star import', () => { "components": [ { "componentName": "Accordion.Root", - "importId": "@ds/accordion", + "importId": "./accordion", "importName": "Root", + "isPackage": false, "localImportName": "Accordion", "namespace": "Accordion", }, ], "imports": [ - "import * as Accordion from "@ds/accordion";", + "import * as Accordion from "./accordion";", ], } ` @@ -232,7 +254,8 @@ test('Meta component with member and star import', () => { test('Keeps multiple named specifiers and drops unused ones from same import', () => { const code = dedent` - import { Button, ButtonGroup, useHook } from '@ds/button'; + import { Button, useHook } from '@ds/button'; + import { ButtonGroup } from './button-group'; const meta = {}; export default meta; @@ -246,17 +269,22 @@ test('Keeps multiple named specifiers and drops unused ones from same import', ( "componentName": "Button", "importId": "@ds/button", "importName": "Button", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Button", + "path": "./src/stories/Button.tsx", }, { "componentName": "ButtonGroup", - "importId": "@ds/button", + "importId": "./button-group", "importName": "ButtonGroup", + "isPackage": false, "localImportName": "ButtonGroup", }, ], "imports": [ - "import { Button, ButtonGroup } from "@ds/button";", + "import { Button } from "@design-system/components/override";", + "import { ButtonGroup } from "./button-group";", ], } ` @@ -279,11 +307,14 @@ test('Mixed default + named import: keep only default when only default used', ( "componentName": "Button", "importId": "@ds/button", "importName": "default", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Button", + "path": "./src/stories/Button.tsx", }, ], "imports": [ - "import Button from "@ds/button";", + "import { Button } from "@design-system/components/override";", ], } ` @@ -306,11 +337,14 @@ test('Mixed default + named import: keep only named when only named (alias) used "componentName": "Btn", "importId": "@ds/button", "importName": "Button", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Btn", + "path": "./src/stories/Button.tsx", }, ], "imports": [ - "import { Button as Btn } from "@ds/button";", + "import { Button as Btn } from "@design-system/components/override";", ], } ` @@ -334,11 +368,14 @@ test('Per-specifier type import is dropped when mixing with value specifiers', ( "componentName": "Button", "importId": "@ds/button", "importName": "Button", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Button", + "path": "./src/stories/Button.tsx", }, ], "imports": [ - "import { Button } from "@ds/button";", + "import { Button } from "@design-system/components/override";", ], } ` @@ -347,7 +384,7 @@ test('Per-specifier type import is dropped when mixing with value specifiers', ( test('Namespace import used for multiple members kept once', () => { const code = dedent` - import * as DS from '@ds/ds'; + import * as DS from './ds'; const meta = {}; export default meta; @@ -359,21 +396,23 @@ test('Namespace import used for multiple members kept once', () => { "components": [ { "componentName": "DS.A", - "importId": "@ds/ds", + "importId": "./ds", "importName": "A", + "isPackage": false, "localImportName": "DS", "namespace": "DS", }, { "componentName": "DS.B", - "importId": "@ds/ds", + "importId": "./ds", "importName": "B", + "isPackage": false, "localImportName": "DS", "namespace": "DS", }, ], "imports": [ - "import * as DS from "@ds/ds";", + "import * as DS from "./ds";", ], } ` @@ -395,11 +434,14 @@ test('Default import kept when referenced only via meta.component', () => { "componentName": "Button", "importId": "@ds/button", "importName": "default", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Button", + "path": "./src/stories/Button.tsx", }, ], "imports": [ - "import Button from "@ds/button";", + "import { Button } from "@design-system/components/override";", ], } ` @@ -423,11 +465,14 @@ test('Side-effect-only import is ignored', () => { "componentName": "Button", "importId": "@ds/button", "importName": "Button", + "importOverride": "import { Button } from '@design-system/components/override';", + "isPackage": true, "localImportName": "Button", + "path": "./src/stories/Button.tsx", }, ], "imports": [ - "import { Button } from "@ds/button";", + "import { Button } from "@design-system/components/override";", ], } ` @@ -438,93 +483,30 @@ test('Side-effect-only import is ignored', () => { test('Converts default relative import to import override when provided', () => { const code = dedent` - import Header from './Header'; + import Button from './Button'; const meta = {}; export default meta; - export const S =
; + export const S =