create-nextjs-storybook
Version:
Install embedded Storybook route into a NextJS project
160 lines (156 loc) • 6.89 kB
JavaScript
import { existsSync } from 'node:fs';
import { cp, readFile, writeFile, mkdir, readdir } from 'node:fs/promises';
import { dedent } from 'ts-dedent';
import boxen from 'boxen';
import chalk from 'chalk';
import { join } from 'path';
import { render } from 'ejs';
import ora from 'ora';
import { JsPackageManagerFactory } from './cli/js-package-manager/index.js';
import { HandledError } from './cli/HandledError.js';
import { SupportedLanguage, detectLanguage } from './cli/detect.js';
import { addWithStorybook } from './addWithStorybook.js';
import { codeLog } from './cli/helpers.js';
const logger = console;
const DIRNAME = new URL('.', import.meta.url).pathname;
const VERSION = 'next';
const ensureDirShallow = async (path) => mkdir(path).catch(() => { });
const getEmptyDirMessage = (packageManagerType) => {
const generatorCommandsMap = {
npm: 'npm create',
yarn1: 'yarn create',
yarn2: 'yarn create',
pnpm: 'pnpm create',
};
const create = generatorCommandsMap[packageManagerType];
return dedent `
Storybook-NextJS-Server cannot be installed into an empty project.
Please create a new NextJS project using ${chalk.green(create + ' next-app')} or any other tooling of your choice.
Once you've created the project, please re-run ${chalk.green(create + 'storybook-nextjs')} inside the project root.
For more information, see ${chalk.yellowBright('https://storybook.js.org/docs')}.
Good luck! 🚀
`;
};
const isAppDir = (projectRoot) => {
return existsSync(join(projectRoot, 'app')) || existsSync(join(projectRoot, 'src', 'app'));
};
const formatArray = (arr) => `[${arr.map((item) => `'${item}'`).join(', ')}]`;
const createConfig = async ({ appDir, language, srcDir, addons }) => {
const templateDir = join(DIRNAME, '..', 'templates', 'sb');
const configDir = join(process.cwd(), '.storybook');
await ensureDirShallow(configDir);
if (!appDir) {
const previewFile = language == SupportedLanguage.JAVASCRIPT ? 'preview.jsx' : 'preview.tsx';
await cp(join(templateDir, previewFile), join(configDir, previewFile));
}
const mainExt = language === SupportedLanguage.JAVASCRIPT ? 'js' : 'ts';
const stories = formatArray([`../${srcDir}**/*.stories.@(js|jsx|ts|tsx)`]);
const extras = "framework: '@storybook/nextjs-server',\n" + (appDir ? '' : " docs: { autodocs: 'tag' },\n");
const mainTemplate = await readFile(join(templateDir, `main.${mainExt}.ejs`), 'utf-8');
const main = render(mainTemplate, { stories, addons: formatArray(addons), extras });
await writeFile(join(configDir, `main.${mainExt}`), main);
};
const createStories = async ({ srcDir, appDir, language }) => {
const ext = language === SupportedLanguage.JAVASCRIPT ? 'js' : 'ts';
const templateDir = join(DIRNAME, '..', 'templates');
const storiesDir = join(templateDir, appDir ? 'app' : 'pages', ext);
const outputDir = join(process.cwd(), srcDir, 'stories');
await cp(storiesDir, outputDir, { recursive: true });
await Promise.all(['page', 'header', 'button'].map((fname) => cp(join(templateDir, 'css', `${fname}.module.css`), join(outputDir, `${fname}.module.css`))));
};
/**
* NextJS app router has problems if the routes are created dynamically on
* first startup, so let's try to create them on install.
*/
const createRoutes = async ({ srcDir, appDir, language }) => {
if (!appDir)
return;
const templateDir = join(DIRNAME, '..', 'templates', 'app', 'groupLayouts');
const groupDir = join(srcDir, 'app', '(sb)');
await ensureDirShallow(groupDir);
await cp(join(templateDir, 'layout-root.tsx'), join(groupDir, 'layout.tsx'));
const previewDir = join(groupDir, 'storybook-preview');
await ensureDirShallow(previewDir);
await cp(join(templateDir, 'layout-nested.tsx'), join(previewDir, 'layout.tsx'));
};
const updateNextConfig = async () => {
const nextConfigPath = join(process.cwd(), 'next.config.js');
let nextConfig = 'module.exports = {}';
if (existsSync(nextConfigPath)) {
nextConfig = await readFile(nextConfigPath, 'utf-8');
}
const updatedConfig = addWithStorybook(nextConfig);
await writeFile(nextConfigPath, updatedConfig);
};
const _version = (pkgs) => pkgs.map((pkg) => `${pkg}@${VERSION}`);
const init = async () => {
let done = false;
const spinner = ora('Adding Storybook').start();
// add a slight delay so that user can see what's going on
const status = (msg, delay) => {
setTimeout(() => {
if (!done)
spinner.text = msg;
}, delay);
};
// FIXME:
// - telemetry
// - force package manager
// - force install
// - pnp
const entries = await readdir(process.cwd());
const isEmptyDir = entries.length === 0 || entries.every((entry) => entry.startsWith('.'));
const packageManager = JsPackageManagerFactory.getPackageManager();
if (isEmptyDir) {
logger.log(boxen(getEmptyDirMessage(packageManager.type), {
borderStyle: 'round',
padding: 1,
borderColor: '#F1618C',
}));
throw new HandledError('Project was initialized in an empty directory.');
}
const language = await detectLanguage(packageManager);
const appDir = isAppDir(process.cwd());
const corePackages = ['storybook', '@storybook/react'];
const addons = appDir
? ['@storybook/addon-controls']
: [
'@storybook/addon-essentials',
'@storybook/blocks',
'@storybook/addon-interactions',
'@storybook/test',
];
const options = {
srcDir: existsSync(join(process.cwd(), 'src')) ? 'src/' : '',
appDir,
language,
addons,
};
status('Creating .storybook config', 500);
await createConfig(options);
status('Creating example stories', 1000);
await createStories(options);
await createRoutes(options);
await updateNextConfig();
status('Installing package dependencies', 1500);
await packageManager.addDependencies({ installAsDevDependencies: true }, [
'@storybook/nextjs-server',
..._version([...corePackages, ...addons]),
]);
done = true;
spinner.succeed('Done!');
logger.log(`\n1️⃣ Update ${chalk.bold(chalk.cyan('next.config.js'))}:\n`);
codeLog([
"const withStorybook = require('@storybook/nextjs-server/next-config')({/* sb config */});",
'module.exports = withStorybook({/* next config */});',
]);
logger.log('\n2️⃣ Run your NextJS app:\n');
codeLog([packageManager.getRunCommand('dev')]);
logger.log('\n3️⃣ View your Storybook:\n');
logger.log(chalk.bold(chalk.cyan(' https://localhost:3000/storybook')));
logger.log();
};
init().catch((e) => {
console.error(e);
});