diff --git a/.circleci/config.yml b/.circleci/config.yml index 3e5c0b499cc6..72f3bf9dc225 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -628,6 +628,8 @@ jobs: mkdir empty-<< parameters.template >>-no-install cd empty-<< parameters.template >>-no-install npx storybook init --yes --skip-install + npm install + npm run build-storybook environment: IN_STORYBOOK_SANDBOX: true STORYBOOK_INIT_EMPTY_TYPE: << parameters.template >> diff --git a/CHANGELOG.md b/CHANGELOG.md index e0b00c7b7217..0bae092457f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 8.3.2 + +- CLI: Fix skip-install for stable latest releases - [#29133](https://github.com/storybookjs/storybook/pull/29133), thanks @valentinpalkovic! +- Core: Do not add packageManager field to package.json during `storybook dev` - [#29152](https://github.com/storybookjs/storybook/pull/29152), thanks @valentinpalkovic! + +## 8.3.1 + +- Angular: Fix sourceDecorator to apply excludeDecorators flag - [#29069](https://github.com/storybookjs/storybook/pull/29069), thanks @JSMike! +- Core: Do not prebundle better-opn - [#29137](https://github.com/storybookjs/storybook/pull/29137), thanks @valentinpalkovic! +- Core: Do not prebundle jsdoc-type-pratt-parser - [#29134](https://github.com/storybookjs/storybook/pull/29134), thanks @valentinpalkovic! +- Next.js: Upgrade sass-loader from ^12 to ^13 - [#29040](https://github.com/storybookjs/storybook/pull/29040), thanks @HoncharenkoZhenya! + ## 8.3.0 Fresh out of the oven! Storybook 8.3 brings you: diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 692c7edaf2e9..b9179e8d6dcc 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,22 @@ +## 8.4.0-alpha.1 + +- Addon Test: Support story name as test description - [#29147](https://github.com/storybookjs/storybook/pull/29147), thanks @InfiniteXyy! +- Addon-Interactions: Use ansi-to-html for colored test errors - [#29110](https://github.com/storybookjs/storybook/pull/29110), thanks @kasperpeulen! +- Angular: Fix sourceDecorator to apply excludeDecorators flag - [#29069](https://github.com/storybookjs/storybook/pull/29069), thanks @JSMike! +- Builder-vite: Replace .at() call with [] in codegen - [#29048](https://github.com/storybookjs/storybook/pull/29048), thanks @Chudesnov! +- CLI: Ensure `.gitignore` updated via CLI ends with a newline - [#29124](https://github.com/storybookjs/storybook/pull/29124), thanks @3w36zj6! +- CLI: Fix skip-install for stable latest releases - [#29133](https://github.com/storybookjs/storybook/pull/29133), thanks @valentinpalkovic! +- CLI: Show constraints in error when getting depndencies - [#29187](https://github.com/storybookjs/storybook/pull/29187), thanks @andrasczeh! +- Core: Do not add packageManager field to package.json during `storybook dev` - [#29152](https://github.com/storybookjs/storybook/pull/29152), thanks @valentinpalkovic! +- Core: Do not prebundle better-opn - [#29137](https://github.com/storybookjs/storybook/pull/29137), thanks @valentinpalkovic! +- Core: Do not prebundle jsdoc-type-pratt-parser - [#29134](https://github.com/storybookjs/storybook/pull/29134), thanks @valentinpalkovic! +- Core: Replace `fs-extra` with the native APIs - [#29126](https://github.com/storybookjs/storybook/pull/29126), thanks @ziebam! +- Next.js: Upgrade sass-loader from ^12 to ^13 - [#29040](https://github.com/storybookjs/storybook/pull/29040), thanks @HoncharenkoZhenya! +- React-Vite: Downgrade react-docgen-typescript plugin - [#29184](https://github.com/storybookjs/storybook/pull/29184), thanks @shilman! +- UI: Fix composed storybook TooltipLinkList bug where href isn't passed forward - [#29175](https://github.com/storybookjs/storybook/pull/29175), thanks @JSMike! +- Viewport-addon: Add InitialViewportKeys type to viewport addon - [#29182](https://github.com/storybookjs/storybook/pull/29182), thanks @hyeongrok7874! +- Vite: Add jsdoc-type-pratt-parser to `optimizeDeps` - [#29179](https://github.com/storybookjs/storybook/pull/29179), thanks @tobiasdiez! + ## 8.4.0-alpha.0 diff --git a/code/__mocks__/fs.js b/code/__mocks__/fs.js deleted file mode 100644 index 9e608c3ff038..000000000000 --- a/code/__mocks__/fs.js +++ /dev/null @@ -1,29 +0,0 @@ -import { vi } from 'vitest'; - -const fs = vi.createMockFromModule('fs'); - -// This is a custom function that our tests can use during setup to specify -// what the files on the "mock" filesystem should look like when any of the -// `fs` APIs are used. -let mockFiles = Object.create(null); - -// eslint-disable-next-line no-underscore-dangle, @typescript-eslint/naming-convention -function __setMockFiles(newMockFiles) { - mockFiles = newMockFiles; -} - -// A custom version of `readdirSync` that reads from the special mocked out -// file list set via __setMockFiles -const readFileSync = (filePath = '') => mockFiles[filePath]; -const existsSync = (filePath) => !!mockFiles[filePath]; -const lstatSync = (filePath) => ({ - isFile: () => !!mockFiles[filePath], -}); - -// eslint-disable-next-line no-underscore-dangle -fs.__setMockFiles = __setMockFiles; -fs.readFileSync = readFileSync; -fs.existsSync = existsSync; -fs.lstatSync = lstatSync; - -module.exports = fs; diff --git a/code/__mocks__/fs.ts b/code/__mocks__/fs.ts new file mode 100644 index 000000000000..50e34e8e4ca1 --- /dev/null +++ b/code/__mocks__/fs.ts @@ -0,0 +1,32 @@ +import { vi } from 'vitest'; + +// This is a custom function that our tests can use during setup to specify +// what the files on the "mock" filesystem should look like when any of the +// `fs` APIs are used. +let mockFiles = Object.create(null); + +// eslint-disable-next-line no-underscore-dangle, @typescript-eslint/naming-convention +export function __setMockFiles(newMockFiles: Record) { + mockFiles = newMockFiles; +} + +export const readFileSync = (filePath = '') => mockFiles[filePath]; +export const existsSync = (filePath: string) => !!mockFiles[filePath]; +export const lstatSync = (filePath: string) => ({ + isFile: () => !!mockFiles[filePath], +}); +export const realpathSync = vi.fn(); +export const readdir = vi.fn(); +export const readdirSync = vi.fn(); +export const readlinkSync = vi.fn(); + +export default { + __setMockFiles, + readFileSync, + existsSync, + lstatSync, + realpathSync, + readdir, + readdirSync, + readlinkSync, +}; diff --git a/code/__mocks__/fs/promises.ts b/code/__mocks__/fs/promises.ts new file mode 100644 index 000000000000..d6673a26e648 --- /dev/null +++ b/code/__mocks__/fs/promises.ts @@ -0,0 +1,32 @@ +import { vi } from 'vitest'; + +// This is a custom function that our tests can use during setup to specify +// what the files on the "mock" filesystem should look like when any of the +// `fs` APIs are used. +let mockFiles = Object.create(null); + +// eslint-disable-next-line no-underscore-dangle, @typescript-eslint/naming-convention +export function __setMockFiles(newMockFiles: Record) { + mockFiles = newMockFiles; +} + +export const writeFile = vi.fn(async (filePath: string, content: string) => { + mockFiles[filePath] = content; +}); +export const readFile = vi.fn(async (filePath: string) => mockFiles[filePath]); +export const lstat = vi.fn(async (filePath: string) => ({ + isFile: () => !!mockFiles[filePath], +})); +export const readdir = vi.fn(); +export const readlink = vi.fn(); +export const realpath = vi.fn(); + +export default { + __setMockFiles, + writeFile, + readFile, + lstat, + readdir, + readlink, + realpath, +}; diff --git a/code/addons/docs/package.json b/code/addons/docs/package.json index d905b004df47..7e38a4d00383 100644 --- a/code/addons/docs/package.json +++ b/code/addons/docs/package.json @@ -103,7 +103,6 @@ "@storybook/global": "^5.0.0", "@storybook/react-dom-shim": "workspace:*", "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "fs-extra": "^11.1.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", "rehype-external-links": "^3.0.0", diff --git a/code/addons/interactions/package.json b/code/addons/interactions/package.json index dd30fb2650d4..9deb3a01105f 100644 --- a/code/addons/interactions/package.json +++ b/code/addons/interactions/package.json @@ -62,6 +62,7 @@ "@devtools-ds/object-inspector": "^1.1.2", "@storybook/icons": "^1.2.5", "@types/node": "^22.0.0", + "ansi-to-html": "^0.7.2", "formik": "^2.2.9", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/code/addons/interactions/src/components/Interaction.tsx b/code/addons/interactions/src/components/Interaction.tsx index c9b557cddc68..610d0c1b032b 100644 --- a/code/addons/interactions/src/components/Interaction.tsx +++ b/code/addons/interactions/src/components/Interaction.tsx @@ -8,7 +8,7 @@ import { type Call, CallStates, type ControlStates } from '@storybook/instrument import { transparentize } from 'polished'; -import { isChaiError, isJestError } from '../utils'; +import { isChaiError, isJestError, useAnsiToHtmlFilter } from '../utils'; import type { Controls } from './InteractionsPanel'; import { MatcherResult } from './MatcherResult'; import { MethodCall } from './MethodCall'; @@ -116,6 +116,7 @@ const RowMessage = styled('div')(({ theme }) => ({ })); export const Exception = ({ exception }: { exception: Call['exception'] }) => { + const filter = useAnsiToHtmlFilter(); if (isJestError(exception)) { return ; } @@ -135,7 +136,7 @@ export const Exception = ({ exception }: { exception: Call['exception'] }) => { const more = paragraphs.length > 1; return ( -
{paragraphs[0]}
+

       {more && 

See the full stack trace in the browser console.

}
); diff --git a/code/addons/interactions/src/components/InteractionsPanel.tsx b/code/addons/interactions/src/components/InteractionsPanel.tsx index c86a1df8ffde..643732f250ca 100644 --- a/code/addons/interactions/src/components/InteractionsPanel.tsx +++ b/code/addons/interactions/src/components/InteractionsPanel.tsx @@ -6,7 +6,7 @@ import { type Call, CallStates, type ControlStates } from '@storybook/instrument import { transparentize } from 'polished'; -import { isTestAssertionError } from '../utils'; +import { isTestAssertionError, useAnsiToHtmlFilter } from '../utils'; import { Empty } from './EmptyState'; import { Interaction } from './Interaction'; import { Subnav } from './Subnav'; @@ -97,6 +97,7 @@ export const InteractionsPanel: React.FC = React.memo( onScrollToEnd, endRef, }) { + const filter = useAnsiToHtmlFilter(); return ( {(interactions.length > 0 || hasException) && ( @@ -131,9 +132,12 @@ export const InteractionsPanel: React.FC = React.memo( Caught exception in play function - - {printSerializedError(caughtException)} - + )} {unhandledErrors && ( diff --git a/code/addons/interactions/src/components/MatcherResult.tsx b/code/addons/interactions/src/components/MatcherResult.tsx index beae8b2146ff..46b5e540ad8d 100644 --- a/code/addons/interactions/src/components/MatcherResult.tsx +++ b/code/addons/interactions/src/components/MatcherResult.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { styled, typography } from 'storybook/internal/theming'; +import { useAnsiToHtmlFilter } from '../utils'; import { Node } from './MethodCall'; const getParams = (line: string, fromIndex = 0): string => { @@ -59,6 +60,7 @@ export const MatcherResult = ({ message: string; style?: React.CSSProperties; }) => { + const filter = useAnsiToHtmlFilter(); const lines = message.split('\n'); return (
{line}, 
]; + return [ + , +
, + ]; })}
); diff --git a/code/addons/interactions/src/utils.ts b/code/addons/interactions/src/utils.ts index 1b08eca12a24..d80d9f4cdbee 100644 --- a/code/addons/interactions/src/utils.ts +++ b/code/addons/interactions/src/utils.ts @@ -1,3 +1,7 @@ +import { type StorybookTheme, useTheme } from 'storybook/internal/theming'; + +import Filter from 'ansi-to-html'; + export function isTestAssertionError(error: unknown) { return isChaiError(error) || isJestError(error); } @@ -21,3 +25,15 @@ export function isJestError(error: unknown) { error.message.startsWith('expect(') ); } + +export function createAnsiToHtmlFilter(theme: StorybookTheme) { + return new Filter({ + fg: theme.color.defaultText, + bg: theme.background.content, + }); +} + +export function useAnsiToHtmlFilter() { + const theme = useTheme(); + return createAnsiToHtmlFilter(theme); +} diff --git a/code/addons/links/package.json b/code/addons/links/package.json index c11fa3e056b3..ca95baea88ac 100644 --- a/code/addons/links/package.json +++ b/code/addons/links/package.json @@ -71,7 +71,6 @@ "ts-dedent": "^2.0.0" }, "devDependencies": { - "fs-extra": "^11.1.0", "typescript": "^5.3.2" }, "peerDependencies": { diff --git a/code/addons/links/scripts/fix-preview-api-reference.ts b/code/addons/links/scripts/fix-preview-api-reference.ts index 71ca667999e2..90d54704768d 100644 --- a/code/addons/links/scripts/fix-preview-api-reference.ts +++ b/code/addons/links/scripts/fix-preview-api-reference.ts @@ -1,4 +1,4 @@ -import { readFile, writeFile } from 'fs-extra'; +import { readFile, writeFile } from 'node:fs/promises'; /* I wish this wasn't needed.. * There seems to be some bug in tsup / the unlaying lib that does DTS bundling diff --git a/code/addons/themes/docs/api.md b/code/addons/themes/docs/api.md index 143178b00047..a45818ab724e 100644 --- a/code/addons/themes/docs/api.md +++ b/code/addons/themes/docs/api.md @@ -148,7 +148,7 @@ export const myCustomDecorator = ({ themes, defaultState, ...rest }) => { Let's use Vuetify as an example. Vuetify uses it's own global state to know which theme to render. To build a custom decorator to accommodate this method we'll need to do the following ```js -// .storybook/withVeutifyTheme.decorator.js +// .storybook/withVuetifyTheme.decorator.js import { DecoratorHelpers } from '@storybook/addon-themes'; import { useTheme } from 'vuetify'; diff --git a/code/addons/viewport/src/defaults.ts b/code/addons/viewport/src/defaults.ts index 4190ccb7f3cc..d29e380ac230 100644 --- a/code/addons/viewport/src/defaults.ts +++ b/code/addons/viewport/src/defaults.ts @@ -1,6 +1,6 @@ import type { ViewportMap } from './types'; -export const INITIAL_VIEWPORTS: ViewportMap = { +const INITIAL_VIEWPORTS_DATA = { iphone5: { name: 'iPhone 5', styles: { @@ -225,7 +225,12 @@ export const INITIAL_VIEWPORTS: ViewportMap = { }, type: 'mobile', }, -}; +} as const; + +export type InitialViewportKeys = keyof typeof INITIAL_VIEWPORTS_DATA; + +export const INITIAL_VIEWPORTS: ViewportMap = INITIAL_VIEWPORTS_DATA; + export const DEFAULT_VIEWPORT = 'responsive'; export const MINIMAL_VIEWPORTS: ViewportMap = { diff --git a/code/builders/builder-vite/package.json b/code/builders/builder-vite/package.json index 5c8bd9b47ea1..94fde0bf8cfb 100644 --- a/code/builders/builder-vite/package.json +++ b/code/builders/builder-vite/package.json @@ -49,7 +49,6 @@ "es-module-lexer": "^1.5.0", "express": "^4.19.2", "find-cache-dir": "^3.0.0", - "fs-extra": "^11.1.0", "magic-string": "^0.30.0", "ts-dedent": "^2.0.0" }, diff --git a/code/builders/builder-vite/src/codegen-modern-iframe-script.ts b/code/builders/builder-vite/src/codegen-modern-iframe-script.ts index 7ad50afec12f..4abf4c22f315 100644 --- a/code/builders/builder-vite/src/codegen-modern-iframe-script.ts +++ b/code/builders/builder-vite/src/codegen-modern-iframe-script.ts @@ -27,7 +27,7 @@ export async function generateModernIframeScriptCode(options: Options, projectRo .map( (previewAnnotation, index) => // Prefer the updated module from an HMR update, otherwise import the original module - `hmrPreviewAnnotationModules.at(${index}) ?? import('${previewAnnotation}')` + `hmrPreviewAnnotationModules[${index}] ?? import('${previewAnnotation}')` ) .join(',\n')}]) return composeConfigs(configs); diff --git a/code/builders/builder-vite/src/index.ts b/code/builders/builder-vite/src/index.ts index a8a775a13344..0962e1676e4e 100644 --- a/code/builders/builder-vite/src/index.ts +++ b/code/builders/builder-vite/src/index.ts @@ -1,4 +1,5 @@ // noinspection JSUnusedGlobalSymbols +import { cp, readFile } from 'node:fs/promises'; import { join, parse } from 'node:path'; import { NoStatsForViteDevError } from 'storybook/internal/server-errors'; @@ -6,7 +7,6 @@ import type { Options } from 'storybook/internal/types'; import type { RequestHandler } from 'express'; import express from 'express'; -import * as fs from 'fs-extra'; import { corePath } from 'storybook/core-path'; import type { ViteDevServer } from 'vite'; @@ -34,10 +34,9 @@ function iframeMiddleware(options: Options, server: ViteDevServer): RequestHandl return; } - const indexHtml = await fs.readFile( - require.resolve('@storybook/builder-vite/input/iframe.html'), - 'utf-8' - ); + const indexHtml = await readFile(require.resolve('@storybook/builder-vite/input/iframe.html'), { + encoding: 'utf8', + }); const generated = await transformIframeHtml(indexHtml, options); const transformed = await server.transformIndexHtml('/iframe.html', generated); res.setHeader('Content-Type', 'text/html'); @@ -85,7 +84,7 @@ export const build: ViteBuilder['build'] = async ({ options }) => { const previewDirOrigin = previewResolvedDir; const previewDirTarget = join(options.outputDir || '', `sb-preview`); - const previewFiles = fs.copy(previewDirOrigin, previewDirTarget, { + const previewFiles = cp(previewDirOrigin, previewDirTarget, { filter: (src) => { const { ext } = parse(src); if (ext) { @@ -93,6 +92,7 @@ export const build: ViteBuilder['build'] = async ({ options }) => { } return true; }, + recursive: true, }); const [out] = await Promise.all([viteCompilation, previewFiles]); diff --git a/code/builders/builder-vite/src/optimizeDeps.ts b/code/builders/builder-vite/src/optimizeDeps.ts index 0e81d7d5afc4..cbade7a268a6 100644 --- a/code/builders/builder-vite/src/optimizeDeps.ts +++ b/code/builders/builder-vite/src/optimizeDeps.ts @@ -32,6 +32,7 @@ const INCLUDE_CANDIDATES = [ 'fast-deep-equal', 'html-tags', 'isobject', + 'jsdoc-type-pratt-parser', // TODO: Remove this once it's converted to ESM: https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/173 'loader-utils', 'lodash/camelCase.js', 'lodash/camelCase', diff --git a/code/builders/builder-vite/src/plugins/external-globals-plugin.ts b/code/builders/builder-vite/src/plugins/external-globals-plugin.ts index a71f9f3b0fe8..d6767372a64f 100644 --- a/code/builders/builder-vite/src/plugins/external-globals-plugin.ts +++ b/code/builders/builder-vite/src/plugins/external-globals-plugin.ts @@ -1,8 +1,9 @@ -import { join } from 'node:path'; +import { existsSync } from 'node:fs'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; import { init, parse } from 'es-module-lexer'; import findCacheDirectory from 'find-cache-dir'; -import { ensureFile, writeFile } from 'fs-extra'; import MagicString from 'magic-string'; import type { Alias, Plugin } from 'vite'; @@ -59,7 +60,10 @@ export async function externalGlobalsPlugin(externals: Record) { (Object.keys(externals) as Array).map(async (externalKey) => { const externalCachePath = join(cachePath, `${externalKey}.js`); newAlias.push({ find: new RegExp(`^${externalKey}$`), replacement: externalCachePath }); - await ensureFile(externalCachePath); + if (!existsSync(externalCachePath)) { + const directory = dirname(externalCachePath); + await mkdir(directory, { recursive: true }); + } await writeFile(externalCachePath, `module.exports = ${externals[externalKey]};`); }) ); diff --git a/code/builders/builder-webpack5/package.json b/code/builders/builder-webpack5/package.json index 913991d99689..2d4c5330bd01 100644 --- a/code/builders/builder-webpack5/package.json +++ b/code/builders/builder-webpack5/package.json @@ -74,7 +74,6 @@ "es-module-lexer": "^1.5.0", "express": "^4.19.2", "fork-ts-checker-webpack-plugin": "^8.0.0", - "fs-extra": "^11.1.0", "html-webpack-plugin": "^5.5.0", "magic-string": "^0.30.5", "path-browserify": "^1.0.1", diff --git a/code/builders/builder-webpack5/src/index.ts b/code/builders/builder-webpack5/src/index.ts index f9a83f8efaf2..ffc11812562b 100644 --- a/code/builders/builder-webpack5/src/index.ts +++ b/code/builders/builder-webpack5/src/index.ts @@ -1,3 +1,4 @@ +import { cp } from 'node:fs/promises'; import { join, parse } from 'node:path'; import { PREVIEW_BUILDER_PROGRESS } from 'storybook/internal/core-events'; @@ -12,7 +13,6 @@ import type { Builder, Options } from 'storybook/internal/types'; import { checkWebpackVersion } from '@storybook/core-webpack'; import express from 'express'; -import fs from 'fs-extra'; import prettyTime from 'pretty-hrtime'; import { corePath } from 'storybook/core-path'; import type { Configuration, Stats, StatsOptions } from 'webpack'; @@ -292,7 +292,7 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, const previewDirOrigin = previewResolvedDir; const previewDirTarget = join(options.outputDir || '', `sb-preview`); - const previewFiles = fs.copy(previewDirOrigin, previewDirTarget, { + const previewFiles = cp(previewDirOrigin, previewDirTarget, { filter: (src) => { const { ext } = parse(src); if (ext) { @@ -300,6 +300,7 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, } return true; }, + recursive: true, }); const [webpackCompilationOutput] = await Promise.all([webpackCompilation, previewFiles]); diff --git a/code/core/package.json b/code/core/package.json index 181b04312985..893e2c0234f2 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -279,10 +279,12 @@ "dependencies": { "@storybook/csf": "^0.1.11", "@types/express": "^4.17.21", + "better-opn": "^3.0.2", "browser-assert": "^1.2.1", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0", "esbuild-register": "^3.5.0", "express": "^4.19.2", + "jsdoc-type-pratt-parser": "^4.0.0", "process": "^0.11.10", "recast": "^0.23.5", "semver": "^7.6.2", @@ -320,7 +322,6 @@ "@types/diff": "^5.0.9", "@types/ejs": "^3.1.1", "@types/find-cache-dir": "^5.0.0", - "@types/fs-extra": "^11.0.1", "@types/js-yaml": "^4.0.5", "@types/lodash": "^4.14.167", "@types/node": "^22.0.0", @@ -340,7 +341,6 @@ "ansi-to-html": "^0.7.2", "assert": "^2.1.0", "babel-plugin-react-docgen": "4.2.1", - "better-opn": "^3.0.2", "boxen": "^7.1.1", "browser-dtector": "^3.4.0", "camelcase": "^8.0.0", @@ -371,14 +371,13 @@ "find-cache-dir": "^5.0.0", "find-up": "^7.0.0", "flush-promises": "^1.0.2", - "fs-extra": "^11.1.0", "fuse.js": "^3.6.1", "get-npm-tarball-url": "^2.0.3", "glob": "^10.0.0", "globby": "^14.0.1", "handlebars": "^4.7.7", + "jiti": "^1.21.6", "js-yaml": "^4.1.0", - "jsdoc-type-pratt-parser": "^4.0.0", "lazy-universal-dotenv": "^4.0.0", "leven": "^4.0.0", "lodash": "^4.17.21", diff --git a/code/core/scripts/helpers/dependencies.ts b/code/core/scripts/helpers/dependencies.ts index 82b22a022ad4..89e67fc554f3 100644 --- a/code/core/scripts/helpers/dependencies.ts +++ b/code/core/scripts/helpers/dependencies.ts @@ -1,7 +1,6 @@ +import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { readJson } from 'fs-extra'; - export async function flattenDependencies( list: string[], output: string[] = [], @@ -18,7 +17,9 @@ export async function flattenDependencies( console.log(dep + ' not found'); return; } - const { dependencies = {}, peerDependencies = {} } = await readJson(path); + const { dependencies = {}, peerDependencies = {} } = JSON.parse( + await readFile(path, { encoding: 'utf8' }) + ); const all: string[] = [ ...new Set([...Object.keys(dependencies), ...Object.keys(peerDependencies)]), ] diff --git a/code/core/scripts/helpers/generatePackageJsonFile.ts b/code/core/scripts/helpers/generatePackageJsonFile.ts index 9ecf2f7679e5..271f775cba23 100644 --- a/code/core/scripts/helpers/generatePackageJsonFile.ts +++ b/code/core/scripts/helpers/generatePackageJsonFile.ts @@ -1,7 +1,6 @@ -import { writeFile } from 'node:fs/promises'; +import { readFile, writeFile } from 'node:fs/promises'; import { join, relative } from 'node:path'; -import { readJSON } from 'fs-extra'; import slash from 'slash'; import { sortPackageJson } from '../../../../scripts/prepare/tools'; @@ -11,7 +10,7 @@ const cwd = process.cwd(); export async function generatePackageJsonFile(entries: ReturnType) { const location = join(cwd, 'package.json'); - const pkgJson = await readJSON(location); + const pkgJson = JSON.parse(await readFile(location, { encoding: 'utf8' })); /** * Re-create the `exports` field in `code/core/package.json` This way we only need to update the diff --git a/code/core/scripts/helpers/generateTypesMapperFiles.ts b/code/core/scripts/helpers/generateTypesMapperFiles.ts index 2f79f80643d8..5283321c84c0 100644 --- a/code/core/scripts/helpers/generateTypesMapperFiles.ts +++ b/code/core/scripts/helpers/generateTypesMapperFiles.ts @@ -1,7 +1,6 @@ -import { writeFile } from 'node:fs/promises'; -import { join, relative } from 'node:path'; - -import { ensureFile } from 'fs-extra'; +import { existsSync } from 'node:fs'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { dirname, join, relative } from 'node:path'; import { dedent } from '../../../../scripts/prepare/tools'; import type { getEntries } from '../entries'; @@ -34,7 +33,10 @@ export async function generateTypesMapperFiles(entries: ReturnType { const location = filePath.replace('src', 'dist').replace(/\.tsx?/, '.d.ts'); - await ensureFile(location); + if (!existsSync(location)) { + const directory = dirname(location); + await mkdir(directory, { recursive: true }); + } await writeFile(location, await generateTypesMapperContent(filePath)); }) ); diff --git a/code/core/scripts/prep.ts b/code/core/scripts/prep.ts index 49c2bd00cfa0..20c979ebd9aa 100644 --- a/code/core/scripts/prep.ts +++ b/code/core/scripts/prep.ts @@ -1,10 +1,8 @@ /* eslint-disable local-rules/no-uncategorized-errors */ -import { watch } from 'node:fs'; +import { existsSync, mkdirSync, watch } from 'node:fs'; import { mkdir, rm, writeFile } from 'node:fs/promises'; import { dirname, join } from 'node:path'; -import { ensureDir } from 'fs-extra'; - import { chalk, dedent, @@ -320,7 +318,9 @@ async function run() { const outName = keys.length === 1 ? dirname(keys[0]).replace('dist/', '') : `meta-${format}-${index}`; - await ensureDir('report'); + if (!existsSync('report')) { + mkdirSync('report'); + } await writeFile(`report/${outName}.json`, JSON.stringify(out.metafile, null, 2)); await writeFile( `report/${outName}.txt`, diff --git a/code/core/src/builder-manager/index.ts b/code/core/src/builder-manager/index.ts index 1e246b5d1cb1..b068caf206c7 100644 --- a/code/core/src/builder-manager/index.ts +++ b/code/core/src/builder-manager/index.ts @@ -1,3 +1,4 @@ +import { cp, rm, writeFile } from 'node:fs/promises'; import { dirname, join, parse } from 'node:path'; import { stringifyProcessEnvs } from '@storybook/core/common'; @@ -9,7 +10,6 @@ import { globalExternals } from '@fal-works/esbuild-plugin-global-externals'; import { pnpPlugin } from '@yarnpkg/esbuild-plugin-pnp'; import aliasPlugin from 'esbuild-plugin-alias'; import express from 'express'; -import fs from 'fs-extra'; import type { BuilderBuildResult, @@ -149,7 +149,7 @@ const starter: StarterFunction = async function* starterGeneratorFn({ // make sure we clear output directory of addons dir before starting // this could cause caching issues where addons are loaded when they shouldn't const addonsDir = config.outdir; - await fs.remove(addonsDir); + await rm(addonsDir, { recursive: true, force: true }); yield; @@ -256,7 +256,7 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, yield; - const managerFiles = fs.copy(coreDirOrigin, coreDirTarget, { + const managerFiles = cp(coreDirOrigin, coreDirTarget, { filter: (src) => { const { ext } = parse(src); if (ext) { @@ -264,6 +264,7 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, } return true; }, + recursive: true, }); const { cssFiles, jsFiles } = await readOrderedFiles(addonsDir, compilation?.outputFiles); @@ -288,11 +289,7 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime, globals ); - await Promise.all([ - // - fs.writeFile(join(options.outputDir, 'index.html'), html), - managerFiles, - ]); + await Promise.all([writeFile(join(options.outputDir, 'index.html'), html), managerFiles]); logger.trace({ message: '=> Manager built', time: process.hrtime(startTime) }); diff --git a/code/core/src/builder-manager/utils/files.ts b/code/core/src/builder-manager/utils/files.ts index 94ec31677c4a..352d558dd981 100644 --- a/code/core/src/builder-manager/utils/files.ts +++ b/code/core/src/builder-manager/utils/files.ts @@ -1,7 +1,8 @@ -import { join, normalize } from 'node:path'; +import { existsSync } from 'node:fs'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { dirname, join, normalize } from 'node:path'; import type { OutputFile } from 'esbuild'; -import fs from 'fs-extra'; import slash from 'slash'; import type { Compilation } from '../types'; @@ -15,8 +16,11 @@ export async function readOrderedFiles( // convert deeply nested paths to a single level, also remove special characters const { location, url } = sanitizePath(file, addonsDir); - await fs.ensureFile(location); - await fs.writeFile(location, file.contents); + if (!existsSync(location)) { + const directory = dirname(location); + await mkdir(directory, { recursive: true }); + } + await writeFile(location, file.contents); return url; }) || [] ); diff --git a/code/core/src/builder-manager/utils/managerEntries.ts b/code/core/src/builder-manager/utils/managerEntries.ts index d808596f52e4..787116d05c0e 100644 --- a/code/core/src/builder-manager/utils/managerEntries.ts +++ b/code/core/src/builder-manager/utils/managerEntries.ts @@ -1,8 +1,9 @@ -import { join, parse, relative, sep } from 'node:path'; +import { existsSync } from 'node:fs'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { dirname, join, parse, relative, sep } from 'node:path'; import { resolvePathInStorybookCache } from '@storybook/core/common'; -import fs from 'fs-extra'; import slash from 'slash'; const sanitizeBase = (path: string) => { @@ -55,8 +56,11 @@ export async function wrapManagerEntries(entrypoints: string[], uniqueId?: strin sanitizeFinal(join(`${sanitizeBase(base)}-${i}`, `${sanitizeBase(name)}-bundle.js`)) ); - await fs.ensureFile(location); - await fs.writeFile(location, `import '${slash(entry)}';`); + if (!existsSync(location)) { + const directory = dirname(location); + await mkdir(directory, { recursive: true }); + } + await writeFile(location, `import '${slash(entry)}';`); return location; }) diff --git a/code/core/src/builder-manager/utils/template.ts b/code/core/src/builder-manager/utils/template.ts index b264a3d5e02e..1ca99e07e2e6 100644 --- a/code/core/src/builder-manager/utils/template.ts +++ b/code/core/src/builder-manager/utils/template.ts @@ -1,9 +1,9 @@ +import { readFile } from 'node:fs/promises'; import { dirname, join } from 'node:path'; import type { DocsOptions, Options, Ref, TagsOptions } from '@storybook/core/types'; import { render } from 'ejs'; -import fs from 'fs-extra'; export const getTemplatePath = async (template: string) => { return join(dirname(require.resolve('@storybook/core/package.json')), 'assets/server', template); @@ -12,7 +12,7 @@ export const getTemplatePath = async (template: string) => { export const readTemplate = async (template: string) => { const path = await getTemplatePath(template); - return fs.readFile(path, 'utf8'); + return readFile(path, { encoding: 'utf8' }); }; export async function getManagerMainTemplate() { diff --git a/code/core/src/cli/detect.test.ts b/code/core/src/cli/detect.test.ts index f12ece4314fa..95c1b126dca3 100644 --- a/code/core/src/cli/detect.test.ts +++ b/code/core/src/cli/detect.test.ts @@ -26,10 +26,6 @@ vi.mock('fs', () => ({ default: vi.fn(), })); -vi.mock('fs-extra', () => ({ - pathExistsSync: vi.fn(() => true), -})); - vi.mock('@storybook/core/node-logger'); const MOCK_FRAMEWORK_FILES: { diff --git a/code/core/src/cli/eslintPlugin.ts b/code/core/src/cli/eslintPlugin.ts index 4b8d47b293a2..fafb91b2652a 100644 --- a/code/core/src/cli/eslintPlugin.ts +++ b/code/core/src/cli/eslintPlugin.ts @@ -1,4 +1,5 @@ import { existsSync } from 'node:fs'; +import { readFile, writeFile } from 'node:fs/promises'; import type { JsPackageManager } from '@storybook/core/common'; import { paddedLog } from '@storybook/core/common'; @@ -7,7 +8,6 @@ import { readConfig, writeConfig } from '@storybook/core/csf-tools'; import chalk from 'chalk'; import detectIndent from 'detect-indent'; -import { readFile, readJson, writeJson } from 'fs-extra'; import prompts from 'prompts'; import { dedent } from 'ts-dedent'; @@ -72,13 +72,15 @@ export async function configureEslintPlugin( if (eslintFile) { paddedLog(`Configuring Storybook ESLint plugin at ${eslintFile}`); if (eslintFile.endsWith('json')) { - const eslintConfig = (await readJson(eslintFile)) as { extends?: string[] }; + const eslintConfig = JSON.parse(await readFile(eslintFile, { encoding: 'utf8' })) as { + extends?: string[]; + }; const existingExtends = normalizeExtends(eslintConfig.extends).filter(Boolean); eslintConfig.extends = [...existingExtends, 'plugin:storybook/recommended'] as string[]; - const eslintFileContents = await readFile(eslintFile, 'utf8'); + const eslintFileContents = await readFile(eslintFile, { encoding: 'utf8' }); const spaces = detectIndent(eslintFileContents).amount || 2; - await writeJson(eslintFile, eslintConfig, { spaces }); + await writeFile(eslintFile, JSON.stringify(eslintConfig, undefined, spaces)); } else { const eslint = await readConfig(eslintFile); const existingExtends = normalizeExtends(eslint.getFieldValue(['extends'])).filter(Boolean); diff --git a/code/core/src/cli/helpers.test.ts b/code/core/src/cli/helpers.test.ts index cff797a8f505..4615f0cfdd84 100644 --- a/code/core/src/cli/helpers.test.ts +++ b/code/core/src/cli/helpers.test.ts @@ -1,8 +1,10 @@ +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; + import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { JsPackageManager } from '@storybook/core/common'; -import fse from 'fs-extra'; import { sep } from 'path'; import { IS_WINDOWS } from '../../../vitest.helpers'; @@ -13,21 +15,18 @@ import { SupportedLanguage } from './project_types'; const normalizePath = (path: string) => (IS_WINDOWS ? path.replace(/\//g, sep) : path); const fsMocks = vi.hoisted(() => ({ + cpSync: vi.fn(() => ({})), existsSync: vi.fn(), })); -const fseMocks = vi.hoisted(() => ({ - copySync: vi.fn(() => ({})), - copy: vi.fn(() => ({})), - ensureDir: vi.fn(() => {}), - existsSync: vi.fn(), - pathExists: vi.fn(), +const fspMocks = vi.hoisted(() => ({ + cp: vi.fn(() => ({})), readFile: vi.fn(() => ''), writeFile: vi.fn(), })); -vi.mock('fs', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, ...fsMocks, @@ -42,14 +41,14 @@ vi.mock('./dirs', () => ({ normalizePath(`@storybook/${renderer}`), })); -vi.mock('fs-extra', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('node:fs/promises', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, - ...fseMocks, + ...fspMocks, default: { ...actual, - ...fseMocks, + ...fspMocks, }, }; }); @@ -83,7 +82,7 @@ describe('Helpers', () => { helpers.copyTemplate(''); - expect(fse.copySync).toHaveBeenCalledWith( + expect(fs.cpSync).toHaveBeenCalledWith( expect.stringMatching(csfDirectory), expect.anything(), expect.anything() @@ -115,7 +114,7 @@ describe('Helpers', () => { const componentsDirectory = exists.map((folder: string) => normalizePath(`@storybook/react/template/cli/${folder}`) ); - fseMocks.pathExists.mockImplementation( + fsMocks.existsSync.mockImplementation( (filePath) => componentsDirectory.includes(filePath) || filePath === normalizePath('@storybook/react/template/cli') @@ -127,7 +126,7 @@ describe('Helpers', () => { commonAssetsDir: normalizePath('create-storybook/rendererAssets/common'), }); - expect(fse.copy).toHaveBeenNthCalledWith( + expect(fsp.cp).toHaveBeenNthCalledWith( 1, normalizePath('create-storybook/rendererAssets/common'), './stories', @@ -135,17 +134,12 @@ describe('Helpers', () => { ); const expectedDirectory = normalizePath(`@storybook/react/template/cli${expected}`); - expect(fse.copy).toHaveBeenNthCalledWith( - 2, - expectedDirectory, - './stories', - expect.anything() - ); + expect(fsp.cp).toHaveBeenNthCalledWith(2, expectedDirectory, './stories', expect.anything()); } ); it(`should copy to src folder when exists`, async () => { - vi.mocked(fse.pathExists).mockImplementation((filePath) => { + vi.mocked(fs.existsSync).mockImplementation((filePath) => { return filePath === normalizePath('@storybook/react/template/cli') || filePath === './src'; }); await helpers.copyTemplateFiles({ @@ -153,11 +147,11 @@ describe('Helpers', () => { language: SupportedLanguage.JAVASCRIPT, packageManager: packageManagerMock, }); - expect(fse.copy).toHaveBeenCalledWith(expect.anything(), './src/stories', expect.anything()); + expect(fsp.cp).toHaveBeenCalledWith(expect.anything(), './src/stories', expect.anything()); }); it(`should copy to root folder when src doesn't exist`, async () => { - vi.mocked(fse.pathExists).mockImplementation((filePath) => { + vi.mocked(fs.existsSync).mockImplementation((filePath) => { return filePath === normalizePath('@storybook/react/template/cli'); }); await helpers.copyTemplateFiles({ @@ -165,7 +159,7 @@ describe('Helpers', () => { language: SupportedLanguage.JAVASCRIPT, packageManager: packageManagerMock, }); - expect(fse.copy).toHaveBeenCalledWith(expect.anything(), './stories', expect.anything()); + expect(fsp.cp).toHaveBeenCalledWith(expect.anything(), './stories', expect.anything()); }); it(`should throw an error for unsupported renderer`, async () => { diff --git a/code/core/src/cli/helpers.ts b/code/core/src/cli/helpers.ts index 355e142cece1..5f567ebe5cbc 100644 --- a/code/core/src/cli/helpers.ts +++ b/code/core/src/cli/helpers.ts @@ -1,4 +1,5 @@ -import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { cpSync, existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { cp, readFile, writeFile } from 'node:fs/promises'; import { join, resolve } from 'node:path'; import { @@ -12,7 +13,6 @@ import type { SupportedFrameworks, SupportedRenderers } from '@storybook/core/ty import chalk from 'chalk'; import { findUpSync } from 'find-up'; -import { copy, copySync, pathExists, readFile, writeFile } from 'fs-extra'; import { coerce, satisfies } from 'semver'; import stripJsonComments from 'strip-json-comments'; import invariant from 'tiny-invariant'; @@ -130,7 +130,7 @@ export function copyTemplate(templateRoot: string, destination = '.') { throw new Error(`Couldn't find template dir`); } - copySync(templateDir, destination, { overwrite: true }); + cpSync(templateDir, destination, { recursive: true }); } type CopyTemplateFilesOptions = { @@ -197,30 +197,30 @@ export async function copyTemplateFiles({ const assetsTS38 = join(assetsDir, languageFolderMapping[SupportedLanguage.TYPESCRIPT_3_8]); // Ideally use the assets that match the language & version. - if (await pathExists(assetsLanguage)) { + if (existsSync(assetsLanguage)) { return assetsLanguage; } // Use fallback typescript 3.8 assets if new ones aren't available - if (language === SupportedLanguage.TYPESCRIPT_4_9 && (await pathExists(assetsTS38))) { + if (language === SupportedLanguage.TYPESCRIPT_4_9 && existsSync(assetsTS38)) { return assetsTS38; } // Fallback further to TS (for backwards compatibility purposes) - if (await pathExists(assetsTS)) { + if (existsSync(assetsTS)) { return assetsTS; } // Fallback further to JS - if (await pathExists(assetsJS)) { + if (existsSync(assetsJS)) { return assetsJS; } // As a last resort, look for the root of the asset directory - if (await pathExists(assetsDir)) { + if (existsSync(assetsDir)) { return assetsDir; } throw new Error(`Unsupported renderer: ${renderer} (${baseDir})`); }; const targetPath = async () => { - if (await pathExists('./src')) { + if (existsSync('./src')) { return './src/stories'; } return './stories'; @@ -228,11 +228,11 @@ export async function copyTemplateFiles({ const destinationPath = destination ?? (await targetPath()); if (commonAssetsDir) { - await copy(commonAssetsDir, destinationPath, { - overwrite: true, + await cp(commonAssetsDir, destinationPath, { + recursive: true, }); } - await copy(await templatePath(), destinationPath, { overwrite: true }); + await cp(await templatePath(), destinationPath, { recursive: true }); if (commonAssetsDir) { let rendererType = frameworkToRenderer[renderer] || 'react'; @@ -248,7 +248,7 @@ export async function copyTemplateFiles({ export async function adjustTemplate(templatePath: string, templateData: Record) { // for now, we're just doing a simple string replace // in the future we might replace this with a proper templating engine - let template = await readFile(templatePath, 'utf8'); + let template = await readFile(templatePath, { encoding: 'utf8' }); Object.keys(templateData).forEach((key) => { template = template.replaceAll(`{{${key}}}`, `${templateData[key]}`); diff --git a/code/core/src/common/js-package-manager/JsPackageManager.test.ts b/code/core/src/common/js-package-manager/JsPackageManager.test.ts new file mode 100644 index 000000000000..153edd0be596 --- /dev/null +++ b/code/core/src/common/js-package-manager/JsPackageManager.test.ts @@ -0,0 +1,54 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { JsPackageManager } from './JsPackageManager'; + +vi.mock('../versions', () => ({ + default: { + '@storybook/react': '8.3.0', + }, +})); + +describe('JsPackageManager', () => { + let jsPackageManager: JsPackageManager; + let mockLatestVersion: ReturnType; + let mockStorybookPackagesVersions: Record; + + beforeEach(() => { + mockLatestVersion = vi.fn(); + mockStorybookPackagesVersions = { + '@storybook/react': '8.3.0', + }; + + // @ts-expect-error Ignore abstract class error + jsPackageManager = new JsPackageManager(); + jsPackageManager.latestVersion = mockLatestVersion; + + vi.clearAllMocks(); + }); + + describe('getVersionedPackages method', () => { + it('should return the latest stable release version when current version is the latest stable release', async () => { + mockLatestVersion.mockResolvedValue('8.3.0'); + + const result = await jsPackageManager.getVersionedPackages(['@storybook/react']); + + expect(result).toEqual(['@storybook/react@^8.3.0']); + }); + + it('should return the current version when it is not the latest stable release', async () => { + mockLatestVersion.mockResolvedValue('8.3.1'); + + const result = await jsPackageManager.getVersionedPackages(['@storybook/react']); + + expect(result).toEqual(['@storybook/react@8.3.0']); + }); + + it('should return the latest stable release version when there is no current version', async () => { + mockLatestVersion.mockResolvedValue('2.0.0'); + + const result = await jsPackageManager.getVersionedPackages(['@storybook/new-addon@^8.3.0']); + + expect(result).toEqual(['@storybook/new-addon@^2.0.0']); + }); + }); +}); diff --git a/code/core/src/common/js-package-manager/JsPackageManager.ts b/code/core/src/common/js-package-manager/JsPackageManager.ts index 4b3e00c2f5e7..1d1f2ef5367c 100644 --- a/code/core/src/common/js-package-manager/JsPackageManager.ts +++ b/code/core/src/common/js-package-manager/JsPackageManager.ts @@ -337,13 +337,13 @@ export abstract class JsPackageManager { const k = packageName as keyof typeof storybookPackagesVersions; const currentVersion = storybookPackagesVersions[k]; - if (currentVersion === latestInRange) { - return `${packageName}`; - } - if (currentVersion) { - return `${packageName}@${currentVersion}`; + const isLatestStableRelease = currentVersion === latestInRange; + + if (isLatestStableRelease || !currentVersion) { + return `${packageName}@^${latestInRange}`; } - return `${packageName}@^${latestInRange}`; + + return `${packageName}@${currentVersion}`; }) ); } @@ -418,7 +418,7 @@ export abstract class JsPackageManager { .find((version) => satisfies(version, constraint)); invariant( latestVersionSatisfyingTheConstraint != null, - 'No version satisfying the constraint.' + `No version satisfying the constraint: ${packageName}${constraint}` ); return latestVersionSatisfyingTheConstraint; } diff --git a/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts b/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts index 7477a1e5f2c0..4b3c6184e59f 100644 --- a/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts +++ b/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts @@ -125,17 +125,35 @@ export class JsPackageManagerFactory { } function hasNPM(cwd?: string) { - const npmVersionCommand = spawnSync('npm', ['--version'], { cwd, shell: true }); + const npmVersionCommand = spawnSync('npm', ['--version'], { + cwd, + shell: true, + env: { + COREPACK_ENABLE_STRICT: '0', + }, + }); return npmVersionCommand.status === 0; } function hasPNPM(cwd?: string) { - const pnpmVersionCommand = spawnSync('pnpm', ['--version'], { cwd, shell: true }); + const pnpmVersionCommand = spawnSync('pnpm', ['--version'], { + cwd, + shell: true, + env: { + COREPACK_ENABLE_STRICT: '0', + }, + }); return pnpmVersionCommand.status === 0; } function getYarnVersion(cwd?: string): 1 | 2 | undefined { - const yarnVersionCommand = spawnSync('yarn', ['--version'], { cwd, shell: true }); + const yarnVersionCommand = spawnSync('yarn', ['--version'], { + cwd, + shell: true, + env: { + COREPACK_ENABLE_STRICT: '0', + }, + }); if (yarnVersionCommand.status !== 0) { return undefined; diff --git a/code/core/src/common/js-package-manager/PNPMProxy.ts b/code/core/src/common/js-package-manager/PNPMProxy.ts index f8df230d0fe6..3f4073c6f35e 100644 --- a/code/core/src/common/js-package-manager/PNPMProxy.ts +++ b/code/core/src/common/js-package-manager/PNPMProxy.ts @@ -4,7 +4,6 @@ import { join } from 'node:path'; import { FindPackageVersionsError } from '@storybook/core/server-errors'; import { findUpSync } from 'find-up'; -import { pathExistsSync } from 'fs-extra'; import { dedent } from 'ts-dedent'; import { createLogStream } from '../utils/cli'; @@ -42,7 +41,7 @@ export class PNPMProxy extends JsPackageManager { const CWD = process.cwd(); const pnpmWorkspaceYaml = `${CWD}/pnpm-workspace.yaml`; - return pathExistsSync(pnpmWorkspaceYaml); + return existsSync(pnpmWorkspaceYaml); } async initPackageJson() { diff --git a/code/core/src/common/utils/cli.ts b/code/core/src/common/utils/cli.ts index 00b831133d2a..05f4704360ea 100644 --- a/code/core/src/common/utils/cli.ts +++ b/code/core/src/common/utils/cli.ts @@ -1,9 +1,9 @@ -import { realpath } from 'node:fs/promises'; +import type { WriteStream } from 'node:fs'; +import { createWriteStream, mkdirSync } from 'node:fs'; +import { readFile, realpath, rename, rm, writeFile } from 'node:fs/promises'; import os from 'node:os'; import { join } from 'node:path'; -import type { WriteStream } from 'fs-extra'; -import { createWriteStream, mkdirSync, move, readFile, remove, writeFile } from 'fs-extra'; import { type MergeExclusive } from 'type-fest'; import uniqueString from 'unique-string'; @@ -17,7 +17,7 @@ const getPath = async (prefix = '') => join(await tempDir(), prefix + uniqueStri export async function temporaryDirectory({ prefix = '' } = {}) { const directory = await getPath(prefix); - await mkdirSync(directory); + mkdirSync(directory); return directory; } @@ -146,12 +146,10 @@ export const createLogStream = async ( return new Promise((resolve, reject) => { logStream.once('open', () => { - const moveLogFile = async () => move(temporaryLogPath, finalLogPath, { overwrite: true }); + const moveLogFile = async () => rename(temporaryLogPath, finalLogPath); const clearLogFile = async () => writeFile(temporaryLogPath, ''); - const removeLogFile = async () => remove(temporaryLogPath); - const readLogFile = async () => { - return readFile(temporaryLogPath, 'utf8'); - }; + const removeLogFile = async () => rm(temporaryLogPath, { recursive: true, force: true }); + const readLogFile = async () => readFile(temporaryLogPath, { encoding: 'utf8' }); resolve({ logStream, moveLogFile, clearLogFile, removeLogFile, readLogFile }); }); logStream.once('error', reject); diff --git a/code/core/src/common/utils/get-storybook-info.ts b/code/core/src/common/utils/get-storybook-info.ts index af65546949eb..a7e887a87d63 100644 --- a/code/core/src/common/utils/get-storybook-info.ts +++ b/code/core/src/common/utils/get-storybook-info.ts @@ -1,10 +1,9 @@ +import { existsSync } from 'node:fs'; import { join } from 'node:path'; import type { SupportedFrameworks } from '@storybook/core/types'; import type { CoreCommon_StorybookInfo, PackageJson } from '@storybook/core/types'; -import { pathExistsSync } from 'fs-extra'; - import { getStorybookConfiguration } from './get-storybook-configuration'; export const rendererPackages: Record = { @@ -92,9 +91,7 @@ const validConfigExtensions = ['ts', 'js', 'tsx', 'jsx', 'mjs', 'cjs']; export const findConfigFile = (prefix: string, configDir: string) => { const filePrefix = join(configDir, prefix); - const extension = validConfigExtensions.find((ext: string) => - pathExistsSync(`${filePrefix}.${ext}`) - ); + const extension = validConfigExtensions.find((ext: string) => existsSync(`${filePrefix}.${ext}`)); return extension ? `${filePrefix}.${extension}` : null; }; diff --git a/code/core/src/common/utils/get-storybook-refs.ts b/code/core/src/common/utils/get-storybook-refs.ts index 053ae17abc93..4108ba969412 100644 --- a/code/core/src/common/utils/get-storybook-refs.ts +++ b/code/core/src/common/utils/get-storybook-refs.ts @@ -1,3 +1,4 @@ +import { readFile } from 'node:fs/promises'; import { dirname, join } from 'node:path'; import type { Options, Ref } from '@storybook/core/types'; @@ -5,7 +6,6 @@ import type { Options, Ref } from '@storybook/core/types'; import { logger } from '@storybook/core/node-logger'; import { findUp } from 'find-up'; -import { readJSON } from 'fs-extra'; import resolveFrom from 'resolve-from'; export const getAutoRefs = async (options: Options): Promise> => { @@ -15,7 +15,8 @@ export const getAutoRefs = async (options: Options): Promise } const directory = dirname(location); - const { dependencies = [], devDependencies = [] } = (await readJSON(location)) || {}; + const { dependencies = [], devDependencies = [] } = + JSON.parse(await readFile(location, { encoding: 'utf8' })) || {}; const deps = Object.keys({ ...dependencies, ...devDependencies }); const list = await Promise.all( @@ -23,7 +24,8 @@ export const getAutoRefs = async (options: Options): Promise try { const l = resolveFrom(directory, join(d, 'package.json')); - const { storybook, name, version } = (await readJSON(l)) || {}; + const { storybook, name, version } = + JSON.parse(await readFile(l, { encoding: 'utf8' })) || {}; if (storybook?.url) { return { id: name, ...storybook, version }; diff --git a/code/core/src/components/components/tooltip/ListItem.tsx b/code/core/src/components/components/tooltip/ListItem.tsx index 068ab88d59c2..8d50a05273de 100644 --- a/code/core/src/components/components/tooltip/ListItem.tsx +++ b/code/core/src/components/components/tooltip/ListItem.tsx @@ -169,7 +169,7 @@ const getItemProps = memoize(100)((onClick, href, LinkWrapper) => ({ export type LinkWrapperType = (props: any) => ReactNode; -export interface ListItemProps extends Omit, 'href' | 'title'> { +export interface ListItemProps extends Omit, 'title'> { loading?: boolean; title?: ReactNode; center?: ReactNode; diff --git a/code/core/src/components/components/tooltip/TooltipLinkList.tsx b/code/core/src/components/components/tooltip/TooltipLinkList.tsx index 51540335d034..f1467babec3a 100644 --- a/code/core/src/components/components/tooltip/TooltipLinkList.tsx +++ b/code/core/src/components/components/tooltip/TooltipLinkList.tsx @@ -22,7 +22,7 @@ export interface Link extends Omit { id: string; onClick?: ( event: SyntheticEvent, - item: Pick + item: Pick ) => void; } @@ -31,11 +31,11 @@ interface ItemProps extends Link { } const Item = ({ id, onClick, ...rest }: ItemProps) => { - const { active, disabled, title } = rest; + const { active, disabled, title, href } = rest; const handleClick = useCallback( - (event: SyntheticEvent) => onClick?.(event, { id, active, disabled, title }), - [onClick, id, active, disabled, title] + (event: SyntheticEvent) => onClick?.(event, { id, active, disabled, title, href }), + [onClick, id, active, disabled, title, href] ); return ; diff --git a/code/core/src/core-server/build-dev.ts b/code/core/src/core-server/build-dev.ts index 5c87a7d9ccd8..c8b336a22e09 100644 --- a/code/core/src/core-server/build-dev.ts +++ b/code/core/src/core-server/build-dev.ts @@ -1,3 +1,4 @@ +import { readFile } from 'node:fs/promises'; import { join, relative, resolve } from 'node:path'; import { @@ -18,7 +19,6 @@ import { global } from '@storybook/global'; import { deprecate } from '@storybook/core/node-logger'; import { MissingBuilderError, NoStatsForViteDevError } from '@storybook/core/server-errors'; -import { readFile } from 'fs-extra'; import prompts from 'prompts'; import invariant from 'tiny-invariant'; import { dedent } from 'ts-dedent'; @@ -156,7 +156,7 @@ export async function buildDevStandalone( if (/\.c[jt]s$/.test(mainJsPath)) { deprecate(deprecationMessage); } - const mainJsContent = await readFile(mainJsPath, 'utf-8'); + const mainJsContent = await readFile(mainJsPath, { encoding: 'utf8' }); // Regex that matches any CommonJS-specific syntax, stolen from Vite: https://github.com/vitejs/vite/blob/91a18c2f7da796ff8217417a4bf189ddda719895/packages/vite/src/node/ssr/ssrExternal.ts#L87 const CJS_CONTENT_REGEX = /\bmodule\.exports\b|\bexports[.[]|\brequire\s*\(|\bObject\.(?:defineProperty|defineProperties|assign)\s*\(\s*exports\b/; diff --git a/code/core/src/core-server/build-static.ts b/code/core/src/core-server/build-static.ts index 248b2746ea8c..45ba7c7bc6d3 100644 --- a/code/core/src/core-server/build-static.ts +++ b/code/core/src/core-server/build-static.ts @@ -1,3 +1,6 @@ +import { existsSync } from 'node:fs'; +import { cp, mkdir, readdir } from 'node:fs/promises'; +import { rm } from 'node:fs/promises'; import { dirname, join, relative, resolve } from 'node:path'; import { @@ -14,7 +17,6 @@ import { global } from '@storybook/global'; import { logger } from '@storybook/core/node-logger'; import chalk from 'chalk'; -import { copy, emptyDir, ensureDir } from 'fs-extra'; import { StoryIndexGenerator } from './utils/StoryIndexGenerator'; import { buildOrThrow } from './utils/build-or-throw'; @@ -43,8 +45,22 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption if (options.outputDir === '/') { throw new Error("Won't remove directory '/'. Check your outputDir!"); } - await emptyDir(options.outputDir); - await ensureDir(options.outputDir); + + try { + const outputDirFiles = await readdir(options.outputDir); + for (const file of outputDirFiles) { + await rm(file, { recursive: true, force: true }); + } + } catch { + await mkdir(options.outputDir, { recursive: true }); + } + + if (!existsSync(options.outputDir)) { + await mkdir(options.outputDir, { recursive: true }); + } else if ((await readdir(options.outputDir)).length > 0) { + await rm(options.outputDir, { recursive: true, force: true }); + await mkdir(options.outputDir, { recursive: true }); + } const config = await loadMainConfig(options); const { framework } = config; @@ -127,7 +143,7 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption dirname(require.resolve('@storybook/core/package.json')), 'assets/browser' ); - effects.push(copy(coreServerPublicDir, options.outputDir)); + effects.push(cp(coreServerPublicDir, options.outputDir, { recursive: true })); let initializedStoryIndexGenerator: Promise = Promise.resolve(undefined); diff --git a/code/core/src/core-server/presets/common-preset.ts b/code/core/src/core-server/presets/common-preset.ts index bca9a1c9cfe6..10f8be05614f 100644 --- a/code/core/src/core-server/presets/common-preset.ts +++ b/code/core/src/core-server/presets/common-preset.ts @@ -1,3 +1,5 @@ +import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; import { dirname, isAbsolute, join } from 'node:path'; import type { Channel } from '@storybook/core/channels'; @@ -21,7 +23,6 @@ import type { import { readCsf } from '@storybook/core/csf-tools'; import { logger } from '@storybook/core/node-logger'; -import { pathExists, readFile } from 'fs-extra'; import { dedent } from 'ts-dedent'; import { initCreateNewStoryChannel } from '../server-channel/create-new-story-channel'; @@ -75,14 +76,14 @@ export const favicon = async ( if (targetEndpoint === '/') { const url = 'favicon.svg'; const path = join(staticPath, url); - if (await pathExists(path)) { + if (existsSync(path)) { results.push(path); } } if (targetEndpoint === '/') { const url = 'favicon.ico'; const path = join(staticPath, url); - if (await pathExists(path)) { + if (existsSync(path)) { results.push(path); } } @@ -256,8 +257,8 @@ export const docs: PresetProperty<'docs'> = (docsOptions, { docs: docsMode }: CL export const managerHead = async (_: any, options: Options) => { const location = join(options.configDir, 'manager-head.html'); - if (await pathExists(location)) { - const contents = readFile(location, 'utf-8'); + if (existsSync(location)) { + const contents = readFile(location, { encoding: 'utf8' }); const interpolations = options.presets.apply>('env'); return interpolate(await contents, await interpolations); diff --git a/code/core/src/core-server/presets/favicon.test.ts b/code/core/src/core-server/presets/favicon.test.ts index 7af2bfbfb34d..4e6c72cc0618 100644 --- a/code/core/src/core-server/presets/favicon.test.ts +++ b/code/core/src/core-server/presets/favicon.test.ts @@ -1,11 +1,10 @@ +import * as fs from 'node:fs'; import { dirname, join } from 'node:path'; import { expect, it, vi } from 'vitest'; import { logger } from '@storybook/core/node-logger'; -import * as fs from 'fs-extra'; - import * as m from './common-preset'; const defaultFavicon = join( @@ -30,17 +29,6 @@ const createOptions = (locations: string[]): Parameters[1] => }, }); -vi.mock('fs-extra', () => { - return { - pathExists: vi.fn((p: string) => { - return false; - }), - existsSync: vi.fn((p: string) => { - return false; - }), - }; -}); - vi.mock('@storybook/core/node-logger', () => { return { logger: { @@ -49,7 +37,13 @@ vi.mock('@storybook/core/node-logger', () => { }; }); -const pathExists = vi.mocked(fs.pathExists); +vi.mock('node:fs', async (importOriginal) => ({ + ...(await importOriginal()), + existsSync: vi.fn((p: string) => { + return false; + }), +})); +const existsSyncMock = vi.mocked(fs.existsSync); it('with no staticDirs favicon should return default', async () => { const options = createOptions([]); @@ -59,7 +53,7 @@ it('with no staticDirs favicon should return default', async () => { it('with staticDirs containing a single favicon.ico should return the found favicon', async () => { const location = 'static'; - pathExists.mockImplementation((p: string) => { + existsSyncMock.mockImplementation((p: string | Buffer | URL) => { if (p === createPath(location)) { return true; } @@ -75,7 +69,7 @@ it('with staticDirs containing a single favicon.ico should return the found favi it('with staticDirs containing a single favicon.svg should return the found favicon', async () => { const location = 'static'; - pathExists.mockImplementation((p: string) => { + existsSyncMock.mockImplementation((p: string | Buffer | URL) => { if (p === createPath(location)) { return true; } @@ -91,7 +85,7 @@ it('with staticDirs containing a single favicon.svg should return the found favi it('with staticDirs containing a multiple favicons should return the first favicon and warn', async () => { const location = 'static'; - pathExists.mockImplementation((p: string) => { + existsSyncMock.mockImplementation((p: string | Buffer | URL) => { if (p === createPath(location)) { return true; } @@ -113,7 +107,7 @@ it('with staticDirs containing a multiple favicons should return the first favic it('with multiple staticDirs containing a multiple favicons should return the first favicon and warn', async () => { const locationA = 'static-a'; const locationB = 'static-b'; - pathExists.mockImplementation((p: string) => { + existsSyncMock.mockImplementation((p: string | Buffer | URL) => { if (p === createPath(locationA)) { return true; } diff --git a/code/core/src/core-server/utils/StoryIndexGenerator.ts b/code/core/src/core-server/utils/StoryIndexGenerator.ts index 0c5338debf88..fb2a0f4b887c 100644 --- a/code/core/src/core-server/utils/StoryIndexGenerator.ts +++ b/code/core/src/core-server/utils/StoryIndexGenerator.ts @@ -1,4 +1,6 @@ /* eslint-disable no-underscore-dangle */ +import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; import { dirname, extname, join, normalize, relative, resolve, sep } from 'node:path'; import { commonGlobOptions, normalizeStoryPath } from '@storybook/core/common'; @@ -23,7 +25,6 @@ import { sortStoriesV7, userOrAutoTitleFromSpecifier } from '@storybook/core/pre import chalk from 'chalk'; import { findUp } from 'find-up'; -import fs from 'fs-extra'; import slash from 'slash'; import invariant from 'tiny-invariant'; import { dedent } from 'ts-dedent'; @@ -324,7 +325,7 @@ export class StoryIndexGenerator { const absoluteComponentPath = resolve(dirname(absolutePath), rawPath); const existing = ['', '.js', '.ts', '.jsx', '.tsx', '.mjs', '.mts'] .map((ext) => `${absoluteComponentPath}${ext}`) - .find((candidate) => fs.existsSync(candidate)); + .find((candidate) => existsSync(candidate)); if (existing) { const relativePath = relative(this.options.workingDir, existing); return slash(normalizeStoryPath(relativePath)); @@ -432,7 +433,7 @@ export class StoryIndexGenerator { const normalizedPath = normalizeStoryPath(relativePath); const importPath = slash(normalizedPath); - const content = await fs.readFile(absolutePath, 'utf8'); + const content = await readFile(absolutePath, { encoding: 'utf8' }); const { analyze } = await import('@storybook/docs-mdx'); const result = await analyze(content); @@ -753,9 +754,9 @@ export class StoryIndexGenerator { async getPreviewCode() { const previewFile = ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'mts'] .map((ext) => join(this.options.configDir, `preview.${ext}`)) - .find((fname) => fs.existsSync(fname)); + .find((fname) => existsSync(fname)); - return previewFile && (await fs.readFile(previewFile, 'utf-8')).toString(); + return previewFile && (await readFile(previewFile, { encoding: 'utf8' })).toString(); } getProjectTags(previewCode?: string) { diff --git a/code/core/src/core-server/utils/__tests__/server-statics.test.ts b/code/core/src/core-server/utils/__tests__/server-statics.test.ts index ca532a8b04f9..0dfaea67f5df 100644 --- a/code/core/src/core-server/utils/__tests__/server-statics.test.ts +++ b/code/core/src/core-server/utils/__tests__/server-statics.test.ts @@ -1,19 +1,17 @@ +import fs from 'node:fs'; import { resolve } from 'node:path'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import fs from 'fs-extra'; - import { onlyWindows, skipWindows } from '../../../../../vitest.helpers'; import { parseStaticDir } from '../server-statics'; -vi.mock('fs-extra'); -const pathExistsMock = vi.mocked(fs.pathExists); +vi.mock('node:fs'); +const existsSyncMock = vi.mocked(fs.existsSync); describe('parseStaticDir', () => { beforeEach(() => { - // @ts-expect-error for some reason vitest does not match the return type with one of the overloads from pathExists - pathExistsMock.mockResolvedValue(true); + existsSyncMock.mockReturnValue(true); }); it('returns the static dir/path and default target', async () => { @@ -57,8 +55,7 @@ describe('parseStaticDir', () => { }); it('checks that the path exists', async () => { - // @ts-expect-error for some reason vitest does not match the return type with one of the overloads from pathExists - pathExistsMock.mockResolvedValue(false); + existsSyncMock.mockReturnValueOnce(false); await expect(parseStaticDir('nonexistent')).rejects.toThrow(resolve('nonexistent')); }); diff --git a/code/core/src/core-server/utils/copy-all-static-files.ts b/code/core/src/core-server/utils/copy-all-static-files.ts index bc9a515731ef..66a0194cfd94 100644 --- a/code/core/src/core-server/utils/copy-all-static-files.ts +++ b/code/core/src/core-server/utils/copy-all-static-files.ts @@ -1,3 +1,4 @@ +import { cp } from 'node:fs/promises'; import { join, relative } from 'node:path'; import { getDirectoryFromWorkingDir } from '@storybook/core/common'; @@ -5,7 +6,6 @@ import { getDirectoryFromWorkingDir } from '@storybook/core/common'; import { logger } from '@storybook/core/node-logger'; import chalk from 'chalk'; -import fs from 'fs-extra'; import { parseStaticDir } from './server-statics'; @@ -26,10 +26,11 @@ export async function copyAllStaticFiles(staticDirs: any[] | undefined, outputDi // Storybook's own files should not be overwritten, so we skip such files if we find them const skipPaths = ['index.html', 'iframe.html'].map((f) => join(targetPath, f)); - await fs.copy(staticPath, targetPath, { + await cp(staticPath, targetPath, { dereference: true, preserveTimestamps: true, filter: (_, dest) => !skipPaths.includes(dest), + recursive: true, }); } catch (e) { if (e instanceof Error) { @@ -68,10 +69,11 @@ export async function copyAllStaticFilesRelativeToMain( `=> Copying static files: ${chalk.cyan(print(from))} at ${chalk.cyan(print(targetPath))}` ); } - await fs.copy(from, targetPath, { + await cp(from, targetPath, { dereference: true, preserveTimestamps: true, filter: (_, dest) => !skipPaths.includes(dest), + recursive: true, }); }, Promise.resolve()); } diff --git a/code/core/src/core-server/utils/metadata.ts b/code/core/src/core-server/utils/metadata.ts index cc047d841768..a617b3e1f52c 100644 --- a/code/core/src/core-server/utils/metadata.ts +++ b/code/core/src/core-server/utils/metadata.ts @@ -1,12 +1,13 @@ +import { writeFile } from 'node:fs/promises'; + import { getStorybookMetadata } from '@storybook/core/telemetry'; import type { Request, Response, Router } from 'express'; -import { writeJSON } from 'fs-extra'; export async function extractStorybookMetadata(outputFile: string, configDir: string) { const storybookMetadata = await getStorybookMetadata(configDir); - await writeJSON(outputFile, storybookMetadata); + await writeFile(outputFile, JSON.stringify(storybookMetadata)); } export function useStorybookMetadata(router: Router, configDir?: string) { diff --git a/code/core/src/core-server/utils/output-stats.ts b/code/core/src/core-server/utils/output-stats.ts index c3a5b9f8aad9..1d5047862247 100644 --- a/code/core/src/core-server/utils/output-stats.ts +++ b/code/core/src/core-server/utils/output-stats.ts @@ -1,3 +1,4 @@ +import { createWriteStream } from 'node:fs'; import { join } from 'node:path'; import type { Stats } from '@storybook/core/types'; @@ -6,7 +7,6 @@ import { logger } from '@storybook/core/node-logger'; import { stringifyStream } from '@discoveryjs/json-ext'; import chalk from 'chalk'; -import fs from 'fs-extra'; export async function outputStats(directory: string, previewStats?: any, managerStats?: any) { if (previewStats) { @@ -25,7 +25,7 @@ export const writeStats = async (directory: string, name: string, stats: Stats) await new Promise((resolve, reject) => { stringifyStream(data, null, 2) .on('error', reject) - .pipe(fs.createWriteStream(filePath)) + .pipe(createWriteStream(filePath)) .on('error', reject) .on('finish', resolve); }); diff --git a/code/core/src/core-server/utils/server-init.ts b/code/core/src/core-server/utils/server-init.ts index b63d663e6857..60710cb19cfb 100644 --- a/code/core/src/core-server/utils/server-init.ts +++ b/code/core/src/core-server/utils/server-init.ts @@ -1,7 +1,8 @@ +import { readFile } from 'node:fs/promises'; + import { logger } from '@storybook/core/node-logger'; import type { Express } from 'express'; -import { readFile } from 'fs-extra'; import http from 'http'; import https from 'https'; @@ -29,9 +30,9 @@ export async function getServer( } const sslOptions = { - ca: await Promise.all((options.sslCa || []).map((ca) => readFile(ca, 'utf-8'))), - cert: await readFile(options.sslCert, 'utf-8'), - key: await readFile(options.sslKey, 'utf-8'), + ca: await Promise.all((options.sslCa || []).map((ca) => readFile(ca, { encoding: 'utf8' }))), + cert: await readFile(options.sslCert, { encoding: 'utf8' }), + key: await readFile(options.sslKey, { encoding: 'utf8' }), }; return https.createServer(sslOptions, app); diff --git a/code/core/src/core-server/utils/server-statics.ts b/code/core/src/core-server/utils/server-statics.ts index b10a573e7d90..edd1a4f45fb2 100644 --- a/code/core/src/core-server/utils/server-statics.ts +++ b/code/core/src/core-server/utils/server-statics.ts @@ -1,3 +1,4 @@ +import { existsSync } from 'node:fs'; import { basename, isAbsolute, posix, resolve, sep, win32 } from 'node:path'; import { getDirectoryFromWorkingDir } from '@storybook/core/common'; @@ -8,7 +9,6 @@ import { logger } from '@storybook/core/node-logger'; import chalk from 'chalk'; import type { Router } from 'express'; import express from 'express'; -import { pathExists } from 'fs-extra'; import { dedent } from 'ts-dedent'; export async function useStatics(router: Router, options: Options) { @@ -69,7 +69,7 @@ export const parseStaticDir = async (arg: string) => { const targetDir = target.replace(/^\/?/, './'); const targetEndpoint = targetDir.substring(1); - if (!(await pathExists(staticPath))) { + if (!existsSync(staticPath)) { throw new Error( dedent` Failed to load static files, no such directory: ${chalk.cyan(staticPath)} diff --git a/code/core/src/core-server/utils/stories-json.ts b/code/core/src/core-server/utils/stories-json.ts index 8956a4ed7ab1..d34ba9d72186 100644 --- a/code/core/src/core-server/utils/stories-json.ts +++ b/code/core/src/core-server/utils/stories-json.ts @@ -1,3 +1,4 @@ +import { writeFile } from 'node:fs/promises'; import { basename } from 'node:path'; import type { NormalizedStoriesSpecifier, StoryIndex } from '@storybook/core/types'; @@ -5,7 +6,6 @@ import type { NormalizedStoriesSpecifier, StoryIndex } from '@storybook/core/typ import { STORY_INDEX_INVALIDATED } from '@storybook/core/core-events'; import type { Request, Response, Router } from 'express'; -import { writeJSON } from 'fs-extra'; import debounce from 'lodash/debounce.js'; import type { StoryIndexGenerator } from './StoryIndexGenerator'; @@ -22,7 +22,7 @@ export async function extractStoriesJson( ) { const generator = await initializedStoryIndexGenerator; const storyIndex = await generator.getIndex(); - await writeJSON(outputFile, transform ? transform(storyIndex) : storyIndex); + await writeFile(outputFile, JSON.stringify(transform ? transform(storyIndex) : storyIndex)); } export function useStoriesJson({ diff --git a/code/core/src/core-server/utils/warnWhenUsingArgTypesRegex.ts b/code/core/src/core-server/utils/warnWhenUsingArgTypesRegex.ts index dd543bff54bc..d8fa4dcc521c 100644 --- a/code/core/src/core-server/utils/warnWhenUsingArgTypesRegex.ts +++ b/code/core/src/core-server/utils/warnWhenUsingArgTypesRegex.ts @@ -1,17 +1,20 @@ +import { readFile } from 'node:fs/promises'; + import { type BabelFile, core } from '@storybook/core/babel'; import type { StorybookConfig } from '@storybook/core/types'; import { babelParse } from '@storybook/core/csf-tools'; import chalk from 'chalk'; -import { readFile } from 'fs-extra'; import { dedent } from 'ts-dedent'; export async function warnWhenUsingArgTypesRegex( previewConfigPath: string | undefined, config: StorybookConfig ) { - const previewContent = previewConfigPath ? await readFile(previewConfigPath, 'utf8') : ''; + const previewContent = previewConfigPath + ? await readFile(previewConfigPath, { encoding: 'utf8' }) + : ''; const hasVisualTestAddon = config?.addons?.some((it) => diff --git a/code/core/src/core-server/utils/whats-new.ts b/code/core/src/core-server/utils/whats-new.ts index 1091d77ab74f..cb523f78318e 100644 --- a/code/core/src/core-server/utils/whats-new.ts +++ b/code/core/src/core-server/utils/whats-new.ts @@ -1,3 +1,5 @@ +import { writeFile } from 'node:fs/promises'; + import type { Channel } from '@storybook/core/channels'; import { findConfigFile } from '@storybook/core/common'; import { telemetry } from '@storybook/core/telemetry'; @@ -14,7 +16,6 @@ import { import { printConfig, readConfig } from '@storybook/core/csf-tools'; import { logger } from '@storybook/core/node-logger'; -import fs from 'fs-extra'; import invariant from 'tiny-invariant'; import { sendTelemetryError } from '../withTelemetry'; @@ -93,7 +94,7 @@ export function initializeWhatsNew( invariant(mainPath, `unable to find storybook main file in ${options.configDir}`); const main = await readConfig(mainPath); main.setFieldValue(['core', 'disableWhatsNewNotifications'], disableWhatsNewNotifications); - await fs.writeFile(mainPath, printConfig(main).code); + await writeFile(mainPath, printConfig(main).code); if (isTelemetryEnabled) { await telemetry('core-config', { disableWhatsNewNotifications }); } diff --git a/code/core/src/csf-tools/vitest-plugin/transformer.test.ts b/code/core/src/csf-tools/vitest-plugin/transformer.test.ts index 72a1d05ab403..84d9ac273718 100644 --- a/code/core/src/csf-tools/vitest-plugin/transformer.test.ts +++ b/code/core/src/csf-tools/vitest-plugin/transformer.test.ts @@ -213,6 +213,55 @@ describe('transformer', () => { `); }); + describe("use the story's name as test title", () => { + it('should support CSF v3 via name property', async () => { + const code = ` + export default { component: Button } + export const Primary = { name: "custom name" };`; + const result = await transform({ code }); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const _meta = { + component: Button, + title: "automatic/calculated/title" + }; + export default _meta; + export const Primary = { + name: "custom name" + }; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("custom name", _testStory("Primary", Primary, _meta, [])); + } + `); + }); + + it('should support CSF v1/v2 via storyName property', async () => { + const code = ` + export default { component: Button } + export const Story = () => {} + Story.storyName = 'custom name';`; + const result = await transform({ code: code }); + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const _meta = { + component: Button, + title: "automatic/calculated/title" + }; + export default _meta; + export const Story = () => {}; + Story.storyName = 'custom name'; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("custom name", _testStory("Story", Story, _meta, [])); + } + `); + }); + }); + it('should add test statement to const declared exported stories', async () => { const code = ` export default {}; diff --git a/code/core/src/csf-tools/vitest-plugin/transformer.ts b/code/core/src/csf-tools/vitest-plugin/transformer.ts index 51ea1169f6c3..778ea752f1d0 100644 --- a/code/core/src/csf-tools/vitest-plugin/transformer.ts +++ b/code/core/src/csf-tools/vitest-plugin/transformer.ts @@ -202,15 +202,17 @@ export async function vitestTransform({ const getTestStatementForStory = ({ exportName, + testTitle, node, }: { exportName: string; + testTitle: string; node: t.Node; }): t.ExpressionStatement => { // Create the _test expression directly using the exportName identifier const testStoryCall = t.expressionStatement( t.callExpression(vitestTestId, [ - t.stringLiteral(exportName), + t.stringLiteral(testTitle), t.callExpression(testStoryId, [ t.stringLiteral(exportName), t.identifier(exportName), @@ -239,10 +241,9 @@ export async function vitestTransform({ return; } - return getTestStatementForStory({ - exportName, - node, - }); + // use the story's name as the test title for vitest, and fallback to exportName + const testTitle = parsed._stories[exportName].name ?? exportName; + return getTestStatementForStory({ testTitle, exportName, node }); }) .filter((st) => !!st) as t.ExpressionStatement[]; diff --git a/code/core/src/telemetry/get-monorepo-type.test.ts b/code/core/src/telemetry/get-monorepo-type.test.ts index 530da9f4abc9..3f8ecfca723a 100644 --- a/code/core/src/telemetry/get-monorepo-type.test.ts +++ b/code/core/src/telemetry/get-monorepo-type.test.ts @@ -1,13 +1,12 @@ /* eslint-disable no-underscore-dangle */ +import * as fs from 'node:fs'; import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; -import * as fsExtra from 'fs-extra'; - import { getMonorepoType, monorepoConfigs } from './get-monorepo-type'; -vi.mock('fs-extra', async () => import('../../../__mocks__/fs-extra')); +vi.mock('node:fs', async () => import('../../../__mocks__/fs')); vi.mock('@storybook/core/common', async (importOriginal) => { return { @@ -25,7 +24,7 @@ const checkMonorepoType = ({ monorepoConfigFile, isYarnWorkspace = false }: any) mockFiles[join('root', monorepoConfigFile)] = '{}'; } - vi.mocked(fsExtra as any).__setMockFiles(mockFiles); + vi.mocked(fs as any).__setMockFiles(mockFiles); return getMonorepoType(); }; diff --git a/code/core/src/telemetry/get-monorepo-type.ts b/code/core/src/telemetry/get-monorepo-type.ts index 41dcc0087044..143241b0a214 100644 --- a/code/core/src/telemetry/get-monorepo-type.ts +++ b/code/core/src/telemetry/get-monorepo-type.ts @@ -1,10 +1,9 @@ +import { existsSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { getProjectRoot } from '@storybook/core/common'; import type { PackageJson } from '@storybook/core/types'; -import { existsSync, readJsonSync } from 'fs-extra'; - export const monorepoConfigs = { Nx: 'nx.json', Turborepo: 'turbo.json', @@ -36,7 +35,9 @@ export const getMonorepoType = (): MonorepoType => { return undefined; } - const packageJson = readJsonSync(join(projectRootPath, 'package.json')) as PackageJson; + const packageJson = JSON.parse( + readFileSync(join(projectRootPath, 'package.json'), { encoding: 'utf8' }) + ) as PackageJson; if (packageJson?.workspaces) { return 'Workspaces'; diff --git a/code/core/src/telemetry/package-json.ts b/code/core/src/telemetry/package-json.ts index b88ebc237ddc..4104425fc2d0 100644 --- a/code/core/src/telemetry/package-json.ts +++ b/code/core/src/telemetry/package-json.ts @@ -1,7 +1,6 @@ +import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { readJson } from 'fs-extra'; - import type { Dependency } from './types'; export const getActualPackageVersions = async (packages: Record>) => { @@ -25,6 +24,6 @@ export const getActualPackageJson = async (packageName: string) => { const resolvedPackageJson = require.resolve(join(packageName, 'package.json'), { paths: [process.cwd()], }); - const packageJson = await readJson(resolvedPackageJson); + const packageJson = JSON.parse(await readFile(resolvedPackageJson, { encoding: 'utf8' })); return packageJson; }; diff --git a/code/e2e-tests/addon-docs.spec.ts b/code/e2e-tests/addon-docs.spec.ts index c7486bc55923..bcf6e6fea9fc 100644 --- a/code/e2e-tests/addon-docs.spec.ts +++ b/code/e2e-tests/addon-docs.spec.ts @@ -196,11 +196,7 @@ test.describe('addon-docs', () => { // Arrange - Setup expectations let expectedReactVersionRange = /^18/; - if ( - templateName.includes('preact') || - templateName.includes('react-webpack/17') || - templateName.includes('react-vite/17') - ) { + if (templateName.includes('react-webpack/17') || templateName.includes('react-vite/17')) { expectedReactVersionRange = /^17/; } else if (templateName.includes('react16')) { expectedReactVersionRange = /^16/; diff --git a/code/frameworks/angular/src/client/docs/sourceDecorator.ts b/code/frameworks/angular/src/client/docs/sourceDecorator.ts index cd3335c8efa2..cbd3f838b340 100644 --- a/code/frameworks/angular/src/client/docs/sourceDecorator.ts +++ b/code/frameworks/angular/src/client/docs/sourceDecorator.ts @@ -1,6 +1,6 @@ import { SNIPPET_RENDERED, SourceType } from 'storybook/internal/docs-tools'; import { addons, useEffect } from 'storybook/internal/preview-api'; -import { PartialStoryFn } from 'storybook/internal/types'; +import { ArgsStoryFn, PartialStoryFn } from 'storybook/internal/types'; import { computesTemplateSourceFromComponent } from '../../renderer'; import { AngularRenderer, StoryContext } from '../types'; @@ -32,9 +32,11 @@ export const sourceDecorator = ( return story; } const channel = addons.getChannel(); - const { props, template, userDefinedTemplate } = story; - + const { props, userDefinedTemplate } = story; const { component, argTypes, parameters } = context; + const template: string = parameters.docs?.source?.excludeDecorators + ? (context.originalStoryFn as ArgsStoryFn)(context.args, context).template + : story.template; let toEmit: string; diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index 856982c811dd..d5d81422b9fb 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -148,7 +148,6 @@ "babel-loader": "^9.1.3", "css-loader": "^6.7.3", "find-up": "^5.0.0", - "fs-extra": "^11.1.0", "image-size": "^1.0.0", "loader-utils": "^3.2.1", "node-polyfill-webpack-plugin": "^2.0.1", @@ -157,7 +156,7 @@ "postcss-loader": "^8.1.1", "react-refresh": "^0.14.0", "resolve-url-loader": "^5.0.0", - "sass-loader": "^12.4.0", + "sass-loader": "^13.2.0", "semver": "^7.3.5", "style-loader": "^3.3.1", "styled-jsx": "^5.1.6", diff --git a/code/frameworks/react-vite/package.json b/code/frameworks/react-vite/package.json index ba7b57702d3f..fa1a682656bb 100644 --- a/code/frameworks/react-vite/package.json +++ b/code/frameworks/react-vite/package.json @@ -47,7 +47,7 @@ "prep": "jiti ../../../scripts/prepare/bundle.ts" }, "dependencies": { - "@joshwooding/vite-plugin-react-docgen-typescript": "0.3.1", + "@joshwooding/vite-plugin-react-docgen-typescript": "0.3.0", "@rollup/pluginutils": "^5.0.2", "@storybook/builder-vite": "workspace:*", "@storybook/react": "workspace:*", diff --git a/code/lib/cli-storybook/package.json b/code/lib/cli-storybook/package.json index 5e13a89d5b30..7623c68b2f4d 100644 --- a/code/lib/cli-storybook/package.json +++ b/code/lib/cli-storybook/package.json @@ -51,7 +51,6 @@ "envinfo": "^7.7.3", "fd-package-json": "^1.2.0", "find-up": "^5.0.0", - "fs-extra": "^11.1.0", "giget": "^1.0.0", "glob": "^10.0.0", "globby": "^14.0.1", diff --git a/code/lib/cli-storybook/src/automigrate/fixes/eslint-plugin.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/eslint-plugin.test.ts index 2120abd7098f..5c8bee41c2f0 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/eslint-plugin.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/eslint-plugin.test.ts @@ -1,17 +1,17 @@ /* eslint-disable no-underscore-dangle */ import * as fs from 'node:fs'; +import * as fsp from 'node:fs/promises'; import { describe, expect, it, vi } from 'vitest'; import type { PackageJson } from 'storybook/internal/common'; -import * as fsExtra from 'fs-extra'; import { dedent } from 'ts-dedent'; import { makePackageManager } from '../helpers/testing-helpers'; import { eslintPlugin } from './eslint-plugin'; -vi.mock('fs-extra', async () => import('../../../../../__mocks__/fs-extra')); +vi.mock('node:fs/promises', async () => import('../../../../../__mocks__/fs/promises')); vi.mock('fs'); const checkEslint = async ({ @@ -23,7 +23,7 @@ const checkEslint = async ({ hasEslint?: boolean; eslintExtension?: string; }) => { - vi.mocked(fsExtra as any).__setMockFiles({ + vi.mocked(fsp as any).__setMockFiles({ [`.eslintrc.${eslintExtension}`]: !hasEslint ? null : dedent(` diff --git a/code/lib/cli-storybook/src/automigrate/fixes/initial-globals.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/initial-globals.test.ts index e74d5ce810da..b78a80fe79b3 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/initial-globals.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/initial-globals.test.ts @@ -1,17 +1,16 @@ /* eslint-disable no-underscore-dangle */ +import * as fsp from 'node:fs/promises'; import { join } from 'node:path'; import { expect, it, vi } from 'vitest'; -import * as fsExtra from 'fs-extra'; - import { initialGlobals } from './initial-globals'; -vi.mock('fs-extra', async () => import('../../../../../__mocks__/fs-extra')); +vi.mock('node:fs/promises', async () => import('../../../../../__mocks__/fs/promises')); const previewConfigPath = join('.storybook', 'preview.js'); const check = async (previewContents: string) => { - vi.mocked(fsExtra as any).__setMockFiles({ + vi.mocked(fsp as any).__setMockFiles({ [previewConfigPath]: previewContents, }); return initialGlobals.check({ diff --git a/code/lib/cli-storybook/src/automigrate/fixes/initial-globals.ts b/code/lib/cli-storybook/src/automigrate/fixes/initial-globals.ts index d4a96033a8c5..fe40749ffa16 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/initial-globals.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/initial-globals.ts @@ -1,9 +1,10 @@ +import { readFile, writeFile } from 'node:fs/promises'; + import type { ConfigFile } from 'storybook/internal/csf-tools'; import { formatConfig, loadConfig } from 'storybook/internal/csf-tools'; import type { Expression } from '@babel/types'; import chalk from 'chalk'; -import { readFile, writeFile } from 'fs-extra'; import { dedent } from 'ts-dedent'; import type { Fix } from '../types'; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/mdx-1-to-3.ts b/code/lib/cli-storybook/src/automigrate/fixes/mdx-1-to-3.ts index 221edd9298e0..bf87b5fed74f 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/mdx-1-to-3.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/mdx-1-to-3.ts @@ -1,7 +1,7 @@ +import { readFile, writeFile } from 'node:fs/promises'; import { basename } from 'node:path'; import chalk from 'chalk'; -import fse from 'fs-extra'; import { dedent } from 'ts-dedent'; import type { Fix } from '../types'; @@ -74,14 +74,14 @@ export const mdx1to3: Fix = { async run({ result: { storiesMdxFiles }, dryRun }) { await Promise.all([ ...storiesMdxFiles.map(async (fname) => { - const contents = await fse.readFile(fname, 'utf-8'); + const contents = await readFile(fname, { encoding: 'utf8' }); const updated = fixMdxComments(fixMdxStyleTags(contents)); if (updated === contents) { logger.info(`🆗 Unmodified ${basename(fname)}`); } else { logger.info(`✅ Modified ${basename(fname)}`); if (!dryRun) { - await fse.writeFile(fname, updated); + await writeFile(fname, updated); } } }), diff --git a/code/lib/cli-storybook/src/automigrate/fixes/remove-global-client-apis.test.ts b/code/lib/cli-storybook/src/automigrate/fixes/remove-global-client-apis.test.ts index 6ae48ad95018..3e1af96c38f6 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/remove-global-client-apis.test.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/remove-global-client-apis.test.ts @@ -1,19 +1,18 @@ /* eslint-disable no-underscore-dangle */ +import * as fsp from 'node:fs/promises'; import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; import type { JsPackageManager } from 'storybook/internal/common'; -import * as fsExtra from 'fs-extra'; - import { RemovedAPIs, removedGlobalClientAPIs as migration } from './remove-global-client-apis'; -vi.mock('fs-extra', async () => import('../../../../../__mocks__/fs-extra')); +vi.mock('node:fs/promises', async () => import('../../../../../__mocks__/fs/promises')); const check = async ({ contents, previewConfigPath }: any) => { if (contents) { - vi.mocked(fsExtra as any).__setMockFiles({ + vi.mocked(fsp as any).__setMockFiles({ [join('.storybook', 'preview.js')]: contents, }); } diff --git a/code/lib/cli-storybook/src/automigrate/fixes/remove-global-client-apis.ts b/code/lib/cli-storybook/src/automigrate/fixes/remove-global-client-apis.ts index 232ac4fea188..e7caf9027d4b 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/remove-global-client-apis.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/remove-global-client-apis.ts @@ -1,5 +1,6 @@ +import { readFile } from 'node:fs/promises'; + import chalk from 'chalk'; -import { readFile } from 'fs-extra'; import { dedent } from 'ts-dedent'; import type { Fix } from '../types'; @@ -26,7 +27,7 @@ export const removedGlobalClientAPIs: Fix = { async check({ previewConfigPath }) { if (previewConfigPath) { - const contents = await readFile(previewConfigPath, 'utf8'); + const contents = await readFile(previewConfigPath, { encoding: 'utf8' }); const usedAPIs = Object.values(RemovedAPIs).reduce((acc, item) => { if (contents.includes(item)) { diff --git a/code/lib/cli-storybook/src/automigrate/index.ts b/code/lib/cli-storybook/src/automigrate/index.ts index 690c3d1b15f7..ff727783dff3 100644 --- a/code/lib/cli-storybook/src/automigrate/index.ts +++ b/code/lib/cli-storybook/src/automigrate/index.ts @@ -1,3 +1,5 @@ +import { createWriteStream } from 'node:fs'; +import { rename, rm } from 'node:fs/promises'; import { join } from 'node:path'; import { @@ -10,7 +12,6 @@ import { import boxen from 'boxen'; import chalk from 'chalk'; -import { createWriteStream, move, remove } from 'fs-extra'; import prompts from 'prompts'; import semver from 'semver'; import invariant from 'tiny-invariant'; @@ -182,9 +183,9 @@ export const automigrate = async ({ // if migration failed, display a log file in the users cwd if (hasFailures) { - await move(TEMP_LOG_FILE_PATH, join(process.cwd(), LOG_FILE_NAME), { overwrite: true }); + await rename(TEMP_LOG_FILE_PATH, join(process.cwd(), LOG_FILE_NAME)); } else { - await remove(TEMP_LOG_FILE_PATH); + await rm(TEMP_LOG_FILE_PATH, { recursive: true, force: true }); } if (!hideMigrationSummary) { diff --git a/code/lib/cli-storybook/src/doctor/index.ts b/code/lib/cli-storybook/src/doctor/index.ts index 08e798288753..53439e244f00 100644 --- a/code/lib/cli-storybook/src/doctor/index.ts +++ b/code/lib/cli-storybook/src/doctor/index.ts @@ -1,3 +1,5 @@ +import { createWriteStream } from 'node:fs'; +import { rename, rm } from 'node:fs/promises'; import { join } from 'node:path'; import { JsPackageManagerFactory, temporaryFile } from 'storybook/internal/common'; @@ -5,7 +7,6 @@ import type { PackageManagerName } from 'storybook/internal/common'; import boxen from 'boxen'; import chalk from 'chalk'; -import { createWriteStream, move, remove } from 'fs-extra'; import { dedent } from 'ts-dedent'; import { cleanLog } from '../automigrate/helpers/cleanLog'; @@ -161,11 +162,11 @@ export const doctor = async ({ logger.info(`Full logs are available in ${chalk.cyan(LOG_FILE_PATH)}`); - await move(TEMP_LOG_FILE_PATH, join(process.cwd(), LOG_FILE_NAME), { overwrite: true }); + await rename(TEMP_LOG_FILE_PATH, join(process.cwd(), LOG_FILE_NAME)); } else { logger.info(`🥳 Your Storybook project looks good!`); logger.info(commandMessage); - await remove(TEMP_LOG_FILE_PATH); + await rm(TEMP_LOG_FILE_PATH, { recursive: true, force: true }); } logger.info(); diff --git a/code/lib/cli-storybook/src/link.ts b/code/lib/cli-storybook/src/link.ts index 369d668d2c2a..8f0b2ed6718c 100644 --- a/code/lib/cli-storybook/src/link.ts +++ b/code/lib/cli-storybook/src/link.ts @@ -1,10 +1,10 @@ +import { mkdir, readFile } from 'node:fs/promises'; import { basename, extname, join } from 'node:path'; import { logger } from 'storybook/internal/node-logger'; import chalk from 'chalk'; import { spawn as spawnAsync, sync as spawnSync } from 'cross-spawn'; -import fse from 'fs-extra'; type ExecOptions = Parameters[2]; @@ -61,7 +61,7 @@ export const exec = async ( export const link = async ({ target, local, start }: LinkOptions) => { const storybookDir = process.cwd(); try { - const packageJson = await fse.readJSON('package.json'); + const packageJson = JSON.parse(await readFile('package.json', { encoding: 'utf8' })); if (packageJson.name !== '@storybook/root') { throw new Error(); } @@ -75,7 +75,9 @@ export const link = async ({ target, local, start }: LinkOptions) => { if (!local) { const reprosDir = join(storybookDir, '../storybook-repros'); logger.info(`Ensuring directory ${reprosDir}`); - await fse.ensureDir(reprosDir); + // Passing `recursive: true` ensures that the method doesn't throw when + // the directory already exists. + await mkdir(reprosDir, { recursive: true }); logger.info(`Cloning ${target}`); await exec(`git clone ${target}`, { cwd: reprosDir }); @@ -84,7 +86,9 @@ export const link = async ({ target, local, start }: LinkOptions) => { reproDir = join(reprosDir, reproName); } - const reproPackageJson = await fse.readJSON(join(reproDir, 'package.json')); + const reproPackageJson = JSON.parse( + await readFile(join(reproDir, 'package.json'), { encoding: 'utf8' }) + ); const version = spawnSync('yarn', ['--version'], { cwd: reproDir, diff --git a/code/lib/cli-storybook/src/sandbox.ts b/code/lib/cli-storybook/src/sandbox.ts index a6b4fe40f043..9b02ca82dd4e 100644 --- a/code/lib/cli-storybook/src/sandbox.ts +++ b/code/lib/cli-storybook/src/sandbox.ts @@ -1,3 +1,5 @@ +import { existsSync } from 'node:fs'; +import { readdir } from 'node:fs/promises'; import { isAbsolute, join } from 'node:path'; import type { PackageManagerName } from 'storybook/internal/common'; @@ -7,7 +9,6 @@ import { versions } from 'storybook/internal/common'; import boxen from 'boxen'; import chalk from 'chalk'; import { initiate } from 'create-storybook'; -import { existsSync, readdir } from 'fs-extra'; import { downloadTemplate } from 'giget'; import prompts from 'prompts'; import { lt, prerelease } from 'semver'; @@ -206,7 +207,7 @@ export const sandbox = async ({ force: true, dir: templateDestination, }); - // throw an error if templateDestination is an empty directory using fs-extra + // throw an error if templateDestination is an empty directory if ((await readdir(templateDestination)).length === 0) { const selected = chalk.yellow(templateId); throw new Error(dedent` diff --git a/code/lib/cli/scripts/update-core-portal.ts b/code/lib/cli/scripts/update-core-portal.ts index b5896549366d..520bc3587848 100644 --- a/code/lib/cli/scripts/update-core-portal.ts +++ b/code/lib/cli/scripts/update-core-portal.ts @@ -1,7 +1,6 @@ +import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { readJSON } from 'fs-extra'; - import { sortPackageJson } from '../../../../scripts/node_modules/sort-package-json'; import { generateMapperContent, mapCoreExportToSelf, write } from './utils'; @@ -22,8 +21,12 @@ import { generateMapperContent, mapCoreExportToSelf, write } from './utils'; * remove those manually here. */ async function run() { - const selfPackageJson = await readJSON(join(__dirname, '../package.json')); - const corePackageJson = await readJSON(join(__dirname, '../../../core/package.json')); + const selfPackageJson = JSON.parse( + await readFile(join(__dirname, '../package.json'), { encoding: 'utf8' }) + ); + const corePackageJson = await JSON.parse( + await readFile(join(__dirname, '../../../core/package.json'), { encoding: 'utf8' }) + ); await Promise.all( Object.entries>(corePackageJson.exports) diff --git a/code/lib/cli/scripts/utils.ts b/code/lib/cli/scripts/utils.ts index e453988fad64..1c27080075c5 100644 --- a/code/lib/cli/scripts/utils.ts +++ b/code/lib/cli/scripts/utils.ts @@ -1,8 +1,14 @@ -import { ensureFile, writeFile } from 'fs-extra'; +import { existsSync } from 'node:fs'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { dirname } from 'node:path'; + import { dedent } from 'ts-dedent'; export const write = async (location: string, data: string) => { - await ensureFile(location); + if (!existsSync(location)) { + const directory = dirname(location); + await mkdir(directory, { recursive: true }); + } return writeFile(location, data); }; diff --git a/code/lib/create-storybook/package.json b/code/lib/create-storybook/package.json index 2a08fc5deed2..612b559bcae2 100644 --- a/code/lib/create-storybook/package.json +++ b/code/lib/create-storybook/package.json @@ -61,7 +61,6 @@ "execa": "^5.0.0", "fd-package-json": "^1.2.0", "find-up": "^5.0.0", - "fs-extra": "^11.1.0", "ora": "^5.4.1", "prettier": "^3.1.1", "prompts": "^2.4.0", diff --git a/code/lib/create-storybook/src/generators/baseGenerator.ts b/code/lib/create-storybook/src/generators/baseGenerator.ts index 20318b78a68e..f5d0c6ed828c 100644 --- a/code/lib/create-storybook/src/generators/baseGenerator.ts +++ b/code/lib/create-storybook/src/generators/baseGenerator.ts @@ -1,3 +1,4 @@ +import { mkdir } from 'node:fs/promises'; import { dirname, join } from 'node:path'; import type { NpmOptions } from 'storybook/internal/cli'; @@ -10,7 +11,6 @@ import type { JsPackageManager } from 'storybook/internal/common'; import { getPackageDetails, versions as packageVersions } from 'storybook/internal/common'; import type { SupportedFrameworks } from 'storybook/internal/types'; -import fse from 'fs-extra'; import ora from 'ora'; import invariant from 'tiny-invariant'; import { dedent } from 'ts-dedent'; @@ -328,7 +328,9 @@ export async function baseGenerator( addDependenciesSpinner.succeed(); } - await fse.ensureDir(`./${storybookConfigFolder}`); + // Passing `recursive: true` ensures that the method doesn't throw when + // the directory already exists. + await mkdir(`./${storybookConfigFolder}`, { recursive: true }); if (addMainFile) { const prefixes = shouldApplyRequireWrapperOnPackageNames diff --git a/code/lib/create-storybook/src/generators/configure.test.ts b/code/lib/create-storybook/src/generators/configure.test.ts index b1ce1005d230..db099baf5590 100644 --- a/code/lib/create-storybook/src/generators/configure.test.ts +++ b/code/lib/create-storybook/src/generators/configure.test.ts @@ -1,17 +1,20 @@ +import type { Stats } from 'node:fs'; +import * as fsp from 'node:fs/promises'; + import { beforeAll, describe, expect, it, vi } from 'vitest'; import { SupportedLanguage } from 'storybook/internal/cli'; -import fse from 'fs-extra'; import { dedent } from 'ts-dedent'; import { configureMain, configurePreview } from './configure'; -vi.mock('fs-extra'); +vi.mock('node:fs/promises'); describe('configureMain', () => { beforeAll(() => { vi.clearAllMocks(); + vi.mocked(fsp.stat).mockRejectedValue({}); }); it('should generate main.js', async () => { @@ -25,7 +28,7 @@ describe('configureMain', () => { }, }); - const { calls } = vi.mocked(fse.writeFile).mock; + const { calls } = vi.mocked(fsp.writeFile).mock; const [mainConfigPath, mainConfigContent] = calls[0]; expect(mainConfigPath).toEqual('./.storybook/main.js'); @@ -54,7 +57,7 @@ describe('configureMain', () => { }, }); - const { calls } = vi.mocked(fse.writeFile).mock; + const { calls } = vi.mocked(fsp.writeFile).mock; const [mainConfigPath, mainConfigContent] = calls[0]; expect(mainConfigPath).toEqual('./.storybook/main.ts'); @@ -89,7 +92,7 @@ describe('configureMain', () => { }, }); - const { calls } = vi.mocked(fse.writeFile).mock; + const { calls } = vi.mocked(fsp.writeFile).mock; const [mainConfigPath, mainConfigContent] = calls[0]; expect(mainConfigPath).toEqual('./.storybook/main.js'); @@ -123,7 +126,7 @@ describe('configurePreview', () => { rendererId: 'react', }); - const { calls } = vi.mocked(fse.writeFile).mock; + const { calls } = vi.mocked(fsp.writeFile).mock; const [previewConfigPath, previewConfigContent] = calls[0]; expect(previewConfigPath).toEqual('./.storybook/preview.js'); @@ -152,7 +155,7 @@ describe('configurePreview', () => { rendererId: 'react', }); - const { calls } = vi.mocked(fse.writeFile).mock; + const { calls } = vi.mocked(fsp.writeFile).mock; const [previewConfigPath, previewConfigContent] = calls[0]; expect(previewConfigPath).toEqual('./.storybook/preview.ts'); @@ -176,13 +179,13 @@ describe('configurePreview', () => { }); it('should not do anything if the framework template already included a preview', async () => { - vi.mocked(fse.pathExists).mockImplementationOnce(() => Promise.resolve(true)); + vi.mocked(fsp.stat).mockResolvedValueOnce({} as Stats); await configurePreview({ language: SupportedLanguage.TYPESCRIPT_4_9, storybookConfigFolder: '.storybook', rendererId: 'react', }); - expect(fse.writeFile).not.toHaveBeenCalled(); + expect(fsp.writeFile).not.toHaveBeenCalled(); }); it('should add prefix if frameworkParts are passed', async () => { @@ -199,7 +202,7 @@ describe('configurePreview', () => { }, }); - const { calls } = vi.mocked(fse.writeFile).mock; + const { calls } = vi.mocked(fsp.writeFile).mock; const [previewConfigPath, previewConfigContent] = calls[0]; expect(previewConfigPath).toEqual('./.storybook/preview.ts'); diff --git a/code/lib/create-storybook/src/generators/configure.ts b/code/lib/create-storybook/src/generators/configure.ts index 94304f4c2d06..c7002c58c045 100644 --- a/code/lib/create-storybook/src/generators/configure.ts +++ b/code/lib/create-storybook/src/generators/configure.ts @@ -1,9 +1,9 @@ +import { stat, writeFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import { SupportedLanguage, externalFrameworks } from 'storybook/internal/cli'; import { logger } from 'storybook/internal/node-logger'; -import fse from 'fs-extra'; import { dedent } from 'ts-dedent'; interface ConfigureMainOptions { @@ -35,6 +35,12 @@ interface ConfigurePreviewOptions { rendererId: string; } +const pathExists = async (path: string) => { + return stat(path) + .then(() => true) + .catch(() => false); +}; + /** * We need to clean up the paths in case of pnp input: * `path.dirname(require.resolve(path.join('@storybook/react-webpack5', 'package.json')))` output: @@ -59,7 +65,7 @@ export async function configureMain({ ...custom }: ConfigureMainOptions) { const srcPath = resolve(storybookConfigFolder, '../src'); - const prefix = (await fse.pathExists(srcPath)) ? '../src' : '../stories'; + const prefix = (await pathExists(srcPath)) ? '../src' : '../stories'; const config = { stories: [`${prefix}/**/*.mdx`, `${prefix}/**/*.stories.@(${extensions.join('|')})`], addons, @@ -114,7 +120,7 @@ export async function configureMain({ logger.verbose(`Failed to prettify ${mainPath}`); } - await fse.writeFile(mainPath, mainJsContents, { encoding: 'utf8' }); + await writeFile(mainPath, mainJsContents, { encoding: 'utf8' }); } export async function configurePreview(options: ConfigurePreviewOptions) { @@ -134,7 +140,7 @@ export async function configurePreview(options: ConfigurePreviewOptions) { const previewPath = `./${options.storybookConfigFolder}/preview.${isTypescript ? 'ts' : 'js'}`; // If the framework template included a preview then we have nothing to do - if (await fse.pathExists(previewPath)) { + if (await pathExists(previewPath)) { return; } @@ -177,5 +183,5 @@ export async function configurePreview(options: ConfigurePreviewOptions) { logger.verbose(`Failed to prettify ${previewPath}`); } - await fse.writeFile(previewPath, preview, { encoding: 'utf8' }); + await writeFile(previewPath, preview, { encoding: 'utf8' }); } diff --git a/code/lib/create-storybook/src/initiate.ts b/code/lib/create-storybook/src/initiate.ts index 787e16288cc2..bed1870fd2a9 100644 --- a/code/lib/create-storybook/src/initiate.ts +++ b/code/lib/create-storybook/src/initiate.ts @@ -375,7 +375,7 @@ export async function doInitiate(options: CommandOptions): Promise< if (foundGitIgnoreFile && foundGitIgnoreFile.includes(rootDirectory)) { const contents = await readFile(foundGitIgnoreFile, 'utf-8'); if (!contents.includes('*storybook.log')) { - await appendFile(foundGitIgnoreFile, '\n*storybook.log'); + await appendFile(foundGitIgnoreFile, '\n*storybook.log\n'); } } diff --git a/code/lib/create-storybook/src/scaffold-new-project.ts b/code/lib/create-storybook/src/scaffold-new-project.ts index f80ff2281f23..f2d64fa794bd 100644 --- a/code/lib/create-storybook/src/scaffold-new-project.ts +++ b/code/lib/create-storybook/src/scaffold-new-project.ts @@ -1,3 +1,6 @@ +import { readdirSync } from 'node:fs'; +import { rm } from 'node:fs/promises'; + import type { PackageManagerName } from 'storybook/internal/common'; import { logger } from 'storybook/internal/node-logger'; import { GenerateNewProjectOnInitError } from 'storybook/internal/server-errors'; @@ -6,7 +9,6 @@ import { telemetry } from 'storybook/internal/telemetry'; import boxen from 'boxen'; import chalk from 'chalk'; import execa from 'execa'; -import { readdirSync, remove } from 'fs-extra'; import prompts from 'prompts'; import { dedent } from 'ts-dedent'; @@ -173,7 +175,7 @@ export const scaffoldNewProject = async ( try { // If target directory has a .cache folder, remove it // so that it does not block the creation of the new project - await remove(`${targetDir}/.cache`); + await rm(`${targetDir}/.cache`, { recursive: true, force: true }); // Create new project in temp directory await execa.command(createScript, { diff --git a/code/package.json b/code/package.json index e348ef665900..a6bd01ea00a2 100644 --- a/code/package.json +++ b/code/package.json @@ -168,7 +168,6 @@ "@testing-library/user-event": "^14.5.2", "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/express": "^4.17.21", - "@types/fs-extra": "^11.0.1", "@types/lodash": "^4.14.167", "@types/mock-require": "^2.0.3", "@types/node": "^22.0.0", @@ -194,7 +193,6 @@ "eslint-plugin-local-rules": "portal:../scripts/eslint-plugin-local-rules", "eslint-plugin-playwright": "^1.6.2", "eslint-plugin-storybook": "^0.8.0", - "fs-extra": "^11.1.0", "github-release-from-changelog": "^2.1.1", "glob": "^10.0.0", "happy-dom": "^14.12.0", @@ -295,5 +293,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.4.0-alpha.1" } diff --git a/code/presets/react-webpack/package.json b/code/presets/react-webpack/package.json index d22f61a94932..8f3e04e671b7 100644 --- a/code/presets/react-webpack/package.json +++ b/code/presets/react-webpack/package.json @@ -70,7 +70,6 @@ "@types/node": "^22.0.0", "@types/semver": "^7.3.4", "find-up": "^5.0.0", - "fs-extra": "^11.1.0", "magic-string": "^0.30.5", "react-docgen": "^7.0.0", "resolve": "^1.22.8", diff --git a/code/presets/server-webpack/package.json b/code/presets/server-webpack/package.json index 48fc4ac5927c..0dcf0288a02e 100644 --- a/code/presets/server-webpack/package.json +++ b/code/presets/server-webpack/package.json @@ -63,7 +63,6 @@ "yaml-loader": "^0.8.0" }, "devDependencies": { - "fs-extra": "^11.1.0", "typescript": "^5.3.2", "yaml": "^2.3.1" }, diff --git a/code/presets/server-webpack/src/lib/compiler/json-to-csf-compiler.test.ts b/code/presets/server-webpack/src/lib/compiler/json-to-csf-compiler.test.ts index 0c44665bdc25..f0d8237215cb 100644 --- a/code/presets/server-webpack/src/lib/compiler/json-to-csf-compiler.test.ts +++ b/code/presets/server-webpack/src/lib/compiler/json-to-csf-compiler.test.ts @@ -1,14 +1,15 @@ +import { readdirSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; -import fs from 'fs-extra'; import YAML from 'yaml'; import { compileCsfModule } from '.'; async function generate(filePath: string) { - const content = await fs.readFile(filePath, 'utf8'); + const content = await readFile(filePath, { encoding: 'utf8' }); const parsed = filePath.endsWith('.json') ? JSON.parse(content) : YAML.parse(content); return compileCsfModule(parsed); } @@ -18,7 +19,7 @@ async function generate(filePath: string) { describe(`${fileType}-to-csf-compiler`, () => { const transformFixturesDir = join(__dirname, '__testfixtures__'); - fs.readdirSync(transformFixturesDir) + readdirSync(transformFixturesDir) .filter((fileName: string) => inputRegExp.test(fileName)) .forEach((fixtureFile: string) => { it(`${fixtureFile}`, async () => { diff --git a/code/renderers/react/src/__test__/portable-stories.test.tsx b/code/renderers/react/src/__test__/portable-stories.test.tsx index 94de89e093a5..85f33c9a714a 100644 --- a/code/renderers/react/src/__test__/portable-stories.test.tsx +++ b/code/renderers/react/src/__test__/portable-stories.test.tsx @@ -8,7 +8,8 @@ import React from 'react'; import { addons } from 'storybook/internal/preview-api'; -import type { Meta } from '@storybook/react'; +import type { ProjectAnnotations } from '@storybook/csf'; +import type { Meta, ReactRenderer } from '@storybook/react'; import * as addonActionsPreview from '@storybook/addon-actions/preview'; @@ -124,7 +125,7 @@ describe('projectAnnotations', () => { const Story = composeStory( ButtonStories.WithActionArgType, ButtonStories.default, - addonActionsPreview + addonActionsPreview as ProjectAnnotations ); expect(Story.args.someActionArg).toHaveProperty('isAction', true); }); diff --git a/code/renderers/server/package.json b/code/renderers/server/package.json index 8214690cb7de..67d3343a72b3 100644 --- a/code/renderers/server/package.json +++ b/code/renderers/server/package.json @@ -51,8 +51,6 @@ "@storybook/manager-api": "workspace:^", "@storybook/preview-api": "workspace:^", "@storybook/theming": "workspace:^", - "@types/fs-extra": "^11.0.1", - "fs-extra": "^11.1.0", "ts-dedent": "^2.0.0", "yaml": "^2.3.1" }, diff --git a/code/renderers/server/src/preset.ts b/code/renderers/server/src/preset.ts index 018f50623eee..197a303bb15b 100644 --- a/code/renderers/server/src/preset.ts +++ b/code/renderers/server/src/preset.ts @@ -1,8 +1,8 @@ +import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import type { ComponentTitle, PresetProperty, StoryName, Tag } from 'storybook/internal/types'; -import fs from 'fs-extra'; import yaml from 'yaml'; type FileContent = { @@ -18,9 +18,10 @@ export const experimental_indexers: PresetProperty<'experimental_indexers'> = ( { test: /(stories|story)\.(json|ya?ml)$/, createIndex: async (fileName) => { + const rawFile = await readFile(fileName, { encoding: 'utf8' }); const content: FileContent = fileName.endsWith('.json') - ? await fs.readJson(fileName, 'utf-8') - : yaml.parse((await fs.readFile(fileName, 'utf-8')).toString()); + ? JSON.parse(rawFile) + : yaml.parse(rawFile); return content.stories.map((story) => { const tags = Array.from(new Set([...(content.tags ?? []), ...(story.tags ?? [])])); diff --git a/code/renderers/svelte/package.json b/code/renderers/svelte/package.json index aa804d3d0ce6..5972c0ef2ac8 100644 --- a/code/renderers/svelte/package.json +++ b/code/renderers/svelte/package.json @@ -68,7 +68,6 @@ "@sveltejs/vite-plugin-svelte": "^3.0.2", "@testing-library/svelte": "patch:@testing-library/svelte@npm%3A4.1.0#~/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch", "expect-type": "^0.15.0", - "fs-extra": "^11.1.0", "svelte": "^5.0.0-next.65", "svelte-check": "^3.6.4", "typescript": "^5.3.2" diff --git a/code/renderers/svelte/scripts/copy-unbundled-to-dist.ts b/code/renderers/svelte/scripts/copy-unbundled-to-dist.ts index a82964679960..0444a5960183 100644 --- a/code/renderers/svelte/scripts/copy-unbundled-to-dist.ts +++ b/code/renderers/svelte/scripts/copy-unbundled-to-dist.ts @@ -1,4 +1,5 @@ -import { copy } from 'fs-extra'; +import { cp } from 'node:fs/promises'; + import { join } from 'path'; const src = join(__dirname, '..', 'src'); @@ -11,7 +12,7 @@ const run = async () => { console.log('Copying unbundled files to dist...'); await Promise.all( PATHS_TO_COPY.map((pathToCopy) => - copy(join(src, pathToCopy), join(dist, pathToCopy), { overwrite: true }) + cp(join(src, pathToCopy), join(dist, pathToCopy), { recursive: true, force: true }) ) ); console.log('Done!'); diff --git a/code/yarn.lock b/code/yarn.lock index 52f251440bb3..08b73cfd20cc 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -3677,9 +3677,9 @@ __metadata: languageName: node linkType: hard -"@joshwooding/vite-plugin-react-docgen-typescript@npm:0.3.1": - version: 0.3.1 - resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.3.1" +"@joshwooding/vite-plugin-react-docgen-typescript@npm:0.3.0": + version: 0.3.0 + resolution: "@joshwooding/vite-plugin-react-docgen-typescript@npm:0.3.0" dependencies: glob: "npm:^7.2.0" glob-promise: "npm:^4.2.0" @@ -3691,7 +3691,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/a9c7a03d7d1daf5bd64949255516ba64c88d5600366c8c74dcdb6f37c2a6099daaec02860b7587d2220e61afa47a0b2de17ef70d723c2db02f24e0890edfd9f3 + checksum: 10c0/31098ad8fcc2440437534599c111d9f2951dd74821e8ba46c521b969bae4c918d830b7bb0484efbad29a51711bb62d3bc623d5a1ed5b1695b5b5594ea9dd4ca0 languageName: node linkType: hard @@ -5437,7 +5437,6 @@ __metadata: "@storybook/global": "npm:^5.0.0" "@storybook/react-dom-shim": "workspace:*" "@types/react": "npm:^16.8.0 || ^17.0.0 || ^18.0.0" - fs-extra: "npm:^11.1.0" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" rehype-external-links: "npm:^3.0.0" @@ -5492,6 +5491,7 @@ __metadata: "@storybook/instrumenter": "workspace:*" "@storybook/test": "workspace:*" "@types/node": "npm:^22.0.0" + ansi-to-html: "npm:^0.7.2" formik: "npm:^2.2.9" polished: "npm:^4.2.2" react: "npm:^18.2.0" @@ -5527,7 +5527,6 @@ __metadata: dependencies: "@storybook/csf": "npm:^0.1.11" "@storybook/global": "npm:^5.0.0" - fs-extra: "npm:^11.1.0" ts-dedent: "npm:^2.0.0" typescript: "npm:^5.3.2" peerDependencies: @@ -5802,7 +5801,6 @@ __metadata: es-module-lexer: "npm:^1.5.0" express: "npm:^4.19.2" find-cache-dir: "npm:^3.0.0" - fs-extra: "npm:^11.1.0" glob: "npm:^10.0.0" magic-string: "npm:^0.30.0" slash: "npm:^5.0.0" @@ -5843,7 +5841,6 @@ __metadata: es-module-lexer: "npm:^1.5.0" express: "npm:^4.19.2" fork-ts-checker-webpack-plugin: "npm:^8.0.0" - fs-extra: "npm:^11.1.0" html-webpack-plugin: "npm:^5.5.0" magic-string: "npm:^0.30.5" path-browserify: "npm:^1.0.1" @@ -5896,7 +5893,6 @@ __metadata: envinfo: "npm:^7.7.3" fd-package-json: "npm:^1.2.0" find-up: "npm:^5.0.0" - fs-extra: "npm:^11.1.0" giget: "npm:^1.0.0" glob: "npm:^10.0.0" globby: "npm:^14.0.1" @@ -6038,7 +6034,6 @@ __metadata: "@types/ejs": "npm:^3.1.1" "@types/express": "npm:^4.17.21" "@types/find-cache-dir": "npm:^5.0.0" - "@types/fs-extra": "npm:^11.0.1" "@types/js-yaml": "npm:^4.0.5" "@types/lodash": "npm:^4.14.167" "@types/node": "npm:^22.0.0" @@ -6091,12 +6086,12 @@ __metadata: find-cache-dir: "npm:^5.0.0" find-up: "npm:^7.0.0" flush-promises: "npm:^1.0.2" - fs-extra: "npm:^11.1.0" fuse.js: "npm:^3.6.1" get-npm-tarball-url: "npm:^2.0.3" glob: "npm:^10.0.0" globby: "npm:^14.0.1" handlebars: "npm:^4.7.7" + jiti: "npm:^1.21.6" js-yaml: "npm:^4.1.0" jsdoc-type-pratt-parser: "npm:^4.0.0" lazy-universal-dotenv: "npm:^4.0.0" @@ -6434,7 +6429,6 @@ __metadata: babel-loader: "npm:^9.1.3" css-loader: "npm:^6.7.3" find-up: "npm:^5.0.0" - fs-extra: "npm:^11.1.0" image-size: "npm:^1.0.0" loader-utils: "npm:^3.2.1" next: "npm:^14.1.0" @@ -6444,7 +6438,7 @@ __metadata: postcss-loader: "npm:^8.1.1" react-refresh: "npm:^0.14.0" resolve-url-loader: "npm:^5.0.0" - sass-loader: "npm:^12.4.0" + sass-loader: "npm:^13.2.0" semver: "npm:^7.3.5" sharp: "npm:^0.33.3" style-loader: "npm:^3.3.1" @@ -6583,7 +6577,6 @@ __metadata: "@types/node": "npm:^22.0.0" "@types/semver": "npm:^7.3.4" find-up: "npm:^5.0.0" - fs-extra: "npm:^11.1.0" magic-string: "npm:^0.30.5" react-docgen: "npm:^7.0.0" resolve: "npm:^1.22.8" @@ -6609,7 +6602,6 @@ __metadata: "@storybook/global": "npm:^5.0.0" "@storybook/server": "workspace:*" "@types/node": "npm:^22.0.0" - fs-extra: "npm:^11.1.0" safe-identifier: "npm:^0.4.1" ts-dedent: "npm:^2.0.0" typescript: "npm:^5.3.2" @@ -6708,7 +6700,7 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/react-vite@workspace:frameworks/react-vite" dependencies: - "@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.3.1" + "@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.3.0" "@rollup/pluginutils": "npm:^5.0.2" "@storybook/builder-vite": "workspace:*" "@storybook/react": "workspace:*" @@ -6876,7 +6868,6 @@ __metadata: "@testing-library/user-event": "npm:^14.5.2" "@trivago/prettier-plugin-sort-imports": "npm:^4.3.0" "@types/express": "npm:^4.17.21" - "@types/fs-extra": "npm:^11.0.1" "@types/lodash": "npm:^4.14.167" "@types/mock-require": "npm:^2.0.3" "@types/node": "npm:^22.0.0" @@ -6902,7 +6893,6 @@ __metadata: eslint-plugin-local-rules: "portal:../scripts/eslint-plugin-local-rules" eslint-plugin-playwright: "npm:^1.6.2" eslint-plugin-storybook: "npm:^0.8.0" - fs-extra: "npm:^11.1.0" github-release-from-changelog: "npm:^2.1.1" glob: "npm:^10.0.0" happy-dom: "npm:^14.12.0" @@ -6984,8 +6974,6 @@ __metadata: "@storybook/manager-api": "workspace:^" "@storybook/preview-api": "workspace:^" "@storybook/theming": "workspace:^" - "@types/fs-extra": "npm:^11.0.1" - fs-extra: "npm:^11.1.0" ts-dedent: "npm:^2.0.0" typescript: "npm:^5.3.2" yaml: "npm:^2.3.1" @@ -7060,7 +7048,6 @@ __metadata: "@sveltejs/vite-plugin-svelte": "npm:^3.0.2" "@testing-library/svelte": "patch:@testing-library/svelte@npm%3A4.1.0#~/.yarn/patches/@testing-library-svelte-npm-4.1.0-34b7037bc0.patch" expect-type: "npm:^0.15.0" - fs-extra: "npm:^11.1.0" svelte: "npm:^5.0.0-next.65" svelte-check: "npm:^3.6.4" sveltedoc-parser: "npm:^4.2.1" @@ -7868,16 +7855,6 @@ __metadata: languageName: node linkType: hard -"@types/fs-extra@npm:^11.0.1": - version: 11.0.4 - resolution: "@types/fs-extra@npm:11.0.4" - dependencies: - "@types/jsonfile": "npm:*" - "@types/node": "npm:*" - checksum: 10c0/9e34f9b24ea464f3c0b18c3f8a82aefc36dc524cc720fc2b886e5465abc66486ff4e439ea3fb2c0acebf91f6d3f74e514f9983b1f02d4243706bdbb7511796ad - languageName: node - linkType: hard - "@types/fs-extra@npm:^5.0.5": version: 5.1.0 resolution: "@types/fs-extra@npm:5.1.0" @@ -12501,7 +12478,6 @@ __metadata: execa: "npm:^5.0.0" fd-package-json: "npm:^1.2.0" find-up: "npm:^5.0.0" - fs-extra: "npm:^11.1.0" ora: "npm:^5.4.1" prettier: "npm:^3.1.1" prompts: "npm:^2.4.0" @@ -18365,6 +18341,15 @@ __metadata: languageName: node linkType: hard +"jiti@npm:^1.21.6": + version: 1.21.6 + resolution: "jiti@npm:1.21.6" + bin: + jiti: bin/jiti.js + checksum: 10c0/05b9ed58cd30d0c3ccd3c98209339e74f50abd9a17e716f65db46b6a35812103f6bde6e134be7124d01745586bca8cc5dae1d0d952267c3ebe55171949c32e56 + languageName: node + linkType: hard + "jju@npm:^1.4.0": version: 1.4.0 resolution: "jju@npm:1.4.0" @@ -25304,15 +25289,14 @@ __metadata: languageName: node linkType: hard -"sass-loader@npm:^12.4.0": - version: 12.6.0 - resolution: "sass-loader@npm:12.6.0" +"sass-loader@npm:^13.2.0": + version: 13.3.3 + resolution: "sass-loader@npm:13.3.3" dependencies: - klona: "npm:^2.0.4" neo-async: "npm:^2.6.2" peerDependencies: fibers: ">= 3.1.0" - node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + 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 @@ -25325,7 +25309,7 @@ __metadata: optional: true sass-embedded: optional: true - checksum: 10c0/e1ef655f3898cc4c45f02b3c627f8baf998139993a9a79c524153a80814282bfe20d8d8d703b8cf1d05457c1930940b65e2156d11285ed0861f9a1016f993e53 + checksum: 10c0/5e955a4ffce35ee0a46fce677ce51eaa69587fb5371978588c83af00f49e7edc36dcf3bb559cbae27681c5e24a71284463ebe03a1fb65e6ecafa1db0620e3fc8 languageName: node linkType: hard diff --git a/docs/versions/next.json b/docs/versions/next.json index 9963ff265181..144772e63ef6 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"8.4.0-alpha.0","info":{"plain":""}} +{"version":"8.4.0-alpha.1","info":{"plain":"- Addon Test: Support story name as test description - [#29147](https://github.com/storybookjs/storybook/pull/29147), thanks @InfiniteXyy!\n- Addon-Interactions: Use ansi-to-html for colored test errors - [#29110](https://github.com/storybookjs/storybook/pull/29110), thanks @kasperpeulen!\n- Angular: Fix sourceDecorator to apply excludeDecorators flag - [#29069](https://github.com/storybookjs/storybook/pull/29069), thanks @JSMike!\n- Builder-vite: Replace .at() call with [] in codegen - [#29048](https://github.com/storybookjs/storybook/pull/29048), thanks @Chudesnov!\n- CLI: Ensure `.gitignore` updated via CLI ends with a newline - [#29124](https://github.com/storybookjs/storybook/pull/29124), thanks @3w36zj6!\n- CLI: Fix skip-install for stable latest releases - [#29133](https://github.com/storybookjs/storybook/pull/29133), thanks @valentinpalkovic!\n- CLI: Show constraints in error when getting depndencies - [#29187](https://github.com/storybookjs/storybook/pull/29187), thanks @andrasczeh!\n- Core: Do not add packageManager field to package.json during `storybook dev` - [#29152](https://github.com/storybookjs/storybook/pull/29152), thanks @valentinpalkovic!\n- Core: Do not prebundle better-opn - [#29137](https://github.com/storybookjs/storybook/pull/29137), thanks @valentinpalkovic!\n- Core: Do not prebundle jsdoc-type-pratt-parser - [#29134](https://github.com/storybookjs/storybook/pull/29134), thanks @valentinpalkovic!\n- Core: Replace `fs-extra` with the native APIs - [#29126](https://github.com/storybookjs/storybook/pull/29126), thanks @ziebam!\n- Next.js: Upgrade sass-loader from ^12 to ^13 - [#29040](https://github.com/storybookjs/storybook/pull/29040), thanks @HoncharenkoZhenya!\n- React-Vite: Downgrade react-docgen-typescript plugin - [#29184](https://github.com/storybookjs/storybook/pull/29184), thanks @shilman!\n- UI: Fix composed storybook TooltipLinkList bug where href isn't passed forward - [#29175](https://github.com/storybookjs/storybook/pull/29175), thanks @JSMike!\n- Viewport-addon: Add InitialViewportKeys type to viewport addon - [#29182](https://github.com/storybookjs/storybook/pull/29182), thanks @hyeongrok7874!\n- Vite: Add jsdoc-type-pratt-parser to `optimizeDeps` - [#29179](https://github.com/storybookjs/storybook/pull/29179), thanks @tobiasdiez!"}} diff --git a/docs/writing-tests/vitest-plugin.mdx b/docs/writing-tests/vitest-plugin.mdx index 09940ef230d9..1a0e4b9708b2 100644 --- a/docs/writing-tests/vitest-plugin.mdx +++ b/docs/writing-tests/vitest-plugin.mdx @@ -379,6 +379,16 @@ We recommend running tests in a browser using Playwright, but you can use WebDri We recommend using Chromium, because it is most likely to best match the experience of a majority of your users. However, you can use other browsers by adjusting the [browser name in the Vitest configuration file](https://vitest.dev/config/#browser-name). Note that [Playwright and WebDriverIO support different browsers](https://vitest.dev/guide/browser/#browser-option-types). +### How do I customize a test name? + +By default, the export name of a story is mapped to the test name. To create a more descriptive test description, you can provide a `name` property for the story. This allows you to include spaces, brackets, or other special characters. + +```js +export const Story = { + name: 'custom, descriptive name' +}; +``` + ## API ### Exports