scripts/storybook/README.md
Utils for storybook.
NOTE: This module contains solely v9 packages storybook tools.
loadWorkspaceAddonFunction that registers custom workspace(monorepo) storybook addon written in TypeScript without need to "build first" before use.
// @filename .storybook/main.js
const { loadWorkspaceAddon } = require('@fluentui/scripts/storybook');
const tsConfigPath = path.join(workspaceRoot, 'tsconfig.base.json');
module.exports = {
addons: [
// 3rd party packages/addons
'@storybook/addon-a11y',
// workspace custom addon
loadWorkspaceAddon('@fluentui/custom-storybook-addon', { tsConfigPath }),
],
};
Before going into details we need to understand how storybook registers addons.
Addons are registered via addon property that needs to contain one of following:
'my-react-addon','@org/some-addon')preset.js js modulepreset registration module (eg /User/foo/bar/package/preset.js)Custom addon needs to contain preset.js at its package root, which contains registration logic for manager and preset (API defined by storybook).
file structure:
|- custom-addon/
|- preset.js
|- package.json
preset.js:
function config(entry = []) {
return [
...entry,
require.resolve('./lib/preset/preview'), // (A)
];
}
function managerEntries(entry = []) {
return [
...entry,
require.resolve('./lib/preset/manager'), // (B)
];
}
module.exports = { managerEntries, config };
Beside standard boilerplate the important part is (line A,B) the relative path, which points to compiled assets instead of source code (written in TypeScript), which implies that we will need to build our addon source and all its dependencies prior usage.
This is something that we wanna avoid.
To enable build-less(in-memory compilation) for custom workspace addon we have 2 options how to proceed:
NOTE: our implementation uses THIS OPTION
1. create mock of preset.ts on the file system which will point to source instead of build assets
This mocked file is will live within
tempfolder (which is ignored from git).
function config(entry = []) {
return [
...entry,
- require.resolve('./lib/preset/preview'),
+ require.resolve('./src/preset/preview.ts'),
];
}
function managerEntries(entry = []) {
return [
...entry,
- require.resolve('./lib/preset/manager'),
+ require.resolve('./src/preset/manager.ts'),
];
}
2. re-use Storybook Webpack processing
managerWebpack which will register TsconfigPathsPlugin that will be able to resolve absolute paths to other workspace packages. (A)managerWebpack as part of addon preset API (B)+ const { registerTsPaths } = require('@fluentui/scripts-storybook');
function config(entry = []) {
return [
...entry,
require.resolve('./src/preset/preview.ts'),
];
}
function managerEntries(entry = []) {
return [
...entry,
require.resolve('./src/preset/manager.ts'),
];
}
+ function managerWebpack(config, options) { (A)
+ registerTsPaths({config, tsConfigPath: '/Users/martinhochel/Projects/msft/fluentui/tsconfig.base.json'});
+ return config;
+ }
- module.exports = { managerEntries, config };
+ module.exports = { managerWebpack, managerEntries, config }; (B)
3. return absolute path to TypeScript mocked which contains mocked preset.js
instead of registering custom addon by its package name, we need to return absolute path which points to mocked preset.ts. This will turn on TS resolution mechanism that are defined within storybook thus we don't need to register manually any node TS files module resolutions.
flowchart
A["loadWorkspaceAddon('@proj/my-addon')"] --> P(create mocked 'my-addon/temp/preset.ts') --return--> R["/users/proj/packages/my-addon/temp/preset.ts"]
flowchart
S[storybook start]-->RA[register installed addons]
S --> CAP
subgraph CAP[custom addons processing]
RCA[register custom addon] --> MW["managerWebpack() with TS path plugin"]-->SS["storybook loads compiled addon"]
end
CAP --> SB[storybook boots]
RA --> SB
1. create mock of preset.js on the file system which will point to source instead of build assets
This mocked file is will live within
tempfolder (which is ignored from git).
function config(entry = []) {
return [
...entry,
- require.resolve('./lib/preset/preview'),
+ require.resolve('./src/preset/preview'),
];
}
function managerEntries(entry = []) {
return [
...entry,
- require.resolve('./lib/preset/manager'),
+ require.resolve('./src/preset/manager'),
];
}
2. modify require NodeJS/Webpack resolution
so it will properly load TypeScript sources referenced from mocked preset.js (via on of @swc-node/register, @ts-node/register, @babel/register). (A)
@swc-node/register (abstracted by registerTsProject function from nx package)to understand our workspace (path aliases in tsconfig.base.json) by registering TsconfigPathsPlugin within managerWebpack hook. (B)
register managerWebpack as part of addon preset API (C)
+ const { registerTsPaths } = require('@fluentui/scripts-storybook');
+ const { registerTsProject } = require('@nx/js/src/internal');
+ registerTsProject('/Users/martinhochel/Projects/msft/fluentui/tsconfig.base.json') (A)
function config(entry = []) {
return [
...entry,
require.resolve('./src/preset/preview'),
];
}
function managerEntries(entry = []) {
return [
...entry,
require.resolve('./src/preset/manager'),
];
}
+ function managerWebpack(config, options) { (B)
+ registerTsPaths({config, tsConfigPath: '/Users/martinhochel/Projects/msft/fluentui/tsconfig.base.json'});
+ return config;
+ }
- module.exports = { managerEntries, config };
+ module.exports = { managerWebpack, managerEntries, config }; (C)
3. return absolute path to directory which contains mocked preset.js
instead of registering custom addon by its package name, we need to return absolute path which points to directory in which mocked preset.js will be created
flowchart
A["loadWorkspaceAddon('@proj/my-addon')"] --> P(create mocked 'my-addon/temp/preset.js') --return--> R["/users/proj/packages/my-addon/temp"]
flowchart
S[storybook start]-->RA[register installed addons]
S --> CAP
subgraph CAP[custom addons processing]
RCA[register custom addon] --> TSR["registerTsProject()"]-->MW["managerWebpack() with TS path plugin"]-->SS["storybook loads compiled addon"]
end
CAP --> SB[storybook boots]
RA --> SB