diff --git a/apps/frontpage/package.json b/apps/frontpage/package.json index 1b333555..a2a25209 100644 --- a/apps/frontpage/package.json +++ b/apps/frontpage/package.json @@ -8,7 +8,7 @@ "start": "next start", "lint": "next lint", "typecheck": "tsc --noEmit", - "fetch-docs": "tsx --tsconfig tsconfig.json scripts/get-local-docs.ts", + "fetch-docs": "tsx --tsconfig tsconfig.json scripts/get-local-docs.ts && tsx --tsconfig tsconfig.json scripts/append-package-tags-in-snippets.ts", "generate-redirects": "tsx --tsconfig tsconfig.json scripts/generate-redirects.ts", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next", "storybook": "storybook dev -p 6006", diff --git a/apps/frontpage/scripts/append-package-tags-in-snippets.test.ts b/apps/frontpage/scripts/append-package-tags-in-snippets.test.ts new file mode 100644 index 00000000..d4289b9b --- /dev/null +++ b/apps/frontpage/scripts/append-package-tags-in-snippets.test.ts @@ -0,0 +1,278 @@ +import { describe, test, expect } from 'vitest'; + +import { updateFile, updateSnippet } from './append-package-tags-in-snippets'; + +describe('updateFile', () => { + test('matches block shell snippets and appropriate inline snippets', () => { + expect( + updateFile( + ` + +\`\`\`sh +npx storybook init +\`\`\` + + +\`\`\`shell +npx storybook init +\`\`\` + + +\`\`\`bash +npx storybook init +\`\`\` + + +\`\`\`sh +yarn add -D @storybook/testing-library +\`\`\` + + + \`\`\`sh + npx storybook init + \`\`\` + + +\`\`\`sh +# With comment +npx storybook init +\`\`\` + + +\`npx storybook init\` + + +\`\`\`tsx +npx storybook init +\`\`\` + + +\`@storybook/testing-library\` + `, + true + ) + ).toMatchInlineSnapshot(` + " + + \`\`\`sh + npx storybook@next init + \`\`\` + + + \`\`\`shell + npx storybook@next init + \`\`\` + + + \`\`\`bash + npx storybook@next init + \`\`\` + + + \`\`\`sh + yarn add -D @storybook/testing-library@next + \`\`\` + + + \`\`\`sh + npx storybook@next init + \`\`\` + + + \`\`\`sh + # With comment + npx storybook@next init + \`\`\` + + + \`npx storybook@next init\` + + + \`\`\`tsx + npx storybook init + \`\`\` + + + \`@storybook/testing-library\` + " + `); + }); +}); + +describe('updateSnippet', () => { + test('updates desired inline snippets', () => { + expect(updateSnippet('npx storybook automigrate')).toMatchInlineSnapshot( + `"npx storybook@latest automigrate"` + ); + expect(updateSnippet('npx storybook babelrc')).toMatchInlineSnapshot( + `"npx storybook@latest babelrc"` + ); + expect(updateSnippet('npx storybook extract')).toMatchInlineSnapshot( + `"npx storybook@latest extract"` + ); + expect(updateSnippet('npx storybook init')).toMatchInlineSnapshot( + `"npx storybook@latest init"` + ); + expect(updateSnippet('npx storybook migrate')).toMatchInlineSnapshot( + `"npx storybook@latest migrate"` + ); + expect(updateSnippet('npx storybook upgrade')).toMatchInlineSnapshot( + `"npx storybook@latest upgrade"` + ); + expect(updateSnippet('npx storybook add @storybook/addon-pkg')).toMatchInlineSnapshot( + `"npx storybook@latest add @storybook/addon-pkg"` + ); + }); + test('does not update undesired inline snippets', () => { + // Must use preRelease here to test non-effect (non-preRelease should not append tag) + expect(updateSnippet('@storybook/testing-library', false, true)).toMatchInlineSnapshot( + `"@storybook/testing-library"` + ); + expect(updateSnippet('storybook upgrade', false, false)).toMatchInlineSnapshot( + `"storybook upgrade"` + ); + expect(updateSnippet('storybook upgrade', false, true)).toMatchInlineSnapshot( + `"storybook upgrade"` + ); + }); + test('updates desired block snippets', () => { + expect(updateSnippet('npx storybook automigrate', true)).toMatchInlineSnapshot( + `"npx storybook@latest automigrate"` + ); + expect(updateSnippet('npx storybook babelrc', true)).toMatchInlineSnapshot( + `"npx storybook@latest babelrc"` + ); + expect(updateSnippet('npx storybook extract', true)).toMatchInlineSnapshot( + `"npx storybook@latest extract"` + ); + expect(updateSnippet('npx storybook init', true)).toMatchInlineSnapshot( + `"npx storybook@latest init"` + ); + expect(updateSnippet('npx storybook migrate', true)).toMatchInlineSnapshot( + `"npx storybook@latest migrate"` + ); + expect(updateSnippet('npx storybook remove', true)).toMatchInlineSnapshot( + `"npx storybook@latest remove"` + ); + expect(updateSnippet('npx storybook upgrade', true)).toMatchInlineSnapshot( + `"npx storybook@latest upgrade"` + ); + expect(updateSnippet('npx storybook add @storybook/addon-pkg', true)).toMatchInlineSnapshot( + `"npx storybook@latest add @storybook/addon-pkg"` + ); + // Must use preRelease here to test effect (non-preRelease should not append tag) + expect( + updateSnippet('yarn add -D @storybook/testing-library', true, true) + ).toMatchInlineSnapshot(`"yarn add -D @storybook/testing-library@next"`); + }); + test('does not update disallowed packages in block snippets', () => { + // Must use preRelease here to test non-effect (preRelease should not append tag) + expect( + updateSnippet('npx storybook add @storybook/addon-coverage', true, true) + ).toMatchInlineSnapshot(`"npx storybook@next add @storybook/addon-coverage"`); + expect( + updateSnippet('npx storybook add @storybook/addon-webpack5-compiler-babel', true, true) + ).toMatchInlineSnapshot(`"npx storybook@next add @storybook/addon-webpack5-compiler-babel"`); + expect( + updateSnippet('npx storybook add @storybook/addon-webpack5-compiler-swc', true, true) + ).toMatchInlineSnapshot(`"npx storybook@next add @storybook/addon-webpack5-compiler-swc"`); + }); + test('handles multiple matches', () => { + // Must use preRelease here to test effect (non-preRelease should not append tag) + expect( + updateSnippet( + 'yarn add -D @storybook/testing-library @storybook/jest @storybook/addon-interactions', + true, + true + ) + ).toMatchInlineSnapshot( + `"yarn add -D @storybook/testing-library@next @storybook/jest@next @storybook/addon-interactions@next"` + ); + // Must use preRelease here to test effect (non-preRelease should not append tag) + expect( + updateSnippet( + 'yarn remove -D @storybook/testing-library @storybook/jest @storybook/addon-interactions', + true, + true + ) + ).toMatchInlineSnapshot( + `"yarn remove -D @storybook/testing-library@next @storybook/jest@next @storybook/addon-interactions@next"` + ); + }); + test('appends the correct tag', () => { + expect(updateSnippet('npx storybook init')).toMatchInlineSnapshot( + `"npx storybook@latest init"` + ); + expect(updateSnippet('npx storybook init', false, true)).toMatchInlineSnapshot( + `"npx storybook@next init"` + ); + // Must use block here to test effect (inline should not append tag) + // When not prerelease, we do NOT append the tag to installed packages + expect(updateSnippet('yarn add -D @storybook/testing-library', true)).toMatchInlineSnapshot( + `"yarn add -D @storybook/testing-library"` + ); + // Must use block here to test effect (inline should not append tag) + expect( + updateSnippet('yarn add -D @storybook/testing-library', true, true) + ).toMatchInlineSnapshot(`"yarn add -D @storybook/testing-library@next"`); + }); + test('does not append tag to removed packages', () => { + // Must use block here to test effect (inline should not append tag) + expect( + updateSnippet('npx storybook remove @storybook/testing-library', true) + ).toMatchInlineSnapshot(`"npx storybook@latest remove @storybook/testing-library"`); + // Must use block here to test effect (inline should not append tag) + expect( + updateSnippet('npx storybook remove @storybook/testing-library', true, true) + ).toMatchInlineSnapshot(`"npx storybook@next remove @storybook/testing-library"`); + }); + test('removes existing tags', () => { + expect(updateSnippet('npx storybook@latest init', false, true)).toMatchInlineSnapshot( + `"npx storybook@next init"` + ); + expect(updateSnippet('npx storybook@next init')).toMatchInlineSnapshot( + `"npx storybook@latest init"` + ); + // Must use block here to test effect (inline should do nothing) + expect( + updateSnippet('npx storybook@latest remove @storybook/testing-library', true, true) + ).toMatchInlineSnapshot(`"npx storybook@next remove @storybook/testing-library"`); + // Must use block here to test effect (inline should do nothing) + expect( + updateSnippet('npx storybook@next remove @storybook/testing-library', true) + ).toMatchInlineSnapshot(`"npx storybook@latest remove @storybook/testing-library"`); + // Must use block here to test effect (inline should not append tag) + expect( + updateSnippet('yarn add -D @storybook/testing-library@latest', true, true) + ).toMatchInlineSnapshot(`"yarn add -D @storybook/testing-library@next"`); + // Must use block here to test effect (inline should not append tag) + // When not prerelease, we do NOT append the tag to installed packages + expect( + updateSnippet('yarn add -D @storybook/testing-library@next', true) + ).toMatchInlineSnapshot(`"yarn add -D @storybook/testing-library"`); + }); + test('handles CLI command flags and subcommands', () => { + expect(updateSnippet('npx storybook init --builder ')).toMatchInlineSnapshot( + `"npx storybook@latest init --builder "` + ); + expect( + updateSnippet('npx storybook migrate storiesof-to-csf --glob="src/**/*.stories.tsx"') + ).toMatchInlineSnapshot( + `"npx storybook@latest migrate storiesof-to-csf --glob="src/**/*.stories.tsx""` + ); + expect( + // Must use block here to test effect (inline should not append tag) + // Must use preRelease here to test effect (non-preRelease should not append tag) + updateSnippet('npm install @storybook/addon-a11y --save-dev', true, true) + ).toMatchInlineSnapshot(`"npm install @storybook/addon-a11y@next --save-dev"`); + }); + test('does not match `npm run storybook` or `yarn storybook`', () => { + expect(updateSnippet('npm run storybook')).toMatchInlineSnapshot(`"npm run storybook"`); + expect(updateSnippet('yarn storybook')).toMatchInlineSnapshot(`"yarn storybook"`); + }); + test('does nothing for `storybook@next upgrade --prerelease`', () => { + expect(updateSnippet('npx storybook@next upgrade --prerelease')).toMatchInlineSnapshot( + `"npx storybook@next upgrade --prerelease"` + ); + }); +}); \ No newline at end of file diff --git a/apps/frontpage/scripts/append-package-tags-in-snippets.ts b/apps/frontpage/scripts/append-package-tags-in-snippets.ts new file mode 100644 index 00000000..a46c6066 --- /dev/null +++ b/apps/frontpage/scripts/append-package-tags-in-snippets.ts @@ -0,0 +1,133 @@ +import fs from 'node:fs'; +import { readdir } from 'node:fs/promises'; +import path from 'node:path'; + +import { docsVersions } from '@repo/utils'; + +const DOCS_CONTENT_DIR = path.resolve(__dirname, '../content/docs'); +const DOCS_SNIPPETS_DIR = path.resolve(__dirname, '../content/snippets'); + +const FILE_DISALLOW_LIST = [ + // Content + 'api/cli-options.mdx', + 'configure/upgrading.mdx', + // Snippets + 'storybook-upgrade.md', + 'storybook-upgrade-prerelease.md', +]; + +const PKG_DISALLOW_LIST = [ + '@storybook/addon-coverage', + '@storybook/addon-webpack5-compiler-babel', + '@storybook/addon-webpack5-compiler-swc', +]; + +const INLINE_CODE_REGEX = /`(?!`)(.*)`/g; +const CODE_BLOCK_REGEX = /```(?:sh|shell|bash).*\n\s*(?:#.*\n)?(.*)\n\s*```/g; + +const INLINE_CODE_MATCH_LIST = (preRelease?: boolean) => [ + { + test: /((?:npx|pnpm dlx|yarn dlx) storybook)(?:@\w+)? (add|automigrate|babelrc|extract|init|migrate|remove|upgrade)(?! --prerelease)/g, + replacer: `$1${preRelease ? '@next' : '@latest'} $2`, + }, +]; + +const CODE_BLOCK_MATCH_LIST = (preRelease?: boolean) => [ + ...INLINE_CODE_MATCH_LIST(preRelease), + { + test: /(@storybook\/(?:\w+-?)+)(?:@\w+)?/g, + replacer: (_: string, pkg: string) => + `${pkg}${preRelease && !PKG_DISALLOW_LIST.includes(pkg) ? '@next' : ''}`, + }, + { + test: /(storybook(?:@\w+)? remove) (@storybook\/(?:\w+-?)+)(?:@\w+)?/g, + replacer: (_: string, start: string, pkg: string) => `${start} ${pkg}`, + }, +]; + +export function updateSnippet( + snippetSrc: string, + isBlockCodeSnippet?: boolean, + preRelease?: boolean, +) { + const matchList = isBlockCodeSnippet + ? CODE_BLOCK_MATCH_LIST + : INLINE_CODE_MATCH_LIST; + + let updatedSnippetSrc = snippetSrc; + + matchList(preRelease).forEach(({ test, replacer }) => { + if (snippetSrc.match(test)) { + // @ts-expect-error — The overload for `str.replace` which accepts a fn replacer isn't taking effect? + updatedSnippetSrc = updatedSnippetSrc.replace(test, replacer); + } + }); + + return updatedSnippetSrc; +} + +export function updateFile(fileContents: string, preRelease?: boolean) { + let updatedContents = fileContents; + + const inlineCodeSnippets = (fileContents.match(INLINE_CODE_REGEX) || []).map( + (code) => code.replace(INLINE_CODE_REGEX, '$1'), + ); + + if (inlineCodeSnippets.length > 0) { + inlineCodeSnippets.forEach((snippet) => { + const updatedSnippet = updateSnippet(snippet, false, preRelease); + updatedContents = updatedContents.replace(snippet, updatedSnippet); + }); + } + + const blockCodeSnippets = (fileContents.match(CODE_BLOCK_REGEX) || []).map( + (code) => code.replace(CODE_BLOCK_REGEX, '$1'), + ); + + if (blockCodeSnippets.length > 0) { + blockCodeSnippets.forEach((snippet) => { + const updatedSnippet = updateSnippet(snippet, true, preRelease); + updatedContents = updatedContents.replace(snippet, updatedSnippet); + }); + } + + return updatedContents; +} + +// https://stackoverflow.com/a/45130990 +async function getFiles(dir: string): Promise { + const dirents = await readdir(dir, { withFileTypes: true }); + const files = await Promise.all( + dirents.map((dirent) => { + const res = path.resolve(dir, dirent.name); + return dirent.isDirectory() ? getFiles(res) : res; + }), + ); + return Array.prototype.concat(...files); +} + +(async () => { + docsVersions.forEach(async ({ id, preRelease }) => { + const docsContentDir = `${DOCS_CONTENT_DIR}/${id}`; + const docsSnippetsDir = `${DOCS_SNIPPETS_DIR}/${id}`; + + const docsContentFiles = (await getFiles(docsContentDir)).filter((file) => + file.endsWith('.mdx'), + ); + const docsSnippetsFiles = (await getFiles(docsSnippetsDir)).filter((file) => + file.endsWith('.md'), + ); + const docsFiles = [...docsContentFiles, ...docsSnippetsFiles]; + + docsFiles.forEach((file) => { + const shortFile = file + .replace(`${docsContentDir}/`, '') + .replace(`${docsSnippetsDir}/`, ''); + if (!FILE_DISALLOW_LIST.includes(shortFile)) { + const fileContents = fs.readFileSync(file, 'utf8'); + const updatedContents = updateFile(fileContents, preRelease); + fs.writeFileSync(file, updatedContents, 'utf8'); + } + }); + }); +})(); diff --git a/apps/frontpage/scripts/copy-other-sitemaps.ts b/apps/frontpage/scripts/copy-other-sitemaps.ts deleted file mode 100644 index de8a24f3..00000000 --- a/apps/frontpage/scripts/copy-other-sitemaps.ts +++ /dev/null @@ -1,62 +0,0 @@ -import path from 'node:path'; -import fs from 'fs-extra'; -import fetch from 'node-fetch'; - -async function getRemoteSitemapContent(p: string): Promise { - const response = await fetch(`https://storybook.js.org${p}`); - const content = await response.text(); - return content; -} - -const OTHER_SITEMAPS = { - blog: { - async getContent() { - return getRemoteSitemapContent('/blog/sitemap/sitemap-0.xml'); - }, - }, - showcase: { - async getContent() { - return getRemoteSitemapContent('/showcase/sitemap-0.xml'); - }, - }, - tutorials: { - async getContent() { - return getRemoteSitemapContent('/tutorials/sitemap/sitemap-0.xml'); - }, - }, -}; - -const DESTINATION = path.join(__dirname, '../public/sitemap'); -const SITEMAP_FILENAME = 'sitemap.xml'; - -function stripDirname(file: string): string { - return file.replace(/.*(\/public\/.*)/, '$1'); -} - -async function copySitemaps(): Promise { - for (const sitemapId of Object.keys(OTHER_SITEMAPS)) { - const directory = `${DESTINATION}/${sitemapId}`; - if (!fs.existsSync(directory)) { - fs.mkdirSync(directory); - } - - try { - const file = `${DESTINATION}/${sitemapId}/${SITEMAP_FILENAME}`; - const content = - // eslint-disable-next-line no-await-in-loop -- TODO: Fix it - await OTHER_SITEMAPS[ - sitemapId as keyof typeof OTHER_SITEMAPS - ].getContent(); - fs.writeFileSync(file, content); - // eslint-disable-next-line no-console -- Showing off console.log - console.log('Wrote file:', stripDirname(file)); - } catch (error) { - // eslint-disable-next-line no-console -- Showing off error handling - console.error(error); - } - } -} - -void (async () => { - await copySitemaps(); -})(); diff --git a/packages/utils/src/docs-versions.tsx b/packages/utils/src/docs-versions.tsx index 364b82a9..e44917c5 100644 --- a/packages/utils/src/docs-versions.tsx +++ b/packages/utils/src/docs-versions.tsx @@ -32,12 +32,12 @@ export const docsVersions: DocsVersion[] = [ id: '8.2', branch: 'main', }, - // { - // label: '8.3 (beta)', - // id: '8.3', - // branch: 'next', - // preRelease: true, - // }, + { + label: '8.3 (beta)', + id: '8.3', + branch: 'next', + preRelease: true, + }, { label: 'Version 7', id: '7.6',