1
0
mirror of https://github.com/storybookjs/storybook.git synced 2025-03-13 05:02:42 +08:00

364 lines
12 KiB
TypeScript
Raw Normal View History

2022-10-05 15:49:58 +11:00
// This file requires many imports from `../code`, which requires both an install and bootstrap of
// the repo to work properly. So we load it async in the task runner *after* those steps.
2022-10-05 11:25:31 +11:00
/* eslint-disable no-restricted-syntax, no-await-in-loop */
2022-10-05 15:49:58 +11:00
import { copy, ensureSymlink, ensureDir, existsSync, pathExists } from 'fs-extra';
2022-10-05 11:25:31 +11:00
import { join, resolve, sep } from 'path';
import dedent from 'ts-dedent';
import { Task } from '../task';
2022-10-05 11:25:31 +11:00
import { executeCLIStep, steps } from '../utils/cli-step';
import { installYarn2, configureYarn2ForVerdaccio, addPackageResolutions } from '../utils/yarn';
import { exec } from '../utils/exec';
import { ConfigFile, writeConfig } from '../../code/lib/csf-tools';
import { filterExistsInCodeDir } from '../utils/filterExistsInCodeDir';
import { findFirstPath } from '../utils/paths';
import { detectLanguage } from '../../code/lib/cli/src/detect';
import { SupportedLanguage } from '../../code/lib/cli/src/project_types';
import { updatePackageScripts } from '../utils/package-json';
2022-10-05 11:25:31 +11:00
import { addPreviewAnnotations, readMainConfig } from '../utils/main-js';
import { JsPackageManagerFactory } from '../../code/lib/cli/src/js-package-manager';
import { workspacePath } from '../utils/workspace';
import { babelParse } from '../../code/lib/csf-tools/src/babelParse';
const reprosDir = resolve(__dirname, '../../repros');
const codeDir = resolve(__dirname, '../../code');
const logger = console;
export const essentialsAddons = [
'actions',
'backgrounds',
'controls',
'docs',
'highlight',
'measure',
'outline',
'toolbars',
'viewport',
];
2022-10-05 11:25:31 +11:00
export const create: Task['run'] = async (
{ key, template, sandboxDir },
{ addon: addons, fromLocalRepro, dryRun, debug }
) => {
const parentDir = resolve(sandboxDir, '..');
await ensureDir(parentDir);
if (fromLocalRepro) {
const srcDir = join(reprosDir, key, 'after-storybook');
if (!existsSync(srcDir)) {
throw new Error(dedent`
Missing repro directory '${srcDir}'!
To run sandbox against a local repro, you must have already generated
the repro template in the /repros directory using:
the repro template in the /repros directory using:
yarn generate-repros-next --template ${key}
`);
}
await copy(srcDir, sandboxDir);
} else {
await executeCLIStep(steps.repro, {
argument: key,
optionValues: { output: sandboxDir, branch: 'next' },
cwd: parentDir,
dryRun,
debug,
});
}
const cwd = sandboxDir;
for (const addon of addons) {
const addonName = `@storybook/addon-${addon}`;
await executeCLIStep(steps.add, { argument: addonName, cwd, dryRun, debug });
}
const mainConfig = await readMainConfig({ cwd });
2022-10-05 11:25:31 +11:00
mainConfig.setFieldValue(['core', 'disableTelemetry'], true);
if (template.expected.builder === '@storybook/builder-vite') forceViteRebuilds(mainConfig);
await writeConfig(mainConfig);
};
export const install: Task['run'] = async ({ sandboxDir }, { link, dryRun, debug }) => {
const cwd = sandboxDir;
await installYarn2({ cwd, dryRun, debug });
if (link) {
await executeCLIStep(steps.link, {
argument: sandboxDir,
cwd: codeDir,
optionValues: { local: true, start: false },
dryRun,
debug,
});
} else {
// We need to add package resolutions to ensure that we only ever install the latest version
// of any storybook packages as verdaccio is not able to both proxy to npm and publish over
// the top. In theory this could mask issues where different versions cause problems.
await addPackageResolutions({ cwd, dryRun, debug });
await configureYarn2ForVerdaccio({ cwd, dryRun, debug });
await exec(
'yarn install',
{ cwd },
{
dryRun,
startMessage: `⬇️ Installing local dependencies`,
errorMessage: `🚨 Installing local dependencies failed`,
}
);
}
logger.info(`🔢 Adding package scripts:`);
await updatePackageScripts({
2022-10-05 11:25:31 +11:00
cwd,
prefix: 'NODE_OPTIONS="--preserve-symlinks --preserve-symlinks-main"',
2022-10-05 11:25:31 +11:00
});
};
// Ensure that sandboxes can refer to story files defined in `code/`.
// Most WP-based build systems will not compile files outside of the project root or 'src/` or
// similar. Plus they aren't guaranteed to handle TS files. So we need to patch in esbuild
// loader for such files. NOTE this isn't necessary for Vite, as far as we know.
function addEsbuildLoaderToStories(mainConfig: ConfigFile) {
// NOTE: the test regexp here will apply whether the path is symlink-preserved or otherwise
2022-10-10 17:15:43 +11:00
const esbuildLoaderPath = require.resolve('../../code/node_modules/esbuild-loader');
const storiesMdxLoaderPath = require.resolve(
'../../code/node_modules/@storybook/mdx1-csf/loader'
);
const babelLoaderPath = require.resolve('babel-loader');
const jsxPluginPath = require.resolve('@babel/plugin-transform-react-jsx');
2022-10-05 11:25:31 +11:00
const webpackFinalCode = `
(config) => ({
...config,
module: {
...config.modules,
rules: [
// Ensure esbuild-loader applies to all files in ./template-stories
{
test: [/\\/template-stories\\//],
exclude: [/\\.mdx$/],
loader: '${esbuildLoaderPath}',
2022-10-05 11:25:31 +11:00
options: {
loader: 'tsx',
target: 'es2015',
},
},
// Handle MDX files per the addon-docs presets (ish)
{
test: [/\\/template-stories\\//],
include: [/\\.stories\\.mdx$/],
use: [
{
loader: '${babelLoaderPath}',
options: {
babelrc: false,
configFile: false,
plugins: ['${jsxPluginPath}'],
}
},
{
loader: '${storiesMdxLoaderPath}',
options: {
skipCsf: false,
}
}
],
},
{
test: [/\\/template-stories\\//],
include: [/\\.mdx$/],
exclude: [/\\.stories\\.mdx$/],
use: [
{
loader: '${babelLoaderPath}',
options: {
babelrc: false,
configFile: false,
plugins: ['${jsxPluginPath}'],
}
},
{
loader: '${storiesMdxLoaderPath}',
options: {
skipCsf: true,
}
}
],
},
2022-10-05 11:25:31 +11:00
// Ensure no other loaders from the framework apply
...config.module.rules.map(rule => ({
...rule,
exclude: [/\\/template-stories\\//].concat(rule.exclude || []),
})),
],
},
})`;
mainConfig.setFieldNode(
['webpackFinal'],
// @ts-expect-error (not sure why TS complains here, it does exist)
babelParse(webpackFinalCode).program.body[0].expression
);
}
// Recompile optimized deps on each startup, so you can change @storybook/* packages and not
// have to clear caches.
function forceViteRebuilds(mainConfig: ConfigFile) {
const viteFinalCode = `
(config) => ({
...config,
optimizeDeps: {
...config.optimizeDeps,
force: true,
},
})`;
mainConfig.setFieldNode(
['viteFinal'],
// @ts-expect-error (not sure why TS complains here, it does exist)
babelParse(viteFinalCode).program.body[0].expression
);
}
// packageDir is eg 'renderers/react', 'addons/actions'
async function linkPackageStories(
packageDir: string,
{ mainConfig, cwd, linkInDir }: { mainConfig: ConfigFile; cwd: string; linkInDir?: string }
) {
const source = join(codeDir, packageDir, 'template', 'stories');
// By default we link `stories` directories
// e.g '../../../code/lib/store/template/stories' to 'template-stories/lib/store'
// if the directory <code>/lib/store/template/stories exists
//
// The files must be linked in the cwd, in order to ensure that any dependencies they
// reference are resolved in the cwd. In particular 'react' resolved by MDX files.
const target = linkInDir
? resolve(linkInDir, packageDir)
: resolve(cwd, 'template-stories', packageDir);
await ensureSymlink(source, target);
// Add `previewAnnotation` entries of the form
2022-10-10 13:05:51 +11:00
// './template-stories/lib/store/preview.[tj]s'
// if the file <code>/lib/store/template/stories/preview.[jt]s exists
await Promise.all(
['js', 'ts'].map(async (ext) => {
const previewFile = `preview.${ext}`;
const previewPath = join(codeDir, packageDir, 'template', 'stories', previewFile);
if (await pathExists(previewPath)) {
addPreviewAnnotations(mainConfig, [
`./${join(linkInDir ? 'src/stories' : 'template-stories', packageDir, previewFile)}`,
]);
}
})
);
2022-10-05 11:25:31 +11:00
}
// Update the stories field to ensure that:
// a) no TS files that are linked from the renderer are picked up in non-TS projects
// b) files in ./template-stories are not matched by the default glob
async function updateStoriesField(mainConfig: ConfigFile, isJs: boolean) {
const stories = mainConfig.getFieldValue(['stories']) as string[];
// If the project is a JS project, let's make sure any linked in TS stories from the
// renderer inside src|stories are simply ignored.
const updatedStories = isJs
? stories.map((specifier) => specifier.replace('js|jsx|ts|tsx', 'js|jsx'))
: stories;
// FIXME: '*.@(mdx|stories.mdx|stories.tsx|stories.ts|stories.jsx|stories.js'
const linkedStories = join('..', 'template-stories', '**', '*.stories.@(js|jsx|ts|tsx|mdx)');
const linkedMdx = join('..', 'template-stories/addons/docs/docs2', '**', '*.@(mdx)');
2022-10-05 11:25:31 +11:00
mainConfig.setFieldValue(['stories'], [...updatedStories, linkedStories, linkedMdx]);
2022-10-05 11:25:31 +11:00
}
function addExtraDependencies({
cwd,
dryRun,
debug,
}: {
cwd: string;
dryRun: boolean;
debug: boolean;
}) {
// web-components doesn't install '@storybook/testing-library' by default
const extraDeps = ['@storybook/jest', '@storybook/testing-library@0.0.14-next.0'];
2022-10-05 11:25:31 +11:00
if (debug) logger.log('🎁 Adding extra deps', extraDeps);
if (!dryRun) {
const packageManager = JsPackageManagerFactory.getPackageManager(false, cwd);
packageManager.addDependencies({ installAsDevDependencies: true }, extraDeps);
}
}
export const addStories: Task['run'] = async (
{ sandboxDir, template },
{ addon: extraAddons, dryRun, debug }
2022-10-05 11:25:31 +11:00
) => {
const cwd = sandboxDir;
const storiesPath = await findFirstPath([join('src', 'stories'), 'stories'], { cwd });
const mainConfig = await readMainConfig({ cwd });
// Link in the template/components/index.js from store, the renderer and the addons
const rendererPath = await workspacePath('renderer', template.expected.renderer);
await ensureSymlink(
join(codeDir, rendererPath, 'template', 'components'),
resolve(cwd, storiesPath, 'components')
);
addPreviewAnnotations(mainConfig, [`.${sep}${join(storiesPath, 'components')}`]);
// Add stories for the renderer. NOTE: these *do* need to be processed by the framework build system
await linkPackageStories(rendererPath, {
mainConfig,
cwd,
linkInDir: resolve(cwd, storiesPath),
});
// Add stories for lib/store (and addons below). NOTE: these stories will be in the
// template-stories folder and *not* processed by the framework build config (instead by esbuild-loader)
await linkPackageStories(await workspacePath('core package', '@storybook/store'), {
mainConfig,
cwd,
});
const mainAddons = mainConfig.getFieldValue(['addons']).reduce((acc: string[], addon: any) => {
const name = typeof addon === 'string' ? addon : addon.name;
const match = /@storybook\/addon-(.*)/.exec(name);
if (!match) return acc;
const suffix = match[1];
if (suffix === 'essentials') {
return [...acc, ...essentialsAddons];
}
return [...acc, suffix];
}, []);
2022-10-05 11:25:31 +11:00
const addonDirs = await Promise.all(
[...mainAddons, ...extraAddons].map(async (addon) =>
workspacePath('addon', `@storybook/addon-${addon}`)
2022-10-05 11:25:31 +11:00
)
);
2022-10-05 11:25:31 +11:00
const existingStories = await filterExistsInCodeDir(addonDirs, join('template', 'stories'));
await Promise.all(
existingStories.map(async (packageDir) => linkPackageStories(packageDir, { mainConfig, cwd }))
);
// Ensure that we match stories from the template-stories dir
const packageJson = await import(join(cwd, 'package.json'));
await updateStoriesField(
mainConfig,
detectLanguage(packageJson) === SupportedLanguage.JAVASCRIPT
);
// Add some extra settings (see above for what these do)
if (template.expected.builder === '@storybook/builder-webpack5')
addEsbuildLoaderToStories(mainConfig);
// Some addon stories require extra dependencies
addExtraDependencies({ cwd, dryRun, debug });
2022-10-06 13:57:17 +11:00
await writeConfig(mainConfig);
2022-10-05 11:25:31 +11:00
};