Skip to content

Commit

Permalink
Merge branch 'storybookjs:next' into remove-cpy
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris Plummer authored Jun 21, 2022
2 parents 044e020 + 1edd3a6 commit 45ab4ae
Show file tree
Hide file tree
Showing 18 changed files with 370 additions and 71 deletions.
54 changes: 33 additions & 21 deletions addons/interactions/src/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,32 +134,12 @@ export const Panel: React.FC<AddonPanelProps> = (props) => {
const [isRerunAnimating, setIsRerunAnimating] = React.useState(false);
const [scrollTarget, setScrollTarget] = React.useState<HTMLElement>();
const [collapsed, setCollapsed] = React.useState<Set<Call['id']>>(new Set());
const [log, setLog] = React.useState<LogItem[]>([]);

// Calls are tracked in a ref so we don't needlessly rerender.
const calls = React.useRef<Map<Call['id'], Omit<Call, 'status'>>>(new Map());
const setCall = ({ status, ...call }: Call) => calls.current.set(call.id, call);

const [log, setLog] = React.useState<LogItem[]>([]);
const childCallMap = new Map<Call['id'], Call['id'][]>();
const interactions = log
.filter((call) => {
if (!call.parentId) return true;
childCallMap.set(call.parentId, (childCallMap.get(call.parentId) || []).concat(call.callId));
return !collapsed.has(call.parentId);
})
.map(({ callId, status }) => ({
...calls.current.get(callId),
status,
childCallIds: childCallMap.get(callId),
isCollapsed: collapsed.has(callId),
toggleCollapsed: () =>
setCollapsed((ids) => {
if (ids.has(callId)) ids.delete(callId);
else ids.add(callId);
return new Set(ids);
}),
}));

const endRef = React.useRef();
React.useEffect(() => {
let observer: IntersectionObserver;
Expand Down Expand Up @@ -212,6 +192,38 @@ export const Panel: React.FC<AddonPanelProps> = (props) => {
const showStatus = log.length > 0 && !isPlaying;
const hasException = log.some((item) => item.status === CallStates.ERROR);

const interactions = React.useMemo(() => {
const callsById = new Map<Call['id'], Call>();
const childCallMap = new Map<Call['id'], Call['id'][]>();
return log
.filter(({ callId, parentId }) => {
if (!parentId) return true;
childCallMap.set(parentId, (childCallMap.get(parentId) || []).concat(callId));
return !collapsed.has(parentId);
})
.map(({ callId, status }) => ({ ...calls.current.get(callId), status } as Call))
.map((call) => {
const status =
call.status === CallStates.ERROR &&
callsById.get(call.parentId)?.status === CallStates.ACTIVE
? CallStates.ACTIVE
: call.status;
callsById.set(call.id, { ...call, status });
return {
...call,
status,
childCallIds: childCallMap.get(call.id),
isCollapsed: collapsed.has(call.id),
toggleCollapsed: () =>
setCollapsed((ids) => {
if (ids.has(call.id)) ids.delete(call.id);
else ids.add(call.id);
return new Set(ids);
}),
};
});
}, [log, collapsed]);

return (
<React.Fragment key="interactions">
<TabStatus>
Expand Down
2 changes: 2 additions & 0 deletions app/react/src/client/preview/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,6 @@ export async function renderToDOM(
}

await renderElement(element, domElement);

return () => unmountElement(domElement);
}
2 changes: 1 addition & 1 deletion examples/react-ts/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const config: StorybookConfig = {
},
features: {
postcss: false,
// modernInlineRender: true,
modernInlineRender: true,
storyStoreV7: !global.navigator?.userAgent?.match?.('jsdom'),
buildStoriesJson: true,
babelModeV7: true,
Expand Down
45 changes: 45 additions & 0 deletions examples/react-ts/src/__snapshots__/storyshots.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4041,6 +4041,51 @@ exports[`Storyshots Demo/Examples / Emoji Button With Args 1`] = `
</button>
`;

exports[`Storyshots Demo/button2 One 1`] = `
<button
type="button"
>
one
</button>
`;

exports[`Storyshots Demo/button2 Three 1`] = `
<button
type="button"
>
three
</button>
`;

exports[`Storyshots Demo/button2 Two 1`] = `
<button
type="button"
>
two
</button>
`;

exports[`Storyshots Demo/button3 Five 1`] = `
<button
type="button"
>
five
</button>
`;

exports[`Storyshots Demo/button3 Four 1`] = `
<button
type="button"
>
four
</button>
`;

exports[`Storyshots Docs/ButtonMdx Basic 1`] = `
<button
type="button"
Expand Down
19 changes: 13 additions & 6 deletions examples/react-ts/src/button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ComponentType, ButtonHTMLAttributes } from 'react';
import React, { ComponentType, ButtonHTMLAttributes, useEffect } from 'react';

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/**
Expand All @@ -12,8 +12,15 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
icon?: ComponentType;
}

export const Button = ({ label = 'Hello', icon: Icon, ...props }: ButtonProps) => (
<button type="button" {...props}>
{Icon ? <Icon /> : null} {label}
</button>
);
export const Button = ({ label = 'Hello', icon: Icon, ...props }: ButtonProps) => {
useEffect(() => {
const fn = () => console.log(`click ${label}`);
global.window.document.querySelector('body')?.addEventListener('click', fn);
return () => global.window.document.querySelector('body')?.removeEventListener('click', fn);
});
return (
<button type="button" {...props}>
{Icon ? <Icon /> : null} {label}
</button>
);
};
10 changes: 10 additions & 0 deletions examples/react-ts/src/button2.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Button } from './button';

export default {
component: Button,
title: 'button2',
};

export const one = { args: { label: 'one' } };
export const two = { args: { label: 'two' } };
export const three = { args: { label: 'three' } };
9 changes: 9 additions & 0 deletions examples/react-ts/src/button3.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Button } from './button';

export default {
component: Button,
title: 'button3',
};

export const four = { args: { label: 'four' } };
export const five = { args: { label: 'five' } };
2 changes: 2 additions & 0 deletions lib/cli/src/automigrate/fixes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { vue3 } from './vue3';
import { mainjsFramework } from './mainjsFramework';
import { eslintPlugin } from './eslint-plugin';
import { builderVite } from './builder-vite';
import { npm7 } from './npm7';
import { Fix } from '../types';

export * from '../types';
Expand All @@ -16,4 +17,5 @@ export const fixes: Fix[] = [
mainjsFramework,
eslintPlugin,
builderVite,
npm7,
];
47 changes: 47 additions & 0 deletions lib/cli/src/automigrate/fixes/npm7.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { NPMProxy } from '../../js-package-manager/NPMProxy';
import { npm7 } from './npm7';

const mockExecuteCommand = jest.fn();
class MockedNPMProxy extends NPMProxy {
executeCommand(...args) {
return mockExecuteCommand(...args);
}
}

function mockExecuteResults(map: Record<string, string>) {
mockExecuteCommand.mockImplementation((command, args) => {
const commandString = `${command} ${args.join(' ')}`;
if (map[commandString]) return map[commandString];

throw new Error(`Unexpected execution of '${commandString}'`);
});
}

describe('npm7 fix', () => {
describe('npm < 7', () => {
it('does not match', async () => {
mockExecuteResults({ 'npm --version': '6.0.0' });
expect(await npm7.check({ packageManager: new MockedNPMProxy() })).toEqual(null);
});
});

describe('npm 7+', () => {
it('matches if config is not installed', async () => {
mockExecuteResults({
'npm --version': '7.0.0',
'npm config get legacy-peer-deps --location=project': 'false',
});
expect(await npm7.check({ packageManager: new MockedNPMProxy() })).toEqual({
npmVersion: '7.0.0',
});
});

it('does not match if config is installed', async () => {
mockExecuteResults({
'npm --version': '7.0.0',
'npm config get legacy-peer-deps --location=project': 'true',
});
expect(await npm7.check({ packageManager: new MockedNPMProxy() })).toEqual(null);
});
});
});
41 changes: 41 additions & 0 deletions lib/cli/src/automigrate/fixes/npm7.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import chalk from 'chalk';
import dedent from 'ts-dedent';
import { Fix } from '../types';
import { NPMProxy } from '../../js-package-manager/NPMProxy';

interface Npm7RunOptions {
npmVersion: string;
}

/**
* Is the user using npm7+? If so create a .npmrc with legacy-peer-deps=true
*/
export const npm7: Fix<Npm7RunOptions> = {
id: 'npm7',

async check({ packageManager }) {
if (packageManager.type !== 'npm') return null;

const npmVersion = (packageManager as NPMProxy).getNpmVersion();
if ((packageManager as NPMProxy).needsLegacyPeerDeps(npmVersion)) {
return { npmVersion };
}
return null;
},

prompt({ npmVersion }) {
const npmFormatted = chalk.cyan(`npm ${npmVersion}`);
return dedent`
We've detected you are running ${npmFormatted} which has peer dependency semantics which Storybook is incompatible with.
In order to work with Storybook's package structure, you'll need to run \`npm\` with the
\`--legacy-peer-deps=true\` flag. We can generate an \`.npmrc\` which will do that automatically.
More info: ${chalk.yellow('https://github.com/storybookjs/storybook/issues/18298')}
`;
},

async run({ packageManager }) {
(packageManager as NPMProxy).setLegacyPeerDeps();
},
};
25 changes: 23 additions & 2 deletions lib/cli/src/js-package-manager/NPMProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,31 @@ export class NPMProxy extends JsPackageManager {
return `npm run ${command}`;
}

getNpmVersion(): string {
return this.executeCommand('npm', ['--version']);
}

hasLegacyPeerDeps() {
const result = this.executeCommand('npm', [
'config',
'get',
'legacy-peer-deps',
'--location=project',
]);
return result.trim() === 'true';
}

setLegacyPeerDeps() {
this.executeCommand('npm', ['config', 'set', 'legacy-peer-deps=true', '--location=project']);
}

needsLegacyPeerDeps(version: string) {
return semver.gte(version, '7.0.0') && !this.hasLegacyPeerDeps();
}

getInstallArgs(): string[] {
if (!this.installArgs) {
const version = this.executeCommand('npm', ['--version']);
this.installArgs = semver.gte(version, '7.0.0')
this.installArgs = this.needsLegacyPeerDeps(this.getNpmVersion())
? ['install', '--legacy-peer-deps']
: ['install'];
}
Expand Down
3 changes: 3 additions & 0 deletions lib/instrumenter/src/instrumenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ export class Instrumenter {
this.channel.on(STORY_RENDER_PHASE_CHANGED, ({ storyId, newPhase }) => {
const { isDebugging } = this.getState(storyId);
this.setState(storyId, { renderPhase: newPhase });
if (newPhase === 'preparing' && isDebugging) {
resetState({ storyId });
}
if (newPhase === 'playing') {
resetState({ storyId, isDebugging });
}
Expand Down
3 changes: 2 additions & 1 deletion lib/preview-web/src/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
StoryIndex,
PromiseLike,
WebProjectAnnotations,
RenderToDOM,
} from '@storybook/store';

import { StoryRender } from './StoryRender';
Expand All @@ -45,7 +46,7 @@ export class Preview<TFramework extends AnyFramework> {

importFn?: ModuleImportFn;

renderToDOM: WebProjectAnnotations<TFramework>['renderToDOM'];
renderToDOM: RenderToDOM<TFramework>;

storyRenders: StoryRender<TFramework>[] = [];

Expand Down
5 changes: 3 additions & 2 deletions lib/preview-web/src/PreviewWeb.mockdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
STORY_RENDER_PHASE_CHANGED,
STORY_THREW_EXCEPTION,
} from '@storybook/core-events';
import { StoryIndex } from '@storybook/store';
import { StoryIndex, TeardownRenderToDOM } from '@storybook/store';
import { RenderPhase } from './PreviewWeb';

export const componentOneExports = {
Expand All @@ -32,12 +32,13 @@ export const importFn = jest.fn(async (path) => {
return path === './src/ComponentOne.stories.js' ? componentOneExports : componentTwoExports;
});

export const teardownRenderToDOM: jest.Mock<TeardownRenderToDOM> = jest.fn();
export const projectAnnotations = {
globals: { a: 'b' },
globalTypes: {},
decorators: [jest.fn((s) => s())],
render: jest.fn(),
renderToDOM: jest.fn(),
renderToDOM: jest.fn().mockReturnValue(teardownRenderToDOM),
};
export const getProjectAnnotations = () => projectAnnotations;

Expand Down
Loading

0 comments on commit 45ab4ae

Please sign in to comment.