Skip to content

Commit 6c420a1

Browse files
authored
chore: implement new storybook architecture - for converged packages/react-menu (#17866)
* chore(.storybook): implement root sb config * chore(react-menu): implement local storybook with stories * chore(react-examples): process collocated stories properly and remove react-menu from dependencies * Change files * chore(.storybook): use react-storybook instead storybook * chore: update base.json to with new react-menu deps * chore: update codeowners
1 parent 5e6aa4d commit 6c420a1

18 files changed

+237
-37
lines changed

.github/CODEOWNERS

+4
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@
1616
1717

1818
#### Build stuff
19+
/.storybook/ @microsoft/fluentui-react-build
1920
scripts/ @microsoft/fluentui-react-build
2021
package.json @microsoft/fluentui-react-build
2122
yarn.lock @microsoft/fluentui-react-build
2223
*.config.js @microsoft/fluentui-react-build
2324
*.sh @microsoft/fluentui-react-build
2425
*.yml @microsoft/fluentui-react-build
2526
tsconfig.json @microsoft/fluentui-react-build
27+
/tsconfig.base.json @microsoft/fluentui-react-build
28+
/jest.config.js @microsoft/fluentui-react-build
29+
/jest.preset.js @microsoft/fluentui-react-build
2630
lerna.json @microsoft/fluentui-react-build
2731
.codesandbox @microsoft/fluentui-react-build
2832
.devops @microsoft/fluentui-react-build

.storybook/main.js

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const path = require('path');
2+
const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin');
3+
4+
/**
5+
* @callback StorybookWebpackConfig
6+
* @param {import("webpack").Configuration} config
7+
* @param {{configType: 'DEVELOPMENT' | 'PRODUCTION'}} options - change the build configuration. 'PRODUCTION' is used when building the static version of storybook.
8+
* @returns {import("webpack").Configuration}
9+
*/
10+
11+
/**
12+
* @typedef {{check:boolean; checkOptions: Record<string,unknown>; reactDocgen: string | boolean; reactDocgenTypescriptOptions: Record<string,unknown>}} StorybookTsConfig
13+
*/
14+
15+
/**
16+
* @typedef {{stories: string[] ; addons: string[]; typescript: StorybookTsConfig; babel: (options:Record<string,unknown>)=>Promise<Record<string,unknown>>; webpackFinal: StorybookWebpackConfig}} StorybookConfig
17+
*/
18+
19+
module.exports = /** @type {Pick<StorybookConfig,'addons' |'stories' |'webpackFinal'>} */ ({
20+
stories: [],
21+
addons: [
22+
'@storybook/addon-essentials',
23+
'@storybook/addon-a11y',
24+
'@storybook/addon-knobs/preset',
25+
'storybook-addon-performance',
26+
],
27+
webpackFinal: config => {
28+
const tsPaths = new TsconfigPathsPlugin({
29+
configFile: path.resolve(__dirname, '../tsconfig.base.json'),
30+
});
31+
32+
if (config.resolve) {
33+
config.resolve.plugins ? config.resolve.plugins.push(tsPaths) : (config.resolve.plugins = [tsPaths]);
34+
}
35+
36+
return config;
37+
},
38+
});

.storybook/preview.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { withFluentProvider, withStrictMode } from '@fluentui/react-storybook';
2+
3+
export const decorators = [withFluentProvider, withStrictMode];

.storybook/tsconfig.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "../tsconfig.base.json",
3+
"compilerOptions": {
4+
"noEmit": true,
5+
"allowJs": true,
6+
"checkJs": true,
7+
"jsx": "preserve"
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "chore(react-examples): process collocated stories properly and remove react-menu from dependencies",
4+
"packageName": "@fluentui/react-examples",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "none",
3+
"comment": "chore(react-menu): implement local storybook with stories",
4+
"packageName": "@fluentui/react-menu",
5+
"email": "[email protected]",
6+
"dependentChangeType": "none"
7+
}

package.json

+4-3
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@
8080
"@storybook/channels": "6.0.28",
8181
"@storybook/core": "6.0.28",
8282
"@storybook/react": "6.0.28",
83+
"@testing-library/jest-dom": "5.11.9",
8384
"@testing-library/react": "10.4.9",
8485
"@testing-library/react-hooks": "5.0.3",
85-
"@testing-library/jest-dom": "5.11.9",
8686
"@types/copy-webpack-plugin": "6.4.0",
8787
"@types/jest": "24.9.1",
8888
"@types/jest-axe": "3.2.2",
@@ -101,9 +101,9 @@
101101
"css-loader": "5.0.1",
102102
"cypress": "6.6.0",
103103
"cypress-real-events": "1.2.0",
104-
"eslint-plugin-es": "4.1.0",
105104
"chalk": "2.4.2",
106105
"danger": "^6.0.5",
106+
"eslint-plugin-es": "4.1.0",
107107
"file-loader": "6.2.0",
108108
"gulp": "^4.0.2",
109109
"html-webpack-plugin": "5.1.0",
@@ -129,8 +129,9 @@
129129
"syncpack": "^5.6.10",
130130
"ts-jest": "24.3.0",
131131
"ts-loader": "8.0.14",
132-
"webpack": "5.21.2",
132+
"tsconfig-paths-webpack-plugin": "3.5.1",
133133
"typescript": "4.1.5",
134+
"webpack": "5.21.2",
134135
"webpack-cli": "4.3.1",
135136
"webpack-dev-server": "4.0.0-beta.0",
136137
"tmp": "0.2.1",

packages/react-examples/.storybook/preview-loader.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,23 @@ export default function loader(source) {
3131
// get unscoped dep names
3232
const reactDeps = Object.keys(reactPackageJson.dependencies).map(d => d.split('/')[1] || d);
3333
const reactDepsWithExamples = packagesWithExamples.filter(p => reactDeps.includes(p));
34-
source = source.replace(/REACT_DEPS/g, reactDepsWithExamples.join('|'));
34+
35+
// @TODO
36+
// - this is a temporary solution until all converged packages use new storybook configuration
37+
// - after new config is in place remove this whole IF
38+
//
39+
// NOTE:
40+
// - if we run storybook for react-components we wanna include all possible package collocated stories
41+
// based on react-components package.json
42+
if (packageName === 'react-components') {
43+
const _convergedDependencies = reactDeps.filter(dependencyName => {
44+
return dependencyName.startsWith('react-');
45+
});
46+
47+
reactDepsWithExamples.push(..._convergedDependencies);
48+
}
49+
50+
source = source.replace(/REACT_DEPS/g, [...new Set(reactDepsWithExamples)].join('|'));
3551
}
3652

3753
return source;

packages/react-examples/.storybook/preview.js

+97-27
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import { withInfo } from '@storybook/addon-info';
55
import { withPerformance } from 'storybook-addon-performance';
66
import { withFluentProvider, withKeytipLayer, withStrictMode } from '@fluentui/storybook';
77

8+
/**
9+
* "PACKAGE_NAME" placeholder is being replaced by webpack loader - @link {./preview.loader}
10+
* @type {string}
11+
*/
12+
const packageNamePlaceholder = 'PACKAGE_NAME';
13+
814
addDecorator(withInfo);
915
addDecorator(withPerformance);
1016
addDecorator(withKeytipLayer);
@@ -36,7 +42,7 @@ export const parameters = {
3642
* NOTE:
3743
* - this is a temporary workaround until we migrate to new storybook 6 APIs -> old `addDecorator` duplicates rendered decorators
3844
* - source of this function is interpolated during runtime with webpack
39-
* - "PACKAGE_NAME" placeholder is being replaced
45+
*
4046
*/
4147
function addCustomDecorators() {
4248
/**
@@ -46,7 +52,7 @@ function addCustomDecorators() {
4652

4753
if (
4854
['react-button', 'react-cards', 'react-checkbox', 'react-slider', 'react-tabs', 'react-toggle'].includes(
49-
'PACKAGE_NAME',
55+
packageNamePlaceholder,
5056
)
5157
) {
5258
initializeIcons();
@@ -66,7 +72,7 @@ function addCustomDecorators() {
6672
'react-text',
6773
'react-components',
6874
'react-portal',
69-
].includes('PACKAGE_NAME')
75+
].includes(packageNamePlaceholder)
7076
) {
7177
customDecorators.add(withFluentProvider).add(withStrictMode);
7278
}
@@ -109,15 +115,22 @@ function loadStories() {
109115
require.context('../src/PACKAGE_NAME', true, /\.(Example|stories)\.tsx$/),
110116
];
111117

112-
// @ts-ignore -- PACKAGE_NAME is replaced by a loader
113-
if ('PACKAGE_NAME' === 'react' || 'PACKAGE_NAME' === 'react-components') {
118+
if (packageNamePlaceholder === 'react' || packageNamePlaceholder === 'react-components') {
114119
// For suite package storybooks, also show the examples of re-exported component packages.
115120
// preview-loader will replace REACT_ DEPS with the actual list.
116121
contexts.push(
117122
require.context('../src', true, /(REACT_DEPS|PACKAGE_NAME)\/\w+\/[\w.]+\.(Example|stories)\.(tsx|mdx)$/),
118123
);
119124
}
120125

126+
// @TODO
127+
// - this is a temporary solution until all converged packages use new storybook configuration
128+
// - after new config is in place remove this whole IF
129+
if (packageNamePlaceholder === 'react-components') {
130+
// include package collocated stories within react-components
131+
contexts.push(require.context('../../', true, /(REACT_DEPS)\/src\/[\w./]+\.(Example|stories)\.(tsx|mdx)$/));
132+
}
133+
121134
for (const req of contexts) {
122135
req.keys().forEach(key => {
123136
generateStoriesFromExamples(key, stories, req);
@@ -150,8 +163,11 @@ function generateStoriesFromExamples(key, stories, req) {
150163
// Depending on the starting point of the context, and the package layout, the key will be like one of these:
151164
// ./ComponentName/ComponentName.Something.Example.tsx
152165
// ./package-name/ComponentName/ComponentName.Something.Example.tsx
166+
// ./package-name/src/.../ComponentName.stories.tsx - @TODO remove this line after new storybook setup has been applied for all converged packages
153167
const segments = key.split('/');
168+
154169
if (segments.length < 3) {
170+
console.warn(`Invalid storybook context location found: key: ${key} | segments: ${segments}`);
155171
return;
156172
}
157173

@@ -161,28 +177,7 @@ function generateStoriesFromExamples(key, stories, req) {
161177
return;
162178
}
163179

164-
/** @type {string} */
165-
let componentName;
166-
167-
// Story URLs are generated based off the story name
168-
// In the case of `react-components` a (package name) suffix is added to each story
169-
// This results in a difference name and URL between individual storybooks and the react-components suite storyboo
170-
// https://storybook.js.org/docs/react/configure/sidebar-and-urls#permalinking-to-stories
171-
// Use the id property in stories to ensure the same URL between individual and suite storyboo
172-
/** @type {string} */
173-
let componentId;
174-
175-
if (segments.length === 3) {
176-
// ./ComponentName/ComponentName.Something.Example.tsx
177-
componentName = segments[1];
178-
componentId = segments[1];
179-
} else {
180-
// ./package-name/ComponentName/ComponentName.Something.Example.tsx
181-
// For @fluentui/react, don't include the package name in the sidebar
182-
// @ts-ignore -- PACKAGE_NAME is replaced by a loader
183-
componentName = 'PACKAGE_NAME' === 'react' ? segments[2] : `${segments[2]} (${segments[1]})`;
184-
componentId = segments[2];
185-
}
180+
const { componentName, componentId } = generateComponentName(segments);
186181

187182
if (!stories.has(componentName)) {
188183
stories.set(componentName, {
@@ -220,4 +215,79 @@ function generateStoriesFromExamples(key, stories, req) {
220215
}
221216
}
222217
}
218+
219+
/**
220+
*
221+
* @param {string[]} segments
222+
* @returns {{componentName:string; componentId:string}}
223+
*/
224+
function generateComponentName(segments) {
225+
/**
226+
* @TODO
227+
* - this is a temporary solution until all converged packages use new storybook configuration
228+
* - after new config is in place remove this
229+
*
230+
* ./<package-name>/src/.../ComponentName.Something.Example.tsx
231+
*/
232+
const isCollocatedStory = segments.includes('src');
233+
234+
/**
235+
* ./ComponentName/ComponentName.Something.Example.tsx
236+
*/
237+
const isReactExamplesStory = segments.length === 3;
238+
239+
/**
240+
* For @fluentui/react, don't include the package name in the sidebar
241+
* ./package-name/ComponentName/ComponentName.Something.Example.tsx
242+
*/
243+
// @ts-ignore -- PACKAGE_NAME is replaced by a loader
244+
const isReactPackageStory = 'PACKAGE_NAME' === 'react';
245+
246+
// @TODO
247+
// - this is a temporary solution until all converged packages use new storybook configuration
248+
// - after new config is in place remove this whole IF
249+
if (isCollocatedStory) {
250+
// ./<package-name>/src/.../ComponentName.Something.Example.tsx
251+
// ↓↓↓
252+
// [., <package-name>, src, ..., ComponentName, ComponentName.Something.Example.tsx]
253+
const packageName = segments[1];
254+
const storyFileName = segments[segments.length - 1];
255+
const [, storyName] = /(\w+)\.(Example|stories)\.(tsx|mdx)$/.exec(storyFileName) || [];
256+
257+
const componentName = `${storyName} (${packageName})`;
258+
const componentId = storyName;
259+
260+
return { componentName, componentId };
261+
}
262+
263+
if (isReactExamplesStory) {
264+
// ./ComponentName/ComponentName.Something.Example.tsx
265+
// ↓↓↓
266+
// [., ComponentName, ComponentName.Something.Example.tsx]
267+
const componentName = segments[1];
268+
const componentId = segments[1];
269+
270+
return { componentName, componentId };
271+
}
272+
273+
if (isReactPackageStory) {
274+
// ./package-name/ComponentName/ComponentName.Something.Example.tsx
275+
// ↓↓↓
276+
// [., <package-name>, ComponentName, ComponentName.Something.Example.tsx]
277+
const componentName = segments[1];
278+
const componentId = segments[2];
279+
280+
return { componentName, componentId };
281+
}
282+
283+
return {
284+
componentName: `${segments[2]} (${segments[1]})`,
285+
// Story URLs are generated based off the story name
286+
// In the case of `react-components` a (package name) suffix is added to each story
287+
// This results in a difference name and URL between individual storybooks and the react-components suite storybook
288+
// https://storybook.js.org/docs/react/configure/sidebar-and-urls#permalinking-to-stories
289+
// Use the id property in stories to ensure the same URL between individual and suite storybook
290+
componentId: segments[2],
291+
};
292+
}
223293
}

packages/react-examples/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
"@fluentui/react-image": "^9.0.0-alpha.30",
6161
"@fluentui/react-link": "^9.0.0-alpha.30",
6262
"@fluentui/react-make-styles": "^9.0.0-alpha.29",
63-
"@fluentui/react-menu": "^9.0.0-alpha.16",
6463
"@fluentui/react-portal": "^9.0.0-alpha.6",
6564
"@fluentui/react-provider": "^9.0.0-alpha.30",
6665
"@fluentui/react-slider": "^1.0.0-beta.86",
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const rootMain = require('../../../.storybook/main');
2+
3+
module.exports = /** @type {Pick<import('../../../.storybook/main').StorybookConfig,'addons'|'stories'|'webpackFinal'>} */ ({
4+
stories: [...rootMain.stories, '../src/**/*.stories.mdx', '../src/**/*.stories.@(ts|tsx)'],
5+
addons: [...rootMain.addons],
6+
webpackFinal: (config, options) => {
7+
const localConfig = { ...rootMain.webpackFinal(config, options) };
8+
9+
return localConfig;
10+
},
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import * as rootPreview from '../../../.storybook/preview';
2+
3+
export const decorators = [...rootPreview.decorators];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "../tsconfig.json",
3+
"compilerOptions": {
4+
"allowJs": true,
5+
"checkJs": true
6+
},
7+
"exclude": ["../**/*.test.ts", "../**/*.test.js", "../**/*.test.tsx", "../**/*.test.jsx"],
8+
"include": ["../src/**/*", "*.js"]
9+
}

packages/react-menu/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"e2e": "node e2e.js",
2121
"just": "just-scripts",
2222
"lint": "just-scripts lint",
23-
"start": "just-scripts dev:storybook",
23+
"start": "echo \"This is DEPRECATED instead use 'storybook'\" && just-scripts dev:storybook",
24+
"storybook": "start-storybook",
2425
"start-test": "echo \"This is DEPRECATED instead use 'test --watch'\" && just-scripts jest-watch",
2526
"test": "jest",
2627
"update-snapshots": "echo \"This is DEPRECATED instead use 'test -u'\" && just-scripts jest -u"

packages/react-examples/src/react-menu/Menu/Menu.stories.tsx packages/react-menu/src/Menu.stories.tsx

+8-2
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import {
1010
MenuDivider,
1111
MenuGroupHeader,
1212
MenuProps,
13-
} from '@fluentui/react-menu';
14-
import { CutIcon, PasteIcon, EditIcon, AcceptIcon } from '@fluentui/react-icons-mdl2';
13+
} from './index';
1514
import { boolean } from '@storybook/addon-knobs';
1615

16+
import { CutIcon, PasteIcon, EditIcon, AcceptIcon } from './tmp-icons.stories';
17+
1718
export const TextOnly = (props: Pick<MenuProps, 'openOnHover' | 'openOnContext' | 'defaultOpen'>) => (
1819
<Menu openOnHover={props.openOnHover} openOnContext={props.openOnContext} defaultOpen={props.defaultOpen}>
1920
<MenuTrigger>
@@ -236,3 +237,8 @@ export const SelectionGroup = () => (
236237
</MenuList>
237238
</Menu>
238239
);
240+
241+
export default {
242+
title: 'Menu',
243+
component: Menu,
244+
};

0 commit comments

Comments
 (0)