diff --git a/.github/workflows/cron-weekly.yml b/.github/workflows/cron-weekly.yml index 07026c97fb8a..60d9ba29824e 100644 --- a/.github/workflows/cron-weekly.yml +++ b/.github/workflows/cron-weekly.yml @@ -6,6 +6,7 @@ on: jobs: check-links: + if: github.repository_owner == 'storybookjs' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/fork-checks.yml b/.github/workflows/fork-checks.yml new file mode 100644 index 000000000000..a8a33580f1f1 --- /dev/null +++ b/.github/workflows/fork-checks.yml @@ -0,0 +1,69 @@ +name: Fork checks + +# This workflow is only for forks, so they can get basic checks in without a CircleCI API key +on: + push: + +env: + NODE_OPTIONS: '--max_old_space_size=4096' + +jobs: + check: + name: Core Type Checking + if: github.repository_owner != 'storybookjs' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Setup Node.js and Install Dependencies + uses: ./.github/actions/setup-node-and-install + with: + install-code-deps: true + + - name: check + run: yarn task --task check + + prettier: + name: Core Formatting + if: github.repository_owner != 'storybookjs' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Setup Node.js and Install Dependencies + uses: ./.github/actions/setup-node-and-install + with: + install-code-deps: true + + - name: prettier + run: cd code && yarn lint:prettier --check . + + test: + strategy: + matrix: + os: [windows-latest, ubuntu-latest] + runs-on: ${{ matrix.os }} + name: Core Unit Tests, ${{ matrix.os }} + if: github.repository_owner != 'storybookjs' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Setup Node.js and Install Dependencies + uses: ./.github/actions/setup-node-and-install + with: + install-code-deps: true + + - name: compile + run: yarn task --task compile --start-from=compile + + - name: Install Playwright Dependencies + run: cd code && yarn exec playwright install chromium --with-deps + + - name: test + run: yarn test diff --git a/.github/workflows/generate-sandboxes.yml b/.github/workflows/generate-sandboxes.yml index b30b4a7b7f2f..3e86e63c2547 100644 --- a/.github/workflows/generate-sandboxes.yml +++ b/.github/workflows/generate-sandboxes.yml @@ -24,6 +24,7 @@ defaults: jobs: generate-next: name: Generate to next + if: github.repository_owner == 'storybookjs' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -76,6 +77,7 @@ jobs: generate-main: name: Generate to main + if: github.repository_owner == 'storybookjs' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/handle-release-branches.yml b/.github/workflows/handle-release-branches.yml index 84cebf0aee54..021ed04934ff 100644 --- a/.github/workflows/handle-release-branches.yml +++ b/.github/workflows/handle-release-branches.yml @@ -5,6 +5,7 @@ on: jobs: branch-checks: + if: github.repository_owner == 'storybookjs' runs-on: ubuntu-latest steps: - id: get-branch @@ -68,7 +69,7 @@ jobs: branch: ${{ needs.get-next-release-branch.outputs.branch }} next-release-branch-check: - if: ${{ always() }} + if: ${{ always() && github.repository_owner == 'storybookjs' }} needs: [branch-checks, get-next-release-branch] runs-on: ubuntu-latest steps: @@ -87,7 +88,7 @@ jobs: check: ${{ env.is-next-release-branch }} request-create-frontpage-branch: - if: ${{ always() }} + if: ${{ always() && github.repository_owner == 'storybookjs' }} needs: [branch-checks, next-release-branch-check, create-next-release-branch] runs-on: ubuntu-latest diff --git a/.github/workflows/prepare-non-patch-release.yml b/.github/workflows/prepare-non-patch-release.yml index 7443d0d4df42..d57d6bfc8fc5 100644 --- a/.github/workflows/prepare-non-patch-release.yml +++ b/.github/workflows/prepare-non-patch-release.yml @@ -36,6 +36,7 @@ concurrency: jobs: prepare-non-patch-pull-request: name: Prepare non-patch pull request + if: github.repository_owner == 'storybookjs' runs-on: ubuntu-latest environment: Release defaults: diff --git a/.github/workflows/prepare-patch-release.yml b/.github/workflows/prepare-patch-release.yml index f8012dcb7e69..e8c7d2250154 100644 --- a/.github/workflows/prepare-patch-release.yml +++ b/.github/workflows/prepare-patch-release.yml @@ -18,6 +18,7 @@ concurrency: jobs: prepare-patch-pull-request: name: Prepare patch pull request + if: github.repository_owner == 'storybookjs' runs-on: ubuntu-latest environment: Release defaults: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 691a9e9c31fe..800056ca139a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -39,6 +39,7 @@ jobs: name: Publish normal version runs-on: ubuntu-latest if: | + github.repository_owner == 'storybookjs' && github.event_name == 'push' && (github.ref_name == 'latest-release' || github.ref_name == 'next-release') && contains(github.event.head_commit.message, '[skip ci]') != true @@ -221,6 +222,7 @@ jobs: name: Publish canary version runs-on: ubuntu-latest if: | + github.repository_owner == 'storybookjs' && ( github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && endsWith(github.head_ref, 'with-canary-release')) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index dfedf4d4ca4c..d70dbdd80059 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -6,6 +6,7 @@ on: jobs: stale: runs-on: ubuntu-latest + if: github.repository_owner == 'storybookjs' steps: - uses: actions/stale@v9 with: diff --git a/.github/workflows/tests-unit.yml b/.github/workflows/tests-unit.yml index d3b64ec3b796..cf48ebae5015 100644 --- a/.github/workflows/tests-unit.yml +++ b/.github/workflows/tests-unit.yml @@ -8,11 +8,12 @@ on: types: [opened, synchronize, reopened] env: - NODE_OPTIONS: "--max_old_space_size=4096" + NODE_OPTIONS: '--max_old_space_size=4096' jobs: build: name: Core Unit Tests, windows-latest + if: github.repository_owner == 'storybookjs' runs-on: windows-11-arm steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index adf6ad3b9bfc..af45b109da06 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -15,6 +15,7 @@ permissions: jobs: triage: name: Nissuer + if: github.repository_owner == 'storybookjs' runs-on: ubuntu-latest steps: - uses: balazsorban44/nissuer@1.10.0 diff --git a/.github/workflows/trigger-circle-ci-workflow.yml b/.github/workflows/trigger-circle-ci-workflow.yml index a35a74868f10..7b1cedda19f1 100644 --- a/.github/workflows/trigger-circle-ci-workflow.yml +++ b/.github/workflows/trigger-circle-ci-workflow.yml @@ -16,6 +16,7 @@ concurrency: jobs: get-branch: + if: github.repository_owner == 'storybookjs' runs-on: ubuntu-latest steps: - id: get-branch @@ -37,6 +38,7 @@ jobs: branch: ${{ env.branch }} get-parameters: + if: github.repository_owner == 'storybookjs' runs-on: ubuntu-latest steps: - if: github.event_name == 'pull_request_target' && (contains(github.event.pull_request.labels.*.name, 'ci:normal')) @@ -55,7 +57,7 @@ jobs: trigger-circle-ci-workflow: runs-on: ubuntu-latest needs: [get-branch, get-parameters] - if: needs.get-parameters.outputs.workflow != '' + if: github.repository_owner == 'storybookjs' && needs.get-parameters.outputs.workflow != '' steps: - name: Trigger Normal tests uses: fjogeleit/http-request-action@v1 diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 062b24cab7d0..222657de1c60 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,7 @@ +## 10.1.0-alpha.3 + +- React: Add manifests/components.html page - [#32905](https://github.com/storybookjs/storybook/pull/32905), thanks @kasperpeulen! + ## 10.1.0-alpha.2 - A11y: Add aria-selected attribute to tab buttons - [#32656](https://github.com/storybookjs/storybook/pull/32656), thanks @Nischit-Ekbote! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f0f32191b85..034296f85bf4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -131,7 +131,9 @@ Here's a highlight of notable directories and files: ### Fork the repository -If you plan to contribute to Storybook's codebase, you should fork the repository to your GitHub account. This will allow you to make changes to the codebase and submit a pull request to the main repository when you're ready to contribute your changes. Once you've forked the repository, you should [disable Github Actions for your forked repository](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository) as most of them (e.g., pushing to sandbox) will fail without proper authorization. In your forked repository, go to Settings > Actions > General > set the Actions Permissions to **Disable actions**. Additionally, adding our codebase as upstream ensures you can rebase against the latest changes in the main repository. To do this, run the following commands: +If you plan to contribute to Storybook's codebase, you should fork the repository to your GitHub account. This will allow you to make changes to the codebase and submit a pull request to the main repository when you're ready to contribute your changes. + +Additionally, adding our codebase as upstream ensures you can rebase against the latest changes in the main repository. To do this, run the following commands: ```shell git remote add upstream https://github.com/storybookjs/storybook.git diff --git a/code/.prettierignore b/code/.prettierignore index a66b6e6cf489..6f24e6a798d8 100644 --- a/code/.prettierignore +++ b/code/.prettierignore @@ -7,4 +7,10 @@ bench /.nx/cache core/report -/.nx/workspace-data \ No newline at end of file +/.nx/workspace-data + +# This file contains imports with an order that must be preserved. +core/src/core-server/presets/common-manager.ts + +# This file is modified by build/check, causing errors when changed in CI jobs. +lib/eslint-plugin/docs/rules/no-stories-of.md \ No newline at end of file diff --git a/code/core/package.json b/code/core/package.json index 3c7337112b96..95b9c26a8416 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -314,6 +314,7 @@ "react-textarea-autosize": "^8.3.0", "react-transition-group": "^4.4.5", "require-from-string": "^2.0.2", + "resolve": "^1.22.11", "resolve.exports": "^2.0.3", "sirv": "^2.0.4", "slash": "^5.0.0", diff --git a/code/core/src/common/index.ts b/code/core/src/common/index.ts index e0c73e661ab0..af3093d20c02 100644 --- a/code/core/src/common/index.ts +++ b/code/core/src/common/index.ts @@ -45,6 +45,7 @@ export * from './js-package-manager'; export * from './utils/scan-and-transform-files'; export * from './utils/transform-imports'; export * from '../shared/utils/module'; +export * from './utils/utils'; export { versions }; diff --git a/code/core/src/common/utils/interpret-files.ts b/code/core/src/common/utils/interpret-files.ts index 6f792473e8de..97930031ae4a 100644 --- a/code/core/src/common/utils/interpret-files.ts +++ b/code/core/src/common/utils/interpret-files.ts @@ -1,14 +1,19 @@ 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) { @@ -16,3 +21,38 @@ export function getInterpretedFile(pathToFile: string) { .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: [] }); + } +} diff --git a/code/core/src/common/utils/utils.ts b/code/core/src/common/utils/utils.ts new file mode 100644 index 000000000000..2aaccefe716e --- /dev/null +++ b/code/core/src/common/utils/utils.ts @@ -0,0 +1,26 @@ +// Object.groupBy polyfill +export const groupBy = ( + items: T[], + keySelector: (item: T, index: number) => K +) => { + return items.reduce>( + (acc, item, index) => { + const key = keySelector(item, index); + acc[key] ??= []; + acc[key].push(item); + return acc; + }, + {} as Record + ); +}; + +// 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'); +} diff --git a/code/core/src/core-server/build-static.ts b/code/core/src/core-server/build-static.ts index 396915646d5f..ef1bc814288f 100644 --- a/code/core/src/core-server/build-static.ts +++ b/code/core/src/core-server/build-static.ts @@ -11,7 +11,7 @@ import { import { logger } from 'storybook/internal/node-logger'; import { getPrecedingUpgrade, telemetry } from 'storybook/internal/telemetry'; import type { BuilderOptions, CLIOptions, LoadOptions, Options } from 'storybook/internal/types'; -import { type ComponentManifestGenerator } from 'storybook/internal/types'; +import { type ComponentManifestGenerator, type ComponentsManifest } from 'storybook/internal/types'; import { global } from '@storybook/global'; @@ -19,6 +19,7 @@ import { join, relative, resolve } from 'pathe'; import picocolors from 'picocolors'; import { resolvePackageDir } from '../shared/utils/module'; +import { renderManifestComponentsPage } from './manifest'; import { StoryIndexGenerator } from './utils/StoryIndexGenerator'; import { buildOrThrow } from './utils/build-or-throw'; import { copyAllStaticFilesRelativeToMain } from './utils/copy-all-static-files'; @@ -180,6 +181,10 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption join(options.outputDir, 'manifests', 'components.json'), JSON.stringify(manifests) ); + await writeFile( + join(options.outputDir, 'manifests', 'components.html'), + renderManifestComponentsPage(manifests) + ); } catch (e) { logger.error('Failed to generate manifests/components.json'); logger.error(e instanceof Error ? e : String(e)); diff --git a/code/core/src/core-server/dev-server.ts b/code/core/src/core-server/dev-server.ts index 8b25f5438d6a..b5ec0b04dc90 100644 --- a/code/core/src/core-server/dev-server.ts +++ b/code/core/src/core-server/dev-server.ts @@ -1,7 +1,7 @@ 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'; @@ -9,6 +9,7 @@ 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) => { + 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(`
No component manifest generator configured.
`); + 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)); + } catch (e) { + // logger?.error?.(e instanceof Error ? e : String(e)); + res.statusCode = 500; + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + res.end(`
${e instanceof Error ? e.toString() : String(e)}
`); + } + }); } // Now the preview has successfully started, we can count this as a 'dev' event. doTelemetry(app, core, initializedStoryIndexGenerator as Promise, options); diff --git a/code/core/src/core-server/manifest.ts b/code/core/src/core-server/manifest.ts new file mode 100644 index 000000000000..fb7f33677298 --- /dev/null +++ b/code/core/src/core-server/manifest.ts @@ -0,0 +1,710 @@ +import { groupBy } from 'storybook/internal/common'; + +import type { ComponentManifest, ComponentsManifest } from '../types'; + +// AI generated manifests/components.html page +// Only HTML/CSS no JS +export function renderManifestComponentsPage(manifest: ComponentsManifest) { + const entries = Object.entries(manifest?.components ?? {}).sort((a, b) => + (a[1].name || a[0]).localeCompare(b[1].name || b[0]) + ); + + const analyses = entries.map(([, c]) => analyzeComponent(c)); + const totals = { + components: entries.length, + componentsWithError: analyses.filter((a) => a.hasComponentError).length, + componentsWithWarnings: analyses.filter((a) => a.hasWarns).length, + examples: analyses.reduce((sum, a) => sum + a.totalExamples, 0), + exampleErrors: analyses.reduce((sum, a) => sum + a.exampleErrors, 0), + }; + + // Top filters (clickable), no tags; 1px active ring lives in CSS via :target + const allPill = `All`; + const compErrorsPill = + totals.componentsWithError > 0 + ? `${totals.componentsWithError}/${totals.components} component ${plural(totals.componentsWithError, 'error')}` + : `${totals.components} components ok`; + const compWarningsPill = + totals.componentsWithWarnings > 0 + ? `${totals.componentsWithWarnings}/${totals.components} component ${plural(totals.componentsWithWarnings, 'warning')}` + : ''; + const examplesPill = + totals.exampleErrors > 0 + ? `${totals.exampleErrors}/${totals.examples} example errors` + : `${totals.examples} examples ok`; + + const grid = entries.map(([key, c], idx) => renderComponentCard(key, c, `${idx}`)).join(''); + + const errorGroups = Object.entries( + groupBy( + entries.map(([, it]) => it).filter((it) => it.error), + (manifest) => manifest.error?.name ?? 'Error' + ) + ); + + const errorGroupsHTML = errorGroups + .map(([error, grouped]) => { + const id = error.toLowerCase().replace(/[^a-z0-9]+/g, '-'); + const headerText = `${esc(error)}`; + const cards = grouped + .map((manifest, id) => renderComponentCard(manifest.id, manifest, `error-${id}`)) + .join(''); + return ` +
+ + +
${cards}
+
+ `; + }) + .join(''); + + return ` + + + + + Components Manifest + + + + + + + + +
+
+

