From cd28625d06a6755bcc814e32d210b67e2b941511 Mon Sep 17 00:00:00 2001 From: Zlodej43sm <8863227+Zlodej43sm@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:03:00 +0200 Subject: [PATCH 1/2] feat: CTORNDSD-679 Update Storybook and documentation links for React components, integrate Web Components support, and enhance build workflows. --- .dockerignore | 10 + .github/workflows/pr-head.yaml | 6 + .gitignore | 1 + CLAUDE.md | 5 +- Dockerfile.storybook | 11 +- README.md | 32 +- bin/storybook/check-links.mjs | 115 ++++ bin/storybook/legacy-react-ids.json | 584 ++++++++++++++++++ bin/storybook/link-validation.mjs | 42 ++ bin/storybook/link-validation.test.mjs | 46 ++ bin/storybook/package-build.mjs | 10 + bin/storybook/routes.d.mts | 4 + bin/storybook/routes.mjs | 40 ++ bin/storybook/routes.test.mjs | 57 ++ bin/storybook/smoke-test.mjs | 104 ++++ libs/ui/.storybook/main.ts | 16 +- libs/ui/.storybook/preview.ts | 53 +- libs/ui/.storybook/public/defaultTheme.json | 6 +- libs/ui/README.md | 4 +- libs/ui/llms.txt | 36 +- libs/ui/project.json | 3 + libs/ui/src/ai/README.md | 4 +- libs/ui/src/ai/a2ui/system-prompt.ts | 2 +- .../organisms/Chart/Chart.stories.tsx | 2 +- .../stories/Introduction/Getting-started.mdx | 4 +- .../stories/Introduction/How-to-use-theme.mdx | 8 +- .../Introduction/Theme-tokens-usage.mdx | 6 +- .../Introduction/Theming-and-tokens.mdx | 42 +- libs/ui/src/stories/Introduction/Welcome.mdx | 4 + .../AI-Experience.mdx | 4 +- .../Prompt-Engineering-AI-Integration.mdx | 4 +- libs/ui/src/stories/chart/01-Introduction.mdx | 2 +- .../form-configurator/01-GettingStarted.mdx | 10 +- .../form-configurator/02-DataSchema.mdx | 6 +- .../stories/form-configurator/03-Controls.mdx | 8 +- .../stories/form-configurator/04-UISchema.mdx | 6 +- .../stories/form-configurator/05-Logger.mdx | 4 +- .../form-configurator/06-CustomControls.mdx | 6 +- libs/web-components/.storybook/main.ts | 15 + libs/web-components/.storybook/preview.ts | 15 + libs/web-components/.storybook/vite.config.ts | 3 + libs/web-components/README.md | 6 + libs/web-components/project.json | 3 + .../stories/Overview.stories.ts | 26 + libs/web-components/tsconfig.storybook.json | 11 + nginx.conf | 8 + package-lock.json | 40 ++ package.json | 8 +- 48 files changed, 1325 insertions(+), 117 deletions(-) create mode 100644 .dockerignore create mode 100644 bin/storybook/check-links.mjs create mode 100644 bin/storybook/legacy-react-ids.json create mode 100644 bin/storybook/link-validation.mjs create mode 100644 bin/storybook/link-validation.test.mjs create mode 100644 bin/storybook/package-build.mjs create mode 100644 bin/storybook/routes.d.mts create mode 100644 bin/storybook/routes.mjs create mode 100644 bin/storybook/routes.test.mjs create mode 100644 bin/storybook/smoke-test.mjs create mode 100644 libs/web-components/.storybook/main.ts create mode 100644 libs/web-components/.storybook/preview.ts create mode 100644 libs/web-components/.storybook/vite.config.ts create mode 100644 libs/web-components/stories/Overview.stories.ts create mode 100644 libs/web-components/tsconfig.storybook.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..839254a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +.nx +node_modules +**/node_modules +dist +tmp +**/storybook-static +**/coverage +**/.vitest-artifacts +**/.vitest-attachments diff --git a/.github/workflows/pr-head.yaml b/.github/workflows/pr-head.yaml index 745494c..b485d47 100644 --- a/.github/workflows/pr-head.yaml +++ b/.github/workflows/pr-head.yaml @@ -37,6 +37,11 @@ jobs: - name: Build Storybook run: npm run build-storybook + - name: Verify combined Storybook in Chromium + run: | + npx playwright install --with-deps chromium + node bin/storybook/smoke-test.mjs + - name: Build Form Configurator run: npm run build:form-configurator @@ -62,6 +67,7 @@ jobs: npx nx build ui cp libs/ui/package.json dist/libs/ui/package.json node libs/ui/scripts/postbuild-types.mjs + node bin/storybook/check-links.mjs --package - name: Upload dist/libs/ui artifact uses: actions/upload-artifact@v7 diff --git a/.gitignore b/.gitignore index c35a2fe..10dbba2 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ libs/ui/coverage/* # storybook storybook-static/* libs/ui/storybook-static/* +libs/web-components/storybook-static/* *storybook.log vite.config.*.timestamp* diff --git a/CLAUDE.md b/CLAUDE.md index 74c0398..5e1b053 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,7 +18,7 @@ Node >= 22.17.0, npm workspaces. Run `npm install` after cloning. ```bash # Development -npm run storybook # Storybook dev server at http://localhost:6006 +npm run storybook # Shared React + Web Components Storybook at http://localhost:6006 npm test # Unit tests for gd-design-library (vitest, jsdom) npm run test:ai # AI integration tests in src/ai/__tests__/ npm run test:form-configurator # Unit tests for both form-configurator packages (jest) @@ -39,6 +39,7 @@ npm run storybook:visual # Run visual regression tests npm run build:ui # Full gd-design-library build: readme-stats + ai-docs + type-check + nx build + postbuild-types npm run build:form-configurator npm run build-storybook +npm run storybook:check-links # Built story indexes + source/shipped AI link validation # Verification (run on built dist — requires npm run build:ui first) npm run verify:ui:full # All 10 verification phases + Verdaccio smoke test @@ -67,6 +68,8 @@ Path aliases defined in `libs/ui/vitest.alias.ts` and `libs/ui/tsconfig.json`: ` **Build output** — Vite produces both ESM (`.js`, `preserveModules`) and CJS (`.cjs`) to `dist/libs/ui`. Post-build step (`scripts/postbuild-types.mjs`) patches `.d.ts` exports. React and `@emotion/*` are externalized. `bin/export-theme.js` serializes `defaultTheme` to JSON from the built dist; it runs automatically as part of `npm run storybook` and `npm run build-storybook`. +**Shared Storybook** — `npm run storybook` manages the React host (6006) and native Web Components preview (6007) together. Open only the host. The React catalog receives a central `React/` title prefix; do not duplicate that prefix in stories or MDX. The combined static artifact is `libs/ui/storybook-static`, with the child under `web-components/`. Use generated story IDs for links and run `storybook:check-links` after the combined build. Package-only validation uses `node bin/storybook/check-links.mjs --package` and indexes source stories without building previews. Do not regenerate the frozen legacy route inventory to conceal missing entries. Phase 1 contains only the native overview; component coverage and harness cleanup are later phases. + **Package subpaths** — the published package exposes four explicit subpaths (wildcards are intentionally removed to prevent internal path coupling): | Subpath | Contents | diff --git a/Dockerfile.storybook b/Dockerfile.storybook index 7b99bc0..eb26d28 100644 --- a/Dockerfile.storybook +++ b/Dockerfile.storybook @@ -3,13 +3,16 @@ WORKDIR /app COPY package.json package-lock.json nx.json tsconfig.base.json .npmrc eslint.config.mjs ./ COPY libs/ui/ libs/ui/ -COPY bin/export-theme.js bin/export-theme.js +COPY libs/design-core/ libs/design-core/ +COPY libs/web-components/ libs/web-components/ +COPY libs/form-configurator/ libs/form-configurator/ +COPY libs/react-native/package.json libs/react-native/package.json +COPY bin/ bin/ +COPY README.md CLAUDE.md ./ RUN npm ci -RUN npx nx build ui \ - && node ./bin/export-theme.js \ - && npx nx run ui:build-storybook +RUN npm run build-storybook FROM nginx:alpine AS runner WORKDIR /usr/share/nginx/html diff --git a/README.md b/README.md index f3f08f3..d8ad8dd 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ export default function App() { } ``` -See the [Storybook](https://github.com/griddynamics/gd-design-system#storybook) for interactive component examples and full API documentation. +See the [React catalog in Storybook](https://storybook.cto-rnd-system-design.griddynamics.net/?path=/docs/react-introduction-welcome--docs) for interactive component examples and full API documentation. ## Setup @@ -52,7 +52,7 @@ npm install ## Development ```bash -npm run storybook # Storybook at http://localhost:6006 +npm run storybook # Shared React + Web Components Storybook at http://localhost:6006 npm test # gd-design-library unit tests npm run test:form-configurator # form-configurator tests npm run type-check # TypeScript check @@ -64,9 +64,35 @@ npm run lint # ESLint ```bash npm run build:ui # Build gd-design-library (ESM + CJS + types) npm run build:form-configurator # Build both form-configurator packages -npm run build-storybook # Build static Storybook +npm run build-storybook # Build and validate the complete static Storybook ``` +## Storybook + +One entry point contains two peer catalogs: **React** (the existing stories and documentation) +and **Web Components** (native custom-element stories). The Web Components catalog currently +contains the Phase 1 overview; Phase 2 adds stories for the six existing atoms and two molecules. + +`npm run storybook` manages both preview servers automatically; open `http://localhost:6006`. +Ports 6006 and 6007 must be free. There is no separate Web Components startup command. +`npm run build-storybook` produces one deployable directory at `libs/ui/storybook-static`, +including the composed preview under `web-components/`. + +```bash +npm run storybook:check-links # Validate built Storybooks and shipped AI documentation links +npm run storybook:test # Existing React interaction tests (native coverage follows in Phase 2) +npm run storybook:visual # Existing React visual tests +``` + +React titles receive their `React/` prefix in `.storybook/main.ts`; do not add it again in +individual story/MDX titles. Use the IDs in the generated `index.json` for links. Old published +React manager/iframe links redirect to their new IDs while retaining args, globals, and anchors. +`bin/storybook/legacy-react-ids.json` is the frozen pre-migration inventory, not a generated +list to refresh when a story disappears. Link checks cover source documentation and the +`llms.txt`, AI Markdown/JSON, and AI JavaScript copied into the UI package. + +The Angular/Vue and other Web Components harnesses are unchanged in Phase 1. + ## Verification (gd-design-library) ```bash diff --git a/bin/storybook/check-links.mjs b/bin/storybook/check-links.mjs new file mode 100644 index 0000000..fd1549e --- /dev/null +++ b/bin/storybook/check-links.mjs @@ -0,0 +1,115 @@ +import { execFileSync } from 'node:child_process'; +import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { extname, join, relative, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { legacyReactIds } from './routes.mjs'; +import { validateLinks } from './link-validation.mjs'; + +const root = fileURLToPath(new URL('../../', import.meta.url)); +const built = process.argv.includes('--built'); +const checkPackage = built || process.argv.includes('--package'); +const readJson = (path) => JSON.parse(readFileSync(path, 'utf8')); +let indexes; +if (built) { + indexes = { + react: readJson(join(root, 'libs/ui/storybook-static/index.json')), + webComponents: readJson(join(root, 'libs/ui/storybook-static/web-components/index.json')), + }; +} else { + // Indexing does not build the UI or either preview. Package builds can check + // links without creating a UI -> Storybook -> UI build dependency cycle. + const temporary = mkdtempSync(join(tmpdir(), 'gridkit-storybook-index-')); + try { + indexes = {}; + for (const [catalog, project] of [ + ['react', 'ui'], + ['webComponents', 'web-components'], + ]) { + const output = join(temporary, `${catalog}.json`); + execFileSync( + process.execPath, + [ + join(root, 'node_modules/storybook/dist/bin/dispatcher.js'), + 'index', + '-c', + '.storybook', + '-o', + output, + '--disable-telemetry', + ], + { cwd: join(root, 'libs', project), stdio: 'inherit' } + ); + indexes[catalog] = readJson(output); + } + } finally { + rmSync(temporary, { recursive: true, force: true }); + } +} + +const errors = []; +for (const id of legacyReactIds) { + if (!indexes.react.entries[`react-${id}`]) errors.push(`Missing migrated React entry: ${id}`); +} +for (const entry of Object.values(indexes.react.entries)) { + if (!entry.title.startsWith('React/')) errors.push(`Entry is outside the React root: ${entry.id}`); +} +if (!indexes.webComponents.entries['introduction-overview--overview']) errors.push('Missing Web Components overview'); + +const excluded = new Set(['node_modules', '.git', 'dist', 'storybook-static', 'coverage', 'output', '.nx']); +const extensions = new Set([ + '.md', + '.mdx', + '.txt', + '.json', + '.ts', + '.tsx', + '.js', + '.jsx', + '.mjs', + '.cjs', + '.html', + '.yml', + '.yaml', +]); +function filesIn(path) { + if (!existsSync(path)) return []; + return readdirSync(path, { withFileTypes: true }).flatMap((entry) => { + if (entry.isSymbolicLink() || excluded.has(entry.name)) return []; + const file = join(path, entry.name); + return entry.isDirectory() ? filesIn(file) : extensions.has(extname(file)) ? [file] : []; + }); +} + +// The frozen migration registry and its tests intentionally contain old IDs. +const sourceFiles = ['README.md', 'CLAUDE.md'] + .map((file) => join(root, file)) + .filter(existsSync) + .concat( + ...['libs', '.github'].map((dir) => filesIn(join(root, dir))), + filesIn(join(root, 'bin')).filter((file) => !file.startsWith(join(root, 'bin', 'storybook') + sep)) + ); +const packageFiles = []; +if (checkPackage) { + for (const file of ['llms.txt', 'ai/README.md']) { + if (!existsSync(join(root, 'dist/libs/ui', file))) errors.push(`Missing package artifact: ${file}`); + } + packageFiles.push(...filesIn(join(root, 'dist/libs/ui/ai'))); + for (const file of readdirSync(join(root, 'dist/libs/ui'))) { + if (/\.(md|txt)$/.test(file)) packageFiles.push(join(root, 'dist/libs/ui', file)); + } +} +let checked = 0; +for (const file of [...sourceFiles, ...packageFiles]) { + const result = validateLinks(readFileSync(file, 'utf8'), indexes); + checked += result.checked; + errors.push(...result.errors.map(({ line, message }) => `${relative(root, file)}:${line}: ${message}`)); +} +if (errors.length) { + console.error(errors.join('\n')); + process.exitCode = 1; +} else { + console.log( + `Verified ${legacyReactIds.length} migrated entries and ${checked} Storybook links in source${checkPackage ? ' and built package artifacts' : ''}.` + ); +} diff --git a/bin/storybook/legacy-react-ids.json b/bin/storybook/legacy-react-ids.json new file mode 100644 index 0000000..1c52f15 --- /dev/null +++ b/bin/storybook/legacy-react-ids.json @@ -0,0 +1,584 @@ +[ + "atoms-avatar--default", + "atoms-avatar--default-tokens", + "atoms-avatar--docs", + "atoms-avatar--with-accessibility", + "atoms-avatar--with-badge", + "atoms-avatar--with-custom-background-color", + "atoms-avatar--with-custom-badge-color", + "atoms-avatar--with-different-size", + "atoms-avatar--with-icon", + "atoms-avatar--with-image-and-fallback", + "atoms-avatar--with-initials", + "atoms-badge--default", + "atoms-badge--default-tokens", + "atoms-badge--disabled", + "atoms-badge--docs", + "atoms-badge--sizes", + "atoms-badge--variants", + "atoms-badge--with-accessibility", + "atoms-badge--with-box-styles", + "atoms-badge--with-icons", + "atoms-box--bordered", + "atoms-box--centered-content", + "atoms-box--default", + "atoms-box--default-tokens", + "atoms-box--docs", + "atoms-box--examples", + "atoms-box--highlighted", + "atoms-box--horizontal-layout", + "atoms-box--vertical-layout", + "atoms-box--with-accessibility", + "atoms-box--with-shadow-hover", + "atoms-button--all-variants", + "atoms-button--button-states-using-class", + "atoms-button--custom-styled-button", + "atoms-button--default", + "atoms-button--default-tokens", + "atoms-button--disabled-button", + "atoms-button--docs", + "atoms-button--full-width-button", + "atoms-button--icon-only", + "atoms-button--is-loading", + "atoms-button--real-world-examples", + "atoms-button--rounded-button", + "atoms-button--with-accessibility", + "atoms-button--with-icons", + "atoms-checkbox--controlled", + "atoms-checkbox--default", + "atoms-checkbox--default-tokens", + "atoms-checkbox--disabled", + "atoms-checkbox--docs", + "atoms-checkbox--indeterminate", + "atoms-checkbox--sizes", + "atoms-icon--all-icons", + "atoms-icon--default", + "atoms-icon--default-tokens", + "atoms-icon--docs", + "atoms-icon--registering-custom-icons", + "atoms-icon--with-accessibility", + "atoms-icon--with-defined-size", + "atoms-image--clickable", + "atoms-image--default", + "atoms-image--default-tokens", + "atoms-image--docs", + "atoms-image--fallback-image", + "atoms-image--with-accessibility", + "atoms-image--with-as-prop", + "atoms-image--with-caption", + "atoms-image--with-caption-as-prop", + "atoms-image--with-placeholder", + "atoms-input--checkbox-with-label", + "atoms-input--custom-styles", + "atoms-input--default-tokens", + "atoms-input--default-with-tailwind", + "atoms-input--disabled", + "atoms-input--docs", + "atoms-input--error-with-label-and-helper-text", + "atoms-input--primary-default-with-label-and-helper-text", + "atoms-input--radio-controlled-with-label", + "atoms-input--radio-group-with-label", + "atoms-input--read-only", + "atoms-input--success-with-label-and-helper-text", + "atoms-input--warning-with-label-and-helper-text", + "atoms-input--with-accessibility", + "atoms-input--with-adornments", + "atoms-input--with-end-adornment-as-icon", + "atoms-input--with-start-adornment", + "atoms-input--wrapper-as-span", + "atoms-inputfile--custom-label", + "atoms-inputfile--default", + "atoms-inputfile--default-tokens", + "atoms-inputfile--disabled", + "atoms-inputfile--docs", + "atoms-inputfile--icon-label", + "atoms-inputfile--multiple", + "atoms-inputfile--with-accept", + "atoms-inputfile--with-accessibility", + "atoms-inputfile--with-custom-styles", + "atoms-label--custom-styles", + "atoms-label--default-tokens", + "atoms-label--default-with-tailwind", + "atoms-label--docs", + "atoms-label--label-default", + "atoms-label--label-with-child-icon", + "atoms-label--with-accessibility", + "atoms-link--custom-styles", + "atoms-link--default-tokens", + "atoms-link--disabled", + "atoms-link--docs", + "atoms-link--inherit-with-button-as-child", + "atoms-link--inherit-with-tailwind", + "atoms-link--inherit-with-typography-as-child", + "atoms-link--inverted", + "atoms-link--target-blank-visited", + "atoms-link--variants", + "atoms-link--with-accessibility", + "atoms-link--with-sizes", + "atoms-link--with-underline", + "atoms-loader--custom-loader-tailwind-class-bounce-animation", + "atoms-loader--default", + "atoms-loader--default-tokens", + "atoms-loader--docs", + "atoms-loader--inline-loader-button-variant", + "atoms-loader--loader-names", + "atoms-loader--loader-rounded", + "atoms-loader--loader-section-variant", + "atoms-loader--loader-sizes", + "atoms-loader--loader-variants-with-wrapper-view-as-header-tag", + "atoms-loader--section-loader-button-variant", + "atoms-loader--with-accessibility", + "atoms-select--color-error", + "atoms-select--color-primary", + "atoms-select--color-success", + "atoms-select--color-warning", + "atoms-select--custom-item-identifier-component", + "atoms-select--default", + "atoms-select--default-tokens", + "atoms-select--disabled", + "atoms-select--docs", + "atoms-select--empty-items", + "atoms-select--multiple-select", + "atoms-select--searchable", + "atoms-select--using-ref-component-with-custom-placeholder", + "atoms-select--with-accessibility", + "atoms-select--with-adornments-and-reset-option-story", + "atoms-select--with-custom-initiator-and-selected-output-value", + "atoms-select--with-file-input-and-custom-dropdown", + "atoms-separator--default", + "atoms-separator--default-tokens", + "atoms-separator--docs", + "atoms-separator--vertical", + "atoms-separator--vertical-with-label", + "atoms-separator--with-accessibility", + "atoms-separator--with-custom-color", + "atoms-separator--with-different-variants", + "atoms-separator--with-label", + "atoms-skeleton--circular", + "atoms-skeleton--composed-layout", + "atoms-skeleton--default", + "atoms-skeleton--default-tokens", + "atoms-skeleton--docs", + "atoms-skeleton--rectangular", + "atoms-skeleton--with-accessibility", + "atoms-skeleton--with-children", + "atoms-skeleton--with-custom-animation", + "atoms-skeleton--with-theme-color", + "atoms-slider--controlled", + "atoms-slider--custom-styles", + "atoms-slider--default", + "atoms-slider--default-tokens", + "atoms-slider--disabled", + "atoms-slider--docs", + "atoms-slider--with-accessibility", + "atoms-slider--with-initial-value", + "atoms-sliderdots--default", + "atoms-sliderdots--default-tokens", + "atoms-sliderdots--docs", + "atoms-sliderdots--interactive", + "atoms-sliderdots--many-dots", + "atoms-switch--checked", + "atoms-switch--controlled", + "atoms-switch--default", + "atoms-switch--default-tokens", + "atoms-switch--disabled", + "atoms-switch--docs", + "atoms-switch--label-left", + "atoms-switch--uncontrolled", + "atoms-switch--with-accessibility", + "atoms-switch--with-loading", + "atoms-textarea--auto-focus", + "atoms-textarea--color-error", + "atoms-textarea--color-primary", + "atoms-textarea--color-success", + "atoms-textarea--color-warning", + "atoms-textarea--default", + "atoms-textarea--default-tokens", + "atoms-textarea--disabled", + "atoms-textarea--docs", + "atoms-textarea--dynamic-height", + "atoms-textarea--max-characters", + "atoms-textarea--read-only", + "atoms-textarea--resizable", + "atoms-textarea--variant-inline", + "atoms-textarea--with-accessibility", + "atoms-textarea--with-char-limit", + "atoms-textarea--with-controlled-state-erase-on-enter-click", + "atoms-toggle--default", + "atoms-toggle--default-tokens", + "atoms-toggle--disabled", + "atoms-toggle--docs", + "atoms-toggle--with-accessibility", + "atoms-toggle--with-custom-render", + "atoms-truncate--default", + "atoms-truncate--docs", + "atoms-truncate--line-truncation", + "atoms-truncate--ref-api-with-tooltip-on-overflow", + "atoms-truncate--with-accessibility", + "atoms-truncate--with-custom-styling", + "atoms-typography--as-custom-component", + "atoms-typography--as-custom-html-tag", + "atoms-typography--body", + "atoms-typography--combined-style-variant", + "atoms-typography--default", + "atoms-typography--default-tokens", + "atoms-typography--disclaimers", + "atoms-typography--display", + "atoms-typography--docs", + "atoms-typography--heading", + "atoms-typography--with-accessibility", + "atoms-wrapper--custom-tag-wrapper", + "atoms-wrapper--default-tokens", + "atoms-wrapper--docs", + "atoms-wrapper--full-page-wrapper", + "atoms-wrapper--inline-wrapper", + "atoms-wrapper--interactive-example", + "atoms-wrapper--section-wrapper", + "atoms-wrapper--with-accessibility", + "domain-solutions-dynamic-builder-actions--docs", + "domain-solutions-dynamic-builder-architecture--docs", + "domain-solutions-dynamic-builder-form-builder--docs", + "domain-solutions-dynamic-builder-how-to-use--docs", + "domain-solutions-dynamic-builder-overview--docs", + "domain-solutions-dynamic-builder-providers--docs", + "domain-solutions-dynamic-builder-validation--docs", + "domain-solutions-e-commerce--docs", + "form-configurator-controls--docs", + "form-configurator-custom-controls--docs", + "form-configurator-data-schema--docs", + "form-configurator-getting-started--docs", + "form-configurator-logger--docs", + "form-configurator-ui-schema-layouts--docs", + "introduction-ai-integration-a2ui-mode--docs", + "introduction-ai-integration-actions--docs", + "introduction-ai-integration-code-mode--docs", + "introduction-ai-integration-custom-components--docs", + "introduction-ai-integration-how-to-use--docs", + "introduction-ai-integration-overview--docs", + "introduction-ai-integration-renderer--docs", + "introduction-ai-integration-schema-and-validation--docs", + "introduction-atomic-design--docs", + "introduction-charts--docs", + "introduction-feedback-contribution--docs", + "introduction-getting-started--docs", + "introduction-logging-and-debugging--docs", + "introduction-theme-tokens-usage--docs", + "introduction-welcome--docs", + "layout-structure-chatcontainer--default", + "layout-structure-chatcontainer--default-tokens", + "layout-structure-chatcontainer--docs", + "layout-structure-chatcontainer--interactive-demo", + "layout-structure-chatcontainer--minimal-layout-with-ref-control", + "layout-structure-column--default", + "layout-structure-column--default-tokens", + "layout-structure-column--docs", + "layout-structure-flexcontainer--default", + "layout-structure-flexcontainer--default-tokens", + "layout-structure-flexcontainer--docs", + "layout-structure-flexcontainer--justify-start", + "layout-structure-portal--default", + "layout-structure-portal--docs", + "layout-structure-portal--rendering-in-target", + "layout-structure-portal--with-wrapper", + "layout-structure-portal--with-wrapper-as-tag-main", + "layout-structure-row--default", + "layout-structure-row--default-tokens", + "layout-structure-row--docs", + "layout-structure-scroll--auto-hide-scrollbars", + "layout-structure-scroll--auto-hide-vertical-only", + "layout-structure-scroll--default-tokens", + "layout-structure-scroll--docs", + "layout-structure-scroll--horizontal-scroll-only", + "layout-structure-scroll--interactive-example", + "layout-structure-scroll--vertical-scroll-only", + "molecules-accordion--allow-multiple-expand-with-icon", + "molecules-accordion--as-inline", + "molecules-accordion--controlled", + "molecules-accordion--default", + "molecules-accordion--default-expanded", + "molecules-accordion--default-tokens", + "molecules-accordion--docs", + "molecules-accordion--with-box-styles", + "molecules-accordion--with-on-change", + "molecules-accordion--without-separator", + "molecules-attachment-file--default", + "molecules-attachment-file--default-tokens", + "molecules-attachment-file--disabled", + "molecules-attachment-file--docs", + "molecules-attachment-file--error", + "molecules-attachment-file--loading", + "molecules-attachment-file--long-file-name", + "molecules-attachment-file--long-file-type", + "molecules-attachment-file--removable", + "molecules-attachment-file--without-meta", + "molecules-attachment-file--without-remove", + "molecules-breadcrumbs--default", + "molecules-breadcrumbs--default-tokens", + "molecules-breadcrumbs--docs", + "molecules-breadcrumbs--with-border", + "molecules-breadcrumbs--with-end-item-link-icon-and-after-last-item", + "molecules-breadcrumbs--with-icon-separator", + "molecules-breadcrumbs--with-start-item-link-icon", + "molecules-breadcrumbs--with-string-separator-and-after-last-item", + "molecules-counter--adjusted-max-value-5", + "molecules-counter--adjusted-min-2-max-value-10", + "molecules-counter--adjusted-min-value-3", + "molecules-counter--default", + "molecules-counter--default-tokens", + "molecules-counter--disabled", + "molecules-counter--docs", + "molecules-counter--with-external-counter-change-handler", + "molecules-dropdown--default", + "molecules-dropdown--default-tokens", + "molecules-dropdown--docs", + "molecules-dropdown--with-accessibility", + "molecules-dropdown--with-checkbox-items", + "molecules-dropdownitem--active", + "molecules-dropdownitem--default", + "molecules-dropdownitem--default-tokens", + "molecules-dropdownitem--docs", + "molecules-dropdownitem--interactive-with-defined-on-select", + "molecules-form--default-tokens", + "molecules-form--docs", + "molecules-form--with-accessibility", + "molecules-form--with-custom-error-handling-on-change-submit", + "molecules-form--with-custom-styles", + "molecules-form--with-on-change", + "molecules-form--with-submit", + "molecules-inline-notification--default-inline-notification", + "molecules-inline-notification--default-tokens", + "molecules-inline-notification--docs", + "molecules-inline-notification--error-inline-notification", + "molecules-inline-notification--info-inline-notification", + "molecules-inline-notification--success-inline-notification", + "molecules-inline-notification--warning-inline-notification", + "molecules-inline-notification--with-custom-content", + "molecules-list--default-tokens", + "molecules-list--docs", + "molecules-list--list-variants", + "molecules-list--size-variants", + "molecules-menu--close-on-select-false", + "molecules-menu--default", + "molecules-menu--default-tokens", + "molecules-menu--docs", + "molecules-menu--placement", + "molecules-menu--with-edit-and-delete-modals", + "molecules-menu--with-height-constraints", + "molecules-menu--with-offset", + "molecules-menu--with-positioning-options", + "molecules-menu--with-ref-control", + "molecules-price--custom-class", + "molecules-price--default", + "molecules-price--default-tokens", + "molecules-price--docs", + "molecules-price--european-convention", + "molecules-price--european-large-price", + "molecules-price--free-price", + "molecules-price--large-price", + "molecules-price--size-variants", + "molecules-price--with-decimals", + "molecules-price--with-old-price", + "molecules-progressbar--custom-styling", + "molecules-progressbar--default", + "molecules-progressbar--default-tokens", + "molecules-progressbar--docs", + "molecules-progressbar--full-width", + "molecules-progressbar--indeterminate", + "molecules-progressbar--interactive", + "molecules-progressbar--with-percentage", + "molecules-radiogroup--default", + "molecules-radiogroup--default-tokens", + "molecules-radiogroup--docs", + "molecules-radiogroup--with-colors", + "molecules-radiogroup--with-custom-items", + "molecules-radiogroup--with-grid-layout", + "molecules-radiogroup--with-horizontal-layout", + "molecules-radiogroup--with-images-and-tooltip", + "molecules-radiogroup--with-size-variants", + "molecules-radiogroup--with-vertical-layout", + "molecules-rating--default-tokens", + "molecules-rating--docs", + "molecules-rating--max-value-custom-10", + "molecules-rating--pre-selected-value", + "molecules-rating--read-only", + "molecules-rating--with-custom-on-change-control", + "molecules-snackbar--default", + "molecules-snackbar--default-tokens", + "molecules-snackbar--docs", + "molecules-snackbar--error", + "molecules-snackbar--example", + "molecules-snackbar--no-auto-dismiss", + "molecules-snackbar--success", + "molecules-snackbar--warning", + "molecules-snackbar--with-action-and-colored", + "molecules-stepper--default-tokens", + "molecules-stepper--docs", + "molecules-stepper--stepper-simple-scenario", + "molecules-stepper--with-active-error-step", + "molecules-stepper--with-custom-icons-view-step", + "molecules-stepper--with-icons-view-active-error-step", + "molecules-stepper--with-icons-view-completed-error-step", + "molecules-table--default", + "molecules-table--default-tokens", + "molecules-table--docs", + "molecules-table--empty-state", + "molecules-table--with-column-widths", + "molecules-table--with-custom-cell-renderers", + "molecules-table--with-custom-header", + "molecules-table--with-expandable-rows", + "molecules-table--with-loading-state", + "molecules-table--with-pagination", + "molecules-table--with-scroll-methods", + "molecules-table--with-sticky-footer", + "molecules-table--with-sticky-header", + "molecules-table--with-sticky-header-and-footer", + "molecules-table--with-sticky-header-and-pagination", + "molecules-table--with-virtualization", + "molecules-tabs--default", + "molecules-tabs--default-tokens", + "molecules-tabs--docs", + "molecules-tabs--with-accessibility", + "molecules-tabs--with-disabled-first-tab", + "molecules-tabs--with-notices-tab", + "molecules-tooltip--custom-delay", + "molecules-tooltip--custom-gap", + "molecules-tooltip--custom-position", + "molecules-tooltip--default", + "molecules-tooltip--default-tokens", + "molecules-tooltip--docs", + "molecules-tooltip--multiple-tooltips", + "molecules-tooltip--with-accessibility", + "organisms-card--default-tokens", + "organisms-card--docs", + "organisms-card--my-card-with-fallback-image", + "organisms-card--with-build-in-components", + "organisms-card--with-horizontal-card", + "organisms-card--with-vertical-card", + "organisms-card--with-wishlist-and-badge", + "organisms-carousel--default", + "organisms-carousel--default-tokens", + "organisms-carousel--docs", + "organisms-carousel--layout-horizontal", + "organisms-carousel--layout-vertical", + "organisms-carousel--responsive-behavior", + "organisms-carousel--with-custom-content", + "organisms-carousel--with-navigation-dots", + "organisms-carousel--without-navigation-arrows", + "organisms-chart--area-chart", + "organisms-chart--axis-config", + "organisms-chart--bar-chart", + "organisms-chart--curve-types", + "organisms-chart--custom-colors", + "organisms-chart--dashboard", + "organisms-chart--default", + "organisms-chart--default-tokens", + "organisms-chart--docs", + "organisms-chart--donut-chart", + "organisms-chart--empty-state", + "organisms-chart--error-state", + "organisms-chart--loading-state", + "organisms-chart--multi-series-with-legend", + "organisms-chart--per-series-colors", + "organisms-chart--pie-chart", + "organisms-chart--stacked-bar-chart", + "organisms-chatbubble--answer-with-actions", + "organisms-chatbubble--answer-with-status", + "organisms-chatbubble--default", + "organisms-chatbubble--default-tokens", + "organisms-chatbubble--docs", + "organisms-chatbubble--with-image-gallery", + "organisms-chatbubble--with-link-preview", + "organisms-chatbubble--with-sizes", + "organisms-contentcarousel--centered-alignment", + "organisms-contentcarousel--custom-scroll-step", + "organisms-contentcarousel--default", + "organisms-contentcarousel--default-tokens", + "organisms-contentcarousel--docs", + "organisms-contentcarousel--multiple-visible-items", + "organisms-draganddropfiles--default", + "organisms-draganddropfiles--default-tokens", + "organisms-draganddropfiles--docs", + "organisms-draganddropfiles--drag-and-drop-lifecycle", + "organisms-draganddropfiles--with-button-styles", + "organisms-draganddropfiles--with-custom-styles", + "organisms-header--complete-header", + "organisms-header--dark-header", + "organisms-header--default-tokens", + "organisms-header--docs", + "organisms-header--interactive-mobile-menu", + "organisms-header--minimal", + "organisms-header--responsive-header", + "organisms-header--with-actions", + "organisms-header--with-custom-children", + "organisms-header--with-menu", + "organisms-header--with-search", + "organisms-imagepreview--default", + "organisms-imagepreview--default-tokens", + "organisms-imagepreview--docs", + "organisms-imagepreview--no-arrows", + "organisms-imagepreview--single-image", + "organisms-imagepreview--thumbnails-left", + "organisms-imagepreview--with-counter", + "organisms-inputarea--attachment-interactive", + "organisms-inputarea--custom-rows", + "organisms-inputarea--custom-send-icon", + "organisms-inputarea--default", + "organisms-inputarea--default-tokens", + "organisms-inputarea--disabled", + "organisms-inputarea--docs", + "organisms-inputarea--recording-states", + "organisms-inputarea--voice-recording-interactive", + "organisms-inputarea--with-character-count", + "organisms-modal--custom-view", + "organisms-modal--default", + "organisms-modal--default-tokens", + "organisms-modal--docs", + "organisms-modal--no-click-outside-close", + "organisms-modal--no-escape-close-content-with-scroll", + "organisms-modal--with-accessibility", + "organisms-search--default", + "organisms-search--default-tokens", + "organisms-search--docs", + "organisms-search--with-items", + "organisms-search--with-loading", + "organisms-search--with-no-results", + "organisms-searchmodal--default", + "organisms-searchmodal--default-tokens", + "organisms-searchmodal--docs", + "organisms-searchmodal--with-search-modal", + "organisms-searchmodal--with-sections", + "organisms-sidebar--collapsed", + "organisms-sidebar--default", + "organisms-sidebar--default-tokens", + "organisms-sidebar--docs", + "organisms-sidebar--with-header-and-footer", + "organisms-sidebar--with-sub-items", + "patterns-best-practices-ai-experience--docs", + "patterns-best-practices-cross-device-ux-notes--docs", + "patterns-best-practices-form-validation-patterns--docs", + "patterns-best-practices-layout-composition-techniques--docs", + "patterns-best-practices-prompt-engineering-ai-integration--docs", + "patterns-best-practices-responsive-design-guide--docs", + "patterns-best-practices-state-handling-strategies--docs", + "patterns-best-practices-ux-ui-design-patterns-and-best-practices--docs", + "support-questions-faq--docs", + "templates-draganddrop--default", + "templates-draganddrop--default-tokens", + "templates-draganddrop--disabled", + "templates-draganddrop--docs", + "templates-draganddrop--error", + "templates-draganddrop--loading", + "templates-draganddrop--usage-as-wrapper", + "theme-tokens-animations--docs", + "theme-tokens-breakpoints--docs", + "theme-tokens-colors--docs", + "theme-tokens-design-tokens-overview--docs", + "theme-tokens-dynamic-theme-switching--docs", + "theme-tokens-fonts--docs", + "theme-tokens-radius--docs", + "theme-tokens-shadows--docs", + "theme-tokens-spacing--docs", + "theme-tokens-theme--docs", + "theme-tokens-values--docs", + "theme-tokens-zindex--docs" +] diff --git a/bin/storybook/link-validation.mjs b/bin/storybook/link-validation.mjs new file mode 100644 index 0000000..a06ac4d --- /dev/null +++ b/bin/storybook/link-validation.mjs @@ -0,0 +1,42 @@ +import { storybookOrigin } from './routes.mjs'; + +// Only Storybook routes, not example application URLs such as /docs/api. +const links = + /https?:\/\/[^\s<>"'`)\]}]+|(?:\.\/)?\?path=\/(?:docs|story)\/[^\s<>"'`)\]}]+|\/(?:docs|story)\/[a-z0-9-]+--[a-z0-9-]+|(?:\.\/|\/)?iframe\.html\?[^\s<>"'`)\]}]+/g; + +export function validateLinks(text, indexes) { + const errors = []; + let checked = 0; + for (const match of text.matchAll(links)) { + const href = match[0].replaceAll('&', '&'); + let url; + try { + url = new URL(href, storybookOrigin); + } catch { + // Other tools' URL templates (e.g. localhost:${PORT}) are not links. + if (href.startsWith(storybookOrigin)) + errors.push({ line: text.slice(0, match.index).split('\n').length, href, message: 'Malformed Storybook URL' }); + continue; + } + if (url.origin !== storybookOrigin && !/^http:\/\/localhost:600[67]$/.test(url.origin)) continue; + const path = url.searchParams.get('path') || url.pathname; + const route = path.match(/^\/(docs|story)\/([^#?]+)/); + let id = route?.[2] || url.searchParams.get('id'); + if (!id) continue; + checked++; + const line = text.slice(0, match.index).split('\n').length; + let catalog = url.pathname.startsWith('/web-components/') || url.port === '6007' ? 'webComponents' : 'react'; + if (id.startsWith('web-components_')) { + catalog = 'webComponents'; + id = id.slice('web-components_'.length); + } + const entry = indexes[catalog].entries[id]; + if (!entry) errors.push({ line, href, message: `Unknown or stale ${catalog} story ID: ${id}` }); + else if (route && entry.type !== route[1]) + errors.push({ line, href, message: `Expected ${entry.type} route for ${id}` }); + if (route && !url.searchParams.has('path')) { + errors.push({ line, href, message: 'Use a manager ?path= URL, not a bare /docs/ or /story/ URL' }); + } + } + return { checked, errors }; +} diff --git a/bin/storybook/link-validation.test.mjs b/bin/storybook/link-validation.test.mjs new file mode 100644 index 0000000..ee73297 --- /dev/null +++ b/bin/storybook/link-validation.test.mjs @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { validateLinks } from './link-validation.mjs'; + +const indexes = { + react: { entries: { 'react-atoms-button--docs': { type: 'docs' } } }, + webComponents: { entries: { 'introduction-overview--overview': { type: 'story' } } }, +}; + +test('validates canonical, composed and child deep links', () => { + const result = validateLinks( + ` + [Button](?path=/docs/react-atoms-button--docs) + https://storybook.cto-rnd-system-design.griddynamics.net/web-components/?path=/story/introduction-overview--overview + ?path=/story/web-components_introduction-overview--overview + iframe.html?id=react-atoms-button--docs&viewMode=docs + `, + indexes + ); + assert.equal(result.checked, 4); + assert.deepEqual(result.errors, []); +}); + +test('catches stale, nonexistent, incorrectly typed and bare routes', () => { + for (const href of [ + '?path=/docs/atoms-button--docs', + '?path=/docs/react-nonexistent--docs', + '?path=/story/react-atoms-button--docs', + '/docs/react-atoms-button--docs', + ]) + assert.ok(validateLinks(href, indexes).errors.length > 0, href); +}); + +test('ignores example application links, external Storybooks, and portal roots', () => { + assert.deepEqual( + validateLinks( + ` + API + https://example.com/?path=/docs/external--docs + https://storybook.cto-rnd-system-design.griddynamics.net/ + `, + indexes + ), + { checked: 0, errors: [] } + ); +}); diff --git a/bin/storybook/package-build.mjs b/bin/storybook/package-build.mjs new file mode 100644 index 0000000..b5589de --- /dev/null +++ b/bin/storybook/package-build.mjs @@ -0,0 +1,10 @@ +import { cp, access } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = fileURLToPath(new URL('../../', import.meta.url)); +const host = resolve(root, 'libs/ui/storybook-static'); +const child = resolve(root, 'libs/web-components/storybook-static'); +await Promise.all([access(`${host}/index.json`), access(`${child}/index.json`)]); +await cp(child, `${host}/web-components`, { recursive: true }); +console.log(`Combined Storybook artifact: ${host}`); diff --git a/bin/storybook/routes.d.mts b/bin/storybook/routes.d.mts new file mode 100644 index 0000000..fde6e2b --- /dev/null +++ b/bin/storybook/routes.d.mts @@ -0,0 +1,4 @@ +export const legacyReactIds: string[]; +export const storybookOrigin: string; +export function legacyRedirect(href: string, ids: string[]): string | null; +export function legacyRedirectHead(): string; diff --git a/bin/storybook/routes.mjs b/bin/storybook/routes.mjs new file mode 100644 index 0000000..b5c0176 --- /dev/null +++ b/bin/storybook/routes.mjs @@ -0,0 +1,40 @@ +import { readFileSync } from 'node:fs'; + +// Frozen from the pre-migration Storybook index. Do not regenerate from the new +// index: that would hide accidentally removed stories and break old bookmarks. +export const legacyReactIds = JSON.parse(readFileSync(new URL('./legacy-react-ids.json', import.meta.url), 'utf8')); +export const storybookOrigin = 'https://storybook.cto-rnd-system-design.griddynamics.net'; + +/** Resolve legacy manager, iframe and bare /docs links without dropping URL state. */ +export function legacyRedirect(href, ids) { + const url = new URL(href); + const known = new Set(ids); + const upgrade = (id) => (known.has(id) ? `react-${id}` : id); + const original = url.href; + const path = url.searchParams.get('path'); + if (path) { + url.searchParams.set( + 'path', + path.replace(/^(\/(?:docs|story)\/)([^#?]+)/, (_, prefix, id) => prefix + upgrade(id)) + ); + } else { + const bare = url.pathname.match(/^\/(docs|story)\/([^/]+)$/); + if (bare && (known.has(bare[2]) || bare[2].startsWith('react-'))) { + url.pathname = '/'; + url.searchParams.set('path', `/${bare[1]}/${upgrade(bare[2])}`); + } + } + const id = url.searchParams.get('id'); + if (id && known.has(id)) url.searchParams.set('id', upgrade(id)); + // URLSearchParams may normalize encoding even when no route changed. + const before = new URL(original); + return url.pathname !== before.pathname || + url.searchParams.get('path') !== before.searchParams.get('path') || + url.searchParams.get('id') !== before.searchParams.get('id') + ? url.href + : null; +} + +export function legacyRedirectHead() { + return ``; +} diff --git a/bin/storybook/routes.test.mjs b/bin/storybook/routes.test.mjs new file mode 100644 index 0000000..d7d8feb --- /dev/null +++ b/bin/storybook/routes.test.mjs @@ -0,0 +1,57 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { legacyReactIds, legacyRedirect, legacyRedirectHead } from './routes.mjs'; + +const origin = 'https://storybook.example'; + +test('every legacy story and docs route redirects to its React ID', () => { + for (const id of legacyReactIds) { + const kind = id.endsWith('--docs') ? 'docs' : 'story'; + const result = new URL( + legacyRedirect(`${origin}/?path=/${kind}/${id}&args=size:sm&globals=theme:dark#example`, legacyReactIds) + ); + assert.equal(result.searchParams.get('path'), `/${kind}/react-${id}`); + assert.equal(result.searchParams.get('args'), 'size:sm'); + assert.equal(result.searchParams.get('globals'), 'theme:dark'); + assert.equal(result.hash, '#example'); + } +}); + +test('iframe links retain their view mode and args', () => { + const result = new URL( + legacyRedirect(`${origin}/iframe.html?id=atoms-button--docs&viewMode=docs&args=size:sm`, legacyReactIds) + ); + assert.equal(result.pathname, '/iframe.html'); + assert.equal(result.searchParams.get('id'), 'react-atoms-button--docs'); + assert.equal(result.searchParams.get('viewMode'), 'docs'); + assert.equal(result.searchParams.get('args'), 'size:sm'); +}); + +test('bare docs links become manager routes', () => { + const result = new URL(legacyRedirect(`${origin}/docs/introduction-welcome--docs#intro`, legacyReactIds)); + assert.equal(result.pathname, '/'); + assert.equal(result.searchParams.get('path'), '/docs/react-introduction-welcome--docs'); + assert.equal(result.hash, '#intro'); +}); + +test('canonical, composed, unrelated and unknown routes are not redirected', () => { + for (const path of [ + '/?path=/docs/react-introduction-welcome--docs', + '/?path=/story/web-components_introduction-overview--overview', + '/web-components/?path=/story/introduction-overview--overview', + '/?path=/docs/unknown--docs', + '/docs/api', + '/', + ]) + assert.equal(legacyRedirect(origin + path, legacyReactIds), null); +}); + +test('early head script is self-contained', () => { + const calls = []; + const script = legacyRedirectHead().replace(/^`; + return ``; } diff --git a/bin/storybook/routes.test.mjs b/bin/storybook/routes.test.mjs index d7d8feb..8213365 100644 --- a/bin/storybook/routes.test.mjs +++ b/bin/storybook/routes.test.mjs @@ -1,16 +1,15 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { legacyReactIds, legacyRedirect, legacyRedirectHead } from './routes.mjs'; +import { legacyReactIds, sharedDocIds, legacyRedirect, legacyRedirectHead } from './routes.mjs'; const origin = 'https://storybook.example'; -test('every legacy story and docs route redirects to its React ID', () => { +test('every legacy story and docs route resolves to its canonical root', () => { for (const id of legacyReactIds) { const kind = id.endsWith('--docs') ? 'docs' : 'story'; - const result = new URL( - legacyRedirect(`${origin}/?path=/${kind}/${id}&args=size:sm&globals=theme:dark#example`, legacyReactIds) - ); - assert.equal(result.searchParams.get('path'), `/${kind}/react-${id}`); + const href = `${origin}/?path=/${kind}/${id}&args=size:sm&globals=theme:dark#example`; + const result = new URL(legacyRedirect(href, legacyReactIds, sharedDocIds) || href); + assert.equal(result.searchParams.get('path'), `/${kind}/${sharedDocIds.includes(id) ? id : `react-${id}`}`); assert.equal(result.searchParams.get('args'), 'size:sm'); assert.equal(result.searchParams.get('globals'), 'theme:dark'); assert.equal(result.hash, '#example'); @@ -28,22 +27,53 @@ test('iframe links retain their view mode and args', () => { }); test('bare docs links become manager routes', () => { - const result = new URL(legacyRedirect(`${origin}/docs/introduction-welcome--docs#intro`, legacyReactIds)); + const result = new URL( + legacyRedirect(`${origin}/docs/introduction-welcome--docs#intro`, legacyReactIds, sharedDocIds) + ); assert.equal(result.pathname, '/'); - assert.equal(result.searchParams.get('path'), '/docs/react-introduction-welcome--docs'); + assert.equal(result.searchParams.get('path'), '/docs/introduction-welcome--docs'); assert.equal(result.hash, '#intro'); }); test('canonical, composed, unrelated and unknown routes are not redirected', () => { for (const path of [ - '/?path=/docs/react-introduction-welcome--docs', + '/?path=/docs/introduction-welcome--docs', + '/?path=/docs/react-introduction-getting-started--docs', '/?path=/story/web-components_introduction-overview--overview', '/web-components/?path=/story/introduction-overview--overview', '/?path=/docs/unknown--docs', '/docs/api', '/', ]) - assert.equal(legacyRedirect(origin + path, legacyReactIds), null); + assert.equal(legacyRedirect(origin + path, legacyReactIds, sharedDocIds), null); +}); + +test('all interim React shared-doc links redirect once, including iframe and bare routes', () => { + for (const id of sharedDocIds) { + for (const path of [`/?path=/docs/react-${id}`, `/iframe.html?id=react-${id}`, `/docs/react-${id}`]) { + const href = `${origin}${path}&args=size:sm#section`; + // Bare routes have no query marker yet. + const input = path.startsWith('/docs/') ? href.replace('&args=', '?args=') : href; + const result = new URL(legacyRedirect(input, legacyReactIds, sharedDocIds)); + assert.equal( + result.searchParams.get('id') || result.searchParams.get('path'), + path.startsWith('/iframe') ? id : `/docs/${id}` + ); + assert.equal(result.searchParams.get('args'), 'size:sm'); + assert.equal(result.hash, '#section'); + assert.equal(legacyRedirect(result.href, legacyReactIds, sharedDocIds), null); + } + } +}); + +test('early head script also redirects shared pages without external dependencies', () => { + const calls = []; + const script = legacyRedirectHead().replace(/^