- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 9.8k
 
React: Create a components manifest html page #32882
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 21 commits
f30520c
              3b1c417
              b361d37
              b36905f
              f93f388
              6f33595
              303372a
              1d2ff16
              da371a0
              eb3381a
              5c7725f
              569fb1b
              20049de
              15b6805
              8f03c22
              2d50a45
              074c9b5
              f09df14
              a7160e6
              059bed1
              d65d0e7
              cf22079
              813ff59
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -1,18 +1,58 @@ | ||
| import { existsSync } from 'node:fs'; | ||
| import { extname } from 'node:path'; | ||
| 
     | 
||
| import resolve from 'resolve'; | ||
| 
     | 
||
| export const supportedExtensions = [ | ||
| '.js', | ||
| '.mjs', | ||
| '.cjs', | ||
| '.jsx', | ||
| '.ts', | ||
| '.jsx', | ||
| '.tsx', | ||
| '.mjs', | ||
| '.mts', | ||
| '.mtsx', | ||
| '.cjs', | ||
| '.cts', | ||
| '.tsx', | ||
| '.ctsx', | ||
| ] as const; | ||
| 
     | 
||
| export function getInterpretedFile(pathToFile: string) { | ||
| return supportedExtensions | ||
| .map((ext) => (pathToFile.endsWith(ext) ? pathToFile : `${pathToFile}${ext}`)) | ||
| .find((candidate) => existsSync(candidate)); | ||
| } | ||
| 
     | 