Components Manifest

+
${allPill}${compErrorsPill}${compWarningsPill}${examplesPill}
+
+
+
+
+
+ ${ + grid || + `
No components.
` + } +
+ ${ + errorGroups.length + ? `
${errorGroupsHTML}
` + : '' + } +
+
+ + `; +} + +const esc = (s: unknown) => + String(s ?? '').replace( + /[&<>"']/g, + (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] as string + ); +const plural = (n: number, one: string, many = `${one}s`) => (n === 1 ? one : many); + +function analyzeComponent(c: ComponentManifest) { + const hasComponentError = !!c.error; + const warns: string[] = []; + + if (!c.description?.trim()) { + warns.push( + 'No description found. Write a jsdoc comment such as /** Component description */ on your component or on your stories meta.' + ); + } + + if (!c.import?.trim()) { + warns.push( + `Specify an @import jsdoc tag on your component or your stories meta such as @import import { ${c.name} } from 'my-design-system';` + ); + } + + const totalExamples = c.examples?.length ?? 0; + const exampleErrors = (c.examples ?? []).filter((e) => !!e?.error).length; + const exampleOk = totalExamples - exampleErrors; + + const hasAnyError = hasComponentError || exampleErrors > 0; // for status dot (red if any errors) + + return { + hasComponentError, + hasAnyError, + hasWarns: warns.length > 0, + warns, + totalExamples, + exampleErrors, + exampleOk, + }; +} + +function note(title: string, bodyHTML: string, kind: 'warn' | 'err') { + return ` +
+
${esc(title)}
+
${bodyHTML}
+
`; +} + +function renderComponentCard(key: string, c: ComponentManifest, id: string) { + const a = analyzeComponent(c); + const statusDot = a.hasAnyError ? 'dot-err' : 'dot-ok'; + const errorExamples = (c.examples ?? []).filter((ex) => !!ex?.error); + + const slug = `c-${id}-${(c.id || key) + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '')}`; + + const componentErrorBadge = a.hasComponentError + ? `` + : ''; + + const warningsBadge = a.hasWarns + ? `` + : ''; + + const examplesBadge = + a.exampleErrors > 0 + ? `` + : `${a.totalExamples} examples ok`; + + const tags = + c.jsDocTags && typeof c.jsDocTags === 'object' + ? Object.entries(c.jsDocTags) + .flatMap(([k, v]) => + (Array.isArray(v) ? v : [v]).map( + (val) => `${esc(k)}: ${esc(val)}` + ) + ) + .join('') + : ''; + + esc(c.error?.message || 'Unknown error'); + return ` +
+
+
+

