From 96af77fb8358b14fb287d4169e258e2648373b86 Mon Sep 17 00:00:00 2001 From: Tobias Diez Date: Fri, 23 Aug 2024 17:26:34 +0200 Subject: [PATCH 01/24] Chore: Fix path to sandbox template file --- .github/PULL_REQUEST_TEMPLATE.md | 25 +++++++++++++------------ docs/contribute/code.mdx | 2 +- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e56e437088eb..98a3f8475e6d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -19,6 +19,7 @@ Thank you for contributing to Storybook! Please submit all PRs to the `next` bra #### The changes in this PR are covered in the following automated tests: + - [ ] stories - [ ] unit tests - [ ] integration tests @@ -46,21 +47,21 @@ _This section is mandatory for all contributions. If you believe no manual test ## Checklist for Maintainers -- [ ] When this PR is ready for testing, make sure to add `ci:normal`, `ci:merged` or `ci:daily` GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in `code/lib/cli/src/sandbox-templates.ts` +- [ ] When this PR is ready for testing, make sure to add `ci:normal`, `ci:merged` or `ci:daily` GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in `code/lib/cli-storybook/src/sandbox-templates.ts` - [ ] Make sure this PR contains **one** of the labels below:
Available labels - - `bug`: Internal changes that fixes incorrect behavior. - - `maintenance`: User-facing maintenance tasks. - - `dependencies`: Upgrading (sometimes downgrading) dependencies. - - `build`: Internal-facing build tooling & test updates. Will not show up in release changelog. - - `cleanup`: Minor cleanup style change. Will not show up in release changelog. - - `documentation`: Documentation **only** changes. Will not show up in release changelog. - - `feature request`: Introducing a new feature. - - `BREAKING CHANGE`: Changes that break compatibility in some way with current major version. - - `other`: Changes that don't fit in the above categories. - + - `bug`: Internal changes that fixes incorrect behavior. + - `maintenance`: User-facing maintenance tasks. + - `dependencies`: Upgrading (sometimes downgrading) dependencies. + - `build`: Internal-facing build tooling & test updates. Will not show up in release changelog. + - `cleanup`: Minor cleanup style change. Will not show up in release changelog. + - `documentation`: Documentation **only** changes. Will not show up in release changelog. + - `feature request`: Introducing a new feature. + - `BREAKING CHANGE`: Changes that break compatibility in some way with current major version. + - `other`: Changes that don't fit in the above categories. +
### 🦋 Canary release @@ -74,4 +75,4 @@ _core team members can create a canary release [here](https://github.com/storybo - \ No newline at end of file + diff --git a/docs/contribute/code.mdx b/docs/contribute/code.mdx index e3b2e3bd0ceb..d735283bd8af 100644 --- a/docs/contribute/code.mdx +++ b/docs/contribute/code.mdx @@ -206,7 +206,7 @@ npx storybook@next link --local /path/to/local-repro-directory ## Developing a template -The first step is to add an entry to `code/lib/cli/src/sandbox-templates.ts`, which is the master list of all repro templates: +The first step is to add an entry to `code/lib/cli-storybook/src/sandbox-templates.ts`, which is the master list of all repro templates: ```ts 'cra/default-js': { From d78222418fe503e183a0e94ab458c6c8a6ee8c25 Mon Sep 17 00:00:00 2001 From: Tobias Diez Date: Fri, 23 Aug 2024 17:28:19 +0200 Subject: [PATCH 02/24] Chore: Fix moving sandbox from temporary dir to another partition --- scripts/sandbox/generate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/sandbox/generate.ts b/scripts/sandbox/generate.ts index d92696383699..1d82a469cd52 100755 --- a/scripts/sandbox/generate.ts +++ b/scripts/sandbox/generate.ts @@ -2,7 +2,7 @@ import * as ghActions from '@actions/core'; import { program } from 'commander'; import type { Options as ExecaOptions } from 'execa'; import { execaCommand } from 'execa'; -import { copy, emptyDir, ensureDir, move, remove, rename, writeFile } from 'fs-extra'; +import { copy, emptyDir, ensureDir, move, remove, writeFile } from 'fs-extra'; import pLimit from 'p-limit'; import { join, relative } from 'path'; import prettyTime from 'pretty-hrtime'; @@ -126,7 +126,7 @@ const addStorybook = async ({ throw e; } - await rename(tmpDir, afterDir); + await copy(tmpDir, afterDir); }; export const runCommand = async (script: string, options: ExecaOptions, debug = false) => { From da4975b1ae707aa71bbfbd3a4fd7e24c5c6fb06f Mon Sep 17 00:00:00 2001 From: Tobias Diez Date: Fri, 23 Aug 2024 21:23:41 +0200 Subject: [PATCH 03/24] Chore: Add output of errors during yarn setup in sandbox --- scripts/sandbox/generate.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/sandbox/generate.ts b/scripts/sandbox/generate.ts index 1d82a469cd52..0b0794c4d867 100755 --- a/scripts/sandbox/generate.ts +++ b/scripts/sandbox/generate.ts @@ -194,7 +194,19 @@ const runGenerators = async ( // We do the creation inside a temp dir to avoid yarn container problems const createBaseDir = await temporaryDirectory(); if (!script.includes('pnp')) { - await setupYarn({ cwd: createBaseDir }); + try { + await setupYarn({ cwd: createBaseDir }); + } catch (error) { + const message = `❌ Failed to setup yarn in template: ${name} (${dirName})`; + if (isCI) { + ghActions.error(dedent`${message} + ${(error as any).stack}`); + } else { + console.error(message); + console.error(error); + } + throw new Error(message); + } } const createBeforeDir = join(createBaseDir, BEFORE_DIR_NAME); From 9e1084d12f21751343210bca6ba74331265dfa61 Mon Sep 17 00:00:00 2001 From: Tobias Diez Date: Fri, 23 Aug 2024 21:26:57 +0200 Subject: [PATCH 04/24] Chore: Yarn setup in sandbox works on Windows now --- scripts/sandbox/utils/yarn.ts | 5 +++-- scripts/utils/yarn.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/sandbox/utils/yarn.ts b/scripts/sandbox/utils/yarn.ts index a2d579b2cde0..cf73bf04cf42 100644 --- a/scripts/sandbox/utils/yarn.ts +++ b/scripts/sandbox/utils/yarn.ts @@ -1,3 +1,4 @@ +import fs from 'fs'; import { move, remove } from 'fs-extra'; import { join } from 'path'; @@ -11,7 +12,7 @@ interface SetupYarnOptions { export async function setupYarn({ cwd, pnp = false, version = 'classic' }: SetupYarnOptions) { // force yarn - await runCommand(`touch yarn.lock`, { cwd }); + fs.writeFileSync(join(cwd, 'yarn.lock'), '', { flag: 'a' }); await runCommand(`yarn set version ${version}`, { cwd }); if (version === 'berry' && !pnp) { await runCommand('yarn config set nodeLinker node-modules', { cwd }); @@ -21,7 +22,7 @@ export async function setupYarn({ cwd, pnp = false, version = 'classic' }: Setup export async function localizeYarnConfigFiles(baseDir: string, beforeDir: string) { await Promise.allSettled([ - runCommand(`touch yarn.lock`, { cwd: beforeDir }), + fs.writeFileSync(join(beforeDir, 'yarn.lock'), '', { flag: 'a' }), move(join(baseDir, '.yarn'), join(beforeDir, '.yarn')), move(join(baseDir, '.yarnrc.yml'), join(beforeDir, '.yarnrc.yml')), move(join(baseDir, '.yarnrc'), join(beforeDir, '.yarnrc')), diff --git a/scripts/utils/yarn.ts b/scripts/utils/yarn.ts index 95f686a382a4..9008273c9bd3 100644 --- a/scripts/utils/yarn.ts +++ b/scripts/utils/yarn.ts @@ -105,7 +105,7 @@ export const configureYarn2ForVerdaccio = async ({ // ⚠️ Need to set registry because Yarn 2 is not using the conf of Yarn 1 (URL is hardcoded in CircleCI config.yml) `yarn config set npmRegistryServer "http://localhost:6001/"`, // Some required magic to be able to fetch deps from local registry - `yarn config set unsafeHttpWhitelist --json '["localhost"]'`, + `yarn config set unsafeHttpWhitelist "localhost"`, // Disable fallback mode to make sure everything is required correctly `yarn config set pnpFallbackMode none`, // We need to be able to update lockfile when bootstrapping the examples From 987feb4467dfec69df16437ebaadf9e3cde825da Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Thu, 19 Sep 2024 13:04:48 +0200 Subject: [PATCH 05/24] SvelteKIT: Support ESM for Vitest test runs --- .../sveltekit/src/plugins/mock-sveltekit-stores.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/code/frameworks/sveltekit/src/plugins/mock-sveltekit-stores.ts b/code/frameworks/sveltekit/src/plugins/mock-sveltekit-stores.ts index 8831e9217052..98ceb6cc7e5c 100644 --- a/code/frameworks/sveltekit/src/plugins/mock-sveltekit-stores.ts +++ b/code/frameworks/sveltekit/src/plugins/mock-sveltekit-stores.ts @@ -1,16 +1,21 @@ -import { resolve } from 'node:path'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; import type { Plugin } from 'vite'; +// @ts-expect-error We are building for CJS and ESM, so we have to use import.meta.url for the ESM output +const filename = __filename ?? fileURLToPath(import.meta.url); +const dir = dirname(filename); + export async function mockSveltekitStores() { return { name: 'storybook:sveltekit-mock-stores', config: () => ({ resolve: { alias: { - '$app/forms': resolve(__dirname, '../src/mocks/app/forms.ts'), - '$app/navigation': resolve(__dirname, '../src/mocks/app/navigation.ts'), - '$app/stores': resolve(__dirname, '../src/mocks/app/stores.ts'), + '$app/forms': resolve(dir, '../src/mocks/app/forms.ts'), + '$app/navigation': resolve(dir, '../src/mocks/app/navigation.ts'), + '$app/stores': resolve(dir, '../src/mocks/app/stores.ts'), }, }, }), From 395173f9484349d5599e4b333d6d0f6c2a2c4e8d Mon Sep 17 00:00:00 2001 From: "yevhenii.honcharenko" Date: Wed, 2 Oct 2024 16:00:50 +0300 Subject: [PATCH 06/24] Next.js: Update dependencies --- code/frameworks/nextjs/package.json | 2 +- code/yarn.lock | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index 3733fd8d85ad..c99456ee48b1 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -157,7 +157,7 @@ "postcss-loader": "^8.1.1", "react-refresh": "^0.14.0", "resolve-url-loader": "^5.0.0", - "sass-loader": "^13.2.0", + "sass-loader": "^14.2.1", "semver": "^7.3.5", "style-loader": "^3.3.1", "styled-jsx": "^5.1.6", diff --git a/code/yarn.lock b/code/yarn.lock index b1dd1154739b..782efca87f92 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6445,7 +6445,7 @@ __metadata: postcss-loader: "npm:^8.1.1" react-refresh: "npm:^0.14.0" resolve-url-loader: "npm:^5.0.0" - sass-loader: "npm:^13.2.0" + sass-loader: "npm:^14.2.1" semver: "npm:^7.3.5" sharp: "npm:^0.33.3" style-loader: "npm:^3.3.1" @@ -25305,19 +25305,19 @@ __metadata: languageName: node linkType: hard -"sass-loader@npm:^13.2.0": - version: 13.3.3 - resolution: "sass-loader@npm:13.3.3" +"sass-loader@npm:^14.2.1": + version: 14.2.1 + resolution: "sass-loader@npm:14.2.1" dependencies: neo-async: "npm:^2.6.2" peerDependencies: - fibers: ">= 3.1.0" + "@rspack/core": 0.x || 1.x node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 sass: ^1.3.0 sass-embedded: "*" webpack: ^5.0.0 peerDependenciesMeta: - fibers: + "@rspack/core": optional: true node-sass: optional: true @@ -25325,7 +25325,9 @@ __metadata: optional: true sass-embedded: optional: true - checksum: 10c0/5e955a4ffce35ee0a46fce677ce51eaa69587fb5371978588c83af00f49e7edc36dcf3bb559cbae27681c5e24a71284463ebe03a1fb65e6ecafa1db0620e3fc8 + webpack: + optional: true + checksum: 10c0/9a48d454584d96d6c562eb323bb9e3c6808e930eeaaa916975b97d45831e0b87936a8655cdb3a4512a25abc9587dea65a9616e42396be0d7e7c507a4795a8146 languageName: node linkType: hard From ef6e2ceeeb2a49110ba818ed259c1d47edf66b33 Mon Sep 17 00:00:00 2001 From: Stephen Jason Wang Date: Thu, 3 Oct 2024 00:35:55 +0800 Subject: [PATCH 07/24] feat: bun support with npm fallback --- .../src/common/js-package-manager/BUNProxy.ts | 332 ++++++++++++++++++ .../js-package-manager/JsPackageManager.ts | 2 +- 2 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 code/core/src/common/js-package-manager/BUNProxy.ts diff --git a/code/core/src/common/js-package-manager/BUNProxy.ts b/code/core/src/common/js-package-manager/BUNProxy.ts new file mode 100644 index 000000000000..f7ea1646713b --- /dev/null +++ b/code/core/src/common/js-package-manager/BUNProxy.ts @@ -0,0 +1,332 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { platform } from 'node:os'; +import { join } from 'node:path'; + +import { logger } from '@storybook/core/node-logger'; +import { FindPackageVersionsError } from '@storybook/core/server-errors'; + +import { findUp } from 'find-up'; +import sort from 'semver/functions/sort.js'; +import { dedent } from 'ts-dedent'; + +import { createLogStream } from '../utils/cli'; +import { JsPackageManager } from './JsPackageManager'; +import type { PackageJson } from './PackageJson'; +import type { InstallationMetadata, PackageMetadata } from './types'; + +type NpmDependency = { + version: string; + resolved?: string; + overridden?: boolean; + dependencies?: NpmDependencies; +}; + +type NpmDependencies = { + [key: string]: NpmDependency; +}; + +export type NpmListOutput = { + dependencies: NpmDependencies; +}; + +const NPM_ERROR_REGEX = /npm ERR! code (\w+)/; +const NPM_ERROR_CODES = { + E401: 'Authentication failed or is required.', + E403: 'Access to the resource is forbidden.', + E404: 'Requested resource not found.', + EACCES: 'Permission issue.', + EAI_FAIL: 'DNS lookup failed.', + EBADENGINE: 'Engine compatibility check failed.', + EBADPLATFORM: 'Platform not supported.', + ECONNREFUSED: 'Connection refused.', + ECONNRESET: 'Connection reset.', + EEXIST: 'File or directory already exists.', + EINVALIDTYPE: 'Invalid type encountered.', + EISGIT: 'Git operation failed or conflicts with an existing file.', + EJSONPARSE: 'Error parsing JSON data.', + EMISSINGARG: 'Required argument missing.', + ENEEDAUTH: 'Authentication needed.', + ENOAUDIT: 'No audit available.', + ENOENT: 'File or directory does not exist.', + ENOGIT: 'Git not found or failed to run.', + ENOLOCK: 'Lockfile missing.', + ENOSPC: 'Insufficient disk space.', + ENOTFOUND: 'Resource not found.', + EOTP: 'One-time password required.', + EPERM: 'Permission error.', + EPUBLISHCONFLICT: 'Conflict during package publishing.', + ERESOLVE: 'Dependency resolution error.', + EROFS: 'File system is read-only.', + ERR_SOCKET_TIMEOUT: 'Socket timed out.', + ETARGET: 'Package target not found.', + ETIMEDOUT: 'Operation timed out.', + ETOOMANYARGS: 'Too many arguments provided.', + EUNKNOWNTYPE: 'Unknown type encountered.', +}; + +export class BUNProxy extends JsPackageManager { + readonly type = 'bun'; + + installArgs: string[] | undefined; + + async initPackageJson() { + await this.executeCommand({ command: 'bun', args: ['init'] }); + } + + getRunStorybookCommand(): string { + return 'bun run storybook'; + } + + getRunCommand(command: string): string { + return `bun run ${command}`; + } + + async getNpmVersion(): Promise { + return this.executeCommand({ command: 'npm', args: ['--version'] }); + } + + public async getPackageJSON( + packageName: string, + basePath = this.cwd + ): Promise { + const packageJsonPath = await findUp( + (dir) => { + const possiblePath = join(dir, 'node_modules', packageName, 'package.json'); + return existsSync(possiblePath) ? possiblePath : undefined; + }, + { cwd: basePath } + ); + + if (!packageJsonPath) { + return null; + } + + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + return packageJson; + } + + getInstallArgs(): string[] { + if (!this.installArgs) { + this.installArgs = []; + } + return this.installArgs; + } + + public runPackageCommandSync( + command: string, + args: string[], + cwd?: string, + stdio?: 'pipe' | 'inherit' + ): string { + return this.executeCommandSync({ + command: 'bun', + args: ['run', command, ...args], + cwd, + stdio, + }); + } + + public async runPackageCommand(command: string, args: string[], cwd?: string): Promise { + return this.executeCommand({ + command: 'bun', + args: ['run', command, ...args], + cwd, + }); + } + + public async findInstallations(pattern: string[], { depth = 99 }: { depth?: number } = {}) { + const exec = async ({ packageDepth }: { packageDepth: number }) => { + const pipeToNull = platform() === 'win32' ? '2>NUL' : '2>/dev/null'; + return this.executeCommand({ + command: 'npm', + args: ['ls', '--json', `--depth=${packageDepth}`, pipeToNull], + env: { + FORCE_COLOR: 'false', + }, + }); + }; + + try { + const commandResult = await exec({ packageDepth: depth }); + const parsedOutput = JSON.parse(commandResult); + + return this.mapDependencies(parsedOutput, pattern); + } catch (e) { + // when --depth is higher than 0, npm can return a non-zero exit code + // in case the user's project has peer dependency issues. So we try again with no depth + try { + const commandResult = await exec({ packageDepth: 0 }); + const parsedOutput = JSON.parse(commandResult); + + return this.mapDependencies(parsedOutput, pattern); + } catch (err) { + logger.warn(`An issue occurred while trying to find dependencies metadata using npm.`); + return undefined; + } + } + } + + protected getResolutions(packageJson: PackageJson, versions: Record) { + return { + overrides: { + ...packageJson.overrides, + ...versions, + }, + }; + } + + protected async runInstall() { + await this.executeCommand({ + command: 'bun', + args: ['install', ...this.getInstallArgs()], + stdio: 'inherit', + }); + } + + public async getRegistryURL() { + const res = await this.executeCommand({ + command: 'npm', + // "npm config" commands are not allowed in workspaces per default + // https://github.com/npm/cli/issues/6099#issuecomment-1847584792 + args: ['config', 'get', 'registry', '-ws=false', '-iwr'], + }); + const url = res.trim(); + return url === 'undefined' ? undefined : url; + } + + protected async runAddDeps(dependencies: string[], installAsDevDependencies: boolean) { + const { logStream, readLogFile, moveLogFile, removeLogFile } = await createLogStream(); + let args = [...dependencies]; + + if (installAsDevDependencies) { + args = ['-D', ...args]; + } + + try { + await this.executeCommand({ + command: 'bun', + args: ['add', ...args, ...this.getInstallArgs()], + stdio: process.env.CI ? 'inherit' : ['ignore', logStream, logStream], + }); + } catch (err) { + const stdout = await readLogFile(); + + const errorMessage = this.parseErrorFromLogs(stdout); + + await moveLogFile(); + + throw new Error( + dedent`${errorMessage} + + Please check the logfile generated at ./storybook.log for troubleshooting and try again.` + ); + } + + await removeLogFile(); + } + + protected async runRemoveDeps(dependencies: string[]) { + const args = [...dependencies]; + + await this.executeCommand({ + command: 'bun', + args: ['remove', ...args, ...this.getInstallArgs()], + stdio: 'inherit', + }); + } + + protected async runGetVersions( + packageName: string, + fetchAllVersions: T + ): Promise { + const args = [fetchAllVersions ? 'versions' : 'version', '--json']; + try { + const commandResult = await this.executeCommand({ + command: 'npm', + args: ['info', packageName, ...args], + }); + + const parsedOutput = JSON.parse(commandResult); + + if (parsedOutput.error?.summary) { + // this will be handled in the catch block below + throw parsedOutput.error.summary; + } + + return parsedOutput; + } catch (error) { + throw new FindPackageVersionsError({ + error, + packageManager: 'NPM', + packageName, + }); + } + } + + /** + * @param input The output of `npm ls --json` + * @param pattern A list of package names to filter the result. * can be used as a placeholder + */ + protected mapDependencies(input: NpmListOutput, pattern: string[]): InstallationMetadata { + const acc: Record = {}; + const existingVersions: Record = {}; + const duplicatedDependencies: Record = {}; + + const recurse = ([name, packageInfo]: [string, NpmDependency]): void => { + // transform pattern into regex where `*` is replaced with `.*` + if (!name || !pattern.some((p) => new RegExp(`^${p.replace(/\*/g, '.*')}$`).test(name))) { + return; + } + + const value = { + version: packageInfo.version, + location: '', + }; + + if (!existingVersions[name]?.includes(value.version)) { + if (acc[name]) { + acc[name].push(value); + } else { + acc[name] = [value]; + } + existingVersions[name] = sort([...(existingVersions[name] || []), value.version]); + + if (existingVersions[name].length > 1) { + duplicatedDependencies[name] = existingVersions[name]; + } + } + + if (packageInfo.dependencies) { + Object.entries(packageInfo.dependencies).forEach(recurse); + } + }; + + Object.entries(input.dependencies).forEach(recurse); + + return { + dependencies: acc, + duplicatedDependencies, + infoCommand: 'npm ls --depth=1', + dedupeCommand: 'npm dedupe', + }; + } + + public parseErrorFromLogs(logs: string): string { + let finalMessage = 'NPM error'; + const match = logs.match(NPM_ERROR_REGEX); + + if (match) { + const errorCode = match[1] as keyof typeof NPM_ERROR_CODES; + if (errorCode) { + finalMessage = `${finalMessage} ${errorCode}`; + } + + const errorMessage = NPM_ERROR_CODES[errorCode]; + if (errorMessage) { + finalMessage = `${finalMessage} - ${errorMessage}`; + } + } + + return finalMessage.trim(); + } +} diff --git a/code/core/src/common/js-package-manager/JsPackageManager.ts b/code/core/src/common/js-package-manager/JsPackageManager.ts index 5b0bdde0fdb6..0bc5063b32ce 100644 --- a/code/core/src/common/js-package-manager/JsPackageManager.ts +++ b/code/core/src/common/js-package-manager/JsPackageManager.ts @@ -16,7 +16,7 @@ import type { InstallationMetadata } from './types'; const logger = console; -export type PackageManagerName = 'npm' | 'yarn1' | 'yarn2' | 'pnpm'; +export type PackageManagerName = 'npm' | 'yarn1' | 'yarn2' | 'pnpm' | 'bun'; type StorybookPackage = keyof typeof storybookPackagesVersions; From 55d22d69f8a2f1fbd32c3877614ed4fbf62b2738 Mon Sep 17 00:00:00 2001 From: Tobias Diez Date: Mon, 7 Oct 2024 10:42:04 +0800 Subject: [PATCH 08/24] Chore: gracefully handle empty folders on dev machines --- scripts/prepare/tools.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/scripts/prepare/tools.ts b/scripts/prepare/tools.ts index eb4214a945ba..9acb4d46d51b 100644 --- a/scripts/prepare/tools.ts +++ b/scripts/prepare/tools.ts @@ -5,7 +5,7 @@ import * as process from 'node:process'; import { globalExternals } from '@fal-works/esbuild-plugin-global-externals'; import { spawn } from 'cross-spawn'; import * as esbuild from 'esbuild'; -import { readJson } from 'fs-extra'; +import { pathExists, readJson } from 'fs-extra'; import { glob } from 'glob'; import limit from 'p-limit'; import picocolors from 'picocolors'; @@ -129,10 +129,19 @@ export const getWorkspace = async () => { return Promise.all( workspaces .flatMap((p) => p.map((i) => join(CODE_DIRECTORY, i))) - .map(async (p) => { - const pkg = await readJson(join(p, 'package.json')); - return { ...pkg, path: p } as typefest.PackageJson & + .map(async (packagePath) => { + const packageJsonPath = join(packagePath, 'package.json'); + if (!(await pathExists(packageJsonPath))) { + // If we delete a package, then an empty folder might still be left behind on some dev machines + // In this case, just ignore the folder + console.warn( + `No package.json found in ${packagePath}. You might want to delete this folder.` + ); + return null; + } + const pkg = await readJson(packageJsonPath); + return { ...pkg, path: packagePath } as typefest.PackageJson & Required> & { path: string }; }) - ); + ).then((packages) => packages.filter((p) => p !== null)); }; From bebaa3ba3bef8139753ca4f7afcee0955df3f3ba Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Fri, 25 Oct 2024 10:55:14 +1100 Subject: [PATCH 09/24] Ensure consistent absolute path for Next.js-bundled react and react-dom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In a pnpm monorepo with multiple Next.js projects, different projects can have different peerDependencies for Next.js, which causes pnpm to install multiple instances of the `next` package in the monorepo. In this situation, prior to this change, Storybook's webpack configuration could consume `react` from one instance of Next and `react-dom` from another, causing React render errors due to the mismatch. This issue was caused by the `configureConfig` function adding webpack module aliases for `react` using `addScopedAlias` (which generates an alias with an absolute filesystem path pointing to the correct instance of `next`, e.g. `'/path/to/next/dist/compiled/react'`), but adding module aliases for `react-dom` using `setAlias` (which generates an alias with a relative path, e.g. `'next/dist/compiled/react-dom'`). This would create a webpack configuration like this: ``` alias: { ⋮ react: '/Users/kyank/Developer/authentication-ui/node_modules/.pnpm/next@14.2.13_@babel+core@7.25.2_@opentelemetry+api@1.8.0_@playwright+test@1.48.1_babel-plugin_z4uy3ayinaafvek4wmyon66ziu/node_modules/next/dist/compiled/react', ⋮ 'react-dom/test-utils': 'next/dist/compiled/react-dom/cjs/react-dom-test-utils.production.js', 'react-dom$': 'next/dist/compiled/react-dom', 'react-dom/client': 'next/dist/compiled/react-dom/client', 'react-dom/server': 'next/dist/compiled/react-dom/server', ⋮ }, ``` This change uses `addScopedAlias` to create aliases with absolute filesystem paths for all of the Next.js-bundled React packages. This fixes the React rendering errors in our monorepo. This issue seems to have been [introduced in Storybook 8.3.0](https://github.com/storybookjs/storybook/pull/29044/files#diff-20144c44999f6f1054f74f56ef1c3fcfcec008fd7b5caea5e10568e95eccb051). --- code/frameworks/nextjs/src/config/webpack.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/code/frameworks/nextjs/src/config/webpack.ts b/code/frameworks/nextjs/src/config/webpack.ts index a0ea2d47bded..76edac25c81c 100644 --- a/code/frameworks/nextjs/src/config/webpack.ts +++ b/code/frameworks/nextjs/src/config/webpack.ts @@ -36,16 +36,16 @@ export const configureConfig = async ({ addScopedAlias(baseConfig, 'react', 'next/dist/compiled/react'); } if (tryResolve('next/dist/compiled/react-dom/cjs/react-dom-test-utils.production.js')) { - setAlias( + addScopedAlias( baseConfig, 'react-dom/test-utils', 'next/dist/compiled/react-dom/cjs/react-dom-test-utils.production.js' ); } if (tryResolve('next/dist/compiled/react-dom')) { - setAlias(baseConfig, 'react-dom$', 'next/dist/compiled/react-dom'); - setAlias(baseConfig, 'react-dom/client', 'next/dist/compiled/react-dom/client'); - setAlias(baseConfig, 'react-dom/server', 'next/dist/compiled/react-dom/server'); + addScopedAlias(baseConfig, 'react-dom$', 'next/dist/compiled/react-dom'); + addScopedAlias(baseConfig, 'react-dom/client', 'next/dist/compiled/react-dom/client'); + addScopedAlias(baseConfig, 'react-dom/server', 'next/dist/compiled/react-dom/server'); } setupRuntimeConfig(baseConfig, nextConfig); From ee5a4f6463c46bb0ee3135a83aa5826c82254e1a Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Mon, 28 Oct 2024 11:59:16 +0100 Subject: [PATCH 10/24] Refactor sandbox generate script to remove temporary directory --- scripts/sandbox/generate.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/sandbox/generate.ts b/scripts/sandbox/generate.ts index 703a75e89921..db3d3ce02f23 100755 --- a/scripts/sandbox/generate.ts +++ b/scripts/sandbox/generate.ts @@ -130,6 +130,7 @@ const addStorybook = async ({ } await copy(tmpDir, afterDir); + await remove(tmpDir); }; export const runCommand = async (script: string, options: ExecaOptions, debug = false) => { From 8773f66276283c1a835ac62606b8c8357e38f66e Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Mon, 28 Oct 2024 13:02:31 +0100 Subject: [PATCH 11/24] Refactor: Remove unused getNpmVersion method from BUNProxy and NPMProxy --- code/core/src/common/js-package-manager/BUNProxy.ts | 4 ---- code/core/src/common/js-package-manager/NPMProxy.ts | 4 ---- 2 files changed, 8 deletions(-) diff --git a/code/core/src/common/js-package-manager/BUNProxy.ts b/code/core/src/common/js-package-manager/BUNProxy.ts index f7ea1646713b..a3920bbf3e13 100644 --- a/code/core/src/common/js-package-manager/BUNProxy.ts +++ b/code/core/src/common/js-package-manager/BUNProxy.ts @@ -81,10 +81,6 @@ export class BUNProxy extends JsPackageManager { return `bun run ${command}`; } - async getNpmVersion(): Promise { - return this.executeCommand({ command: 'npm', args: ['--version'] }); - } - public async getPackageJSON( packageName: string, basePath = this.cwd diff --git a/code/core/src/common/js-package-manager/NPMProxy.ts b/code/core/src/common/js-package-manager/NPMProxy.ts index 90378de122cf..d9975a7f615e 100644 --- a/code/core/src/common/js-package-manager/NPMProxy.ts +++ b/code/core/src/common/js-package-manager/NPMProxy.ts @@ -85,10 +85,6 @@ export class NPMProxy extends JsPackageManager { return 'npx'; } - async getNpmVersion(): Promise { - return this.executeCommand({ command: 'npm', args: ['--version'] }); - } - public async getPackageJSON( packageName: string, basePath = this.cwd From d499768c984cf4544383953e03abcbf0709777f0 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Mon, 28 Oct 2024 13:09:40 +0100 Subject: [PATCH 12/24] Refactor: Add support for BUN package manager --- code/addons/themes/postinstall.js | 1 + .../JsPackageManagerFactory.test.ts | 86 +++++++++++++++++++ .../JsPackageManagerFactory.ts | 24 +++++- code/lib/cli-storybook/src/bin/index.ts | 10 +-- code/lib/create-storybook/src/bin/index.ts | 5 +- 5 files changed, 119 insertions(+), 7 deletions(-) diff --git a/code/addons/themes/postinstall.js b/code/addons/themes/postinstall.js index 87175ac9cbc1..93f2e6159179 100644 --- a/code/addons/themes/postinstall.js +++ b/code/addons/themes/postinstall.js @@ -5,6 +5,7 @@ const PACKAGE_MANAGER_TO_COMMAND = { pnpm: ['pnpm', 'dlx'], yarn1: ['npx'], yarn2: ['yarn', 'dlx'], + bun: ['bunx'], }; const selectPackageManagerCommand = (packageManager) => PACKAGE_MANAGER_TO_COMMAND[packageManager]; diff --git a/code/core/src/common/js-package-manager/JsPackageManagerFactory.test.ts b/code/core/src/common/js-package-manager/JsPackageManagerFactory.test.ts index 9f703d7c5ba3..888f18a598a4 100644 --- a/code/core/src/common/js-package-manager/JsPackageManagerFactory.test.ts +++ b/code/core/src/common/js-package-manager/JsPackageManagerFactory.test.ts @@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { sync as spawnSync } from 'cross-spawn'; import { findUpSync } from 'find-up'; +import { BUNProxy } from './BUNProxy'; import { JsPackageManagerFactory } from './JsPackageManagerFactory'; import { NPMProxy } from './NPMProxy'; import { PNPMProxy } from './PNPMProxy'; @@ -354,6 +355,91 @@ describe('CLASS: JsPackageManagerFactory', () => { }); }); + describe('Yarn 2 proxy', () => { + it('FORCE: it should return a Yarn2 proxy when `force` option is `yarn2`', () => { + expect(JsPackageManagerFactory.getPackageManager({ force: 'yarn2' })).toBeInstanceOf( + Yarn2Proxy + ); + }); + + it('USER AGENT: it should infer yarn2 from the user agent', () => { + process.env.npm_config_user_agent = 'yarn/2.2.10'; + expect(JsPackageManagerFactory.getPackageManager()).toBeInstanceOf(Yarn2Proxy); + }); + + it('ONLY YARN 2: when Yarn command is ok, Yarn version is >=2, NPM is ko, PNPM is ko', () => { + spawnSyncMock.mockImplementation((command) => { + // Yarn is ok + if (command === 'yarn') { + return { + status: 0, + output: '2.0.0-rc.33', + }; + } + // NPM is ko + if (command === 'npm') { + return { + status: 1, + }; + } + // PNPM is ko + if (command === 'pnpm') { + return { + status: 1, + }; + } + // Unknown package manager is ko + return { + status: 1, + } as any; + }); + + expect(JsPackageManagerFactory.getPackageManager()).toBeInstanceOf(Yarn2Proxy); + }); + + it('when Yarn command is ok, Yarn version is >=2, NPM and PNPM are ok, there is a `yarn.lock` file', () => { + spawnSyncMock.mockImplementation((command) => { + // Yarn is ok + if (command === 'yarn') { + return { + status: 0, + output: '2.0.0-rc.33', + }; + } + // NPM is ok + if (command === 'npm') { + return { + status: 0, + output: '6.5.12', + }; + } + // PNPM is ok + if (command === 'pnpm') { + return { + status: 0, + output: '7.9.5', + }; + } + + if (command === 'bun') { + return { + status: 0, + output: '1.0.0', + }; + } + // Unknown package manager is ko + return { + status: 1, + } as any; + }); + + // There is a yarn.lock + findUpSyncMock.mockImplementation(() => '/Users/johndoe/Documents/bun.lockb'); + + expect(JsPackageManagerFactory.getPackageManager()).toBeInstanceOf(BUNProxy); + }); + }); + it('throws an error if Yarn, NPM, and PNPM are not found', () => { spawnSyncMock.mockReturnValue({ status: 1 } as any); expect(() => JsPackageManagerFactory.getPackageManager()).toThrow(); diff --git a/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts b/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts index 521bdecea268..fcd057906057 100644 --- a/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts +++ b/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts @@ -3,6 +3,7 @@ import { basename, parse, relative } from 'node:path'; import { sync as spawnSync } from 'cross-spawn'; import { findUpSync } from 'find-up'; +import { BUNProxy } from './BUNProxy'; import type { JsPackageManager, PackageManagerName } from './JsPackageManager'; import { NPMProxy } from './NPMProxy'; import { PNPMProxy } from './PNPMProxy'; @@ -12,12 +13,14 @@ import { Yarn2Proxy } from './Yarn2Proxy'; const NPM_LOCKFILE = 'package-lock.json'; const PNPM_LOCKFILE = 'pnpm-lock.yaml'; const YARN_LOCKFILE = 'yarn.lock'; +const BUN_LOCKFILE = 'bun.lockb'; type PackageManagerProxy = | typeof NPMProxy | typeof PNPMProxy | typeof Yarn1Proxy - | typeof Yarn2Proxy; + | typeof Yarn2Proxy + | typeof BUNProxy; export class JsPackageManagerFactory { public static getPackageManager( @@ -33,6 +36,7 @@ export class JsPackageManagerFactory { findUpSync(YARN_LOCKFILE, { cwd }), findUpSync(PNPM_LOCKFILE, { cwd }), findUpSync(NPM_LOCKFILE, { cwd }), + findUpSync(BUN_LOCKFILE, { cwd }), ] .filter(Boolean) .sort((a, b) => { @@ -59,6 +63,7 @@ export class JsPackageManagerFactory { const hasNPMCommand = hasNPM(cwd); const hasPNPMCommand = hasPNPM(cwd); + const hasBunCommand = hasBun(cwd); const yarnVersion = getYarnVersion(cwd); if (yarnVersion && (closestLockfile === YARN_LOCKFILE || (!hasNPMCommand && !hasPNPMCommand))) { @@ -73,6 +78,10 @@ export class JsPackageManagerFactory { return new NPMProxy({ cwd }); } + if (hasBunCommand && closestLockfile === BUN_LOCKFILE) { + return new BUNProxy({ cwd }); + } + // Option 3: If the user is running a command via npx/pnpx/yarn create/etc, we infer the package manager from the command const inferredPackageManager = this.inferPackageManagerFromUserAgent(); if (inferredPackageManager && inferredPackageManager in this.PROXY_MAP) { @@ -94,6 +103,7 @@ export class JsPackageManagerFactory { pnpm: PNPMProxy, yarn1: Yarn1Proxy, yarn2: Yarn2Proxy, + bun: BUNProxy, }; /** @@ -136,6 +146,18 @@ function hasNPM(cwd?: string) { return npmVersionCommand.status === 0; } +function hasBun(cwd?: string) { + const pnpmVersionCommand = spawnSync('bun', ['--version'], { + cwd, + shell: true, + env: { + ...process.env, + COREPACK_ENABLE_STRICT: '0', + }, + }); + return pnpmVersionCommand.status === 0; +} + function hasPNPM(cwd?: string) { const pnpmVersionCommand = spawnSync('pnpm', ['--version'], { cwd, diff --git a/code/lib/cli-storybook/src/bin/index.ts b/code/lib/cli-storybook/src/bin/index.ts index c2bd666b02e6..2eaa769e4186 100644 --- a/code/lib/cli-storybook/src/bin/index.ts +++ b/code/lib/cli-storybook/src/bin/index.ts @@ -43,7 +43,7 @@ const command = (name: string) => command('add ') .description('Add an addon to your Storybook') .option( - '--package-manager ', + '--package-manager ', 'Force package manager for installing dependencies' ) .option('-c, --config-dir ', 'Directory where to load Storybook configurations from') @@ -54,7 +54,7 @@ command('add ') command('remove ') .description('Remove an addon from your Storybook') .option( - '--package-manager ', + '--package-manager ', 'Force package manager for installing dependencies' ) .option('-c, --config-dir ', 'Directory where to load Storybook configurations from') @@ -70,7 +70,7 @@ command('remove ') command('upgrade') .description(`Upgrade your Storybook packages to v${versions.storybook}`) .option( - '--package-manager ', + '--package-manager ', 'Force package manager for installing dependencies' ) .option('-y --yes', 'Skip prompting the user') @@ -157,7 +157,7 @@ command('automigrate [fixId]') .description('Check storybook for incompatibilities or migrations and apply fixes') .option('-y --yes', 'Skip prompting the user') .option('-n --dry-run', 'Only check for fixes, do not actually run them') - .option('--package-manager ', 'Force package manager') + .option('--package-manager ', 'Force package manager') .option('-l --list', 'List available migrations') .option('-c, --config-dir ', 'Directory of Storybook configurations to migrate') .option('-s --skip-install', 'Skip installing deps') @@ -174,7 +174,7 @@ command('automigrate [fixId]') command('doctor') .description('Check Storybook for known problems and provide suggestions or fixes') - .option('--package-manager ', 'Force package manager') + .option('--package-manager ', 'Force package manager') .option('-c, --config-dir ', 'Directory of Storybook configuration') .action(async (options) => { await doctor(options).catch((e) => { diff --git a/code/lib/create-storybook/src/bin/index.ts b/code/lib/create-storybook/src/bin/index.ts index 187e2811c38f..6dd321c5aa6a 100644 --- a/code/lib/create-storybook/src/bin/index.ts +++ b/code/lib/create-storybook/src/bin/index.ts @@ -25,7 +25,10 @@ program .option('--enable-crash-reports', 'Enable sending crash reports to telemetry data') .option('-f --force', 'Force add Storybook') .option('-s --skip-install', 'Skip installing deps') - .option('--package-manager ', 'Force package manager for installing deps') + .option( + '--package-manager ', + 'Force package manager for installing deps' + ) .option('--use-pnp', 'Enable pnp mode for Yarn 2+') .option('-p --parser ', 'jscodeshift parser') .option('-t --type ', 'Add Storybook for a specific project type') From 1523974d221d920608368b3c791edbc143fea249 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Mon, 28 Oct 2024 13:41:52 +0100 Subject: [PATCH 13/24] Refactor: Update BUNProxy to support remote run command --- code/core/src/common/js-package-manager/BUNProxy.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/core/src/common/js-package-manager/BUNProxy.ts b/code/core/src/common/js-package-manager/BUNProxy.ts index a3920bbf3e13..01ede64cfa5a 100644 --- a/code/core/src/common/js-package-manager/BUNProxy.ts +++ b/code/core/src/common/js-package-manager/BUNProxy.ts @@ -81,6 +81,10 @@ export class BUNProxy extends JsPackageManager { return `bun run ${command}`; } + getRemoteRunCommand(): string { + return 'bunx'; + } + public async getPackageJSON( packageName: string, basePath = this.cwd From cb540e448f5f7fccc6d0d3b72373a5e3d045072a Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Thu, 31 Oct 2024 14:27:11 +1100 Subject: [PATCH 14/24] Fix spelling in comment --- code/frameworks/nextjs/src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/frameworks/nextjs/src/utils.ts b/code/frameworks/nextjs/src/utils.ts index 198917513166..1ea90ef7703e 100644 --- a/code/frameworks/nextjs/src/utils.ts +++ b/code/frameworks/nextjs/src/utils.ts @@ -75,7 +75,7 @@ export const scopedResolve = (id: string): string => { } const moduleFolderStrPosition = scopedModulePath.lastIndexOf( - id.replace(/\//g /* all '/' occurances */, sep) + id.replace(/\//g /* all '/' occurrences */, sep) ); const beginningOfMainScriptPath = moduleFolderStrPosition + id.length; return scopedModulePath.substring(0, beginningOfMainScriptPath); From e8a9166dddc94301e6452a6fd1791a55ed5385f7 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Thu, 31 Oct 2024 15:13:53 +1100 Subject: [PATCH 15/24] Enable scopedResolve to accept script file references The previous implementation supported relative references to packages (e.g. `next/dist/compiled/react-dom`) and named exports (e.g. `next/dist/compiled/react-dom/client`), but not references to physical script files within packages (e.g. `next/dist/compiled/react-dom/cjs/react-dom-test-utils.production.js`). The latter are now handled by detecting when require.resolve returns a path and filename that ends with the exact string provided to the function. --- code/frameworks/nextjs/src/utils.ts | 42 ++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/code/frameworks/nextjs/src/utils.ts b/code/frameworks/nextjs/src/utils.ts index 1ea90ef7703e..82f15d088f59 100644 --- a/code/frameworks/nextjs/src/utils.ts +++ b/code/frameworks/nextjs/src/utils.ts @@ -51,18 +51,36 @@ export const addScopedAlias = (baseConfig: WebpackConfig, name: string, alias?: }; /** - * @example // before main script path truncation require.resolve('styled-jsx') === - * '/some/path/node_modules/styled-jsx/index.js // after main script path truncation + * @example + * + * ``` + * // before main script path truncation + * require.resolve('styled-jsx') === '/some/path/node_modules/styled-jsx/index.js + * // after main script path truncation * scopedResolve('styled-jsx') === '/some/path/node_modules/styled-jsx' + * ``` + * + * @example + * + * ``` + * // referencing a named export of a package + * scopedResolve('next/dist/compiled/react-dom/client') === + * // returns the path to the package export without the script filename + * '/some/path/node_modules/next/dist/compiled/react-dom/client'; + * + * // referencing a specific file within a CJS package + * scopedResolve('next/dist/compiled/react-dom/cjs/react-dom-test-utils.production.js') === + * // returns the path to the physical file, including the script filename + * '/some/path/node_modules/next/dist/compiled/react-dom/cjs/react-dom-test-utils.production.js'; + * ``` * - * @param id The module id - * @returns A path to the module id scoped to the project folder without the main script path at the - * end + * @param id The module id or script file to resolve + * @returns An absolute path to the specified module id or script file scoped to the project folder * @summary * This is to help the addon in development. * Without it, the addon resolves packages in its node_modules instead of the example's node_modules. * Because require.resolve will also include the main script as part of the path, this function strips - * that to just include the path to the module folder + * that to just include the path to the module folder when the id provided is a package or named export. */ export const scopedResolve = (id: string): string => { let scopedModulePath; @@ -74,9 +92,15 @@ export const scopedResolve = (id: string): string => { scopedModulePath = require.resolve(id); } - const moduleFolderStrPosition = scopedModulePath.lastIndexOf( - id.replace(/\//g /* all '/' occurrences */, sep) - ); + const idWithNativePathSep = id.replace(/\//g /* all '/' occurrences */, sep); + + // If the id referenced the file specifically, return the full module path & filename + if (scopedModulePath.endsWith(idWithNativePathSep)) { + return scopedModulePath; + } + + // Otherwise, return just the path to the module folder or named export + const moduleFolderStrPosition = scopedModulePath.lastIndexOf(idWithNativePathSep); const beginningOfMainScriptPath = moduleFolderStrPosition + id.length; return scopedModulePath.substring(0, beginningOfMainScriptPath); }; From 0d6c4e011cc261669cbebd08714e15f8a3f13a52 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Thu, 31 Oct 2024 13:55:38 +0100 Subject: [PATCH 16/24] Only render the TestingModule component in development mode --- .../components/sidebar/SidebarBottom.tsx | 30 ++++++++++--------- .../components/sidebar/TestingModule.tsx | 7 ++++- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index b25defd9c4bd..3bf16cba8555 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx @@ -228,20 +228,22 @@ export const SidebarBottomBase = ({ api, notifications = [], status = {} }: Side
- + {globalThis.CONFIG_TYPE === 'DEVELOPMENT' && ( + + )}
); diff --git a/code/core/src/manager/components/sidebar/TestingModule.tsx b/code/core/src/manager/components/sidebar/TestingModule.tsx index 5ca7bc8aa972..c020ebe53f09 100644 --- a/code/core/src/manager/components/sidebar/TestingModule.tsx +++ b/code/core/src/manager/components/sidebar/TestingModule.tsx @@ -229,7 +229,12 @@ export const TestingModule = ({ const testing = testProviders.length > 0; return ( - 0}> + 0} + > Date: Thu, 31 Oct 2024 17:08:14 +0100 Subject: [PATCH 17/24] refactor logic --- .../manager/components/sidebar/Sidebar.tsx | 2 +- .../sidebar/SidebarBottom.stories.tsx | 1 + .../components/sidebar/SidebarBottom.tsx | 21 +++++++++++++++---- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/code/core/src/manager/components/sidebar/Sidebar.tsx b/code/core/src/manager/components/sidebar/Sidebar.tsx index 6f4cec12e686..94ce7927965b 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.tsx @@ -229,7 +229,7 @@ export const Sidebar = React.memo(function Sidebar({ )} - {isMobile || isLoading ? null : } + {isMobile || isLoading ? null : } ); diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx index 18a9a83db9fb..8d7d2c42f488 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx @@ -6,6 +6,7 @@ import { SidebarBottomBase } from './SidebarBottom'; export default { component: SidebarBottomBase, args: { + isDevelopment: true, api: { clearNotification: fn(), emit: fn(), diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index 3bf16cba8555..c2e0a242866e 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx @@ -92,9 +92,15 @@ interface SidebarBottomProps { api: API; notifications: State['notifications']; status: State['status']; + isDevelopment?: boolean; } -export const SidebarBottomBase = ({ api, notifications = [], status = {} }: SidebarBottomProps) => { +export const SidebarBottomBase = ({ + api, + notifications = [], + status = {}, + isDevelopment = globalThis.CONFIG_TYPE === 'DEVELOPMENT', +}: SidebarBottomProps) => { const spacerRef = useRef(null); const wrapperRef = useRef(null); const [warningsActive, setWarningsActive] = useState(false); @@ -228,7 +234,7 @@ export const SidebarBottomBase = ({ api, notifications = [], status = {} }: Side
- {globalThis.CONFIG_TYPE === 'DEVELOPMENT' && ( + {isDevelopment && ( { +export const SidebarBottom = ({ isDevelopment }: { isDevelopment?: boolean }) => { const api = useStorybookApi(); const { notifications, status } = useStorybookState(); - return ; + return ( + + ); }; From 54c871270feec80262fda3c69ba9b9378630b427 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Thu, 31 Oct 2024 17:54:12 +0100 Subject: [PATCH 18/24] refactor --- code/core/src/manager/components/sidebar/Sidebar.stories.tsx | 1 + code/core/src/manager/components/sidebar/Sidebar.tsx | 3 ++- code/core/src/manager/components/sidebar/SidebarBottom.tsx | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx index d4698f53e6df..dcc6b06096c2 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx @@ -85,6 +85,7 @@ const meta = { refs: {}, status: {}, showCreateStoryButton: true, + isDevelopment: true, }, decorators: [ (storyFn) => ( diff --git a/code/core/src/manager/components/sidebar/Sidebar.tsx b/code/core/src/manager/components/sidebar/Sidebar.tsx index 94ce7927965b..bb3be73f425d 100644 --- a/code/core/src/manager/components/sidebar/Sidebar.tsx +++ b/code/core/src/manager/components/sidebar/Sidebar.tsx @@ -109,7 +109,6 @@ const useCombination = ( return useMemo(() => ({ hash, entries: Object.entries(hash) }), [hash]); }; -const isDevelopment = global.CONFIG_TYPE === 'DEVELOPMENT'; const isRendererReact = global.STORYBOOK_RENDERER === 'react'; export interface SidebarProps extends API_LoadedRefData { @@ -124,6 +123,7 @@ export interface SidebarProps extends API_LoadedRefData { onMenuClick?: HeadingProps['onMenuClick']; showCreateStoryButton?: boolean; indexJson?: StoryIndex; + isDevelopment?: boolean; } export const Sidebar = React.memo(function Sidebar({ // @ts-expect-error (non strict) @@ -138,6 +138,7 @@ export const Sidebar = React.memo(function Sidebar({ extra, menuHighlighted = false, enableShortcuts = true, + isDevelopment = global.CONFIG_TYPE === 'DEVELOPMENT', refs = {}, onMenuClick, showCreateStoryButton = isDevelopment && isRendererReact, diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx index c2e0a242866e..f6aae845bee3 100644 --- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx +++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx @@ -99,7 +99,7 @@ export const SidebarBottomBase = ({ api, notifications = [], status = {}, - isDevelopment = globalThis.CONFIG_TYPE === 'DEVELOPMENT', + isDevelopment, }: SidebarBottomProps) => { const spacerRef = useRef(null); const wrapperRef = useRef(null); From 4b4943899be1b60cec94949e954bc12fdce91d0c Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 1 Nov 2024 11:25:24 +0100 Subject: [PATCH 19/24] Add capability for groups to TooltipLinkList, use it for main menu and update styling --- .../components/tooltip/ListItem.tsx | 1 + .../components/components/tooltip/Tooltip.tsx | 2 +- .../tooltip/TooltipLinkList.stories.tsx | 42 +++++++++++++++ .../components/tooltip/TooltipLinkList.tsx | 30 ++++++++--- .../src/manager/components/sidebar/Menu.tsx | 29 ++++++----- code/core/src/manager/container/Menu.tsx | 51 ++++++++++--------- 6 files changed, 111 insertions(+), 44 deletions(-) diff --git a/code/core/src/components/components/tooltip/ListItem.tsx b/code/core/src/components/components/tooltip/ListItem.tsx index 8d50a05273de..1752abeb1f8d 100644 --- a/code/core/src/components/components/tooltip/ListItem.tsx +++ b/code/core/src/components/components/tooltip/ListItem.tsx @@ -123,6 +123,7 @@ const Item = styled.div( ({ theme }) => ({ width: '100%', border: 'none', + borderRadius: theme.appBorderRadius, background: 'none', fontSize: theme.typography.size.s1, transition: 'all 150ms ease-out', diff --git a/code/core/src/components/components/tooltip/Tooltip.tsx b/code/core/src/components/components/tooltip/Tooltip.tsx index 77a6d4fc9c25..ef053f1baf5b 100644 --- a/code/core/src/components/components/tooltip/Tooltip.tsx +++ b/code/core/src/components/components/tooltip/Tooltip.tsx @@ -109,7 +109,7 @@ const Wrapper = styled.div( drop-shadow(0px 5px 5px rgba(0,0,0,0.05)) drop-shadow(0 1px 3px rgba(0,0,0,0.1)) `, - borderRadius: theme.appBorderRadius, + borderRadius: theme.appBorderRadius + 2, fontSize: theme.typography.size.s1, } : {} diff --git a/code/core/src/components/components/tooltip/TooltipLinkList.stories.tsx b/code/core/src/components/components/tooltip/TooltipLinkList.stories.tsx index 28952285756d..07e5ed7fc8d3 100644 --- a/code/core/src/components/components/tooltip/TooltipLinkList.stories.tsx +++ b/code/core/src/components/components/tooltip/TooltipLinkList.stories.tsx @@ -191,3 +191,45 @@ export const WithCustomIcon = { ], }, } satisfies Story; + +export const WithGroups = { + args: { + links: [ + [ + { + id: '1', + title: 'Link 1', + center: 'This is an addition description', + href: 'http://google.com', + onClick: onLinkClick, + }, + ], + [ + { + id: '1', + title: 'Link 1', + center: 'This is an addition description', + icon: , + href: 'http://google.com', + onClick: onLinkClick, + }, + { + id: '2', + title: 'Link 2', + center: 'This is an addition description', + href: 'http://google.com', + onClick: onLinkClick, + }, + ], + [ + { + id: '2', + title: 'Link 2', + center: 'This is an addition description', + href: 'http://google.com', + onClick: onLinkClick, + }, + ], + ], + }, +} satisfies Story; diff --git a/code/core/src/components/components/tooltip/TooltipLinkList.tsx b/code/core/src/components/components/tooltip/TooltipLinkList.tsx index f1467babec3a..eaeeba38f5a6 100644 --- a/code/core/src/components/components/tooltip/TooltipLinkList.tsx +++ b/code/core/src/components/components/tooltip/TooltipLinkList.tsx @@ -11,13 +11,20 @@ const List = styled.div( minWidth: 180, overflow: 'hidden', overflowY: 'auto', - maxHeight: 15.5 * 32, // 11.5 items + maxHeight: 15.5 * 32 + 8, // 15.5 items at 32px each + 8px padding }, ({ theme }) => ({ - borderRadius: theme.appBorderRadius, + borderRadius: theme.appBorderRadius + 2, }) ); +const Group = styled.div(({ theme }) => ({ + padding: 4, + '& + &': { + borderTop: `1px solid ${theme.appBorderColor}`, + }, +})); + export interface Link extends Omit { id: string; onClick?: ( @@ -42,17 +49,26 @@ const Item = ({ id, onClick, ...rest }: ItemProps) => { }; export interface TooltipLinkListProps extends ComponentProps { - links: Link[]; + links: Link[] | Link[][]; LinkWrapper?: LinkWrapperType; } export const TooltipLinkList = ({ links, LinkWrapper, ...props }: TooltipLinkListProps) => { - const isIndented = links.some((link) => link.icon); + const groups = Array.isArray(links[0]) ? (links as Link[][]) : [links as Link[]]; return ( - {links.map((link) => ( - - ))} + {groups + .filter((group) => group.length) + .map((group, index) => { + const isIndented = group.some((link) => link.icon); + return ( + link.id).join(`~${index}~`)}> + {group.map((link) => ( + + ))} + + ); + })} ); }; diff --git a/code/core/src/manager/components/sidebar/Menu.tsx b/code/core/src/manager/components/sidebar/Menu.tsx index d24656aa7079..583a2b982616 100644 --- a/code/core/src/manager/components/sidebar/Menu.tsx +++ b/code/core/src/manager/components/sidebar/Menu.tsx @@ -8,9 +8,10 @@ import { CloseIcon, CogIcon } from '@storybook/icons'; import { transparentize } from 'polished'; +import type { useMenu } from '../../container/Menu'; import { useLayout } from '../layout/LayoutProvider'; -export type MenuList = ComponentProps['links']; +export type MenuList = ReturnType; export const SidebarIconButton: FC & { highlighted: boolean }> = styled(IconButton)< @@ -60,17 +61,21 @@ const SidebarMenuList: FC<{ menu: MenuList; onHide: () => void; }> = ({ menu, onHide }) => { - const links = useMemo(() => { - return menu.map(({ onClick, ...rest }) => ({ - ...rest, - onClick: ((event, item) => { - if (onClick) { - onClick(event, item); - } - onHide(); - }) as ClickHandler, - })); - }, [menu, onHide]); + const links = useMemo( + () => + menu.map((group) => + group.map(({ onClick, ...rest }) => ({ + ...rest, + onClick: ((event, item) => { + if (onClick) { + onClick(event, item); + } + onHide(); + }) as ClickHandler, + })) + ), + [menu, onHide] + ); return ; }; diff --git a/code/core/src/manager/container/Menu.tsx b/code/core/src/manager/container/Menu.tsx index b2b92ef17ad1..fdf21a2ab9a1 100644 --- a/code/core/src/manager/container/Menu.tsx +++ b/code/core/src/manager/container/Menu.tsx @@ -9,6 +9,8 @@ import { STORIES_COLLAPSE_ALL } from '@storybook/core/core-events'; import type { API, State } from '@storybook/core/manager-api'; import { shortcutToHumanString } from '@storybook/core/manager-api'; +import type { Link } from '../../components/components/tooltip/TooltipLinkList'; + const focusableUIElements = { storySearchField: 'storybook-explorer-searchfield', storyListMenu: 'storybook-explorer-menu', @@ -58,8 +60,7 @@ export const useMenu = ( isPanelShown: boolean, isNavShown: boolean, enableShortcuts: boolean -) => { - const theme = useTheme(); +): Link[][] => { const shortcutKeys = api.getShortcutKeys(); const about = useMemo( @@ -105,11 +106,8 @@ export const useMenu = ( title: 'Keyboard shortcuts', onClick: () => api.changeSettingsTab('shortcuts'), right: enableShortcuts ? : null, - style: { - borderBottom: `4px solid ${theme.appBorderColor}`, - }, }), - [api, enableShortcuts, shortcutKeys.shortcutsPage, theme.appBorderColor] + [api, enableShortcuts, shortcutKeys.shortcutsPage] ); const sidebarToggle = useMemo( @@ -244,24 +242,29 @@ export const useMenu = ( }, [api, enableShortcuts, shortcutKeys]); return useMemo( - () => [ - about, - ...(state.whatsNewData?.status === 'SUCCESS' ? [whatsNew] : []), - documentation, - shortcuts, - sidebarToggle, - toolbarToogle, - addonsToggle, - addonsOrientationToggle, - fullscreenToggle, - searchToggle, - up, - down, - prev, - next, - collapse, - ...getAddonsShortcuts(), - ], + () => + [ + [ + about, + ...(state.whatsNewData?.status === 'SUCCESS' ? [whatsNew] : []), + documentation, + shortcuts, + ], + [ + sidebarToggle, + toolbarToogle, + addonsToggle, + addonsOrientationToggle, + fullscreenToggle, + searchToggle, + up, + down, + prev, + next, + collapse, + ], + getAddonsShortcuts(), + ] satisfies Link[][], [ about, state, From 18b471057f7610dbc75c861afaf432be44a5c4c5 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 1 Nov 2024 12:21:03 +0100 Subject: [PATCH 20/24] Indent all groups together --- code/core/src/components/components/tooltip/TooltipLinkList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/core/src/components/components/tooltip/TooltipLinkList.tsx b/code/core/src/components/components/tooltip/TooltipLinkList.tsx index eaeeba38f5a6..263c91d5bfa6 100644 --- a/code/core/src/components/components/tooltip/TooltipLinkList.tsx +++ b/code/core/src/components/components/tooltip/TooltipLinkList.tsx @@ -55,12 +55,12 @@ export interface TooltipLinkListProps extends ComponentProps { export const TooltipLinkList = ({ links, LinkWrapper, ...props }: TooltipLinkListProps) => { const groups = Array.isArray(links[0]) ? (links as Link[][]) : [links as Link[]]; + const isIndented = groups.some((group) => group.some((link) => link.icon)); return ( {groups .filter((group) => group.length) .map((group, index) => { - const isIndented = group.some((link) => link.icon); return ( link.id).join(`~${index}~`)}> {group.map((link) => ( From b02bc0aba8ec2c1405129c9c4cbd63babd32f84b Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 1 Nov 2024 13:02:46 +0100 Subject: [PATCH 21/24] Update TagsFilterPanel to use link groups --- .../components/sidebar/TagsFilterPanel.tsx | 84 ++++++++++--------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx b/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx index f70fa6fca313..d9fc85c360d0 100644 --- a/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx @@ -7,6 +7,8 @@ import type { Tag } from '@storybook/types'; import type { API } from '@storybook/core/manager-api'; +import type { Link } from '../../../components/components/tooltip/TooltipLinkList'; + const BUILT_IN_TAGS_SHOW = new Set(['play-fn']); const Wrapper = styled.div({ @@ -29,56 +31,60 @@ export const TagsFilterPanel = ({ toggleTag, isDevelopment, }: TagsFilterPanelProps) => { - const theme = useTheme(); const userTags = allTags.filter((tag) => !BUILT_IN_TAGS_SHOW.has(tag)); const docsUrl = api.getDocsUrl({ subpath: 'writing-stories/tags#filtering-by-custom-tags' }); - const items = allTags.map((tag) => { - const checked = selectedTags.includes(tag); - const id = `tag-${tag}`; - return { - id, - title: tag, - right: ( - { - // The onClick handler higher up the tree will handle the toggle - // For controlled inputs, a onClick handler is needed, though - // Accessibility-wise this isn't optimal, but I guess that's a limitation - // of the current design of TooltipLinkList - }} - /> - ), - onClick: () => toggleTag(tag), - }; - }) as any[]; + + const groups = [ + allTags.map((tag) => { + const checked = selectedTags.includes(tag); + const id = `tag-${tag}`; + return { + id, + title: tag, + right: ( + { + // The onClick handler higher up the tree will handle the toggle + // For controlled inputs, a onClick handler is needed, though + // Accessibility-wise this isn't optimal, but I guess that's a limitation + // of the current design of TooltipLinkList + }} + /> + ), + onClick: () => toggleTag(tag), + }; + }), + ] as Link[][]; if (allTags.length === 0) { - items.push({ - id: 'no-tags', - title: 'There are no tags. Use tags to organize and filter your Storybook.', - isIndented: false, - }); + groups.push([ + { + id: 'no-tags', + title: 'There are no tags. Use tags to organize and filter your Storybook.', + isIndented: false, + }, + ]); } + if (userTags.length === 0 && isDevelopment) { - items.push({ - id: 'tags-docs', - title: 'Learn how to add tags', - icon: , - href: docsUrl, - style: { - borderTop: `4px solid ${theme.appBorderColor}`, + groups.push([ + { + id: 'tags-docs', + title: 'Learn how to add tags', + icon: , + href: docsUrl, }, - }); + ]); } return ( - + ); }; From 6663c5e2d2979d0f536701d2c65f50020639e0c5 Mon Sep 17 00:00:00 2001 From: storybook-bot <32066757+storybook-bot@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:46:07 +0000 Subject: [PATCH 22/24] Update CHANGELOG.md for v8.4.1 [skip ci] --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38cd9876ee7a..f0b60008f0fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 8.4.1 + +- Core: Relax peer dep constraint of shim packages - [#29503](https://github.com/storybookjs/storybook/pull/29503), thanks @kasperpeulen! + ## 8.4.0 Storybook 8.4 comes with a ton of exciting new features designed to give you the best experience developing, testing, and debugging tests in the browser! From a10154ebc7d968640711f5cf20288e12e16fc2bf Mon Sep 17 00:00:00 2001 From: Michael Shilman Date: Sat, 2 Nov 2024 09:53:37 +0800 Subject: [PATCH 23/24] CLI: Fix Solid init by installing `@storybook/test` --- code/lib/create-storybook/src/generators/baseGenerator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/lib/create-storybook/src/generators/baseGenerator.ts b/code/lib/create-storybook/src/generators/baseGenerator.ts index 814cea8a956d..4ba6db064da3 100644 --- a/code/lib/create-storybook/src/generators/baseGenerator.ts +++ b/code/lib/create-storybook/src/generators/baseGenerator.ts @@ -245,7 +245,7 @@ export async function baseGenerator( ].filter(Boolean); // TODO: migrate template stories in solid and qwik to use @storybook/test - if (['solid', 'qwik'].includes(rendererId)) { + if (['qwik'].includes(rendererId)) { addonPackages.push('@storybook/testing-library'); } else { addonPackages.push('@storybook/test'); From 2a0de1274df88256fc31441fcf3fa5f3991ee7bf Mon Sep 17 00:00:00 2001 From: storybook-bot <32066757+storybook-bot@users.noreply.github.com> Date: Sat, 2 Nov 2024 02:43:38 +0000 Subject: [PATCH 24/24] Write changelog for 8.5.0-alpha.2 [skip ci] --- CHANGELOG.prerelease.md | 10 ++++++++++ code/package.json | 3 ++- docs/versions/next.json | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index b5a18a8a0989..f9f62e7c79d9 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,13 @@ +## 8.5.0-alpha.2 + +- Addon Test: Only render the TestingModule component in development mode - [#29501](https://github.com/storybookjs/storybook/pull/29501), thanks @yannbf! +- CLI: Fix Solid init by installing `@storybook/test` - [#29514](https://github.com/storybookjs/storybook/pull/29514), thanks @shilman! +- Core: Add bun support with npm fallback - [#29267](https://github.com/storybookjs/storybook/pull/29267), thanks @stephenjason89! +- Core: Shim CJS-only globals in ESM output - [#29157](https://github.com/storybookjs/storybook/pull/29157), thanks @valentinpalkovic! +- Next.js: Fix bundled react and react-dom in monorepos - [#29444](https://github.com/storybookjs/storybook/pull/29444), thanks @sentience! +- Next.js: Upgrade sass-loader from ^13.2.0 to ^14.2.1 - [#29264](https://github.com/storybookjs/storybook/pull/29264), thanks @HoncharenkoZhenya! +- UI: Add support for groups to `TooltipLinkList` and use it in main menu - [#29507](https://github.com/storybookjs/storybook/pull/29507), thanks @ghengeveld! + ## 8.5.0-alpha.1 - Core: Relax peer dep constraint of shim packages - [#29503](https://github.com/storybookjs/storybook/pull/29503), thanks @kasperpeulen! diff --git a/code/package.json b/code/package.json index f0c40b3c83a0..f5eb8d7194da 100644 --- a/code/package.json +++ b/code/package.json @@ -293,5 +293,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.5.0-alpha.2" } diff --git a/docs/versions/next.json b/docs/versions/next.json index f7ae322dcdb2..641189d11f4c 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"8.5.0-alpha.1","info":{"plain":"- Core: Relax peer dep constraint of shim packages - [#29503](https://github.com/storybookjs/storybook/pull/29503), thanks @kasperpeulen!"}} +{"version":"8.5.0-alpha.2","info":{"plain":"- Addon Test: Only render the TestingModule component in development mode - [#29501](https://github.com/storybookjs/storybook/pull/29501), thanks @yannbf!\n- CLI: Fix Solid init by installing `@storybook/test` - [#29514](https://github.com/storybookjs/storybook/pull/29514), thanks @shilman!\n- Core: Add bun support with npm fallback - [#29267](https://github.com/storybookjs/storybook/pull/29267), thanks @stephenjason89!\n- Core: Shim CJS-only globals in ESM output - [#29157](https://github.com/storybookjs/storybook/pull/29157), thanks @valentinpalkovic!\n- Next.js: Fix bundled react and react-dom in monorepos - [#29444](https://github.com/storybookjs/storybook/pull/29444), thanks @sentience!\n- Next.js: Upgrade sass-loader from ^13.2.0 to ^14.2.1 - [#29264](https://github.com/storybookjs/storybook/pull/29264), thanks @HoncharenkoZhenya!\n- UI: Add support for groups to `TooltipLinkList` and use it in main menu - [#29507](https://github.com/storybookjs/storybook/pull/29507), thanks @ghengeveld!"}}