diff --git a/code/builders/builder-vite/package.json b/code/builders/builder-vite/package.json index b9bff0b1c02a..dc7e9dd9c6b7 100644 --- a/code/builders/builder-vite/package.json +++ b/code/builders/builder-vite/package.json @@ -47,15 +47,15 @@ "@types/find-cache-dir": "^3.2.1", "browser-assert": "^1.2.1", "es-module-lexer": "^1.5.0", - "express": "^4.19.2", "find-cache-dir": "^3.0.0", "magic-string": "^0.30.0", "ts-dedent": "^2.0.0" }, "devDependencies": { - "@types/express": "^4.17.21", "@types/node": "^22.0.0", "glob": "^10.0.0", + "polka": "^1.0.0-next.28", + "sirv": "^2.0.4", "slash": "^5.0.0", "typescript": "^5.3.2", "vite": "^4.0.4" diff --git a/code/builders/builder-vite/src/index.ts b/code/builders/builder-vite/src/index.ts index 0962e1676e4e..9ab66f2771d0 100644 --- a/code/builders/builder-vite/src/index.ts +++ b/code/builders/builder-vite/src/index.ts @@ -3,10 +3,9 @@ import { cp, readFile } from 'node:fs/promises'; import { join, parse } from 'node:path'; import { NoStatsForViteDevError } from 'storybook/internal/server-errors'; -import type { Options } from 'storybook/internal/types'; +import type { Middleware, Options } from 'storybook/internal/types'; -import type { RequestHandler } from 'express'; -import express from 'express'; +import sirv from 'sirv'; import { corePath } from 'storybook/core-path'; import type { ViteDevServer } from 'vite'; @@ -20,16 +19,18 @@ export { hasVitePlugins } from './utils/has-vite-plugins'; export * from './types'; -function iframeMiddleware(options: Options, server: ViteDevServer): RequestHandler { +function iframeMiddleware(options: Options, server: ViteDevServer): Middleware { return async (req, res, next) => { - if (!req.url.match(/^\/iframe\.html($|\?)/)) { + if (!req.url || !req.url.match(/^\/iframe\.html($|\?)/)) { next(); return; } + // the base isn't used for anything, but it's required by the URL constructor + const url = new URL(req.url, 'http://localhost:6006'); // We need to handle `html-proxy` params for style tag HMR https://github.com/storybookjs/builder-vite/issues/266#issuecomment-1055677865 // e.g. /iframe.html?html-proxy&index=0.css - if (req.query['html-proxy'] !== undefined) { + if (url.searchParams.has('html-proxy')) { next(); return; } @@ -40,7 +41,9 @@ function iframeMiddleware(options: Options, server: ViteDevServer): RequestHandl 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); + res.statusCode = 200; + res.write(transformed); + res.end(); }; } @@ -59,10 +62,14 @@ export const start: ViteBuilder['start'] = async ({ server = await createViteServer(options as Options, devServer); const previewResolvedDir = join(corePath, 'dist/preview'); - const previewDirOrigin = previewResolvedDir; - - router.use(`/sb-preview`, express.static(previewDirOrigin, { immutable: true, maxAge: '5m' })); - + router.use( + '/sb-preview', + sirv(previewResolvedDir, { + maxAge: 300000, + dev: true, + immutable: true, + }) + ); router.use(iframeMiddleware(options as Options, server)); router.use(server.middlewares); @@ -81,10 +88,8 @@ export const build: ViteBuilder['build'] = async ({ options }) => { const viteCompilation = viteBuild(options as Options); const previewResolvedDir = join(corePath, 'dist/preview'); - const previewDirOrigin = previewResolvedDir; const previewDirTarget = join(options.outputDir || '', `sb-preview`); - - const previewFiles = cp(previewDirOrigin, previewDirTarget, { + const previewFiles = cp(previewResolvedDir, previewDirTarget, { filter: (src) => { const { ext } = parse(src); if (ext) { diff --git a/code/builders/builder-vite/src/plugins/code-generator-plugin.ts b/code/builders/builder-vite/src/plugins/code-generator-plugin.ts index 77509b97cf93..79dba3844060 100644 --- a/code/builders/builder-vite/src/plugins/code-generator-plugin.ts +++ b/code/builders/builder-vite/src/plugins/code-generator-plugin.ts @@ -50,7 +50,7 @@ export function codeGeneratorPlugin(options: Options): Plugin { }, config(config, { command }) { // If we are building the static distribution, add iframe.html as an entry. - // In development mode, it's not an entry - instead, we use an express middleware + // In development mode, it's not an entry - instead, we use a middleware // 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') { diff --git a/code/builders/builder-webpack5/package.json b/code/builders/builder-webpack5/package.json index e6f8716ffc4f..0c122fc4a33a 100644 --- a/code/builders/builder-webpack5/package.json +++ b/code/builders/builder-webpack5/package.json @@ -72,7 +72,6 @@ "constants-browserify": "^1.0.0", "css-loader": "^6.7.1", "es-module-lexer": "^1.5.0", - "express": "^4.19.2", "fork-ts-checker-webpack-plugin": "^8.0.0", "html-webpack-plugin": "^5.5.0", "magic-string": "^0.30.5", @@ -95,6 +94,7 @@ "@types/terser-webpack-plugin": "^5.2.0", "@types/webpack-hot-middleware": "^2.25.6", "pretty-hrtime": "^1.0.3", + "sirv": "^2.0.4", "slash": "^5.0.0", "typescript": "^5.3.2" }, diff --git a/code/builders/builder-webpack5/src/index.ts b/code/builders/builder-webpack5/src/index.ts index e35ced7be073..a8af6e699ad4 100644 --- a/code/builders/builder-webpack5/src/index.ts +++ b/code/builders/builder-webpack5/src/index.ts @@ -12,8 +12,8 @@ import type { Builder, Options } from 'storybook/internal/types'; import { checkWebpackVersion } from '@storybook/core-webpack'; -import express from 'express'; import prettyTime from 'pretty-hrtime'; +import sirv from 'sirv'; import { corePath } from 'storybook/core-path'; import type { Configuration, Stats, StatsOptions } from 'webpack'; import webpack, { ProgressPlugin } from 'webpack'; @@ -180,10 +180,14 @@ const starter: StarterFunction = async function* starterGeneratorFn({ compilation = webpackDevMiddleware(compiler, middlewareOptions); const previewResolvedDir = join(corePath, 'dist/preview'); - const previewDirOrigin = previewResolvedDir; - - router.use(`/sb-preview`, express.static(previewDirOrigin, { immutable: true, maxAge: '5m' })); - + router.use( + '/sb-preview', + sirv(previewResolvedDir, { + maxAge: 300000, + dev: true, + immutable: true, + }) + ); router.use(compilation); router.use(webpackHotMiddleware(compiler, { log: false })); @@ -289,10 +293,8 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, }); const previewResolvedDir = join(corePath, 'dist/preview'); - const previewDirOrigin = previewResolvedDir; const previewDirTarget = join(options.outputDir || '', `sb-preview`); - - const previewFiles = cp(previewDirOrigin, previewDirTarget, { + const previewFiles = cp(previewResolvedDir, previewDirTarget, { filter: (src) => { const { ext } = parse(src); if (ext) { diff --git a/code/core/package.json b/code/core/package.json index 30f5ec68bc7d..09a26d547178 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -278,12 +278,10 @@ }, "dependencies": { "@storybook/csf": "^0.1.11", - "@types/express": "^4.17.21", "better-opn": "^3.0.2", "browser-assert": "^1.2.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", "esbuild-register": "^3.5.0", - "express": "^4.19.2", "jsdoc-type-pratt-parser": "^4.0.0", "process": "^0.11.10", "recast": "^0.23.5", @@ -307,6 +305,7 @@ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", "@fal-works/esbuild-plugin-global-externals": "^2.1.2", "@ndelangen/get-tarball": "^3.0.7", + "@polka/compression": "^1.0.0-next.28", "@popperjs/core": "^2.6.0", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-scroll-area": "1.2.0-rc.7", @@ -316,7 +315,6 @@ "@storybook/icons": "^1.2.10", "@tanstack/react-virtual": "^3.3.0", "@testing-library/react": "^14.0.0", - "@types/compression": "^1.7.0", "@types/cross-spawn": "^6.0.2", "@types/detect-port": "^1.3.0", "@types/diff": "^5.0.9", @@ -346,7 +344,6 @@ "cli-table3": "^0.6.1", "commander": "^12.1.0", "comment-parser": "^1.4.1", - "compression": "^1.7.4", "copy-to-clipboard": "^3.3.1", "cross-spawn": "^7.0.3", "css": "^3.0.0", @@ -362,7 +359,6 @@ "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0", "esbuild-plugin-alias": "^0.2.1", "execa": "^8.0.1", - "express": "^4.19.2", "fd-package-json": "^1.2.0", "fetch-retry": "^6.0.0", "find-cache-dir": "^5.0.0", @@ -386,6 +382,7 @@ "picomatch": "^2.3.0", "picoquery": "^1.4.0", "polished": "^4.2.2", + "polka": "^1.0.0-next.28", "prettier": "^3.2.5", "pretty-hrtime": "^1.0.3", "prompts": "^2.4.0", @@ -401,6 +398,7 @@ "react-transition-group": "^4.4.5", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0", + "sirv": "^2.0.4", "slash": "^5.0.0", "source-map": "^0.7.4", "store2": "^2.14.2", diff --git a/code/core/src/builder-manager/index.ts b/code/core/src/builder-manager/index.ts index b068caf206c7..faad0a288acf 100644 --- a/code/core/src/builder-manager/index.ts +++ b/code/core/src/builder-manager/index.ts @@ -9,7 +9,7 @@ import { logger } from '@storybook/core/node-logger'; import { globalExternals } from '@fal-works/esbuild-plugin-global-externals'; import { pnpPlugin } from '@yarnpkg/esbuild-plugin-pnp'; import aliasPlugin from 'esbuild-plugin-alias'; -import express from 'express'; +import sirv from 'sirv'; import type { BuilderBuildResult, @@ -26,6 +26,7 @@ import { wrapManagerEntries } from './utils/managerEntries'; import { safeResolve } from './utils/safeResolve'; import { getTemplatePath, renderHTML } from './utils/template'; +const isRootPath = /^\/($|\?)/; let compilation: Compilation; let asyncIterator: ReturnType | ReturnType; @@ -165,8 +166,22 @@ const starter: StarterFunction = async function* starterGeneratorFn({ 'manager' ); - router.use(`/sb-addons`, express.static(addonsDir, { immutable: true, maxAge: '5m' })); - router.use(`/sb-manager`, express.static(coreDirOrigin, { immutable: true, maxAge: '5m' })); + router.use( + '/sb-addons', + sirv(addonsDir, { + maxAge: 300000, + dev: true, + immutable: true, + }) + ); + router.use( + '/sb-manager', + sirv(coreDirOrigin, { + maxAge: 300000, + dev: true, + immutable: true, + }) + ); const { cssFiles, jsFiles } = await readOrderedFiles(addonsDir, compilation?.outputFiles); @@ -193,15 +208,19 @@ const starter: StarterFunction = async function* starterGeneratorFn({ yield; - router.use(`/`, ({ path }, res, next) => { - if (path === '/') { - res.status(200).send(html); + router.use('/', ({ url }, res, next) => { + if (url && isRootPath.test(url)) { + res.statusCode = 200; + res.write(html); + res.end(); } else { next(); } }); - router.use(`/index.html`, ({ path }, res) => { - res.status(200).send(html); + router.use(`/index.html`, (req, res) => { + res.statusCode = 200; + res.write(html); + res.end(); }); return { @@ -250,7 +269,6 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, // TODO: this doesn't watch, we should change this to use the esbuild watch API: https://esbuild.github.io/api/#watch compilation = await instance({ ...config, - minify: true, }); diff --git a/code/core/src/core-server/dev-server.ts b/code/core/src/core-server/dev-server.ts index cdf1027d1872..325d33366514 100644 --- a/code/core/src/core-server/dev-server.ts +++ b/code/core/src/core-server/dev-server.ts @@ -4,8 +4,8 @@ import type { Options } from '@storybook/core/types'; import { logger } from '@storybook/core/node-logger'; import { MissingBuilderError } from '@storybook/core/server-errors'; -import compression from 'compression'; -import express from 'express'; +import compression from '@polka/compression'; +import polka from 'polka'; import invariant from 'tiny-invariant'; import type { StoryIndexGenerator } from './utils/StoryIndexGenerator'; @@ -17,19 +17,13 @@ import { getAccessControlMiddleware } from './utils/getAccessControlMiddleware'; import { getStoryIndexGenerator } from './utils/getStoryIndexGenerator'; import { getMiddleware } from './utils/middleware'; import { openInBrowser } from './utils/open-in-browser'; -import { router } from './utils/router'; import { getServerAddresses } from './utils/server-address'; import { getServer } from './utils/server-init'; import { useStatics } from './utils/server-statics'; export async function storybookDevServer(options: Options) { - const app = express(); - - const [server, features, core] = await Promise.all([ - getServer(app, options), - options.presets.apply('features'), - options.presets.apply('core'), - ]); + const [server, core] = await Promise.all([getServer(options), options.presets.apply('core')]); + const app = polka({ server }); const serverChannel = await options.presets.apply( 'experimental_serverChannel', @@ -39,7 +33,7 @@ export async function storybookDevServer(options: Options) { let indexError: Error | undefined; // try get index generator, if failed, send telemetry without storyCount, then rethrow the error const initializedStoryIndexGenerator: Promise = - getStoryIndexGenerator(features ?? {}, options, serverChannel).catch((err) => { + getStoryIndexGenerator(app, options, serverChannel).catch((err) => { indexError = err; return undefined; }); @@ -53,20 +47,13 @@ export async function storybookDevServer(options: Options) { app.use(getAccessControlMiddleware(core?.crossOriginIsolated ?? false)); app.use(getCachingMiddleware()); - getMiddleware(options.configDir)(router); - - app.use(router); + getMiddleware(options.configDir)(app); const { port, host, initialPath } = options; invariant(port, 'expected options to have a port'); const proto = options.https ? 'https' : 'http'; const { address, networkAddress } = getServerAddresses(port, host, proto, initialPath); - const listening = new Promise((resolve, reject) => { - // @ts-expect-error (Following line doesn't match TypeScript signature at all 🤔) - server.listen({ port, host }, (error: Error) => (error ? reject(error) : resolve())); - }); - if (!core?.builder) { throw new MissingBuilderError(); } @@ -76,7 +63,7 @@ export async function storybookDevServer(options: Options) { const [previewBuilder, managerBuilder] = await Promise.all([ getPreviewBuilder(builderName, options.configDir), getManagerBuilder(), - useStatics(router, options), + useStatics(app, options), ]); if (options.debugWebpack) { @@ -86,7 +73,7 @@ export async function storybookDevServer(options: Options) { const managerResult = await managerBuilder.start({ startTime: process.hrtime(), options, - router, + router: app, server, channel: serverChannel, }); @@ -101,7 +88,7 @@ export async function storybookDevServer(options: Options) { .start({ startTime: process.hrtime(), options, - router, + router: app, server, channel: serverChannel, }) @@ -123,12 +110,17 @@ export async function storybookDevServer(options: Options) { // this is a preview route, the builder has to be started before we can serve it // this handler keeps request to that route pending until the builder is ready to serve it, preventing a 404 - router.get('/iframe.html', (req, res, next) => { + app.use('/iframe.html', (req, res, next) => { // We need to catch here or node will treat any errors thrown by `previewStarted` as // unhandled and exit (even though they are very much handled below) previewStarted.catch(() => {}).then(() => next()); }); + const listening = new Promise((resolve, reject) => { + server.once('error', reject); + app.listen({ port, host }, resolve); + }); + await Promise.all([initializedStoryIndexGenerator, listening]).then(async ([indexGenerator]) => { if (indexGenerator && !options.ci && !options.smokeTest && options.open) { openInBrowser(host ? networkAddress : address); @@ -143,7 +135,7 @@ export async function storybookDevServer(options: Options) { const previewResult = await previewStarted; // Now the preview has successfully started, we can count this as a 'dev' event. - doTelemetry(core, initializedStoryIndexGenerator, options); + doTelemetry(app, core, initializedStoryIndexGenerator, options); return { previewResult, managerResult, address, networkAddress }; } diff --git a/code/core/src/core-server/utils/doTelemetry.ts b/code/core/src/core-server/utils/doTelemetry.ts index 534d409765e0..0e9b48b8c8f5 100644 --- a/code/core/src/core-server/utils/doTelemetry.ts +++ b/code/core/src/core-server/utils/doTelemetry.ts @@ -1,16 +1,17 @@ import { getPrecedingUpgrade, telemetry } from '@storybook/core/telemetry'; import type { CoreConfig, Options } from '@storybook/core/types'; +import type Polka from 'polka'; import invariant from 'tiny-invariant'; import { sendTelemetryError } from '../withTelemetry'; import type { StoryIndexGenerator } from './StoryIndexGenerator'; import { useStorybookMetadata } from './metadata'; -import { router } from './router'; import { summarizeIndex } from './summarizeIndex'; import { versionStatus } from './versionStatus'; export async function doTelemetry( + app: Polka.Polka, core: CoreConfig, initializedStoryIndexGenerator: Promise, options: Options @@ -52,6 +53,6 @@ export async function doTelemetry( } if (!core?.disableProjectJson) { - useStorybookMetadata(router, options.configDir); + useStorybookMetadata(app, options.configDir); } } diff --git a/code/core/src/core-server/utils/get-caching-middleware.ts b/code/core/src/core-server/utils/get-caching-middleware.ts index d01cb3e5e922..88cc96c2ae70 100644 --- a/code/core/src/core-server/utils/get-caching-middleware.ts +++ b/code/core/src/core-server/utils/get-caching-middleware.ts @@ -1,8 +1,8 @@ -import type { RequestHandler } from 'express'; +import type { Middleware } from '../../types'; -export function getCachingMiddleware(): RequestHandler { +export function getCachingMiddleware(): Middleware { return (req, res, next) => { - res.header('Cache-Control', 'no-store'); + res.setHeader('Cache-Control', 'no-store'); next(); }; } diff --git a/code/core/src/core-server/utils/getAccessControlMiddleware.ts b/code/core/src/core-server/utils/getAccessControlMiddleware.ts index 9eb8851e6a5c..0b5c3428b842 100644 --- a/code/core/src/core-server/utils/getAccessControlMiddleware.ts +++ b/code/core/src/core-server/utils/getAccessControlMiddleware.ts @@ -1,16 +1,16 @@ -import type { RequestHandler } from 'express'; +import type { Middleware } from '../../types'; -export function getAccessControlMiddleware(crossOriginIsolated: boolean): RequestHandler { +export function getAccessControlMiddleware(crossOriginIsolated: boolean): Middleware { return (req, res, next) => { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); // These headers are required to enable SharedArrayBuffer // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer if (crossOriginIsolated) { // These headers are required to enable SharedArrayBuffer // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer - res.header('Cross-Origin-Opener-Policy', 'same-origin'); - res.header('Cross-Origin-Embedder-Policy', 'require-corp'); + res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); + res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); } next(); }; diff --git a/code/core/src/core-server/utils/getStoryIndexGenerator.ts b/code/core/src/core-server/utils/getStoryIndexGenerator.ts index 6e5e4960b132..d91373d825cf 100644 --- a/code/core/src/core-server/utils/getStoryIndexGenerator.ts +++ b/code/core/src/core-server/utils/getStoryIndexGenerator.ts @@ -1,15 +1,14 @@ import { normalizeStories } from '@storybook/core/common'; import type { DocsOptions, Options } from '@storybook/core/types'; +import type Polka from 'polka'; + import { StoryIndexGenerator } from './StoryIndexGenerator'; import type { ServerChannel } from './get-server-channel'; -import { router } from './router'; import { useStoriesJson } from './stories-json'; export async function getStoryIndexGenerator( - features: { - argTypeTargetsV7?: boolean; - }, + app: Polka.Polka, options: Options, serverChannel: ServerChannel ): Promise { @@ -34,7 +33,7 @@ export async function getStoryIndexGenerator( const initializedStoryIndexGenerator = generator.initialize().then(() => generator); useStoriesJson({ - router, + app, initializedStoryIndexGenerator, normalizedStories, serverChannel, diff --git a/code/core/src/core-server/utils/metadata.ts b/code/core/src/core-server/utils/metadata.ts index a617b3e1f52c..a9a2ebb337ba 100644 --- a/code/core/src/core-server/utils/metadata.ts +++ b/code/core/src/core-server/utils/metadata.ts @@ -2,7 +2,7 @@ import { writeFile } from 'node:fs/promises'; import { getStorybookMetadata } from '@storybook/core/telemetry'; -import type { Request, Response, Router } from 'express'; +import type Polka from 'polka'; export async function extractStorybookMetadata(outputFile: string, configDir: string) { const storybookMetadata = await getStorybookMetadata(configDir); @@ -10,10 +10,11 @@ export async function extractStorybookMetadata(outputFile: string, configDir: st await writeFile(outputFile, JSON.stringify(storybookMetadata)); } -export function useStorybookMetadata(router: Router, configDir?: string) { - router.use('/project.json', async (req: Request, res: Response) => { +export function useStorybookMetadata(app: Polka.Polka, configDir?: string) { + app.use('/project.json', async (req, res) => { const storybookMetadata = await getStorybookMetadata(configDir); - res.header('Content-Type', 'application/json'); - res.send(JSON.stringify(storybookMetadata)); + res.setHeader('Content-Type', 'application/json'); + res.write(JSON.stringify(storybookMetadata)); + res.end(); }); } diff --git a/code/core/src/core-server/utils/router.ts b/code/core/src/core-server/utils/router.ts deleted file mode 100644 index 894388991b5f..000000000000 --- a/code/core/src/core-server/utils/router.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Router } from 'express'; - -// @ts-expect-error (Converted from ts-ignore) - -export const router: Router = new Router(); diff --git a/code/core/src/core-server/utils/server-init.ts b/code/core/src/core-server/utils/server-init.ts index 60710cb19cfb..d06c3421e7bc 100644 --- a/code/core/src/core-server/utils/server-init.ts +++ b/code/core/src/core-server/utils/server-init.ts @@ -2,21 +2,17 @@ import { readFile } from 'node:fs/promises'; import { logger } from '@storybook/core/node-logger'; -import type { Express } from 'express'; import http from 'http'; import https from 'https'; -export async function getServer( - app: Express, - options: { - https?: boolean; - sslCert?: string; - sslKey?: string; - sslCa?: string[]; - } -) { +export async function getServer(options: { + https?: boolean; + sslCert?: string; + sslKey?: string; + sslCa?: string[]; +}) { if (!options.https) { - return http.createServer(app); + return http.createServer(); } if (!options.sslCert) { @@ -35,5 +31,5 @@ export async function getServer( key: await readFile(options.sslKey, { encoding: 'utf8' }), }; - return https.createServer(sslOptions, app); + return https.createServer(sslOptions); } diff --git a/code/core/src/core-server/utils/server-statics.ts b/code/core/src/core-server/utils/server-statics.ts index b6bc406923e8..3e21b4a3ea58 100644 --- a/code/core/src/core-server/utils/server-statics.ts +++ b/code/core/src/core-server/utils/server-statics.ts @@ -6,22 +6,19 @@ import type { Options } from '@storybook/core/types'; import { logger } from '@storybook/core/node-logger'; -import type { Router } from 'express'; -import express from 'express'; import picocolors from 'picocolors'; +import type Polka from 'polka'; +import sirv from 'sirv'; import { dedent } from 'ts-dedent'; -export async function useStatics(router: Router, options: Options) { +export async function useStatics(app: Polka.Polka, options: Options): Promise { const staticDirs = (await options.presets.apply('staticDirs')) ?? []; const faviconPath = await options.presets.apply('favicon'); - const statics = [ - ...staticDirs.map((dir) => (typeof dir === 'string' ? dir : `${dir.from}:${dir.to}`)), - ]; - - if (statics && statics.length > 0) { - await Promise.all( - statics.map(async (dir) => { + await Promise.all( + staticDirs + .map((dir) => (typeof dir === 'string' ? dir : `${dir.from}:${dir.to}`)) + .map(async (dir) => { try { const normalizedDir = staticDirs && !isAbsolute(dir) @@ -40,17 +37,30 @@ export async function useStatics(router: Router, options: Options) { ); } - router.use(targetEndpoint, express.static(staticPath, { index: false })); + app.use( + targetEndpoint, + sirv(staticPath, { + dev: true, + etag: true, + extensions: [], + }) + ); } catch (e) { if (e instanceof Error) { logger.warn(e.message); } } }) - ); - } + ); - router.get(`/${basename(faviconPath)}`, (req, res) => res.sendFile(faviconPath)); + app.get( + `/${basename(faviconPath)}`, + sirv(faviconPath, { + dev: true, + etag: true, + extensions: [], + }) + ); } export const parseStaticDir = async (arg: string) => { diff --git a/code/core/src/core-server/utils/stories-json.test.ts b/code/core/src/core-server/utils/stories-json.test.ts index 3faa5da2fe19..4868c149b724 100644 --- a/code/core/src/core-server/utils/stories-json.test.ts +++ b/code/core/src/core-server/utils/stories-json.test.ts @@ -7,7 +7,7 @@ import { normalizeStoriesEntry } from '@storybook/core/common'; import { STORY_INDEX_INVALIDATED } from '@storybook/core/core-events'; import { debounce } from 'es-toolkit/compat'; -import type { Request, Response, Router } from 'express'; +import type Polka from 'polka'; import Watchpack from 'watchpack'; import { csfIndexer } from '../presets/common-preset'; @@ -58,30 +58,30 @@ const getInitializedStoryIndexGenerator = async ( describe('useStoriesJson', () => { const use = vi.fn(); - const router: Router = { use } as any; - const send = vi.fn(); + const app: Polka.Polka = { use } as any; + const end = vi.fn(); const write = vi.fn(); - const response: Response = { + const response: Polka.Response = { header: vi.fn(), - send, + send: vi.fn(), status: vi.fn(), setHeader: vi.fn(), flushHeaders: vi.fn(), write, flush: vi.fn(), - end: vi.fn(), + end, on: vi.fn(), } as any; beforeEach(async () => { use.mockClear(); - send.mockClear(); + end.mockClear(); write.mockClear(); vi.mocked(debounce).mockImplementation((cb) => cb as any); Watchpack.mockClear(); }); - const request: Request = { + const request: Polka.Request = { headers: { accept: 'application/json' }, } as any; @@ -90,7 +90,7 @@ describe('useStoriesJson', () => { const mockServerChannel = { emit: vi.fn() } as any as ServerChannel; console.time('useStoriesJson'); useStoriesJson({ - router, + app, serverChannel: mockServerChannel, workingDir, normalizedStories, @@ -105,8 +105,8 @@ describe('useStoriesJson', () => { await route(request, response); console.timeEnd('route'); - expect(send).toHaveBeenCalledTimes(1); - expect(JSON.parse(send.mock.calls[0][0])).toMatchInlineSnapshot(` + expect(end).toHaveBeenCalledTimes(1); + expect(JSON.parse(end.mock.calls[0][0])).toMatchInlineSnapshot(` { "entries": { "a--metaof": { @@ -368,7 +368,7 @@ describe('useStoriesJson', () => { const mockServerChannel = { emit: vi.fn() } as any as ServerChannel; useStoriesJson({ - router, + app, serverChannel: mockServerChannel, workingDir, normalizedStories, @@ -379,14 +379,14 @@ describe('useStoriesJson', () => { const route = use.mock.calls[0][1]; const firstPromise = route(request, response); - const secondResponse = { ...response, send: vi.fn(), status: vi.fn() }; + const secondResponse = { ...response, end: vi.fn(), status: vi.fn() }; const secondPromise = route(request, secondResponse); await Promise.all([firstPromise, secondPromise]); - expect(send).toHaveBeenCalledTimes(1); - expect(response.status).not.toEqual(500); - expect(secondResponse.send).toHaveBeenCalledTimes(1); + expect(end).toHaveBeenCalledTimes(1); + expect(response.statusCode).not.toEqual(500); + expect(secondResponse.end).toHaveBeenCalledTimes(1); expect(secondResponse.status).not.toEqual(500); }); }); @@ -394,13 +394,13 @@ describe('useStoriesJson', () => { describe('SSE endpoint', () => { beforeEach(() => { use.mockClear(); - send.mockClear(); + end.mockClear(); }); it('sends invalidate events', async () => { const mockServerChannel = { emit: vi.fn() } as any as ServerChannel; useStoriesJson({ - router, + app, serverChannel: mockServerChannel, workingDir, normalizedStories, @@ -434,7 +434,7 @@ describe('useStoriesJson', () => { it('only sends one invalidation when multiple event listeners are listening', async () => { const mockServerChannel = { emit: vi.fn() } as any as ServerChannel; useStoriesJson({ - router, + app, serverChannel: mockServerChannel, workingDir, normalizedStories, @@ -476,7 +476,7 @@ describe('useStoriesJson', () => { const mockServerChannel = { emit: vi.fn() } as any as ServerChannel; useStoriesJson({ - router, + app, serverChannel: mockServerChannel, workingDir, normalizedStories, diff --git a/code/core/src/core-server/utils/stories-json.ts b/code/core/src/core-server/utils/stories-json.ts index 3426aa689de1..a33e12eb0393 100644 --- a/code/core/src/core-server/utils/stories-json.ts +++ b/code/core/src/core-server/utils/stories-json.ts @@ -6,7 +6,7 @@ import type { NormalizedStoriesSpecifier, StoryIndex } from '@storybook/core/typ import { STORY_INDEX_INVALIDATED } from '@storybook/core/core-events'; import { debounce } from 'es-toolkit/compat'; -import type { Request, Response, Router } from 'express'; +import type Polka from 'polka'; import type { StoryIndexGenerator } from './StoryIndexGenerator'; import type { ServerChannel } from './get-server-channel'; @@ -26,14 +26,14 @@ export async function extractStoriesJson( } export function useStoriesJson({ - router, + app, initializedStoryIndexGenerator, workingDir = process.cwd(), configDir, serverChannel, normalizedStories, }: { - router: Router; + app: Polka.Polka; initializedStoryIndexGenerator: Promise; serverChannel: ServerChannel; workingDir?: string; @@ -58,15 +58,15 @@ export function useStoriesJson({ }); } - router.use('/index.json', async (req: Request, res: Response) => { + app.use('/index.json', async (req, res) => { try { const generator = await initializedStoryIndexGenerator; const index = await generator.getIndex(); - res.header('Content-Type', 'application/json'); - res.send(JSON.stringify(index)); + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(index)); } catch (err) { - res.status(500); - res.send(err instanceof Error ? err.toString() : String(err)); + res.statusCode = 500; + res.end(err instanceof Error ? err.toString() : String(err)); } }); } diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts index 82fbfb8c7669..dbdabe22122c 100644 --- a/code/core/src/types/modules/core-common.ts +++ b/code/core/src/types/modules/core-common.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import type { Router } from 'express'; // should be node:http, but that caused the ui/manager to fail to build, might be able to switch this back once ui/manager is in the core -import type { Server } from 'http'; -import type * as telejson from 'telejson'; +import type { Server as HttpServer, IncomingMessage, ServerResponse } from 'http'; +import type { Server as NetServer } from 'net'; +import type { Options as TelejsonOptions } from 'telejson'; import type { PackageJson as PackageJsonFromTypeFest } from 'type-fest'; import type { FileSystemCache } from '../../common/utils/file-cache'; @@ -26,7 +26,7 @@ export interface CoreConfig { }; renderer?: RendererName; disableWebpackDefaults?: boolean; - channelOptions?: Partial; + channelOptions?: Partial; /** Disables the generation of project.json, a file containing Storybook metadata */ disableProjectJson?: boolean; /** @@ -156,7 +156,7 @@ export interface LoadOptions { configDir?: string; cacheKey?: string; ignorePreview?: boolean; - extendServer?: (server: Server) => void; + extendServer?: (server: HttpServer) => void; } export interface CLIOptions { @@ -211,13 +211,37 @@ export type Options = LoadOptions & CLIOptions & BuilderOptions & { build?: TestBuildConfig }; +// A minimal version of Polka's interface to avoid exposing internal implementation details +export type Middleware = ( + req: T & IncomingMessage, + res: ServerResponse, + next: (err?: string | Error) => Promise | void +) => Promise | void; + +interface ServerApp { + server: NetServer; + + use(pattern: RegExp | string, ...handlers: Middleware[]): this; + use(...handlers: Middleware[]): this; + + get(pattern: RegExp | string, ...handlers: Middleware[]): this; + post(pattern: RegExp | string, ...handlers: Middleware[]): this; + put(pattern: RegExp | string, ...handlers: Middleware[]): this; + patch(pattern: RegExp | string, ...handlers: Middleware[]): this; + delete(pattern: RegExp | string, ...handlers: Middleware[]): this; + head(pattern: RegExp | string, ...handlers: Middleware[]): this; + options(pattern: RegExp | string, ...handlers: Middleware[]): this; + connect(pattern: RegExp | string, ...handlers: Middleware[]): this; + trace(pattern: RegExp | string, ...handlers: Middleware[]): this; +} + export interface Builder { getConfig: (options: Options) => Promise; start: (args: { options: Options; startTime: ReturnType; - router: Router; - server: Server; + router: ServerApp; + server: HttpServer; channel: ServerChannel; }) => Promise[] = []; server.post('/event-log', (req, res) => { @@ -14,7 +15,7 @@ server.post('/event-log', (req, res) => { server.get('/event-log', (_req, res) => { console.log(`Sending ${events.length} events`); - res.json(events); + res.end(JSON.stringify(events)); }); server.listen(PORT, () => { diff --git a/scripts/package.json b/scripts/package.json index fa651f004959..d7c89c37cb20 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -64,6 +64,7 @@ "@nx/workspace": "18.0.6", "@octokit/graphql": "^5.0.5", "@octokit/request": "^8.1.2", + "@polka/parse": "^1.0.0-next.28", "@storybook/eslint-config-storybook": "^4.0.0", "@storybook/linter-config": "^4.0.0", "@testing-library/dom": "^10.4.0", @@ -75,7 +76,6 @@ "@types/detect-port": "^1.3.5", "@types/ejs": "^3.1.5", "@types/escodegen": "^0.0.6", - "@types/express": "^4.17.21", "@types/fs-extra": "^11.0.4", "@types/http-server": "^0.12.4", "@types/jest": "^29.5.12", @@ -124,7 +124,6 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-storybook": "^0.8.0", "execa": "^6.1.0", - "express": "^4.19.2", "fast-folder-size": "^2.2.0", "fast-glob": "^3.3.2", "find-up": "^5.0.0", @@ -147,6 +146,7 @@ "picocolors": "^1.1.0", "playwright": "1.46.0", "playwright-core": "1.46.0", + "polka": "^1.0.0-next.28", "prettier": "^3.3.2", "prettier-plugin-brace-style": "^0.6.2", "prettier-plugin-css-order": "^2.1.2", diff --git a/scripts/utils/serve.ts b/scripts/utils/serve.ts deleted file mode 100644 index 98ba1da21c63..000000000000 --- a/scripts/utils/serve.ts +++ /dev/null @@ -1,14 +0,0 @@ -import express from 'express'; -import type { Server } from 'http'; -import serveStatic from 'serve-static'; - -export const serve = async (location: string, port: string): Promise => { - return new Promise((resolve) => { - const app = express(); - - app.use(serveStatic(location)); - const server = app.listen(port, () => { - resolve(server); - }); - }); -}; diff --git a/scripts/yarn.lock b/scripts/yarn.lock index 72976374f403..5f52272a0427 100644 --- a/scripts/yarn.lock +++ b/scripts/yarn.lock @@ -1301,6 +1301,20 @@ __metadata: languageName: node linkType: hard +"@polka/parse@npm:^1.0.0-next.28": + version: 1.0.0-next.28 + resolution: "@polka/parse@npm:1.0.0-next.28" + checksum: 10c0/42c53dfdc4b39a3b516fbd4358b2472b22f36e6038e25d463afb3f133fb1d5af84d7eb4d245ab66bc5793d6414299fdd9093803e5214e560f0693f2176fc58b6 + languageName: node + linkType: hard + +"@polka/url@npm:^1.0.0-next.21": + version: 1.0.0-next.28 + resolution: "@polka/url@npm:1.0.0-next.28" + checksum: 10c0/acc5ea62597e4da2fb42dbee02749d07f102ae7d6d2c966bf7e423c79cd65d1621da305af567e6e7c232f3b565e242d1ec932cbb3dcc0db1508d02e9a2cafa2e + languageName: node + linkType: hard + "@rollup/rollup-android-arm-eabi@npm:4.21.0": version: 4.21.0 resolution: "@rollup/rollup-android-arm-eabi@npm:4.21.0" @@ -1527,6 +1541,7 @@ __metadata: "@nx/workspace": "npm:18.0.6" "@octokit/graphql": "npm:^5.0.5" "@octokit/request": "npm:^8.1.2" + "@polka/parse": "npm:^1.0.0-next.28" "@storybook/eslint-config-storybook": "npm:^4.0.0" "@storybook/linter-config": "npm:^4.0.0" "@testing-library/dom": "npm:^10.4.0" @@ -1538,7 +1553,6 @@ __metadata: "@types/detect-port": "npm:^1.3.5" "@types/ejs": "npm:^3.1.5" "@types/escodegen": "npm:^0.0.6" - "@types/express": "npm:^4.17.21" "@types/fs-extra": "npm:^11.0.4" "@types/http-server": "npm:^0.12.4" "@types/jest": "npm:^29.5.12" @@ -1588,7 +1602,6 @@ __metadata: eslint-plugin-react-hooks: "npm:^4.6.2" eslint-plugin-storybook: "npm:^0.8.0" execa: "npm:^6.1.0" - express: "npm:^4.19.2" fast-folder-size: "npm:^2.2.0" fast-glob: "npm:^3.3.2" find-up: "npm:^5.0.0" @@ -1612,6 +1625,7 @@ __metadata: picocolors: "npm:^1.1.0" playwright: "npm:1.46.0" playwright-core: "npm:1.46.0" + polka: "npm:^1.0.0-next.28" prettier: "npm:^3.3.2" prettier-plugin-brace-style: "npm:^0.6.2" prettier-plugin-css-order: "npm:^2.1.2" @@ -1771,16 +1785,6 @@ __metadata: languageName: node linkType: hard -"@types/body-parser@npm:*": - version: 1.19.4 - resolution: "@types/body-parser@npm:1.19.4" - dependencies: - "@types/connect": "npm:*" - "@types/node": "npm:*" - checksum: 10c0/bec2b8a97861a960ee415f7ab3c2aeb7f4d779fd364d27ddee46057897ea571735f1f854f5ee41682964315d4e3699f62427998b9c21851d773398ef535f0612 - languageName: node - linkType: hard - "@types/concat-stream@npm:^2.0.0": version: 2.0.1 resolution: "@types/concat-stream@npm:2.0.1" @@ -1854,30 +1858,6 @@ __metadata: languageName: node linkType: hard -"@types/express-serve-static-core@npm:^4.17.33": - version: 4.17.39 - resolution: "@types/express-serve-static-core@npm:4.17.39" - dependencies: - "@types/node": "npm:*" - "@types/qs": "npm:*" - "@types/range-parser": "npm:*" - "@types/send": "npm:*" - checksum: 10c0/b23b005fddd2ba3f7142ec9713f06b5582c7712cdf99c3419d3972364903b348a103c3264d9a761d6497140e3b89bd416454684c4bdeff206b4c59b86e96428a - languageName: node - linkType: hard - -"@types/express@npm:^4.17.21": - version: 4.17.21 - resolution: "@types/express@npm:4.17.21" - dependencies: - "@types/body-parser": "npm:*" - "@types/express-serve-static-core": "npm:^4.17.33" - "@types/qs": "npm:*" - "@types/serve-static": "npm:*" - checksum: 10c0/12e562c4571da50c7d239e117e688dc434db1bac8be55613294762f84fd77fbd0658ccd553c7d3ab02408f385bc93980992369dd30e2ecd2c68c358e6af8fabf - languageName: node - linkType: hard - "@types/fs-extra@npm:^11.0.4": version: 11.0.4 resolution: "@types/fs-extra@npm:11.0.4" @@ -2099,20 +2079,6 @@ __metadata: languageName: node linkType: hard -"@types/qs@npm:*": - version: 6.9.9 - resolution: "@types/qs@npm:6.9.9" - checksum: 10c0/aede2a4181a49ae8548a1354bac3f8235cb0c5aab066b10875a3e68e88a199e220f4284e7e2bb75a3c18e5d4ff6abe1a6ce0389ef31b63952cc45e0f4d885ba0 - languageName: node - linkType: hard - -"@types/range-parser@npm:*": - version: 1.2.6 - resolution: "@types/range-parser@npm:1.2.6" - checksum: 10c0/46e7fffc54cdacc8fb0cd576f8f9a6436453f0176205d6ec55434a460c7677e78e688673426d5db5e480501b2943ba08a16ececa3a354c222093551c7217fb8f - languageName: node - linkType: hard - "@types/react-dom@npm:^18.3.0": version: 18.3.0 resolution: "@types/react-dom@npm:18.3.0" @@ -2156,7 +2122,7 @@ __metadata: languageName: node linkType: hard -"@types/serve-static@npm:*, @types/serve-static@npm:^1.15.7": +"@types/serve-static@npm:^1.15.7": version: 1.15.7 resolution: "@types/serve-static@npm:1.15.7" dependencies: @@ -6028,7 +5994,7 @@ __metadata: languageName: node linkType: hard -"express@npm:4.19.2, express@npm:^4.19.2": +"express@npm:4.19.2": version: 4.19.2 resolution: "express@npm:4.19.2" dependencies: @@ -10996,6 +10962,16 @@ __metadata: languageName: node linkType: hard +"polka@npm:^1.0.0-next.28": + version: 1.0.0-next.28 + resolution: "polka@npm:1.0.0-next.28" + dependencies: + "@polka/url": "npm:^1.0.0-next.21" + trouter: "npm:^4.0.0" + checksum: 10c0/206bab3eb1c3c44c2fba9704d998b144ec36c4852b0b97192a1fd3953f215b739e2286852492c13dcc9faecea0f7825a76356d6eef7e02afd6a800d1dbb84ead + languageName: node + linkType: hard + "portfinder@npm:^1.0.28": version: 1.0.32 resolution: "portfinder@npm:1.0.32" @@ -11655,6 +11631,13 @@ __metadata: languageName: node linkType: hard +"regexparam@npm:^3.0.0": + version: 3.0.0 + resolution: "regexparam@npm:3.0.0" + checksum: 10c0/a6430d7b97d5a7d5518f37a850b6b73aab479029d02f46af4fa0e8e4a1d7aad05b7a0d2d10c86ded21a14d5f0fa4c68525f873a5fca2efeefcccd93c36627459 + languageName: node + linkType: hard + "remark-cli@npm:^12.0.0, remark-cli@npm:^12.0.1": version: 12.0.1 resolution: "remark-cli@npm:12.0.1" @@ -13537,6 +13520,15 @@ __metadata: languageName: node linkType: hard +"trouter@npm:^4.0.0": + version: 4.0.0 + resolution: "trouter@npm:4.0.0" + dependencies: + regexparam: "npm:^3.0.0" + checksum: 10c0/e27326a831187c6a47a72b62212364e3f88247fa2a8723e5030e14a0002b69756fb51f97efbda2466deb05f4a78ecb83c2a1a7b343e9659cbfb3f370156f1e12 + languageName: node + linkType: hard + "try-catch@npm:^3.0.0": version: 3.0.1 resolution: "try-catch@npm:3.0.1"