Skip to content
108 changes: 108 additions & 0 deletions code/renderers/react/src/componentManifest/getComponentImports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,114 @@ test('Non-relative import remains unchanged even if packageName provided', () =>
);
});

test('Rewrites tilde-prefixed source to packageName', () => {
const code = dedent`
import { Button } from '~/components/Button';

const meta = {};
export default meta;
export const S = <Button/>;
`;
expect(getImports(code, 'pkg')).toMatchInlineSnapshot(
`
{
"components": [
{
"componentName": "Button",
"importId": "~/components/Button",
"importName": "Button",
"localImportName": "Button",
},
],
"imports": [
"import { Button } from \"pkg\";",
],
}
`
);
});

test('Rewrites hash-prefixed source to packageName', () => {
const code = dedent`
import Btn from '#Button';

const meta = {};
export default meta;
export const S = <Btn/>;
`;
expect(getImports(code, 'my-package')).toMatchInlineSnapshot(
`
{
"components": [
{
"componentName": "Btn",
"importId": "#Button",
"importName": "default",
"localImportName": "Btn",
},
],
"imports": [
"import { Btn } from "my-package";",
Comment thread
kasperpeulen marked this conversation as resolved.
],
}
`
);
});

test('Does not rewrite scoped package subpath (valid bare specifier)', () => {
Comment thread
kasperpeulen marked this conversation as resolved.
const code = dedent`
import { Button } from '@scope/ui/components';

const meta = {};
export default meta;
export const S = <Button/>;
`;
expect(getImports(code, 'pkg')).toMatchInlineSnapshot(
`
{
"components": [
{
"componentName": "Button",
"importId": "@scope/ui/components",
"importName": "Button",
"localImportName": "Button",
},
],
"imports": [
"import { Button } from \"@scope/ui/components\";",
],
}
`
);
});

test('Does not rewrite unscoped package subpath (valid bare specifier)', () => {
const code = dedent`
import { Button } from 'ui/components';

const meta = {};
export default meta;
export const S = <Button/>;
`;
expect(getImports(code, 'pkg')).toMatchInlineSnapshot(
`
{
"components": [
{
"componentName": "Button",
"importId": "ui/components",
"importName": "Button",
"localImportName": "Button",
},
],
"imports": [
"import { Button } from \"ui/components\";",
],
}
`
);
});

// Merging imports from same package

test('Merges multiple imports from the same package (defaults and named)', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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 = {
Expand Down Expand Up @@ -256,8 +257,6 @@ export const getImports = ({
order: number;
};

const isRelative = (id: string) => id.startsWith('.') || id === '.';

const withSource = components
.filter((c) => Boolean(c.importId))
.map((c, idx) => {
Expand All @@ -281,7 +280,7 @@ export const getImports = ({
const rewritten =
overrideSource !== undefined
? overrideSource
: packageName && isRelative(importId)
: packageName && !validPackageName(stripSubpath(importId))
? packageName
: importId;
return { c, src: t.stringLiteral(rewritten), key: rewritten, ord: idx };
Expand Down
67 changes: 67 additions & 0 deletions code/renderers/react/src/componentManifest/valid-package-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// inspired by https://github.com/npm/validate-npm-package-name/blob/main/lib/index.js
const scopedPackagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$');

Comment thread
kasperpeulen marked this conversation as resolved.
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];
}

Comment thread
kasperpeulen marked this conversation as resolved.
export function validPackageName(name: string) {
if (!name.length) {
return false;
}

if (name.startsWith('.')) {
return false;
}

if (name.match(/^_/)) {
Comment thread
kasperpeulen marked this conversation as resolved.
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;
Comment thread
kasperpeulen marked this conversation as resolved.
}
}
return false;
}
Comment thread
kasperpeulen marked this conversation as resolved.

return true;
}
Comment thread
kasperpeulen marked this conversation as resolved.