${esc(c.name || key)}

+
+ ${componentErrorBadge} + ${warningsBadge} + ${examplesBadge} +
+
+
${esc(c.id)} · ${esc(c.path)}
+ ${c.summary ? `
${esc(c.summary)}
` : ''} + ${c.description ? `
${esc(c.description)}
` : ''} + ${tags ? `
${tags}
` : ''} +
+ + + ${a.hasComponentError ? `` : ''} + ${a.hasWarns ? `` : ''} + ${a.exampleErrors > 0 ? `` : ''} + +
+ ${ + a.hasComponentError + ? ` +
+ ${note('Component error', `
${esc(c.error?.message || 'Unknown error')}
`, 'err')} +
` + : '' + } + ${ + a.hasWarns + ? ` +
+ ${a.warns.map((w) => note('Warning', esc(w), 'warn')).join('')} +
` + : '' + } + ${ + a.exampleErrors > 0 + ? ` +
+ ${errorExamples + .map( + (ex, j) => ` +
+
+ + ${esc(ex?.name ?? `Example ${j + 1}`)} + example error +
+ ${ex?.snippet ? `
${esc(ex.snippet)}
` : ''} + ${ex?.error?.message ? `
${esc(ex.error.message)}
` : ''} +
` + ) + .join('')} +
` + : '' + } +
+
`; +} diff --git a/code/core/src/core-server/presets/common-manager.ts b/code/core/src/core-server/presets/common-manager.ts index 66b87c66277d..563443222ac3 100644 --- a/code/core/src/core-server/presets/common-manager.ts +++ b/code/core/src/core-server/presets/common-manager.ts @@ -3,7 +3,6 @@ import { global } from '@storybook/global'; import { addons } from 'storybook/manager-api'; -/* eslint-disable prettier/prettier */ // THE ORDER OF THESE IMPORTS MATTERS! IT DEFINES THE ORDER OF PANELS AND TOOLS! import controlsManager from '../../controls/manager'; import actionsManager from '../../actions/manager'; @@ -12,7 +11,6 @@ import backgroundsManager from '../../backgrounds/manager'; import measureManager from '../../measure/manager'; import outlineManager from '../../outline/manager'; import viewportManager from '../../viewport/manager'; -/* eslint-enable prettier/prettier */ const TAG_FILTERS = 'tag-filters'; const STATIC_FILTER = 'static-filter'; diff --git a/code/core/src/core-server/utils/StoryIndexGenerator.ts b/code/core/src/core-server/utils/StoryIndexGenerator.ts index 97849f187cc9..ce9689d65b36 100644 --- a/code/core/src/core-server/utils/StoryIndexGenerator.ts +++ b/code/core/src/core-server/utils/StoryIndexGenerator.ts @@ -29,6 +29,7 @@ import invariant from 'tiny-invariant'; import { dedent } from 'ts-dedent'; import * as TsconfigPaths from 'tsconfig-paths'; +import { resolveImport, supportedExtensions } from '../../common'; import { userOrAutoTitleFromSpecifier } from '../../preview-api/modules/store/autoTitle'; import { sortStoriesV7 } from '../../preview-api/modules/store/sortStories'; import { IndexingError, MultipleIndexingError } from './IndexingError'; @@ -378,21 +379,16 @@ export class StoryIndexGenerator { absolutePath: Path, matchPath: TsconfigPaths.MatchPath | undefined ) { - let rawPath = rawComponentPath; - if (matchPath) { - rawPath = matchPath(rawPath) ?? rawPath; - } - - const absoluteComponentPath = resolve(dirname(absolutePath), rawPath); - const existing = ['', '.js', '.ts', '.jsx', '.tsx', '.mjs', '.mts'] - .map((ext) => `${absoluteComponentPath}${ext}`) - .find((candidate) => existsSync(candidate)); - if (existing) { - const relativePath = relative(this.options.workingDir, existing); - return slash(normalizeStoryPath(relativePath)); + const matchedPath = + matchPath?.(rawComponentPath, undefined, undefined, supportedExtensions) ?? rawComponentPath; + let resolved; + try { + resolved = resolveImport(matchedPath, { basedir: dirname(absolutePath) }); + } catch (_) { + return matchedPath; } - - return rawComponentPath; + const relativePath = relative(this.options.workingDir, resolved); + return slash(normalizeStoryPath(relativePath)); } async extractStories( diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts index 36063b164e73..4918d619c6c4 100644 --- a/code/core/src/types/modules/core-common.ts +++ b/code/core/src/types/modules/core-common.ts @@ -352,9 +352,9 @@ export interface ComponentManifest { description?: string; import?: string; summary?: string; - examples: { name: string; snippet?: string; error?: { message: string } }[]; + examples: { name: string; snippet?: string; error?: { name: string; message: string } }[]; jsDocTags: Record; - error?: { message: string }; + error?: { name: string; message: string }; } export interface ComponentsManifest { diff --git a/code/frameworks/angular/build-schema.json b/code/frameworks/angular/build-schema.json index 12e9e2af7ebe..9753db540f2d 100644 --- a/code/frameworks/angular/build-schema.json +++ b/code/frameworks/angular/build-schema.json @@ -67,27 +67,18 @@ "compodocArgs": { "type": "array", "description": "Compodoc options : https://compodoc.app/guides/options.html. Options `-p` with tsconfig path and `-d` with workspace root is always given.", - "default": [ - "-e", - "json" - ], + "default": ["-e", "json"], "items": { "type": "string" } }, "webpackStatsJson": { - "type": [ - "boolean", - "string" - ], + "type": ["boolean", "string"], "description": "Write Webpack Stats JSON to disk", "default": false }, "statsJson": { - "type": [ - "boolean", - "string" - ], + "type": ["boolean", "string"], "description": "Write stats JSON to disk", "default": false }, @@ -125,10 +116,7 @@ } }, "sourceMap": { - "type": [ - "boolean", - "object" - ], + "type": ["boolean", "object"], "description": "Configure sourcemaps. See: https://angular.io/guide/workspace-config#source-map-configuration", "default": false }, @@ -170,11 +158,7 @@ } }, "additionalProperties": false, - "required": [ - "glob", - "input", - "output" - ] + "required": ["glob", "input", "output"] }, { "type": "string" @@ -202,9 +186,7 @@ } }, "additionalProperties": false, - "required": [ - "input" - ] + "required": ["input"] }, { "type": "string", @@ -213,4 +195,4 @@ ] } } -} \ No newline at end of file +} diff --git a/code/frameworks/angular/start-schema.json b/code/frameworks/angular/start-schema.json index 7befb6f8b727..84d6bd80861b 100644 --- a/code/frameworks/angular/start-schema.json +++ b/code/frameworks/angular/start-schema.json @@ -93,10 +93,7 @@ "compodocArgs": { "type": "array", "description": "Compodoc options : https://compodoc.app/guides/options.html. Options `-p` with tsconfig path and `-d` with workspace root is always given.", - "default": [ - "-e", - "json" - ], + "default": ["-e", "json"], "items": { "type": "string" } @@ -135,18 +132,12 @@ "description": "URL path to be appended when visiting Storybook for the first time" }, "webpackStatsJson": { - "type": [ - "boolean", - "string" - ], + "type": ["boolean", "string"], "description": "Write Webpack Stats JSON to disk", "default": false }, "statsJson": { - "type": [ - "boolean", - "string" - ], + "type": ["boolean", "string"], "description": "Write stats JSON to disk", "default": false }, @@ -160,10 +151,7 @@ "pattern": "(silly|verbose|info|warn|silent)" }, "sourceMap": { - "type": [ - "boolean", - "object" - ], + "type": ["boolean", "object"], "description": "Configure sourcemaps. See: https://angular.io/guide/workspace-config#source-map-configuration", "default": false }, @@ -205,11 +193,7 @@ } }, "additionalProperties": false, - "required": [ - "glob", - "input", - "output" - ] + "required": ["glob", "input", "output"] }, { "type": "string" @@ -237,9 +221,7 @@ } }, "additionalProperties": false, - "required": [ - "input" - ] + "required": ["input"] }, { "type": "string", @@ -248,4 +230,4 @@ ] } } -} \ No newline at end of file +} diff --git a/code/frameworks/server-webpack5/template/cli/page.stories.yaml b/code/frameworks/server-webpack5/template/cli/page.stories.yaml index 08915865c89d..6dcca1651d0a 100644 --- a/code/frameworks/server-webpack5/template/cli/page.stories.yaml +++ b/code/frameworks/server-webpack5/template/cli/page.stories.yaml @@ -1,10 +1,10 @@ -title: "Example/Page" +title: 'Example/Page' parameters: server: - url: "https://storybook-server-demo.netlify.app/api" - id: "page" + url: 'https://storybook-server-demo.netlify.app/api' + id: 'page' stories: - - name: "LoggedIn" + - name: 'LoggedIn' args: user: {} - - name: "LoggedOut" + - name: 'LoggedOut' diff --git a/code/frameworks/sveltekit/tsconfig.json b/code/frameworks/sveltekit/tsconfig.json index 39029c2ce294..f5ab5afaf6a4 100644 --- a/code/frameworks/sveltekit/tsconfig.json +++ b/code/frameworks/sveltekit/tsconfig.json @@ -3,7 +3,7 @@ "baseUrl": ".", "paths": { "storybook/internal/*": ["../../lib/cli/core/*"] - }, + } }, "extends": "../../tsconfig.json", "include": ["src/**/*"] diff --git a/code/package.json b/code/package.json index 2dc0832ff3ac..bc8a880091df 100644 --- a/code/package.json +++ b/code/package.json @@ -283,5 +283,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "10.1.0-alpha.3" } diff --git a/code/renderers/react/src/componentManifest/generateCodeSnippet.test.tsx b/code/renderers/react/src/componentManifest/generateCodeSnippet.test.tsx index 582343a7c0f5..2b4272a0a0c8 100644 --- a/code/renderers/react/src/componentManifest/generateCodeSnippet.test.tsx +++ b/code/renderers/react/src/componentManifest/generateCodeSnippet.test.tsx @@ -195,6 +195,17 @@ test('CSF2 - Template.bind', () => { ); }); +test('CSF2 - with args', () => { + const input = withCSF3(dedent` + const Template = (args) => ;"` + ); +}); + test('Custom Render', () => { const input = withCSF3(dedent` export const CustomRender: Story = { render: () => } diff --git a/code/renderers/react/src/componentManifest/generateCodeSnippet.ts b/code/renderers/react/src/componentManifest/generateCodeSnippet.ts index 583765a9b8d1..3afb9a1d62d9 100644 --- a/code/renderers/react/src/componentManifest/generateCodeSnippet.ts +++ b/code/renderers/react/src/componentManifest/generateCodeSnippet.ts @@ -139,7 +139,9 @@ export function getCodeSnippet( .map((p) => p.get('value')) .find((v) => v.isObjectExpression()); const storyArgs = argsRecordFromObjectPath(storyArgsPath); - const merged: Record = { ...metaArgs, ...storyArgs }; + const storyAssignedArgsPath = storyArgsAssignmentPath(csf._file.path, storyName); + const storyAssignedArgs = argsRecordFromObjectPath(storyAssignedArgsPath); + const merged: Record = { ...metaArgs, ...storyArgs, ...storyAssignedArgs }; // For no-function fallback const entries = Object.entries(merged).filter(([k]) => k !== 'children'); @@ -251,6 +253,32 @@ const argsRecordFromObjectPath = (objPath?: NodePath | null) ) : {}; +/** Find `StoryName.args = { ... }` assignment and return the right-hand ObjectExpression if present. */ +function storyArgsAssignmentPath( + program: NodePath, + storyName: string +): NodePath | null { + let found: NodePath | null = null; + program.traverse({ + AssignmentExpression(p) { + const left = p.get('left'); + const right = p.get('right'); + if (left.isMemberExpression()) { + const obj = left.get('object'); + const prop = left.get('property'); + const isStoryIdent = obj.isIdentifier() && obj.node.name === storyName; + const isArgsProp = + (prop.isIdentifier() && prop.node.name === 'args' && !left.node.computed) || + (t.isStringLiteral(prop.node) && left.node.computed && prop.node.value === 'args'); + if (isStoryIdent && isArgsProp && right.isObjectExpression()) { + found = right as NodePath; + } + } + }, + }); + return found; +} + const argsRecordFromObjectNode = (obj?: t.ObjectExpression | null) => obj ? Object.fromEntries( diff --git a/code/renderers/react/src/componentManifest/generator.test.ts b/code/renderers/react/src/componentManifest/generator.test.ts index 6196a2637956..c330f3c511fd 100644 --- a/code/renderers/react/src/componentManifest/generator.test.ts +++ b/code/renderers/react/src/componentManifest/generator.test.ts @@ -549,6 +549,7 @@ test('component exported from other file', async () => { 9 | > 10 | export { Primary } from './other-file'; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^", + "name": "SyntaxError", }, "name": "Primary", }, @@ -600,6 +601,7 @@ test('unknown expressions', async () => { 9 | > 10 | export const Primary = someWeirdExpression; | ^^^^^^^^^^^^^^^^^^^", + "name": "SyntaxError", }, "name": "Primary", }, diff --git a/code/renderers/react/src/componentManifest/generator.ts b/code/renderers/react/src/componentManifest/generator.ts index a08e63f1047f..63a7694af40a 100644 --- a/code/renderers/react/src/componentManifest/generator.ts +++ b/code/renderers/react/src/componentManifest/generator.ts @@ -50,6 +50,7 @@ export const componentManifestGenerator = async () => { return { name: storyName, error: { + name: e.name, message: e.message, }, }; @@ -66,12 +67,26 @@ export const componentManifestGenerator = async () => { } satisfies Partial; if (!entry.componentPath) { - const message = `No component file found for the "${name}" component.`; + const componentName = csf._meta?.component; + + const error = !componentName + ? { + name: 'No meta.component specified', + message: 'Specify meta.component for the component to be included in the manifest.', + } + : { + name: 'No component import found', + message: `No component file found for the "${componentName}" component.`, + }; return { ...base, name, examples, - error: { message }, + error: { + name: error.name, + message: + csf._metaStatementPath?.buildCodeFrameError(error.message).message ?? error.message, + }, }; } @@ -86,7 +101,8 @@ export const componentManifestGenerator = async () => { name, examples, error: { - message: `Could not read the component file located at ${entry.componentPath}`, + name: 'Component file could not be read', + message: `Could not read the component file located at "${entry.componentPath}".\nPrefer relative imports.`, }, }; } @@ -99,7 +115,13 @@ export const componentManifestGenerator = async () => { const error = !docgen ? { - message: `Could not parse props information for the located at ${entry.componentPath}`, + name: 'Docgen evaluation failed', + message: + `Could not parse props information for the component file located at "${entry.componentPath}"\n` + + `Avoid barrel files when importing your component file.\n` + + `Prefer relative imports if possible.\n` + + `Avoid pointing to transpiled files.\n` + + `You can debug your component file in this playground: https://react-docgen.dev/playground`, } : undefined; diff --git a/code/renderers/react/src/componentManifest/reactDocgen.ts b/code/renderers/react/src/componentManifest/reactDocgen.ts index 4770e958b1cb..8031d64e8a8b 100644 --- a/code/renderers/react/src/componentManifest/reactDocgen.ts +++ b/code/renderers/react/src/componentManifest/reactDocgen.ts @@ -3,6 +3,8 @@ import { sep } from 'node:path'; import { types as t } from 'storybook/internal/babel'; import { getProjectRoot } from 'storybook/internal/common'; +import { supportedExtensions } from 'storybook/internal/common'; +import { resolveImport } from 'storybook/internal/common'; import { type CsfFile } from 'storybook/internal/csf-tools'; import * as find from 'empathic/find'; @@ -16,11 +18,7 @@ import { import * as TsconfigPaths from 'tsconfig-paths'; import actualNameHandler from './reactDocgen/actualNameHandler'; -import { - RESOLVE_EXTENSIONS, - ReactDocgenResolveError, - defaultLookupModule, -} from './reactDocgen/docgenResolver'; +import { ReactDocgenResolveError } from './reactDocgen/docgenResolver'; import exportNameHandler from './reactDocgen/exportNameHandler'; export type DocObj = Documentation & { @@ -92,14 +90,14 @@ export function getReactDocgenImporter(matchPath: TsconfigPaths.MatchPath | unde return makeFsImporter((filename, basedir) => { const mappedFilenameByPaths = (() => { if (matchPath) { - const match = matchPath(filename); + const match = matchPath(filename, undefined, undefined, supportedExtensions); return match || filename; } else { return filename; } })(); - const result = defaultLookupModule(mappedFilenameByPaths, basedir); + const result = resolveImport(mappedFilenameByPaths, { basedir }); if (result.includes(`${sep}react-native${sep}index.js`)) { const replaced = result.replace( @@ -107,12 +105,12 @@ export function getReactDocgenImporter(matchPath: TsconfigPaths.MatchPath | unde `${sep}react-native-web${sep}dist${sep}index.js` ); if (existsSync(replaced)) { - if (RESOLVE_EXTENSIONS.find((ext) => result.endsWith(ext))) { + if (supportedExtensions.find((ext) => result.endsWith(ext))) { return replaced; } } } - if (RESOLVE_EXTENSIONS.find((ext) => result.endsWith(ext))) { + if (supportedExtensions.find((ext) => result.endsWith(ext))) { return result; } diff --git a/code/renderers/react/src/componentManifest/reactDocgen/docgenResolver.ts b/code/renderers/react/src/componentManifest/reactDocgen/docgenResolver.ts index f4ae37407c09..6b69ca55d7dd 100644 --- a/code/renderers/react/src/componentManifest/reactDocgen/docgenResolver.ts +++ b/code/renderers/react/src/componentManifest/reactDocgen/docgenResolver.ts @@ -1,5 +1,7 @@ import { extname } from 'node:path'; +import { supportedExtensions } from 'storybook/internal/common'; + import resolve from 'resolve'; export class ReactDocgenResolveError extends Error { @@ -14,30 +16,11 @@ export class ReactDocgenResolveError extends Error { /* The below code was copied from: * https://github.com/reactjs/react-docgen/blob/df2daa8b6f0af693ecc3c4dc49f2246f60552bcb/packages/react-docgen/src/importer/makeFsImporter.ts#L14-L63 * because it wasn't exported from the react-docgen package. - * watch out: when updating this code, also update the code in code/presets/react-webpack/src/loaders/docgen-resolver.ts */ - -// These extensions are sorted by priority -// resolve() will check for files in the order these extensions are sorted -export const RESOLVE_EXTENSIONS = [ - '.js', - '.cts', // These were originally not in the code, I added them - '.mts', // These were originally not in the code, I added them - '.ctsx', // These were originally not in the code, I added them - '.mtsx', // These were originally not in the code, I added them - '.ts', - '.tsx', - '.mjs', - '.cjs', - '.mts', - '.cts', - '.jsx', -]; - export function defaultLookupModule(filename: string, basedir: string): string { const resolveOptions = { basedir, - extensions: RESOLVE_EXTENSIONS, + extensions: supportedExtensions, // we do not need to check core modules as we cannot import them anyway includeCoreModules: false, }; diff --git a/code/yarn.lock b/code/yarn.lock index 6da9c97418cc..d6028e53f751 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -23100,6 +23100,19 @@ __metadata: languageName: node linkType: hard +"resolve@npm:^1.22.11": + version: 1.22.11 + resolution: "resolve@npm:1.22.11" + dependencies: + is-core-module: "npm:^2.16.1" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10c0/f657191507530f2cbecb5815b1ee99b20741ea6ee02a59c57028e9ec4c2c8d7681afcc35febbd554ac0ded459db6f2d8153382c53a2f266cee2575e512674409 + languageName: node + linkType: hard + "resolve@npm:^2.0.0-next.5": version: 2.0.0-next.5 resolution: "resolve@npm:2.0.0-next.5" @@ -23126,6 +23139,19 @@ __metadata: languageName: node linkType: hard +"resolve@patch:resolve@npm%3A^1.22.11#optional!builtin": + version: 1.22.11 + resolution: "resolve@patch:resolve@npm%3A1.22.11#optional!builtin::version=1.22.11&hash=c3c19d" + dependencies: + is-core-module: "npm:^2.16.1" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10c0/ee5b182f2e37cb1165465e58c6abc797fec0a80b5ba3231607beb4677db0c9291ac010c47cf092b6daa2b7f518d69a0e21888e7e2b633f68d501a874212a8c63 + languageName: node + linkType: hard + "resolve@patch:resolve@npm%3A^2.0.0-next.5#optional!builtin": version: 2.0.0-next.5 resolution: "resolve@patch:resolve@npm%3A2.0.0-next.5#optional!builtin::version=2.0.0-next.5&hash=c3c19d" @@ -24586,6 +24612,7 @@ __metadata: react-transition-group: "npm:^4.4.5" recast: "npm:^0.23.5" require-from-string: "npm:^2.0.2" + resolve: "npm:^1.22.11" resolve.exports: "npm:^2.0.3" semver: "npm:^7.6.2" sirv: "npm:^2.0.4" diff --git a/docs/versions/next.json b/docs/versions/next.json index 55e36f03e63a..689ef2978a92 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"10.1.0-alpha.2","info":{"plain":"- Core: 10.1 features WIP - [#32810](https://github.com/storybookjs/storybook/pull/32810), thanks @JReinhold!"}} \ No newline at end of file +{"version":"10.1.0-alpha.3","info":{"plain":"- React: Add manifests/components.html page - [#32905](https://github.com/storybookjs/storybook/pull/32905), thanks @kasperpeulen!"}} \ No newline at end of file