diff --git a/.eslintignore b/.eslintignore index f3e738f4..19b6425a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ .github .yarn storybook-static +dist diff --git a/.github/workflows/buildAndPublish.yml b/.github/workflows/buildAndPublish.yml index 2e583993..66e985c9 100644 --- a/.github/workflows/buildAndPublish.yml +++ b/.github/workflows/buildAndPublish.yml @@ -26,6 +26,7 @@ jobs: - name: install, build, and test run: | yarn install + yarn prepublish yarn workspaces foreach -p run build-storybook env: CI: true diff --git a/.github/workflows/buildExamples.yml b/.github/workflows/buildExamples.yml index 97b69247..5c5217b9 100644 --- a/.github/workflows/buildExamples.yml +++ b/.github/workflows/buildExamples.yml @@ -34,5 +34,8 @@ jobs: - name: Install dependencies run: yarn install + - name: Compile TypeScript + run: yarn prepublish + - name: Build examples run: yarn workspaces foreach -p run build-storybook diff --git a/.gitignore b/.gitignore index 8ff1e82c..98a14e07 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ node_modules storybook-static .idea +# TypeScript +dist # Yarn stuff /**/.yarn/* diff --git a/.prettierignore b/.prettierignore index c8d6ec8e..e954517e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ .yarn **/*/storybook-static/* *.yml +dist diff --git a/package.json b/package.json index 6e22653d..e18361f4 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "node": ">=16.0.0" }, "scripts": { + "start": "cd packages/storybook-builder-vite && tsc -w", + "prepublish": "cd packages/storybook-builder-vite && tsc", "lint": "yarn lint:prettier && yarn lint:eslint", "lint:prettier": "prettier --write .", "lint:eslint": "eslint \"packages/*/**/*.{ts,tsx,js,jsx,mjs,cjs}\" --fix", diff --git a/packages/storybook-builder-vite/build.js b/packages/storybook-builder-vite/build.ts similarity index 65% rename from packages/storybook-builder-vite/build.js rename to packages/storybook-builder-vite/build.ts index 4228e581..02ab0f6e 100644 --- a/packages/storybook-builder-vite/build.js +++ b/packages/storybook-builder-vite/build.ts @@ -1,9 +1,12 @@ -const path = require('path'); -const { stringifyProcessEnvs, allowedEnvPrefix: envPrefix } = require('./envs'); -const { pluginConfig } = require('./vite-config'); -const { build: viteBuild } = require('vite'); +import * as path from 'path'; +import { build as viteBuild } from 'vite'; +import { allowedEnvPrefix as envPrefix, stringifyProcessEnvs } from './envs'; +import { pluginConfig } from './vite-config'; -module.exports.build = async function build(options) { +import type { UserConfig } from 'vite'; +import type { EnvsRaw, ExtendedOptions } from './types'; + +export async function build(options: ExtendedOptions) { const { presets } = options; const config = { @@ -22,11 +25,11 @@ module.exports.build = async function build(options) { }, }, plugins: await pluginConfig(options, 'build'), - }; + } as UserConfig; const finalConfig = await presets.apply('viteFinal', config, options); - const envsRaw = await presets.apply('env'); + const envsRaw = await presets.apply>('env'); // Stringify env variables after getting `envPrefix` from the final config const envs = stringifyProcessEnvs(envsRaw, finalConfig.envPrefix); // Update `define` @@ -36,4 +39,4 @@ module.exports.build = async function build(options) { }; await viteBuild(finalConfig); -}; +} diff --git a/packages/storybook-builder-vite/code-generator-plugin.js b/packages/storybook-builder-vite/code-generator-plugin.ts similarity index 73% rename from packages/storybook-builder-vite/code-generator-plugin.js rename to packages/storybook-builder-vite/code-generator-plugin.ts index 5d55db99..26c7ebd7 100644 --- a/packages/storybook-builder-vite/code-generator-plugin.js +++ b/packages/storybook-builder-vite/code-generator-plugin.ts @@ -1,15 +1,18 @@ -const fs = require('fs'); -const path = require('path'); -const { transformIframeHtml } = require('./transform-iframe-html'); -const { generateIframeScriptCode } = require('./codegen-iframe-script'); -const { generateModernIframeScriptCode } = require('./codegen-modern-iframe-script'); -const { generateImportFnScriptCode } = require('./codegen-importfn-script'); +import * as fs from 'fs'; +import * as path from 'path'; +import { transformIframeHtml } from './transform-iframe-html'; +import { generateIframeScriptCode } from './codegen-iframe-script'; +import { generateModernIframeScriptCode } from './codegen-modern-iframe-script'; +import { generateImportFnScriptCode } from './codegen-importfn-script'; -module.exports.codeGeneratorPlugin = function codeGeneratorPlugin(options) { +import type { Plugin } from 'vite'; +import type { ExtendedOptions } from './types'; + +export function codeGeneratorPlugin(options: ExtendedOptions): Plugin { const virtualFileId = '/virtual:/@storybook/builder-vite/vite-app.js'; const virtualStoriesFile = '/virtual:/@storybook/builder-vite/storybook-stories.js'; - const iframePath = path.resolve(__dirname, 'input', 'iframe.html'); - let iframeId; + const iframePath = path.resolve(__dirname, '..', 'input', 'iframe.html'); + let iframeId: string; // noinspection JSUnusedGlobalSymbols return { @@ -18,7 +21,7 @@ module.exports.codeGeneratorPlugin = function codeGeneratorPlugin(options) { configureServer(server) { // invalidate the whole vite-app.js script on every file change. // (this might be a little too aggressive?) - server.watcher.on('change', () => { + server.watcher.on('change', (_e) => { const { moduleGraph } = server; const appModule = moduleGraph.getModuleById(virtualFileId); if (appModule) { @@ -36,6 +39,9 @@ module.exports.codeGeneratorPlugin = function codeGeneratorPlugin(options) { // to serve iframe.html. The reason is that Vite's dev server (at the time of writing) // does not support virtual files as entry points. if (command === 'build') { + if (!config.build) { + config.build = {}; + } config.build.rollupOptions = { input: iframePath, }; @@ -67,7 +73,7 @@ module.exports.codeGeneratorPlugin = function codeGeneratorPlugin(options) { } if (id === iframeId) { - return fs.readFileSync(path.resolve(__dirname, 'input', 'iframe.html'), 'utf-8'); + return fs.readFileSync(path.resolve(__dirname, '..', 'input', 'iframe.html'), 'utf-8'); } }, async transformIndexHtml(html, ctx) { @@ -77,4 +83,4 @@ module.exports.codeGeneratorPlugin = function codeGeneratorPlugin(options) { return transformIframeHtml(html, options); }, }; -}; +} diff --git a/packages/storybook-builder-vite/codegen-iframe-script.js b/packages/storybook-builder-vite/codegen-iframe-script.ts similarity index 84% rename from packages/storybook-builder-vite/codegen-iframe-script.js rename to packages/storybook-builder-vite/codegen-iframe-script.ts index d3710db4..607133d0 100644 --- a/packages/storybook-builder-vite/codegen-iframe-script.js +++ b/packages/storybook-builder-vite/codegen-iframe-script.ts @@ -1,16 +1,17 @@ -const path = require('path'); -const glob = require('glob-promise'); -const { normalizePath } = require('vite'); -const { loadPreviewOrConfigFile } = require('@storybook/core-common'); +import { loadPreviewOrConfigFile } from '@storybook/core-common'; +import { normalizePath } from 'vite'; +import { listStories } from './list-stories'; + +import type { ExtendedOptions } from './types'; // This is somewhat of a hack; the problem is that previewEntries resolves to // the CommonJS imports, probably because require.resolve in Node.js land leads // to that. For Vite, we need the ESM modules. -function replaceCJStoESMPath(entryPath) { +function replaceCJStoESMPath(entryPath: string) { return entryPath.replace('/cjs/', '/esm/'); } -module.exports.generateIframeScriptCode = async function generateIframeScriptCode(options) { +export async function generateIframeScriptCode(options: ExtendedOptions) { const { presets, configDir, framework, frameworkPath } = options; const previewEntries = (await presets.apply('previewEntries', [], options)).map(replaceCJStoESMPath); @@ -22,16 +23,12 @@ module.exports.generateIframeScriptCode = async function generateIframeScriptCod const presetEntries = await presets.apply('config', [], options); const configEntries = [...presetEntries, previewOrConfigFile].filter(Boolean); - const storyEntries = ( - await Promise.all( - (await presets.apply('stories')).map((g) => glob(path.isAbsolute(g) ? g : path.join(configDir, g))) - ) - ).reduce((carry, stories) => carry.concat(stories), []); + const storyEntries = await listStories(options); - const absoluteFilesToImport = (files, name) => + const absoluteFilesToImport = (files: string[], name: string) => files.map((el, i) => `import ${name ? `* as ${name}_${i} from ` : ''}'/@fs/${normalizePath(el)}'`).join('\n'); - const importArray = (name, length) => + const importArray = (name: string, length: number) => `[${new Array(length) .fill(0) .map((_, i) => `${name}_${i}`) @@ -109,4 +106,4 @@ module.exports.generateIframeScriptCode = async function generateIframeScriptCod )}.filter(el => el.default), { hot: import.meta.hot }, false); // not sure if the import.meta.hot thing is correct `.trim(); return code; -}; +} diff --git a/packages/storybook-builder-vite/codegen-importfn-script.js b/packages/storybook-builder-vite/codegen-importfn-script.ts similarity index 69% rename from packages/storybook-builder-vite/codegen-importfn-script.js rename to packages/storybook-builder-vite/codegen-importfn-script.ts index 9cd67a8d..a0e3577a 100644 --- a/packages/storybook-builder-vite/codegen-importfn-script.js +++ b/packages/storybook-builder-vite/codegen-importfn-script.ts @@ -1,6 +1,8 @@ -const glob = require('glob-promise'); -const path = require('path'); -const { normalizePath } = require('vite'); +import * as path from 'path'; +import { normalizePath } from 'vite'; +import { listStories } from './list-stories'; + +import type { Options } from '@storybook/core-common'; /** * This file is largely based on https://github.com/storybookjs/storybook/blob/d1195cbd0c61687f1720fefdb772e2f490a46584/lib/core-common/src/utils/to-importFn.ts @@ -10,11 +12,8 @@ const { normalizePath } = require('vite'); * Paths get passed either with no leading './' - e.g. `src/Foo.stories.js`, * or with a leading `../` (etc), e.g. `../src/Foo.stories.js`. * We want to deal in importPaths relative to the working dir, so we normalize - * - * @param {string} relativePath - * @returns {string} */ -function toImportPath(relativePath) { +function toImportPath(relativePath: string) { return relativePath.startsWith('../') ? relativePath : `./${relativePath}`; } @@ -24,9 +23,8 @@ function toImportPath(relativePath) { * to delay loading. It then creates a function, `importFn(path)`, which resolves a path to an import * function and this is called by Storybook to fetch a story dynamically when needed. * @param stories An array of absolute story paths. - * @returns {Promise} */ -async function toImportFn(stories) { +async function toImportFn(stories: string[]) { const objectEntries = stories.map((file) => { return ` '${toImportPath(normalizePath(path.relative(process.cwd(), file)))}': async () => import('/@fs/${file}')`; }); @@ -42,16 +40,10 @@ async function toImportFn(stories) { `; } -module.exports.generateImportFnScriptCode = async function generateImportFnScriptCode(options) { +export async function generateImportFnScriptCode(options: Options) { // First we need to get an array of stories and their absolute paths. - const stories = ( - await Promise.all( - ( - await options.presets.apply('stories', [], options) - ).map((storyEntry) => glob(path.isAbsolute(storyEntry) ? storyEntry : path.join(options.configDir, storyEntry))) - ) - ).reduce((carry, stories) => carry.concat(stories), []); + const stories = await listStories(options); // We can then call toImportFn to create a function that can be used to load each story dynamically. return (await toImportFn(stories)).trim(); -}; +} diff --git a/packages/storybook-builder-vite/codegen-modern-iframe-script.js b/packages/storybook-builder-vite/codegen-modern-iframe-script.ts similarity index 88% rename from packages/storybook-builder-vite/codegen-modern-iframe-script.js rename to packages/storybook-builder-vite/codegen-modern-iframe-script.ts index b4b16391..4635bcd2 100644 --- a/packages/storybook-builder-vite/codegen-modern-iframe-script.js +++ b/packages/storybook-builder-vite/codegen-modern-iframe-script.ts @@ -1,9 +1,15 @@ -const { loadPreviewOrConfigFile } = require('@storybook/core-common'); -const { normalizePath } = require('vite'); +import { loadPreviewOrConfigFile } from '@storybook/core-common'; +import { normalizePath } from 'vite'; -module.exports.generateModernIframeScriptCode = async function generateModernIframeScriptCode( - options, - { storiesFilename } +import type { ExtendedOptions } from './types'; + +export interface GenerateModernIframeScriptCodeOptions { + storiesFilename: string; +} + +export async function generateModernIframeScriptCode( + options: ExtendedOptions, + { storiesFilename }: GenerateModernIframeScriptCodeOptions ) { const { presets, configDir } = options; @@ -76,4 +82,4 @@ module.exports.generateModernIframeScriptCode = async function generateModernIfr } `.trim(); return code; -}; +} diff --git a/packages/storybook-builder-vite/declarations/extract-stories.d.ts b/packages/storybook-builder-vite/declarations/extract-stories.d.ts new file mode 100644 index 00000000..2731f657 --- /dev/null +++ b/packages/storybook-builder-vite/declarations/extract-stories.d.ts @@ -0,0 +1,18 @@ +/** + * @see https://github.com/storybookjs/addon-svelte-csf/blob/f72b8f28dabbb99c92e12d0170d3c1db4397ee7c/src/parser/extract-stories.ts + */ +declare module '@storybook/addon-svelte-csf/dist/cjs/parser/extract-stories' { + interface StoryDef { + name: string; + template: boolean; + source: string; + hasArgs: boolean; + } + + interface StoriesDef { + stories: Record; + allocatedIds: string[]; + } + + function extractStories(component: string): { stories: StoriesDef; allocatedIds: string[] }; +} diff --git a/packages/storybook-builder-vite/declarations/svetle-stories-loader.d.ts b/packages/storybook-builder-vite/declarations/svetle-stories-loader.d.ts new file mode 100644 index 00000000..1ae04708 --- /dev/null +++ b/packages/storybook-builder-vite/declarations/svetle-stories-loader.d.ts @@ -0,0 +1,7 @@ +/** + * @see https://github.com/storybookjs/addon-svelte-csf/blob/f72b8f28dabbb99c92e12d0170d3c1db4397ee7c/src/parser/svelte-stories-loader.ts + * @see https://github.com/sveltejs/svelte/blob/deed340cf5d9c278f9a0605297ad6e4a3a1579d9/src/compiler/compile/utils/get_name_from_filename.ts + */ +declare module '@storybook/addon-svelte-csf/dist/cjs/parser/svelte-stories-loader' { + function getNameFromFilename(filename: string): string; +} diff --git a/packages/storybook-builder-vite/envs.js b/packages/storybook-builder-vite/envs.ts similarity index 74% rename from packages/storybook-builder-vite/envs.js rename to packages/storybook-builder-vite/envs.ts index f5564fa1..e5ad6c50 100644 --- a/packages/storybook-builder-vite/envs.js +++ b/packages/storybook-builder-vite/envs.ts @@ -1,4 +1,7 @@ -const { stringifyEnvs } = require('@storybook/core-common'); +import { stringifyEnvs } from '@storybook/core-common'; + +import type { EnvsRaw } from './types'; +import type { UserConfig } from 'vite'; // Allowed env variables on the client const allowedEnvVariables = [ @@ -13,23 +16,21 @@ const allowedEnvVariables = [ ]; // Env variables starts with env prefix will be exposed to your client source code via `import.meta.env` -module.exports.allowedEnvPrefix = ['VITE_', 'STORYBOOK_']; +export const allowedEnvPrefix = ['VITE_', 'STORYBOOK_']; /** * Customized version of stringifyProcessEnvs from @storybook/core-common which * uses import.meta.env instead of process.env and checks for allowed variables. - * @param {Object} raw - * @param {string[]|string} envPrefix */ -module.exports.stringifyProcessEnvs = function stringifyProcessEnvs(raw, envPrefix) { - const updatedRaw = {}; +export function stringifyProcessEnvs(raw: EnvsRaw, envPrefix: UserConfig['envPrefix']) { + const updatedRaw: EnvsRaw = {}; const envs = Object.entries(raw).reduce( - (acc, [key, value]) => { + (acc: EnvsRaw, [key, value]) => { // Only add allowed values OR values from array OR string started with allowed prefixes if ( allowedEnvVariables.includes(key) || (Array.isArray(envPrefix) && !!envPrefix.find((prefix) => key.startsWith(prefix))) || - key.startsWith(envPrefix) + (typeof envPrefix === 'string' && key.startsWith(envPrefix)) ) { acc[`import.meta.env.${key}`] = JSON.stringify(value); updatedRaw[key] = value; @@ -46,4 +47,4 @@ module.exports.stringifyProcessEnvs = function stringifyProcessEnvs(raw, envPref envs['import.meta.env'] = JSON.stringify(stringifyEnvs(updatedRaw)); return envs; -}; +} diff --git a/packages/storybook-builder-vite/index.js b/packages/storybook-builder-vite/index.js deleted file mode 100644 index 86f01486..00000000 --- a/packages/storybook-builder-vite/index.js +++ /dev/null @@ -1,57 +0,0 @@ -// noinspection JSUnusedGlobalSymbols - -const fs = require('fs'); -const path = require('path'); -const { transformIframeHtml } = require('./transform-iframe-html'); -const { createViteServer } = require('./vite-server.js'); -const { build: viteBuild } = require('./build'); - -function iframeMiddleware(options, server) { - return async (req, res, next) => { - if (!req.url.match(/^\/iframe.html($|\?)/)) { - next(); - return; - } - const indexHtml = fs.readFileSync(path.resolve(__dirname, 'input', 'iframe.html'), 'utf-8'); - const generated = await transformIframeHtml(indexHtml, options); - const transformed = await server.transformIndexHtml('/iframe.html', generated); - res.setHeader('Content-Type', 'text/html'); - res.status(200).send(transformed); - }; -} - -module.exports.start = async function start({ startTime, options, router, server: devServer }) { - const server = await createViteServer(options, devServer); - - // Just mock this endpoint (which is really Webpack-specific) so we don't get spammed with 404 in browser devtools - // TODO: we should either show some sort of progress from Vite, or just try to disable the whole Loader in the Manager UI. - router.get('/progress', (req, res) => { - res.header('Cache-Control', 'no-cache'); - res.header('Content-Type', 'text/event-stream'); - }); - - router.use(await iframeMiddleware(options, server)); - router.use(server.middlewares); - - function bail(e) { - try { - server.close(); - } catch (err) { - console.warn('unable to close vite server'); - } - - throw e; - } - - return { - bail, - totalTime: process.hrtime(startTime), - }; -}; - -module.exports.build = async function build({ options }) { - return viteBuild(options); -}; - -module.exports.corePresets = []; -module.exports.previewPresets = []; diff --git a/packages/storybook-builder-vite/index.ts b/packages/storybook-builder-vite/index.ts new file mode 100644 index 00000000..854afa81 --- /dev/null +++ b/packages/storybook-builder-vite/index.ts @@ -0,0 +1,67 @@ +// noinspection JSUnusedGlobalSymbols + +import * as fs from 'fs'; +import * as path from 'path'; +import { transformIframeHtml } from './transform-iframe-html'; +import { createViteServer } from './vite-server'; +import { build as viteBuild } from './build'; + +import type { Builder } from '@storybook/core-common'; +import type { RequestHandler, Request, Response } from 'express'; +import type { UserConfig, ViteDevServer } from 'vite'; +import type { ExtendedOptions } from './types'; + +export interface ViteStats {} + +export type ViteBuilder = Builder; + +function iframeMiddleware(options: ExtendedOptions, server: ViteDevServer): RequestHandler { + return async (req, res, next) => { + if (!req.url.match(/^\/iframe.html($|\?)/)) { + next(); + return; + } + const indexHtml = fs.readFileSync(path.resolve(__dirname, '..', 'input', 'iframe.html'), 'utf-8'); + const generated = await transformIframeHtml(indexHtml, options); + const transformed = await server.transformIndexHtml('/iframe.html', generated); + res.setHeader('Content-Type', 'text/html'); + res.status(200).send(transformed); + }; +} + +export const start: ViteBuilder['start'] = async ({ startTime, options, router, server: devServer }) => { + const server = await createViteServer(options as ExtendedOptions, devServer); + + // Just mock this endpoint (which is really Webpack-specific) so we don't get spammed with 404 in browser devtools + // TODO: we should either show some sort of progress from Vite, or just try to disable the whole Loader in the Manager UI. + router.get('/progress', (req: Request, res: Response) => { + res.header('Cache-Control', 'no-cache'); + res.header('Content-Type', 'text/event-stream'); + }); + + router.use(await iframeMiddleware(options as ExtendedOptions, server)); + router.use(server.middlewares); + + function bail(e?: Error) { + try { + return server.close(); + } catch (err) { + console.warn('unable to close vite server'); + } + + throw e; + } + + return { + bail, + stats: {} as ViteStats, + totalTime: process.hrtime(startTime), + }; +}; + +export const build: ViteBuilder['build'] = async ({ options }) => { + return viteBuild(options as ExtendedOptions); +}; + +export const corePresets = []; +export const previewPresets = []; diff --git a/packages/storybook-builder-vite/inject-export-order-plugin.js b/packages/storybook-builder-vite/inject-export-order-plugin.ts similarity index 62% rename from packages/storybook-builder-vite/inject-export-order-plugin.js rename to packages/storybook-builder-vite/inject-export-order-plugin.ts index bdf25831..39477126 100644 --- a/packages/storybook-builder-vite/inject-export-order-plugin.js +++ b/packages/storybook-builder-vite/inject-export-order-plugin.ts @@ -1,13 +1,16 @@ -const { parse } = require('es-module-lexer'); +import { parse } from 'es-module-lexer'; -module.exports.injectExportOrderPlugin = { +export const injectExportOrderPlugin = { name: 'storybook-vite-inject-export-order-plugin', // This should only run after the typescript has been transpiled enforce: 'post', - async transform(code, id) { + async transform(code: string, id: string) { if (!/\.stories\.([tj])sx?$/.test(id)) { return; } + // TODO: Maybe convert `injectExportOrderPlugin` to function that returns object, + // and run `await init;` once and then call `parse()` without `await`, + // instead of calling `await parse()` every time. const [, exports] = await parse(code); if (exports.includes('__namedExportsOrder')) { diff --git a/packages/storybook-builder-vite/list-stories.ts b/packages/storybook-builder-vite/list-stories.ts new file mode 100644 index 00000000..f047431e --- /dev/null +++ b/packages/storybook-builder-vite/list-stories.ts @@ -0,0 +1,21 @@ +import * as path from 'path'; +import { promise as glob } from 'glob-promise'; + +import type { Options, StoriesEntry } from '@storybook/core-common'; + +// TODO: Merge with https://github.com/eirslett/storybook-builder-vite/pull/182 +export async function listStories({ presets, configDir }: Options) { + return ( + await Promise.all( + ( + await presets.apply>('stories') + ).map((storiesEntry) => { + const files = typeof storiesEntry === 'string' ? storiesEntry : storiesEntry.files; + if (!files) { + return [] as string[]; + } + return glob(path.isAbsolute(files) ? files : path.join(configDir, files)); + }) + ) + ).reduce((carry, stories) => carry.concat(stories), []); +} diff --git a/packages/storybook-builder-vite/mdx-plugin.js b/packages/storybook-builder-vite/mdx-plugin.ts similarity index 69% rename from packages/storybook-builder-vite/mdx-plugin.js rename to packages/storybook-builder-vite/mdx-plugin.ts index bbdd0578..1f2e3e46 100644 --- a/packages/storybook-builder-vite/mdx-plugin.js +++ b/packages/storybook-builder-vite/mdx-plugin.ts @@ -1,5 +1,5 @@ -const mdx = require('vite-plugin-mdx').default; -const { createCompiler } = require('@storybook/csf-tools/mdx'); +import mdx from 'vite-plugin-mdx'; +import { createCompiler } from '@storybook/csf-tools/mdx'; /** * Storybook uses two different loaders when dealing with MDX: @@ -9,16 +9,16 @@ const { createCompiler } = require('@storybook/csf-tools/mdx'); * * @see https://github.com/storybookjs/storybook/blob/next/addons/docs/docs/recipes.md#csf-stories-with-arbitrary-mdx */ -module.exports.mdxPlugin = function () { +export function mdxPlugin() { return mdx((filename) => { const compilers = []; if (filename.includes('.stories.')) { - compilers.push(createCompiler()); + compilers.push(createCompiler({})); } return { compilers, }; }); -}; +} diff --git a/packages/storybook-builder-vite/mock-core-js.js b/packages/storybook-builder-vite/mock-core-js.ts similarity index 73% rename from packages/storybook-builder-vite/mock-core-js.js rename to packages/storybook-builder-vite/mock-core-js.ts index 19789f14..59a041ea 100644 --- a/packages/storybook-builder-vite/mock-core-js.js +++ b/packages/storybook-builder-vite/mock-core-js.ts @@ -1,17 +1,17 @@ -module.exports.mockCoreJs = function mockCoreJs() { +export function mockCoreJs() { return { name: 'mock-core-js', - resolveId(id) { + resolveId(id: string) { if (id.includes('node_modules/core-js')) { return id; } return undefined; }, - load(id) { + load(id: string) { if (id.includes('node_modules/core-js')) { return ''; } return undefined; }, }; -}; +} diff --git a/packages/storybook-builder-vite/optimizeDeps.js b/packages/storybook-builder-vite/optimizeDeps.ts similarity index 86% rename from packages/storybook-builder-vite/optimizeDeps.js rename to packages/storybook-builder-vite/optimizeDeps.ts index 86856b3c..7fffb166 100644 --- a/packages/storybook-builder-vite/optimizeDeps.js +++ b/packages/storybook-builder-vite/optimizeDeps.ts @@ -1,12 +1,12 @@ -const path = require('path'); -module.exports.getOptimizeDeps = async (root, options) => { - const stories = await Promise.all( - ( - await options.presets.apply('stories', [], options) - ).map((storyEntry) => - path.relative(root, path.isAbsolute(storyEntry) ? storyEntry : path.join(options.configDir, storyEntry)) - ) - ); +import * as path from 'path'; +import { normalizePath } from 'vite'; +import { listStories } from './list-stories'; + +import type { ExtendedOptions } from './types'; + +export async function getOptimizeDeps(root: string, options: ExtendedOptions) { + const absoluteStories = await listStories(options); + const stories = absoluteStories.map((storyPath) => normalizePath(path.relative(root, storyPath))); return { // We don't need to resolve the glob since vite supports globs for entries. @@ -105,4 +105,4 @@ module.exports.getOptimizeDeps = async (root, options) => { } }), }; -}; +} diff --git a/packages/storybook-builder-vite/package.json b/packages/storybook-builder-vite/package.json index 31c3c354..5fbdd70b 100644 --- a/packages/storybook-builder-vite/package.json +++ b/packages/storybook-builder-vite/package.json @@ -2,7 +2,8 @@ "name": "storybook-builder-vite", "version": "0.1.13", "description": "An experimental plugin to run and build Storybooks with Vite", - "main": "index.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", "author": "Eirik Sletteberg", "license": "MIT", "repository": { @@ -22,9 +23,11 @@ "vite-plugin-mdx": "^3.5.6" }, "devDependencies": { + "@types/express": "^4.17.13", "vue-docgen-api": "^4.40.0" }, "peerDependencies": { + "@storybook/core-common": "^6.4.3", "vite": ">=2.6.7" } } diff --git a/packages/storybook-builder-vite/plugins/vue-docgen.js b/packages/storybook-builder-vite/plugins/vue-docgen.ts similarity index 67% rename from packages/storybook-builder-vite/plugins/vue-docgen.js rename to packages/storybook-builder-vite/plugins/vue-docgen.ts index 25c318de..65567e80 100644 --- a/packages/storybook-builder-vite/plugins/vue-docgen.js +++ b/packages/storybook-builder-vite/plugins/vue-docgen.ts @@ -1,10 +1,10 @@ -const { parse } = require('vue-docgen-api'); +import { parse } from 'vue-docgen-api'; -module.exports = function () { +export function vueDocgen() { return { name: 'vue-docgen', - async transform(src, id) { + async transform(src: string, id: string) { if (/\.(vue)$/.test(id)) { const metaData = await parse(id); const metaSource = JSON.stringify(metaData); @@ -13,4 +13,4 @@ module.exports = function () { } }, }; -}; +} diff --git a/packages/storybook-builder-vite/source-loader-plugin.js b/packages/storybook-builder-vite/source-loader-plugin.ts similarity index 62% rename from packages/storybook-builder-vite/source-loader-plugin.js rename to packages/storybook-builder-vite/source-loader-plugin.ts index 64e64ab8..0144d313 100644 --- a/packages/storybook-builder-vite/source-loader-plugin.js +++ b/packages/storybook-builder-vite/source-loader-plugin.ts @@ -1,14 +1,14 @@ -const sourceLoaderTransform = require('@storybook/source-loader').default; +import sourceLoaderTransform from '@storybook/source-loader'; -module.exports.sourceLoaderPlugin = function () { +export function sourceLoaderPlugin() { return { name: 'storybook-vite-source-loader-plugin', enforce: 'pre', - async transform(src, id) { + async transform(src: string, id: string) { if (id.match(/\.stories\.[jt]sx?$/)) { // We need to mock 'this' when calling transform from @storybook/source-loader // noinspection JSUnusedGlobalSymbols - const mockClassLoader = { emitWarning: (message) => console.warn(message), resourcePath: id }; + const mockClassLoader = { emitWarning: (message: string) => console.warn(message), resourcePath: id }; const code = await sourceLoaderTransform.call(mockClassLoader, src); return { @@ -18,4 +18,4 @@ module.exports.sourceLoaderPlugin = function () { } }, }; -}; +} diff --git a/packages/storybook-builder-vite/svelte/csf-plugin.js b/packages/storybook-builder-vite/svelte/csf-plugin.ts similarity index 73% rename from packages/storybook-builder-vite/svelte/csf-plugin.js rename to packages/storybook-builder-vite/svelte/csf-plugin.ts index cdc1d46a..51c69c26 100644 --- a/packages/storybook-builder-vite/svelte/csf-plugin.js +++ b/packages/storybook-builder-vite/svelte/csf-plugin.ts @@ -1,25 +1,25 @@ -const { getNameFromFilename } = require('@storybook/addon-svelte-csf/dist/cjs/parser/svelte-stories-loader'); -const { readFileSync } = require('fs'); -const { extractStories } = require('@storybook/addon-svelte-csf/dist/cjs/parser/extract-stories'); +import { getNameFromFilename } from '@storybook/addon-svelte-csf/dist/cjs/parser/svelte-stories-loader'; +import { readFileSync } from 'fs'; +import { extractStories } from '@storybook/addon-svelte-csf/dist/cjs/parser/extract-stories'; const parser = require.resolve('@storybook/addon-svelte-csf/dist/esm/parser/collect-stories').replace(/[/\\]/g, '/'); -module.exports = { +export default { name: 'storybook-addon-svelte-csf', enforce: 'post', - transform(code, id) { + transform(code: string, id: string) { if (/.stories.svelte/.test(id)) { const component = getNameFromFilename(id); const source = readFileSync(id).toString(); const all = extractStories(source); const { stories } = all; - const storyDef = Object.entries(stories) + const storyDef = Object.entries(stories) .filter(([, def]) => !def.template) .map(([id]) => `export const ${id} = __storiesMetaData.stories[${JSON.stringify(id)}];`) .join('\n'); const codeWithoutDefaultExport = code.replace('export default ', '// export default '); - const namedExportsOrder = Object.entries(stories) + const namedExportsOrder = Object.entries(stories) .filter(([, def]) => !def.template) .map(([id]) => id); diff --git a/packages/storybook-builder-vite/transform-iframe-html.js b/packages/storybook-builder-vite/transform-iframe-html.ts similarity index 59% rename from packages/storybook-builder-vite/transform-iframe-html.js rename to packages/storybook-builder-vite/transform-iframe-html.ts index 6a9c1c0d..c5355aeb 100644 --- a/packages/storybook-builder-vite/transform-iframe-html.js +++ b/packages/storybook-builder-vite/transform-iframe-html.ts @@ -1,17 +1,22 @@ -module.exports.transformIframeHtml = async function transformIframeHtml( - html, - { configType, features, framework, presets, serverChannelUrl, title } +import type { CoreConfig } from '@storybook/core-common'; +import type { ExtendedOptions } from './types'; + +export type PreviewHtml = string | undefined; + +export async function transformIframeHtml( + html: string, + { configType, features, framework, presets, serverChannelUrl, title }: ExtendedOptions ) { - const headHtmlSnippet = await presets.apply('previewHead'); - const bodyHtmlSnippet = await presets.apply('previewBody'); + const headHtmlSnippet = await presets.apply('previewHead'); + const bodyHtmlSnippet = await presets.apply('previewBody'); const logLevel = await presets.apply('logLevel', undefined); const frameworkOptions = await presets.apply(`${framework}Options`, {}); - const coreOptions = await presets.apply('core'); + const coreOptions = await presets.apply('core'); return html .replace('', title || 'Storybook') - .replace('[CONFIG_TYPE HERE]', configType) - .replace('[LOGLEVEL HERE]', logLevel) + .replace('[CONFIG_TYPE HERE]', configType || '') + .replace('[LOGLEVEL HERE]', logLevel || '') .replace(`'[FRAMEWORK_OPTIONS HERE]'`, JSON.stringify(frameworkOptions || {})) .replace( `'[CHANNEL_OPTIONS HERE]'`, @@ -21,4 +26,4 @@ module.exports.transformIframeHtml = async function transformIframeHtml( .replace(`'[SERVER_CHANNEL_URL HERE]'`, JSON.stringify(serverChannelUrl)) .replace('', headHtmlSnippet || '') .replace('', bodyHtmlSnippet || ''); -}; +} diff --git a/packages/storybook-builder-vite/tsconfig.json b/packages/storybook-builder-vite/tsconfig.json new file mode 100644 index 00000000..9caab3e9 --- /dev/null +++ b/packages/storybook-builder-vite/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "strict": true, + "rootDir": ".", + "outDir": "dist" + } +} diff --git a/packages/storybook-builder-vite/types/envs-raw.type.ts b/packages/storybook-builder-vite/types/envs-raw.type.ts new file mode 100644 index 00000000..cb62e513 --- /dev/null +++ b/packages/storybook-builder-vite/types/envs-raw.type.ts @@ -0,0 +1 @@ +export type EnvsRaw = Record; diff --git a/packages/storybook-builder-vite/types/extended-options.type.ts b/packages/storybook-builder-vite/types/extended-options.type.ts new file mode 100644 index 00000000..b9867d36 --- /dev/null +++ b/packages/storybook-builder-vite/types/extended-options.type.ts @@ -0,0 +1,9 @@ +import type { Options } from '@storybook/core-common'; + +// Using instead of `Record` to provide better aware of used options +type IframeOptions = { + frameworkPath: string; + title: string; +}; + +export type ExtendedOptions = Options & IframeOptions; diff --git a/packages/storybook-builder-vite/types/index.ts b/packages/storybook-builder-vite/types/index.ts new file mode 100644 index 00000000..e50c278b --- /dev/null +++ b/packages/storybook-builder-vite/types/index.ts @@ -0,0 +1,2 @@ +export * from './envs-raw.type'; +export * from './extended-options.type'; diff --git a/packages/storybook-builder-vite/vite-config.js b/packages/storybook-builder-vite/vite-config.ts similarity index 69% rename from packages/storybook-builder-vite/vite-config.js rename to packages/storybook-builder-vite/vite-config.ts index cea00d63..fa10049e 100644 --- a/packages/storybook-builder-vite/vite-config.js +++ b/packages/storybook-builder-vite/vite-config.ts @@ -1,10 +1,15 @@ -const { mockCoreJs } = require('./mock-core-js'); -const { codeGeneratorPlugin } = require('./code-generator-plugin'); -const { injectExportOrderPlugin } = require('./inject-export-order-plugin'); -const { mdxPlugin } = require('./mdx-plugin'); -const { sourceLoaderPlugin } = require('./source-loader-plugin'); +import { Plugin } from 'vite'; +import { mockCoreJs } from './mock-core-js'; +import { codeGeneratorPlugin } from './code-generator-plugin'; +import { injectExportOrderPlugin } from './inject-export-order-plugin'; +import { mdxPlugin } from './mdx-plugin'; +import { sourceLoaderPlugin } from './source-loader-plugin'; -module.exports.pluginConfig = async function pluginConfig(options) { +import type { ExtendedOptions } from './types'; + +export type PluginConfigType = 'build' | 'development'; + +export async function pluginConfig(options: ExtendedOptions, _type: PluginConfigType) { const { framework } = options; const svelteOptions = await options.presets.apply('svelteOptions', {}, options); @@ -14,14 +19,15 @@ module.exports.pluginConfig = async function pluginConfig(options) { sourceLoaderPlugin(), mdxPlugin(), injectExportOrderPlugin, - ]; + ] as Plugin[]; if (framework === 'vue' || framework === 'vue3') { try { const vuePlugin = require('@vitejs/plugin-vue'); plugins.push(vuePlugin()); - plugins.push(require('./plugins/vue-docgen')()); + const { vueDocgen } = await import('./plugins/vue-docgen'); + plugins.push(vueDocgen()); } catch (err) { - if (err.code !== 'MODULE_NOT_FOUND') { + if ((err as NodeJS.ErrnoException).code !== 'MODULE_NOT_FOUND') { throw new Error( 'storybook-builder-vite requires @vitejs/plugin-vue to be installed ' + 'when using @storybook/vue or @storybook/vue3.' + @@ -36,7 +42,7 @@ module.exports.pluginConfig = async function pluginConfig(options) { const sveltePlugin = require('@sveltejs/vite-plugin-svelte').svelte; plugins.push(sveltePlugin(svelteOptions)); } catch (err) { - if (err.code !== 'MODULE_NOT_FOUND') { + if ((err as NodeJS.ErrnoException).code !== 'MODULE_NOT_FOUND') { throw new Error( 'storybook-builder-vite requires @sveltejs/vite-plugin-svelte to be installed when using @storybook/svelte.' + ' Please install it and start storybook again.' @@ -49,7 +55,7 @@ module.exports.pluginConfig = async function pluginConfig(options) { const csfPlugin = require('./svelte/csf-plugin'); plugins.push(csfPlugin); } catch (err) { - if (err.code !== 'MODULE_NOT_FOUND') { + if ((err as NodeJS.ErrnoException).code !== 'MODULE_NOT_FOUND') { throw new Error( 'storybook-builder-vite requires @storybook/addon-svelte-csf to be installed when using @storybook/svelte.' + ' Please install it and start storybook again.' @@ -74,4 +80,4 @@ module.exports.pluginConfig = async function pluginConfig(options) { } return plugins; -}; +} diff --git a/packages/storybook-builder-vite/vite-server.js b/packages/storybook-builder-vite/vite-server.ts similarity index 62% rename from packages/storybook-builder-vite/vite-server.js rename to packages/storybook-builder-vite/vite-server.ts index c47a26af..8cb61940 100644 --- a/packages/storybook-builder-vite/vite-server.js +++ b/packages/storybook-builder-vite/vite-server.ts @@ -1,10 +1,14 @@ -const path = require('path'); -const { stringifyProcessEnvs, allowedEnvPrefix: envPrefix } = require('./envs'); -const { getOptimizeDeps } = require('./optimizeDeps'); -const { createServer } = require('vite'); -const { pluginConfig } = require('./vite-config'); +import * as path from 'path'; +import { createServer } from 'vite'; +import { allowedEnvPrefix as envPrefix, stringifyProcessEnvs } from './envs'; +import { getOptimizeDeps } from './optimizeDeps'; +import { pluginConfig } from './vite-config'; -module.exports.createViteServer = async function createViteServer(options, devServer) { +import type { Server } from 'http'; +import type { UserConfig } from 'vite'; +import type { EnvsRaw, ExtendedOptions } from './types'; + +export async function createViteServer(options: ExtendedOptions, devServer: Server) { const { port, presets } = options; const root = path.resolve(options.configDir, '..'); @@ -30,11 +34,11 @@ module.exports.createViteServer = async function createViteServer(options, devSe }, plugins: await pluginConfig(options, 'development'), optimizeDeps: await getOptimizeDeps(root, options), - }; + } as UserConfig; const finalConfig = await presets.apply('viteFinal', defaultConfig, options); - const envsRaw = await presets.apply('env'); + const envsRaw = await presets.apply>('env'); // Stringify env variables after getting `envPrefix` from the final config const envs = stringifyProcessEnvs(envsRaw, finalConfig.envPrefix); // Update `define` @@ -44,4 +48,4 @@ module.exports.createViteServer = async function createViteServer(options, devSe }; return createServer(finalConfig); -}; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..fb2b2b36 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "target": "ES2021", + "declaration": true, + "sourceMap": true, + "inlineSources": true, + "skipLibCheck": true + } +} diff --git a/yarn.lock b/yarn.lock index f638d3e8..d3c4fe15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3680,6 +3680,16 @@ __metadata: languageName: node linkType: hard +"@types/body-parser@npm:*": + version: 1.19.2 + resolution: "@types/body-parser@npm:1.19.2" + dependencies: + "@types/connect": "*" + "@types/node": "*" + checksum: e17840c7d747a549f00aebe72c89313d09fbc4b632b949b2470c5cb3b1cb73863901ae84d9335b567a79ec5efcfb8a28ff8e3f36bc8748a9686756b6d5681f40 + languageName: node + linkType: hard + "@types/color-convert@npm:^2.0.0": version: 2.0.0 resolution: "@types/color-convert@npm:2.0.0" @@ -3696,6 +3706,38 @@ __metadata: languageName: node linkType: hard +"@types/connect@npm:*": + version: 3.4.35 + resolution: "@types/connect@npm:3.4.35" + dependencies: + "@types/node": "*" + checksum: fe81351470f2d3165e8b12ce33542eef89ea893e36dd62e8f7d72566dfb7e448376ae962f9f3ea888547ce8b55a40020ca0e01d637fab5d99567673084542641 + languageName: node + linkType: hard + +"@types/express-serve-static-core@npm:^4.17.18": + version: 4.17.27 + resolution: "@types/express-serve-static-core@npm:4.17.27" + dependencies: + "@types/node": "*" + "@types/qs": "*" + "@types/range-parser": "*" + checksum: fef52b941f903011e31a5886369301d7765580a034cd011a2d3a7dbe6a6edf4f77537710a52e3e2258c6fc59c611f228594c213f984cda767654ab6c5c199e9e + languageName: node + linkType: hard + +"@types/express@npm:^4.17.13": + version: 4.17.13 + resolution: "@types/express@npm:4.17.13" + dependencies: + "@types/body-parser": "*" + "@types/express-serve-static-core": ^4.17.18 + "@types/qs": "*" + "@types/serve-static": "*" + checksum: 12a2a0e6c4b993fc0854bec665906788aea0d8ee4392389d7a98a5de1eefdd33c9e1e40a91f3afd274011119c506f7b4126acb97fae62ae20b654974d44cba12 + languageName: node + linkType: hard + "@types/glob@npm:*, @types/glob@npm:^7.1.1, @types/glob@npm:^7.1.3": version: 7.1.3 resolution: "@types/glob@npm:7.1.3" @@ -3786,6 +3828,13 @@ __metadata: languageName: node linkType: hard +"@types/mime@npm:^1": + version: 1.3.2 + resolution: "@types/mime@npm:1.3.2" + checksum: 0493368244cced1a69cb791b485a260a422e6fcc857782e1178d1e6f219f1b161793e9f87f5fae1b219af0f50bee24fcbe733a18b4be8fdd07a38a8fb91146fd + languageName: node + linkType: hard + "@types/minimatch@npm:*": version: 3.0.4 resolution: "@types/minimatch@npm:3.0.4" @@ -3873,6 +3922,13 @@ __metadata: languageName: node linkType: hard +"@types/qs@npm:*": + version: 6.9.7 + resolution: "@types/qs@npm:6.9.7" + checksum: 7fd6f9c25053e9b5bb6bc9f9f76c1d89e6c04f7707a7ba0e44cc01f17ef5284adb82f230f542c2d5557d69407c9a40f0f3515e8319afd14e1e16b5543ac6cdba + languageName: node + linkType: hard + "@types/qs@npm:^6.9.5": version: 6.9.6 resolution: "@types/qs@npm:6.9.6" @@ -3880,6 +3936,13 @@ __metadata: languageName: node linkType: hard +"@types/range-parser@npm:*": + version: 1.2.4 + resolution: "@types/range-parser@npm:1.2.4" + checksum: b7c0dfd5080a989d6c8bb0b6750fc0933d9acabeb476da6fe71d8bdf1ab65e37c136169d84148034802f48378ab94e3c37bb4ef7656b2bec2cb9c0f8d4146a95 + languageName: node + linkType: hard + "@types/reach__router@npm:^1.3.7": version: 1.3.9 resolution: "@types/reach__router@npm:1.3.9" @@ -3916,6 +3979,16 @@ __metadata: languageName: node linkType: hard +"@types/serve-static@npm:*": + version: 1.13.10 + resolution: "@types/serve-static@npm:1.13.10" + dependencies: + "@types/mime": ^1 + "@types/node": "*" + checksum: eaca858739483e3ded254cad7d7a679dc2c8b3f52c8bb0cd845b3b7eb1984bde0371fdcb0a5c83aa12e6daf61b6beb762545021f520f08a1fe882a3fa4ea5554 + languageName: node + linkType: hard + "@types/source-list-map@npm:*": version: 0.1.2 resolution: "@types/source-list-map@npm:0.1.2" @@ -14061,6 +14134,7 @@ fsevents@^1.2.7: "@mdx-js/mdx": ^1.6.22 "@storybook/csf-tools": ^6.3.3 "@storybook/source-loader": ^6.3.12 + "@types/express": ^4.17.13 "@vitejs/plugin-react": ^1.0.8 es-module-lexer: ^0.9.3 glob: ^7.2.0 @@ -14068,6 +14142,7 @@ fsevents@^1.2.7: vite-plugin-mdx: ^3.5.6 vue-docgen-api: ^4.40.0 peerDependencies: + "@storybook/core-common": ^6.4.3 vite: ">=2.6.7" languageName: unknown linkType: soft