||
| export function resolveImport(id: string, options: resolve.SyncOpts): string { | ||
| const mergedOptions: resolve.SyncOpts = { | ||
| extensions: supportedExtensions, | ||
| packageFilter(pkg) { | ||
| // Prefer 'module' over 'main' if available | ||
| if (pkg.module) { | ||
| pkg.main = pkg.module; | ||
| } | ||
| return pkg; | ||
| }, | ||
| ...options, | ||
| }; | ||
| 
     | 
||
| try { | ||
| return resolve.sync(id, { ...mergedOptions }); | ||
| } catch (error) { | ||
| const ext = extname(id); | ||
| 
     | 
||
| // if we try to import a JavaScript file it might be that we are actually pointing to | ||
| // a TypeScript file. This can happen in ES modules as TypeScript requires to import other | ||
| // TypeScript files with .js extensions | ||
| // https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions | ||
| const newId = ['.js', '.mjs', '.cjs'].includes(ext) | ||
| ? `${id.slice(0, -2)}ts` | ||
| : ext === '.jsx' | ||
| ? `${id.slice(0, -3)}tsx` | ||
| : null; | ||
| 
     | 
||
| if (!newId) { | ||
| throw error; | ||
| } | ||
| return resolve.sync(newId, { ...mergedOptions, extensions: [] }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| // Object.groupBy polyfill | ||
| export const groupBy = <K extends PropertyKey, T>( | ||
| items: T[], | ||
| keySelector: (item: T, index: number) => K | ||
| ) => { | ||
| return items.reduce<Record<K, T[]>>( | ||
| (acc, item, index) => { | ||
| const key = keySelector(item, index); | ||
| acc[key] ??= []; | ||
| acc[key].push(item); | ||
| return acc; | ||
| }, | ||
| {} as Record<K, T[]> | ||
| ); | ||
| }; | ||
| 
     | 
||
| // This invariant allows for lazy evaluation of the message, which we need to avoid excessive computation. | ||
| export function invariant( | ||
| condition: unknown, | ||
| message?: string | (() => string) | ||
| ): asserts condition { | ||
| if (condition) { | ||
| return; | ||
| } | ||
| throw new Error((typeof message === 'function' ? message() : message) ?? 'Invariant failed'); | ||
| } | 
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,14 +1,15 @@ | ||||||||||||||||||||||||||||||
| import { logConfig } from 'storybook/internal/common'; | ||||||||||||||||||||||||||||||
| import { logger } from 'storybook/internal/node-logger'; | ||||||||||||||||||||||||||||||
| import { MissingBuilderError } from 'storybook/internal/server-errors'; | ||||||||||||||||||||||||||||||
| import type { Options } from 'storybook/internal/types'; | ||||||||||||||||||||||||||||||
| import type { ComponentsManifest, Options } from 'storybook/internal/types'; | ||||||||||||||||||||||||||||||
| import { type ComponentManifestGenerator } from 'storybook/internal/types'; | ||||||||||||||||||||||||||||||
| 
     | 
||||||||||||||||||||||||||||||
| import compression from '@polka/compression'; | ||||||||||||||||||||||||||||||
| import polka from 'polka'; | ||||||||||||||||||||||||||||||
| import invariant from 'tiny-invariant'; | ||||||||||||||||||||||||||||||
| 
     | 
||||||||||||||||||||||||||||||
| import { telemetry } from '../telemetry'; | ||||||||||||||||||||||||||||||
| import { renderManifestComponentsPage } from './manifest'; | ||||||||||||||||||||||||||||||
| import { type StoryIndexGenerator } from './utils/StoryIndexGenerator'; | ||||||||||||||||||||||||||||||
| import { doTelemetry } from './utils/doTelemetry'; | ||||||||||||||||||||||||||||||
| import { getManagerBuilder, getPreviewBuilder } from './utils/get-builders'; | ||||||||||||||||||||||||||||||
| 
          
            
          
           | 
    @@ -165,6 +166,34 @@ export async function storybookDevServer(options: Options) { | |||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| 
     | 
||||||||||||||||||||||||||||||
| app.get('/manifests/components.html', async (req, res) => { | ||||||||||||||||||||||||||||||
| 
         There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be nice to also support   | 
||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| const componentManifestGenerator: ComponentManifestGenerator = await options.presets.apply( | ||||||||||||||||||||||||||||||
| 'experimental_componentManifestGenerator' | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| const indexGenerator = await initializedStoryIndexGenerator; | ||||||||||||||||||||||||||||||
| 
     | 
||||||||||||||||||||||||||||||
| if (!componentManifestGenerator || !indexGenerator) { | ||||||||||||||||||||||||||||||
| res.statusCode = 400; | ||||||||||||||||||||||||||||||
| res.setHeader('Content-Type', 'text/html; charset=utf-8'); | ||||||||||||||||||||||||||||||
| res.end(`<pre>No component manifest generator configured.</pre>`); | ||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| 
     | 
||||||||||||||||||||||||||||||
| const manifest = (await componentManifestGenerator( | ||||||||||||||||||||||||||||||
| indexGenerator as unknown as import('storybook/internal/core-server').StoryIndexGenerator | ||||||||||||||||||||||||||||||
| )) as ComponentsManifest; | ||||||||||||||||||||||||||||||
| 
     | 
||||||||||||||||||||||||||||||
| res.setHeader('Content-Type', 'text/html; charset=utf-8'); | ||||||||||||||||||||||||||||||
| res.end(renderManifestComponentsPage(manifest)); | ||||||||||||||||||||||||||||||
                
      
                  JReinhold marked this conversation as resolved.
               
          
            Show resolved
            Hide resolved
         | 
||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||
| // logger?.error?.(e instanceof Error ? e : String(e)); | ||||||||||||||||||||||||||||||
| res.statusCode = 500; | ||||||||||||||||||||||||||||||
| res.setHeader('Content-Type', 'text/html; charset=utf-8'); | ||||||||||||||||||||||||||||||
| res.end(`<pre>${e instanceof Error ? e.toString() : String(e)}</pre>`); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| 
         
      Comment on lines
    
      +191
     to 
      +195
    
   
  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Escape HTML in error responses to avoid XSS; log the error. Interpolating raw error text into HTML is unsafe. Also, keep server logs. Two options: 
 Suggested patch (HTML + escape + logging): -      } catch (e) {
-        // logger?.error?.(e instanceof Error ? e : String(e));
-        res.statusCode = 500;
-        res.setHeader('Content-Type', 'text/html; charset=utf-8');
-        res.end(`<pre>${e instanceof Error ? e.toString() : String(e)}</pre>`);
-      }
+      } catch (e) {
+        logger.error(e instanceof Error ? e : String(e));
+        const esc = (s: unknown) =>
+          String(s ?? '').replace(/[&<>"']/g, (c) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c] as string));
+        res.statusCode = 500;
+        res.setHeader('Content-Type', 'text/html; charset=utf-8');
+        const msg = e instanceof Error ? e.toString() : String(e);
+        res.end(`<pre><code>${esc(msg)}</code></pre>`);
+      }📝 Committable suggestion
 
        Suggested change
       
    
 🤖 Prompt for AI Agents | 
||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| // Now the preview has successfully started, we can count this as a 'dev' event. | ||||||||||||||||||||||||||||||
| doTelemetry(app, core, initializedStoryIndexGenerator as Promise<StoryIndexGenerator>, options); | ||||||||||||||||||||||||||||||
| 
          
            
          
           | 
    ||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.