- 
          
 - 
                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 all 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'; | ||
                
      
                  JReinhold marked this conversation as resolved.
               
          
            Show resolved
            Hide resolved
         | 
||
| 
     | 
||
| 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); | ||||||||||||||||||||||||||||||
| 
          
            
          
           | 
    ||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Good: stable, well-known module resolution library added.
The "resolve" package is a mature, well-established module resolution utility. The caret version constraint (
^1.22.11) is sensible, allowing patch and minor updates while maintaining API stability.Verify that the version is current and free from known vulnerabilities, and confirm it's actually used in the new utilities (e.g., the
resolveImportfunction mentioned in the PR).🌐 Web query:
💡 Result:
Latest published version: 1.22.10 (published ~9 months ago). [1]
Known security status: there are no high‑profile npm security advisories explicitly for the resolve package in the npm advisory database; however third‑party scanners (e.g., ReversingLabs / Snyk feed) flag some indirect/patch‑mandated issues tied to versions/dependencies. I recommend running npm audit / Snyk scan on your project to see any dependency paths and remediations. [1][2]
Sources:
Correct the resolve version: 1.22.11 does not exist as a published release.
The latest published version of resolve is 1.22.10, but the PR specifies
^1.22.11. Update line 317 to use"resolve": "^1.22.10"instead.No high-profile npm security advisories exist for resolve, though third-party scanners flag some indirect/patch-mandated issues. Run
npm auditafter correcting the version to verify dependency security.🤖 Prompt for AI Agents