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/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!
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/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/BUNProxy.ts b/code/core/src/common/js-package-manager/BUNProxy.ts
new file mode 100644
index 000000000000..01ede64cfa5a
--- /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}`;
+ }
+
+ getRemoteRunCommand(): string {
+ return 'bunx';
+ }
+
+ 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 523613b719ca..76a809911021 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;
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/core/src/common/js-package-manager/NPMProxy.ts b/code/core/src/common/js-package-manager/NPMProxy.ts
index ea673f9df301..5acc1a4dd18c 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
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..263c91d5bfa6 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[]];
+ const isIndented = groups.some((group) => group.some((link) => link.icon));
return (
- {links.map((link) => (
-
- ))}
+ {groups
+ .filter((group) => group.length)
+ .map((group, index) => {
+ 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/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 6f4cec12e686..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,
@@ -229,7 +230,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 b25defd9c4bd..f6aae845bee3 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,
+}: SidebarBottomProps) => {
const spacerRef = useRef(null);
const wrapperRef = useRef(null);
const [warningsActive, setWarningsActive] = useState(false);
@@ -228,27 +234,36 @@ export const SidebarBottomBase = ({ api, notifications = [], status = {} }: Side
);
};
-export const SidebarBottom = () => {
+export const SidebarBottom = ({ isDevelopment }: { isDevelopment?: boolean }) => {
const api = useStorybookApi();
const { notifications, status } = useStorybookState();
- return ;
+ return (
+
+ );
};
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 (
-
+
);
};
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}
+ >
{
- 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,
diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json
index 16787067c5ca..324d0a990f07 100644
--- a/code/frameworks/nextjs/package.json
+++ b/code/frameworks/nextjs/package.json
@@ -156,7 +156,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/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);
diff --git a/code/frameworks/nextjs/src/utils.ts b/code/frameworks/nextjs/src/utils.ts
index 198917513166..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 '/' occurances */, 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);
};
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'),
},
},
}),
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')
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');
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/code/yarn.lock b/code/yarn.lock
index ae3c57af5da0..272933533d60 100644
--- a/code/yarn.lock
+++ b/code/yarn.lock
@@ -6484,7 +6484,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"
@@ -25327,19 +25327,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
@@ -25347,7 +25347,9 @@ __metadata:
optional: true
sass-embedded:
optional: true
- checksum: 10c0/5e955a4ffce35ee0a46fce677ce51eaa69587fb5371978588c83af00f49e7edc36dcf3bb559cbae27681c5e24a71284463ebe03a1fb65e6ecafa1db0620e3fc8
+ webpack:
+ optional: true
+ checksum: 10c0/9a48d454584d96d6c562eb323bb9e3c6808e930eeaaa916975b97d45831e0b87936a8655cdb3a4512a25abc9587dea65a9616e42396be0d7e7c507a4795a8146
languageName: node
linkType: hard
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': {
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!"}}
diff --git a/scripts/prepare/tools.ts b/scripts/prepare/tools.ts
index d2c58b420833..6037f3cd301c 100644
--- a/scripts/prepare/tools.ts
+++ b/scripts/prepare/tools.ts
@@ -6,7 +6,7 @@ import { globalExternals } from '@fal-works/esbuild-plugin-global-externals';
import { spawn } from 'cross-spawn';
import * as esbuild from 'esbuild';
// eslint-disable-next-line depend/ban-dependencies
-import { readJson } from 'fs-extra';
+import { pathExists, readJson } from 'fs-extra';
// eslint-disable-next-line depend/ban-dependencies
import { glob } from 'glob';
import limit from 'p-limit';
@@ -131,10 +131,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));
};
diff --git a/scripts/sandbox/generate.ts b/scripts/sandbox/generate.ts
index 495d142c0196..db3d3ce02f23 100755
--- a/scripts/sandbox/generate.ts
+++ b/scripts/sandbox/generate.ts
@@ -5,7 +5,7 @@ import type { Options as ExecaOptions } from 'execa';
// eslint-disable-next-line depend/ban-dependencies
import { execaCommand } from 'execa';
// eslint-disable-next-line depend/ban-dependencies
-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';
@@ -129,7 +129,8 @@ const addStorybook = async ({
throw e;
}
- await rename(tmpDir, afterDir);
+ await copy(tmpDir, afterDir);
+ await remove(tmpDir);
};
export const runCommand = async (script: string, options: ExecaOptions, debug = false) => {
@@ -197,7 +198,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);
diff --git a/scripts/sandbox/utils/yarn.ts b/scripts/sandbox/utils/yarn.ts
index 8f4163256a08..1132bda1472d 100644
--- a/scripts/sandbox/utils/yarn.ts
+++ b/scripts/sandbox/utils/yarn.ts
@@ -1,3 +1,4 @@
+import fs from 'fs';
// eslint-disable-next-line depend/ban-dependencies
import { move, remove } from 'fs-extra';
import { join } from 'path';
@@ -12,7 +13,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 });
@@ -22,7 +23,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 68bfcff3caa3..1b04f358155a 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