diff --git a/MIGRATION.md b/MIGRATION.md index dfa6516349d6..72fd9adff5f0 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -2513,7 +2513,7 @@ const meta: Meta = { // Extract all providers (and nested ones) from a ModuleWithProviders importProvidersFrom(SomeOtherModule.forRoot()), ], - } + }), ], }; diff --git a/code/.storybook/main.ts b/code/.storybook/main.ts index 1527c5dbfb72..0d0f1a7cf190 100644 --- a/code/.storybook/main.ts +++ b/code/.storybook/main.ts @@ -1,11 +1,12 @@ -import path from 'path'; +import { join } from 'node:path'; + // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { mergeConfig } from 'vite'; import type { StorybookConfig } from '../frameworks/react-vite'; -const componentsPath = path.join(__dirname, '../core/src/components'); -const managerApiPath = path.join(__dirname, '../core/src/manager-api'); +const componentsPath = join(__dirname, '../core/src/components'); +const managerApiPath = join(__dirname, '../core/src/manager-api'); const config: StorybookConfig = { stories: [ diff --git a/code/addons/controls/src/preset/checkDocsLoaded.ts b/code/addons/controls/src/preset/checkDocsLoaded.ts index ff29457f59f1..7f97c3267c5d 100644 --- a/code/addons/controls/src/preset/checkDocsLoaded.ts +++ b/code/addons/controls/src/preset/checkDocsLoaded.ts @@ -1,6 +1,6 @@ -import { checkAddonOrder, serverRequire } from 'storybook/internal/common'; +import { isAbsolute, join } from 'node:path'; -import path from 'path'; +import { checkAddonOrder, serverRequire } from 'storybook/internal/common'; export const checkDocsLoaded = (configDir: string) => { checkAddonOrder({ @@ -12,9 +12,9 @@ export const checkDocsLoaded = (configDir: string) => { name: '@storybook/addon-controls', inEssentials: true, }, - configFile: path.isAbsolute(configDir) - ? path.join(configDir, 'main') - : path.join(process.cwd(), configDir, 'main'), + configFile: isAbsolute(configDir) + ? join(configDir, 'main') + : join(process.cwd(), configDir, 'main'), getConfig: (configFile) => serverRequire(configFile), }); }; diff --git a/code/addons/docs/src/plugins/mdx-plugin.ts b/code/addons/docs/src/plugins/mdx-plugin.ts index ec36c994a3e8..2e2112a91a88 100644 --- a/code/addons/docs/src/plugins/mdx-plugin.ts +++ b/code/addons/docs/src/plugins/mdx-plugin.ts @@ -1,7 +1,8 @@ +import { dirname, join } from 'node:path'; + import type { Options } from 'storybook/internal/types'; import { createFilter } from '@rollup/pluginutils'; -import { dirname, join } from 'path'; import rehypeExternalLinks from 'rehype-external-links'; import rehypeSlug from 'rehype-slug'; import type { Plugin } from 'vite'; diff --git a/code/addons/docs/src/preset.ts b/code/addons/docs/src/preset.ts index a9989b90d8c2..90933f7d8a9c 100644 --- a/code/addons/docs/src/preset.ts +++ b/code/addons/docs/src/preset.ts @@ -1,9 +1,10 @@ +import { dirname, isAbsolute, join } from 'node:path'; + import { logger } from 'storybook/internal/node-logger'; import type { DocsOptions, Options, PresetProperty } from 'storybook/internal/types'; import type { CsfPluginOptions } from '@storybook/csf-plugin'; -import { dirname, isAbsolute, join } from 'path'; import rehypeExternalLinks from 'rehype-external-links'; import rehypeSlug from 'rehype-slug'; diff --git a/code/addons/essentials/src/docs/preset.ts b/code/addons/essentials/src/docs/preset.ts index 5c7409f65b14..1d6ed6fbbc54 100644 --- a/code/addons/essentials/src/docs/preset.ts +++ b/code/addons/essentials/src/docs/preset.ts @@ -1,4 +1,4 @@ -import { dirname, join } from 'path'; +import { dirname, join } from 'node:path'; export * from '@storybook/addon-docs/dist/preset'; diff --git a/code/addons/essentials/src/index.ts b/code/addons/essentials/src/index.ts index fdae10f376b8..ce80fc37634e 100644 --- a/code/addons/essentials/src/index.ts +++ b/code/addons/essentials/src/index.ts @@ -1,8 +1,8 @@ +import { isAbsolute, join } from 'node:path'; + import { serverRequire } from 'storybook/internal/common'; import { logger } from 'storybook/internal/node-logger'; -import path from 'path'; - interface PresetOptions { /** * Allow to use @storybook/addon-actions @@ -57,10 +57,8 @@ interface PresetOptions { } const requireMain = (configDir: string) => { - const absoluteConfigDir = path.isAbsolute(configDir) - ? configDir - : path.join(process.cwd(), configDir); - const mainFile = path.join(absoluteConfigDir, 'main'); + const absoluteConfigDir = isAbsolute(configDir) ? configDir : join(process.cwd(), configDir); + const mainFile = join(absoluteConfigDir, 'main'); return serverRequire(mainFile) ?? {}; }; diff --git a/code/addons/interactions/src/preset.ts b/code/addons/interactions/src/preset.ts index 2c60d293c666..b58832d5e36e 100644 --- a/code/addons/interactions/src/preset.ts +++ b/code/addons/interactions/src/preset.ts @@ -1,6 +1,6 @@ -import { checkAddonOrder, serverRequire } from 'storybook/internal/common'; +import { isAbsolute, join } from 'node:path'; -import path from 'path'; +import { checkAddonOrder, serverRequire } from 'storybook/internal/common'; export const checkActionsLoaded = (configDir: string) => { checkAddonOrder({ @@ -12,9 +12,9 @@ export const checkActionsLoaded = (configDir: string) => { name: '@storybook/addon-interactions', inEssentials: false, }, - configFile: path.isAbsolute(configDir) - ? path.join(configDir, 'main') - : path.join(process.cwd(), configDir, 'main'), + configFile: isAbsolute(configDir) + ? join(configDir, 'main') + : join(process.cwd(), configDir, 'main'), getConfig: (configFile) => serverRequire(configFile), }); }; diff --git a/code/addons/onboarding/src/preset.ts b/code/addons/onboarding/src/preset.ts index 171d26baccb8..f93b16235bfd 100644 --- a/code/addons/onboarding/src/preset.ts +++ b/code/addons/onboarding/src/preset.ts @@ -1,9 +1,9 @@ +import { readFileSync } from 'node:fs'; + import type { Channel } from 'storybook/internal/channels'; import { telemetry } from 'storybook/internal/telemetry'; import type { CoreConfig, Options } from 'storybook/internal/types'; -import fs from 'fs'; - import { STORYBOOK_ADDON_ONBOARDING_CHANNEL } from './constants'; type Event = { @@ -20,7 +20,7 @@ export const experimental_serverChannel = async (channel: Channel, options: Opti const packageJsonPath = require.resolve('@storybook/addon-onboarding/package.json'); const { version: addonVersion } = JSON.parse( - fs.readFileSync(packageJsonPath, { encoding: 'utf-8' }) + readFileSync(packageJsonPath, { encoding: 'utf-8' }) ); channel.on(STORYBOOK_ADDON_ONBOARDING_CHANNEL, ({ type, ...event }: Event) => { diff --git a/code/builders/builder-vite/src/codegen-importfn-script.ts b/code/builders/builder-vite/src/codegen-importfn-script.ts index 678468b447ff..abf89ce09a73 100644 --- a/code/builders/builder-vite/src/codegen-importfn-script.ts +++ b/code/builders/builder-vite/src/codegen-importfn-script.ts @@ -1,6 +1,6 @@ -import type { Options } from 'storybook/internal/types'; +import { relative } from 'node:path'; -import * as path from 'path'; +import type { Options } from 'storybook/internal/types'; import { listStories } from './list-stories'; @@ -27,7 +27,7 @@ function toImportPath(relativePath: string) { async function toImportFn(stories: string[]) { const { normalizePath } = await import('vite'); const objectEntries = stories.map((file) => { - const relativePath = normalizePath(path.relative(process.cwd(), file)); + const relativePath = normalizePath(relative(process.cwd(), file)); return ` '${toImportPath(relativePath)}': async () => import('/@fs/${file}')`; }); diff --git a/code/builders/builder-vite/src/index.ts b/code/builders/builder-vite/src/index.ts index edc6a98843d0..a8a775a13344 100644 --- a/code/builders/builder-vite/src/index.ts +++ b/code/builders/builder-vite/src/index.ts @@ -1,11 +1,12 @@ // noinspection JSUnusedGlobalSymbols +import { join, parse } from 'node:path'; + import { NoStatsForViteDevError } from 'storybook/internal/server-errors'; import type { Options } from 'storybook/internal/types'; import type { RequestHandler } from 'express'; import express from 'express'; import * as fs from 'fs-extra'; -import { join, parse } from 'path'; import { corePath } from 'storybook/core-path'; import type { ViteDevServer } from 'vite'; diff --git a/code/builders/builder-vite/src/list-stories.ts b/code/builders/builder-vite/src/list-stories.ts index 98916fb994b1..5eefb9b8fb74 100644 --- a/code/builders/builder-vite/src/list-stories.ts +++ b/code/builders/builder-vite/src/list-stories.ts @@ -1,8 +1,9 @@ +import { isAbsolute, join } from 'node:path'; + import { commonGlobOptions, normalizeStories } from 'storybook/internal/common'; import type { Options } from 'storybook/internal/types'; import { glob } from 'glob'; -import * as path from 'path'; import slash from 'slash'; export async function listStories(options: Options) { @@ -15,10 +16,8 @@ export async function listStories(options: Options) { configDir: options.configDir, workingDir: options.configDir, }).map(({ directory, files }) => { - const pattern = path.join(directory, files); - const absolutePattern = path.isAbsolute(pattern) - ? pattern - : path.join(options.configDir, pattern); + const pattern = join(directory, files); + const absolutePattern = isAbsolute(pattern) ? pattern : join(options.configDir, pattern); return glob(slash(absolutePattern), { ...commonGlobOptions(absolutePattern), diff --git a/code/builders/builder-vite/src/optimizeDeps.ts b/code/builders/builder-vite/src/optimizeDeps.ts index c3899075e1ee..5937c4497720 100644 --- a/code/builders/builder-vite/src/optimizeDeps.ts +++ b/code/builders/builder-vite/src/optimizeDeps.ts @@ -1,6 +1,7 @@ +import { relative } from 'node:path'; + import type { Options } from 'storybook/internal/types'; -import * as path from 'path'; import type { UserConfig, InlineConfig as ViteInlineConfig } from 'vite'; import { listStories } from './list-stories'; @@ -121,7 +122,7 @@ export async function getOptimizeDeps(config: ViteInlineConfig, options: Options const { root = process.cwd() } = config; const { normalizePath, resolveConfig } = await import('vite'); const absoluteStories = await listStories(options); - const stories = absoluteStories.map((storyPath) => normalizePath(path.relative(root, storyPath))); + const stories = absoluteStories.map((storyPath) => normalizePath(relative(root, storyPath))); // TODO: check if resolveConfig takes a lot of time, possible optimizations here const resolvedConfig = await resolveConfig(config, 'serve', 'development'); diff --git a/code/builders/builder-vite/src/plugins/code-generator-plugin.ts b/code/builders/builder-vite/src/plugins/code-generator-plugin.ts index d4d6bfc1e4f2..ac0fe2a3b970 100644 --- a/code/builders/builder-vite/src/plugins/code-generator-plugin.ts +++ b/code/builders/builder-vite/src/plugins/code-generator-plugin.ts @@ -1,6 +1,7 @@ +import { readFileSync } from 'node:fs'; + import type { Options } from 'storybook/internal/types'; -import * as fs from 'fs'; import type { Plugin } from 'vite'; import { generateImportFnScriptCode } from '../codegen-importfn-script'; @@ -69,41 +70,38 @@ export function codeGeneratorPlugin(options: Options): Plugin { }, resolveId(source) { if (source === virtualFileId) { - return virtualFileId; + return `\0${virtualFileId}`; } if (source === iframePath) { return iframeId; } if (source === virtualStoriesFile) { - return virtualStoriesFile; + return `\0${virtualStoriesFile}`; } if (source === virtualPreviewFile) { return virtualPreviewFile; } if (source === virtualAddonSetupFile) { - return virtualAddonSetupFile; + return `\0${virtualAddonSetupFile}`; } return undefined; }, async load(id, config) { - if (id === virtualStoriesFile) { + if (id === `\0${virtualStoriesFile}`) { return generateImportFnScriptCode(options); } - if (id === virtualAddonSetupFile) { + if (id === `\0${virtualAddonSetupFile}`) { return generateAddonSetupCode(); } - if (id === virtualFileId) { + if (id === `\0${virtualFileId}`) { return generateModernIframeScriptCode(options, projectRoot); } if (id === iframeId) { - return fs.readFileSync( - require.resolve('@storybook/builder-vite/input/iframe.html'), - 'utf-8' - ); + return readFileSync(require.resolve('@storybook/builder-vite/input/iframe.html'), 'utf-8'); } return undefined; diff --git a/code/builders/builder-vite/src/plugins/webpack-stats-plugin.ts b/code/builders/builder-vite/src/plugins/webpack-stats-plugin.ts index a318a0d1fbd9..3669bfd4a745 100644 --- a/code/builders/builder-vite/src/plugins/webpack-stats-plugin.ts +++ b/code/builders/builder-vite/src/plugins/webpack-stats-plugin.ts @@ -1,7 +1,8 @@ // This plugin is a direct port of https://github.com/IanVS/vite-plugin-turbosnap +import { relative } from 'node:path'; + import type { BuilderStats } from 'storybook/internal/types'; -import path from 'path'; import slash from 'slash'; import type { Plugin } from 'vite'; @@ -57,7 +58,7 @@ export function pluginWebpackStats({ workingDir }: WebpackStatsPluginOptions): W } // Otherwise, we need them in the format `./path/to/file.js`. else { - const relativePath = path.relative(workingDir, stripQueryParams(filename)); + const relativePath = relative(workingDir, stripQueryParams(filename)); // This seems hacky, got to be a better way to add a `./` to the start of a path. return `./${slash(relativePath)}`; } diff --git a/code/builders/builder-vite/src/utils/process-preview-annotation.ts b/code/builders/builder-vite/src/utils/process-preview-annotation.ts index c7e3db8c23ab..d63dc9d375d1 100644 --- a/code/builders/builder-vite/src/utils/process-preview-annotation.ts +++ b/code/builders/builder-vite/src/utils/process-preview-annotation.ts @@ -1,7 +1,8 @@ +import { isAbsolute, relative, resolve } from 'node:path'; + import { stripAbsNodeModulesPath } from 'storybook/internal/common'; import type { PreviewAnnotation } from 'storybook/internal/types'; -import { isAbsolute, relative, resolve } from 'path'; import slash from 'slash'; /** diff --git a/code/builders/builder-vite/src/vite-config.ts b/code/builders/builder-vite/src/vite-config.ts index e7c5b874c4ff..e3eff12d038e 100644 --- a/code/builders/builder-vite/src/vite-config.ts +++ b/code/builders/builder-vite/src/vite-config.ts @@ -1,3 +1,5 @@ +import { resolve } from 'node:path'; + import { getBuilderOptions, getFrameworkName, @@ -7,7 +9,6 @@ import { import { globalsNameReferenceMap } from 'storybook/internal/preview/globals'; import type { Options } from 'storybook/internal/types'; -import * as path from 'path'; import type { ConfigEnv, InlineConfig, @@ -50,7 +51,7 @@ export async function commonConfig( const { viteConfigPath } = await getBuilderOptions(options); - const projectRoot = path.resolve(options.configDir, '..'); + const projectRoot = resolve(options.configDir, '..'); // I destructure away the `build` property from the user's config object // I do this because I can contain config that breaks storybook, such as we had in a lit project. diff --git a/code/builders/builder-webpack5/src/index.ts b/code/builders/builder-webpack5/src/index.ts index c19779de2f75..12b38b0709ee 100644 --- a/code/builders/builder-webpack5/src/index.ts +++ b/code/builders/builder-webpack5/src/index.ts @@ -1,3 +1,5 @@ +import { join, parse } from 'node:path'; + import { PREVIEW_BUILDER_PROGRESS } from 'storybook/internal/core-events'; import { logger } from 'storybook/internal/node-logger'; import { @@ -11,7 +13,6 @@ import { checkWebpackVersion } from '@storybook/core-webpack'; import express from 'express'; import fs from 'fs-extra'; -import { join, parse } from 'path'; import prettyTime from 'pretty-hrtime'; import { corePath } from 'storybook/core-path'; import type { Configuration, Stats, StatsOptions } from 'webpack'; diff --git a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts index 0cea3b15737f..461a36aec648 100644 --- a/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts +++ b/code/builders/builder-webpack5/src/preview/iframe-webpack.config.ts @@ -1,3 +1,5 @@ +import { dirname, join, resolve } from 'node:path'; + import { getBuilderOptions, isPreservingSymlinks, @@ -14,7 +16,6 @@ import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin'; import type { TransformOptions as EsbuildOptions } from 'esbuild'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; -import { dirname, join, resolve } from 'path'; import TerserWebpackPlugin from 'terser-webpack-plugin'; import { dedent } from 'ts-dedent'; import { DefinePlugin, HotModuleReplacementPlugin, ProgressPlugin, ProvidePlugin } from 'webpack'; diff --git a/code/builders/builder-webpack5/src/preview/virtual-module-mapping.ts b/code/builders/builder-webpack5/src/preview/virtual-module-mapping.ts index 8c43fff17194..2b78d926f12e 100644 --- a/code/builders/builder-webpack5/src/preview/virtual-module-mapping.ts +++ b/code/builders/builder-webpack5/src/preview/virtual-module-mapping.ts @@ -1,3 +1,5 @@ +import { join, resolve } from 'node:path'; + import { getBuilderOptions, handlebars, @@ -9,7 +11,6 @@ import type { Options, PreviewAnnotation } from 'storybook/internal/types'; import { toImportFn } from '@storybook/core-webpack'; -import { join, resolve } from 'path'; import slash from 'slash'; import type { BuilderOptions } from '../types'; diff --git a/code/core/src/builder-manager/utils/framework.test.ts b/code/core/src/builder-manager/utils/framework.test.ts index 8b2af6f9a6c0..cea3453294e2 100644 --- a/code/core/src/builder-manager/utils/framework.test.ts +++ b/code/core/src/builder-manager/utils/framework.test.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; @@ -25,24 +25,24 @@ describe('UTILITIES: Framework information', () => { describe('UTILITY: pluckStorybookPackageFromPath', () => { it('should return the package name if the path is a storybook package', () => { - const packagePath = path.join(process.cwd(), 'node_modules', '@storybook', 'foo'); + const packagePath = join(process.cwd(), 'node_modules', '@storybook', 'foo'); expect(pluckStorybookPackageFromPath(packagePath)).toBe('@storybook/foo'); }); it('should return undefined if the path is not a storybook package', () => { - const packagePath = path.join(process.cwd(), 'foo'); + const packagePath = join(process.cwd(), 'foo'); expect(pluckStorybookPackageFromPath(packagePath)).toBe(undefined); }); }); describe('UTILITY: pluckThirdPartyPackageFromPath', () => { it('should return the package name if the path is a third party package', () => { - const packagePath = path.join(process.cwd(), 'node_modules', 'bar'); + const packagePath = join(process.cwd(), 'node_modules', 'bar'); expect(pluckThirdPartyPackageFromPath(packagePath)).toBe('bar'); }); it('should return the given path if the path is not a third party package', () => { - const packagePath = path.join(process.cwd(), 'foo', 'bar', 'baz'); + const packagePath = join(process.cwd(), 'foo', 'bar', 'baz'); expect(pluckThirdPartyPackageFromPath(packagePath)).toBe(packagePath); }); }); diff --git a/code/core/src/builder-manager/utils/framework.ts b/code/core/src/builder-manager/utils/framework.ts index f29d0bd05065..d399494c049c 100644 --- a/code/core/src/builder-manager/utils/framework.ts +++ b/code/core/src/builder-manager/utils/framework.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { sep } from 'node:path'; import { extractProperRendererNameFromFramework, getFrameworkName } from '@storybook/core/common'; import type { Options } from '@storybook/core/types'; @@ -19,7 +19,7 @@ export const pluckNameFromConfigProperty = (property: Property) => { }; // For replacing Windows backslashes with forward slashes -const normalizePath = (packagePath: string) => packagePath.replaceAll(path.sep, '/'); +const normalizePath = (packagePath: string) => packagePath.replaceAll(sep, '/'); export const pluckStorybookPackageFromPath = (packagePath: string) => normalizePath(packagePath).match(/(@storybook\/.*)$/)?.[1]; diff --git a/code/core/src/cli/angular/helpers.ts b/code/core/src/cli/angular/helpers.ts index ac7f88b713e4..78ef8836996f 100644 --- a/code/core/src/cli/angular/helpers.ts +++ b/code/core/src/cli/angular/helpers.ts @@ -1,9 +1,10 @@ +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + import { logger } from '@storybook/core/node-logger'; import { MissingAngularJsonError } from '@storybook/core/server-errors'; import boxen from 'boxen'; -import fs from 'fs'; -import { join } from 'path'; import prompts from 'prompts'; import { dedent } from 'ts-dedent'; @@ -43,11 +44,11 @@ export class AngularJSON { }; constructor() { - if (!fs.existsSync(ANGULAR_JSON_PATH)) { + if (!existsSync(ANGULAR_JSON_PATH)) { throw new MissingAngularJsonError({ path: join(process.cwd(), ANGULAR_JSON_PATH) }); } - const jsonContent = fs.readFileSync(ANGULAR_JSON_PATH, 'utf8'); + const jsonContent = readFileSync(ANGULAR_JSON_PATH, 'utf8'); this.json = JSON.parse(jsonContent); } @@ -149,6 +150,6 @@ export class AngularJSON { } write() { - fs.writeFileSync(ANGULAR_JSON_PATH, JSON.stringify(this.json, null, 2)); + writeFileSync(ANGULAR_JSON_PATH, JSON.stringify(this.json, null, 2)); } } diff --git a/code/core/src/cli/detect.test.ts b/code/core/src/cli/detect.test.ts index 9a3041cecfa6..f12ece4314fa 100644 --- a/code/core/src/cli/detect.test.ts +++ b/code/core/src/cli/detect.test.ts @@ -1,11 +1,11 @@ +import { existsSync } from 'node:fs'; + import { afterEach, describe, expect, it, vi } from 'vitest'; import type { JsPackageManager, PackageJsonWithMaybeDeps } from '@storybook/core/common'; import { logger } from '@storybook/core/node-logger'; -import * as fs from 'fs'; - import { detect, detectFrameworkPreset, detectLanguage } from './detect'; import { ProjectType, SupportedLanguage } from './project_types'; @@ -421,7 +421,7 @@ describe('Detect', () => { MOCK_FRAMEWORK_FILES.forEach((structure) => { it(`${structure.name}`, () => { - vi.mocked(fs.existsSync).mockImplementation((filePath) => { + vi.mocked(existsSync).mockImplementation((filePath) => { return typeof filePath === 'string' && Object.keys(structure.files).includes(filePath); }); @@ -454,7 +454,7 @@ describe('Detect', () => { '/node_modules/.bin/react-scripts': 'file content', }; - vi.mocked(fs.existsSync).mockImplementation((filePath) => { + vi.mocked(existsSync).mockImplementation((filePath) => { return ( typeof filePath === 'string' && Object.keys(forkedReactScriptsConfig).includes(filePath) ); diff --git a/code/core/src/cli/detect.ts b/code/core/src/cli/detect.ts index 1aef0ed2062d..96ab4c785cee 100644 --- a/code/core/src/cli/detect.ts +++ b/code/core/src/cli/detect.ts @@ -1,3 +1,4 @@ +import { existsSync } from 'node:fs'; import { resolve } from 'node:path'; import type { JsPackageManager, PackageJsonWithMaybeDeps } from '@storybook/core/common'; @@ -6,7 +7,6 @@ import { HandledError, commandLog } from '@storybook/core/common'; import { logger } from '@storybook/core/node-logger'; import { findUpSync } from 'find-up'; -import * as fs from 'fs'; import prompts from 'prompts'; import semver from 'semver'; @@ -90,7 +90,7 @@ const getFrameworkPreset = ( } if (Array.isArray(files) && files.length > 0) { - matcher.files = files.map((name) => fs.existsSync(name)); + matcher.files = files.map((name) => existsSync(name)); } return matcherFunction(matcher) ? preset : null; @@ -160,7 +160,7 @@ export async function detectBuilder(packageManager: JsPackageManager, projectTyp } export function isStorybookInstantiated(configDir = resolve(process.cwd(), '.storybook')) { - return fs.existsSync(configDir); + return existsSync(configDir); } export async function detectPnp() { @@ -170,7 +170,7 @@ export async function detectPnp() { export async function detectLanguage(packageManager: JsPackageManager) { let language = SupportedLanguage.JAVASCRIPT; - if (fs.existsSync('jsconfig.json')) { + if (existsSync('jsconfig.json')) { return language; } diff --git a/code/core/src/cli/eslintPlugin.ts b/code/core/src/cli/eslintPlugin.ts index 3e37b41b5267..53371725f84a 100644 --- a/code/core/src/cli/eslintPlugin.ts +++ b/code/core/src/cli/eslintPlugin.ts @@ -1,4 +1,4 @@ -import fs from 'node:fs'; +import { existsSync } from 'node:fs'; import type { JsPackageManager } from '@storybook/core/common'; import { paddedLog } from '@storybook/core/common'; @@ -17,7 +17,7 @@ const UNSUPPORTED_ESLINT_EXTENSIONS = ['yaml', 'yml']; export const findEslintFile = () => { const filePrefix = '.eslintrc'; const unsupportedExtension = UNSUPPORTED_ESLINT_EXTENSIONS.find((ext: string) => - fs.existsSync(`${filePrefix}.${ext}`) + existsSync(`${filePrefix}.${ext}`) ); if (unsupportedExtension) { @@ -25,7 +25,7 @@ export const findEslintFile = () => { } const extension = SUPPORTED_ESLINT_EXTENSIONS.find((ext: string) => - fs.existsSync(`${filePrefix}.${ext}`) + existsSync(`${filePrefix}.${ext}`) ); return extension ? `${filePrefix}.${extension}` : null; }; diff --git a/code/core/src/cli/helpers.test.ts b/code/core/src/cli/helpers.test.ts index 71661e5cafa4..cff797a8f505 100644 --- a/code/core/src/cli/helpers.test.ts +++ b/code/core/src/cli/helpers.test.ts @@ -78,7 +78,7 @@ describe('Helpers', () => { describe('copyTemplate', () => { it(`should copy template files when directory is present`, () => { - const csfDirectory = /template-csf$/; + const csfDirectory = /template-csf\/$/; fsMocks.existsSync.mockReturnValue(true); helpers.copyTemplate(''); diff --git a/code/core/src/cli/helpers.ts b/code/core/src/cli/helpers.ts index c1dfae542c0a..5970a1276237 100644 --- a/code/core/src/cli/helpers.ts +++ b/code/core/src/cli/helpers.ts @@ -1,3 +1,6 @@ +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; + import { frameworkToRenderer as CoreFrameworkToRenderer, type JsPackageManager, @@ -9,9 +12,7 @@ import type { SupportedFrameworks, SupportedRenderers } from '@storybook/core/ty import chalk from 'chalk'; import { findUpSync } from 'find-up'; -import fs from 'fs'; -import fse from 'fs-extra'; -import path, { join } from 'path'; +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'; @@ -22,12 +23,12 @@ import { CoreBuilder, SupportedLanguage } from './project_types'; const logger = console; export function readFileAsJson(jsonPath: string, allowComments?: boolean) { - const filePath = path.resolve(jsonPath); - if (!fs.existsSync(filePath)) { + const filePath = resolve(jsonPath); + if (!existsSync(filePath)) { return false; } - const fileContent = fs.readFileSync(filePath, 'utf8'); + const fileContent = readFileSync(filePath, 'utf8'); const jsonContent = allowComments ? stripJsonComments(fileContent) : fileContent; try { @@ -39,12 +40,12 @@ export function readFileAsJson(jsonPath: string, allowComments?: boolean) { } export const writeFileAsJson = (jsonPath: string, content: unknown) => { - const filePath = path.resolve(jsonPath); - if (!fs.existsSync(filePath)) { + const filePath = resolve(jsonPath); + if (!existsSync(filePath)) { return false; } - fs.writeFileSync(filePath, `${JSON.stringify(content, null, 2)}\n`); + writeFileSync(filePath, `${JSON.stringify(content, null, 2)}\n`); return true; }; @@ -115,13 +116,13 @@ export function addToDevDependenciesIfNotPresent( } export function copyTemplate(templateRoot: string, destination = '.') { - const templateDir = path.resolve(templateRoot, `template-csf/`); + const templateDir = resolve(templateRoot, `template-csf/`); - if (!fs.existsSync(templateDir)) { + if (!existsSync(templateDir)) { throw new Error(`Couldn't find template dir`); } - fse.copySync(templateDir, destination, { overwrite: true }); + copySync(templateDir, destination, { overwrite: true }); } type CopyTemplateFilesOptions = { @@ -143,6 +144,7 @@ export const frameworkToDefaultBuilder: Record 'html-vite': CoreBuilder.Vite, 'html-webpack5': CoreBuilder.Webpack5, nextjs: CoreBuilder.Webpack5, + 'experimental-nextjs-vite': CoreBuilder.Vite, 'preact-vite': CoreBuilder.Vite, 'preact-webpack5': CoreBuilder.Webpack5, qwik: CoreBuilder.Vite, @@ -183,30 +185,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 fse.pathExists(assetsLanguage)) { + if (await pathExists(assetsLanguage)) { return assetsLanguage; } // Use fallback typescript 3.8 assets if new ones aren't available - if (language === SupportedLanguage.TYPESCRIPT_4_9 && (await fse.pathExists(assetsTS38))) { + if (language === SupportedLanguage.TYPESCRIPT_4_9 && (await pathExists(assetsTS38))) { return assetsTS38; } // Fallback further to TS (for backwards compatibility purposes) - if (await fse.pathExists(assetsTS)) { + if (await pathExists(assetsTS)) { return assetsTS; } // Fallback further to JS - if (await fse.pathExists(assetsJS)) { + if (await pathExists(assetsJS)) { return assetsJS; } // As a last resort, look for the root of the asset directory - if (await fse.pathExists(assetsDir)) { + if (await pathExists(assetsDir)) { return assetsDir; } throw new Error(`Unsupported renderer: ${renderer} (${baseDir})`); }; const targetPath = async () => { - if (await fse.pathExists('./src')) { + if (await pathExists('./src')) { return './src/stories'; } return './stories'; @@ -214,11 +216,11 @@ export async function copyTemplateFiles({ const destinationPath = destination ?? (await targetPath()); if (commonAssetsDir) { - await fse.copy(commonAssetsDir, destinationPath, { + await copy(commonAssetsDir, destinationPath, { overwrite: true, }); } - await fse.copy(await templatePath(), destinationPath, { overwrite: true }); + await copy(await templatePath(), destinationPath, { overwrite: true }); if (commonAssetsDir) { let rendererType = frameworkToRenderer[renderer] || 'react'; @@ -231,13 +233,13 @@ 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 fse.readFile(templatePath, 'utf8'); + let template = await readFile(templatePath, 'utf8'); Object.keys(templateData).forEach((key) => { template = template.replaceAll(`{{${key}}}`, `${templateData[key]}`); }); - await fse.writeFile(templatePath, template); + await writeFile(templatePath, template); } // Given a package.json, finds any official storybook package within it diff --git a/code/core/src/common/js-package-manager/JsPackageManager.ts b/code/core/src/common/js-package-manager/JsPackageManager.ts index 6f1b6f6b79df..d126db20aa8a 100644 --- a/code/core/src/common/js-package-manager/JsPackageManager.ts +++ b/code/core/src/common/js-package-manager/JsPackageManager.ts @@ -1,6 +1,6 @@ import { existsSync, readFileSync } from 'node:fs'; import { readFile, writeFile } from 'node:fs/promises'; -import path from 'node:path'; +import { dirname, resolve } from 'node:path'; import chalk from 'chalk'; import type { CommonOptions } from 'execa'; @@ -99,7 +99,7 @@ export abstract class JsPackageManager { } // Move up to the parent directory - const parentDir = path.dirname(cwd); + const parentDir = dirname(cwd); // Check if we have reached the root of the filesystem if (parentDir === cwd) { @@ -132,7 +132,7 @@ export abstract class JsPackageManager { if (!this.cwd) { throw new Error('Missing cwd'); } - return path.resolve(this.cwd, 'package.json'); + return resolve(this.cwd, 'package.json'); } async readPackageJson(): Promise { diff --git a/code/core/src/common/js-package-manager/JsPackageManagerFactory.test.ts b/code/core/src/common/js-package-manager/JsPackageManagerFactory.test.ts index 0a3a2a813413..9f703d7c5ba3 100644 --- a/code/core/src/common/js-package-manager/JsPackageManagerFactory.test.ts +++ b/code/core/src/common/js-package-manager/JsPackageManagerFactory.test.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { join } from 'node:path'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -153,7 +153,7 @@ describe('CLASS: JsPackageManagerFactory', () => { status: 1, } as any; }); - const fixture = path.join(__dirname, 'fixtures', 'pnpm-workspace', 'package'); + const fixture = join(__dirname, 'fixtures', 'pnpm-workspace', 'package'); expect(JsPackageManagerFactory.getPackageManager({}, fixture)).toBeInstanceOf(PNPMProxy); }); }); @@ -271,7 +271,7 @@ describe('CLASS: JsPackageManagerFactory', () => { status: 1, } as any; }); - const fixture = path.join(__dirname, 'fixtures', 'multiple-lockfiles'); + const fixture = join(__dirname, 'fixtures', 'multiple-lockfiles'); expect(JsPackageManagerFactory.getPackageManager({}, fixture)).toBeInstanceOf(Yarn1Proxy); }); }); diff --git a/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts b/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts index c6ad21626404..0b09f57bac52 100644 --- a/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts +++ b/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts @@ -1,4 +1,4 @@ -import path, { parse, relative } from 'node:path'; +import { basename, parse, relative } from 'node:path'; import { sync as spawnSync } from 'cross-spawn'; import { findUpSync } from 'find-up'; @@ -55,7 +55,7 @@ export class JsPackageManagerFactory { // Option 2: We try to infer the package manager from the closest lockfile const closestLockfilePath = lockFiles[0]; - const closestLockfile = closestLockfilePath && path.basename(closestLockfilePath); + const closestLockfile = closestLockfilePath && basename(closestLockfilePath); const hasNPMCommand = hasNPM(cwd); const hasPNPMCommand = hasPNPM(cwd); diff --git a/code/core/src/common/js-package-manager/NPMProxy.ts b/code/core/src/common/js-package-manager/NPMProxy.ts index 24ad9cfd794b..69a89a741d53 100644 --- a/code/core/src/common/js-package-manager/NPMProxy.ts +++ b/code/core/src/common/js-package-manager/NPMProxy.ts @@ -1,10 +1,10 @@ import { existsSync, readFileSync } from 'node:fs'; -import path from 'node:path'; +import { join } from 'node:path'; import { logger } from '@storybook/core/node-logger'; import { FindPackageVersionsError } from '@storybook/core/server-errors'; -import { findUpSync } from 'find-up'; +import { findUp } from 'find-up'; import { platform } from 'os'; import sort from 'semver/functions/sort.js'; import dedent from 'ts-dedent'; @@ -89,9 +89,9 @@ export class NPMProxy extends JsPackageManager { packageName: string, basePath = this.cwd ): Promise { - const packageJsonPath = await findUpSync( + const packageJsonPath = await findUp( (dir) => { - const possiblePath = path.join(dir, 'node_modules', packageName, 'package.json'); + const possiblePath = join(dir, 'node_modules', packageName, 'package.json'); return existsSync(possiblePath) ? possiblePath : undefined; }, { cwd: basePath } diff --git a/code/core/src/common/js-package-manager/PNPMProxy.ts b/code/core/src/common/js-package-manager/PNPMProxy.ts index d04f78a71b09..8256926e35b6 100644 --- a/code/core/src/common/js-package-manager/PNPMProxy.ts +++ b/code/core/src/common/js-package-manager/PNPMProxy.ts @@ -1,5 +1,5 @@ import { existsSync, readFileSync } from 'node:fs'; -import path from 'node:path'; +import { join } from 'node:path'; import { FindPackageVersionsError } from '@storybook/core/server-errors'; @@ -144,7 +144,7 @@ export class PNPMProxy extends JsPackageManager { const pkg = pnpApi.getPackageInformation(pkgLocator); const packageJSON = JSON.parse( - readFileSync(path.join(pkg.packageLocation, 'package.json'), 'utf-8') + readFileSync(join(pkg.packageLocation, 'package.json'), 'utf-8') ); return packageJSON; @@ -158,7 +158,7 @@ export class PNPMProxy extends JsPackageManager { const packageJsonPath = await findUpSync( (dir) => { - const possiblePath = path.join(dir, 'node_modules', packageName, 'package.json'); + const possiblePath = join(dir, 'node_modules', packageName, 'package.json'); return existsSync(possiblePath) ? possiblePath : undefined; }, { cwd: basePath } diff --git a/code/core/src/common/js-package-manager/Yarn1Proxy.ts b/code/core/src/common/js-package-manager/Yarn1Proxy.ts index 97ca04b63641..8922a581ce55 100644 --- a/code/core/src/common/js-package-manager/Yarn1Proxy.ts +++ b/code/core/src/common/js-package-manager/Yarn1Proxy.ts @@ -1,9 +1,9 @@ import { existsSync, readFileSync } from 'node:fs'; -import path from 'node:path'; +import { join } from 'node:path'; import { FindPackageVersionsError } from '@storybook/core/server-errors'; -import { findUpSync } from 'find-up'; +import { findUp } from 'find-up'; import dedent from 'ts-dedent'; import { createLogStream } from '../utils/cli'; @@ -70,9 +70,9 @@ export class Yarn1Proxy extends JsPackageManager { packageName: string, basePath = this.cwd ): Promise { - const packageJsonPath = await findUpSync( + const packageJsonPath = await findUp( (dir) => { - const possiblePath = path.join(dir, 'node_modules', packageName, 'package.json'); + const possiblePath = join(dir, 'node_modules', packageName, 'package.json'); return existsSync(possiblePath) ? possiblePath : undefined; }, { cwd: basePath } diff --git a/code/core/src/common/js-package-manager/Yarn2Proxy.ts b/code/core/src/common/js-package-manager/Yarn2Proxy.ts index cf81284b1c25..711d34240292 100644 --- a/code/core/src/common/js-package-manager/Yarn2Proxy.ts +++ b/code/core/src/common/js-package-manager/Yarn2Proxy.ts @@ -1,11 +1,11 @@ import { existsSync, readFileSync } from 'node:fs'; -import path from 'node:path'; +import { join } from 'node:path'; import { FindPackageVersionsError } from '@storybook/core/server-errors'; import { PosixFS, VirtualFS, ZipOpenFS } from '@yarnpkg/fslib'; import { getLibzipSync } from '@yarnpkg/libzip'; -import { findUpSync } from 'find-up'; +import { findUp, findUpSync } from 'find-up'; import { dedent } from 'ts-dedent'; import { createLogStream } from '../utils/cli'; @@ -152,7 +152,7 @@ export class Yarn2Proxy extends JsPackageManager { const virtualFs = new VirtualFS({ baseFs: zipOpenFs }); const crossFs = new PosixFS(virtualFs); - const virtualPath = path.join(pkg.packageLocation, 'package.json'); + const virtualPath = join(pkg.packageLocation, 'package.json'); return crossFs.readJsonSync(virtualPath); } catch (error: any) { @@ -163,9 +163,9 @@ export class Yarn2Proxy extends JsPackageManager { } } - const packageJsonPath = await findUpSync( + const packageJsonPath = await findUp( (dir) => { - const possiblePath = path.join(dir, 'node_modules', packageName, 'package.json'); + const possiblePath = join(dir, 'node_modules', packageName, 'package.json'); return existsSync(possiblePath) ? possiblePath : undefined; }, { cwd: basePath } diff --git a/code/core/src/common/presets.test.ts b/code/core/src/common/presets.test.ts index 7c22e514fc26..565abe4521f4 100644 --- a/code/core/src/common/presets.test.ts +++ b/code/core/src/common/presets.test.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { normalize } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -414,7 +414,7 @@ describe('resolveAddonName', () => { it('should resolve managerEntries', () => { expect(resolveAddonName({} as any, '@storybook/addon-actions/register.js', {})).toEqual({ name: '@storybook/addon-actions/register.js', - managerEntries: [path.normalize('@storybook/addon-actions/register')], + managerEntries: [normalize('@storybook/addon-actions/register')], type: 'virtual', }); }); @@ -422,7 +422,7 @@ describe('resolveAddonName', () => { it('should resolve managerEntries from new /manager path', () => { expect(resolveAddonName({} as any, '@storybook/addon-actions/manager', {})).toEqual({ name: '@storybook/addon-actions/manager', - managerEntries: [path.normalize('@storybook/addon-actions/manager')], + managerEntries: [normalize('@storybook/addon-actions/manager')], type: 'virtual', }); }); @@ -549,14 +549,14 @@ describe('loadPreset', () => { name: '@storybook/addon-actions/register.js', options: {}, preset: { - managerEntries: [path.normalize('@storybook/addon-actions/register')], + managerEntries: [normalize('@storybook/addon-actions/register')], }, }, { name: 'addon-foo/register.js', options: {}, preset: { - managerEntries: [path.normalize('addon-foo/register')], + managerEntries: [normalize('addon-foo/register')], }, }, { @@ -578,14 +578,14 @@ describe('loadPreset', () => { name: 'addon-baz/register.js', options: {}, preset: { - managerEntries: [path.normalize('addon-baz/register')], + managerEntries: [normalize('addon-baz/register')], }, }, { name: '@storybook/addon-notes/register-panel', options: {}, preset: { - managerEntries: [path.normalize('@storybook/addon-notes/register-panel')], + managerEntries: [normalize('@storybook/addon-notes/register-panel')], }, }, { diff --git a/code/core/src/common/utils/__tests__/paths.test.ts b/code/core/src/common/utils/__tests__/paths.test.ts index bef3959dfcb5..e353aed6f36e 100644 --- a/code/core/src/common/utils/__tests__/paths.test.ts +++ b/code/core/src/common/utils/__tests__/paths.test.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { join, sep } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; @@ -11,12 +11,12 @@ vi.mock('find-up'); describe('paths - normalizeStoryPath()', () => { it('returns a path starting with "./" unchanged', () => { - const filename = `.${path.sep}${path.join('src', 'Comp.story.js')}`; + const filename = `.${sep}${join('src', 'Comp.story.js')}`; expect(normalizeStoryPath(filename)).toEqual(filename); }); it('returns a path starting with "../" unchanged', () => { - const filename = path.join('..', 'src', 'Comp.story.js'); + const filename = join('..', 'src', 'Comp.story.js'); expect(normalizeStoryPath(filename)).toEqual(filename); }); @@ -31,18 +31,18 @@ describe('paths - normalizeStoryPath()', () => { }); it('adds "./" to a normalized relative path', () => { - const filename = path.join('src', 'Comp.story.js'); - expect(normalizeStoryPath(filename)).toEqual(`.${path.sep}${filename}`); + const filename = join('src', 'Comp.story.js'); + expect(normalizeStoryPath(filename)).toEqual(`.${sep}${filename}`); }); it('adds "./" to a hidden folder', () => { - const filename = path.join('.storybook', 'Comp.story.js'); - expect(normalizeStoryPath(filename)).toEqual(`.${path.sep}${filename}`); + const filename = join('.storybook', 'Comp.story.js'); + expect(normalizeStoryPath(filename)).toEqual(`.${sep}${filename}`); }); it('adds "./" to a hidden file', () => { const filename = `.Comp.story.js`; - expect(normalizeStoryPath(filename)).toEqual(`.${path.sep}${filename}`); + expect(normalizeStoryPath(filename)).toEqual(`.${sep}${filename}`); }); }); diff --git a/code/core/src/common/utils/formatter.test.ts b/code/core/src/common/utils/formatter.test.ts index 647ada8b404a..c60ecb09b84f 100644 --- a/code/core/src/common/utils/formatter.test.ts +++ b/code/core/src/common/utils/formatter.test.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { resolve } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; @@ -34,7 +34,7 @@ type Story = StoryObj; describe('formatter', () => { describe('withPrettierConfig', () => { - const testPath = path.resolve(__dirname, '__tests-formatter__', 'withPrettierConfig'); + const testPath = resolve(__dirname, '__tests-formatter__', 'withPrettierConfig'); describe('prettier', async () => { const prettierV3 = await import('prettier'); @@ -44,7 +44,7 @@ describe('formatter', () => { mockPrettier.version.mockReturnValue(prettierV3.version); mockPrettier.resolveConfig.mockImplementation(prettierV3.resolveConfig); - const filePath = path.resolve(testPath, 'testFile.ts'); + const filePath = resolve(testPath, 'testFile.ts'); const result = await formatFileContent(filePath, dummyContent); @@ -54,7 +54,7 @@ describe('formatter', () => { }); describe('withoutPrettierConfigAndWithEditorConfig', () => { - const testPath = path.resolve(__dirname, '__tests-formatter__', 'withoutPrettierConfig'); + const testPath = resolve(__dirname, '__tests-formatter__', 'withoutPrettierConfig'); describe('prettier-v3', async () => { const prettierV3 = await import('prettier'); @@ -64,7 +64,7 @@ describe('formatter', () => { mockPrettier.version.mockReturnValue(prettierV3.version); mockPrettier.resolveConfig.mockImplementation(prettierV3.resolveConfig); - const filePath = path.resolve(testPath, 'testFile.ts'); + const filePath = resolve(testPath, 'testFile.ts'); const result = await formatFileContent(filePath, dummyContent); @@ -74,7 +74,7 @@ describe('formatter', () => { }); describe('withoutPrettierConfigAndWithEditorConfig', () => { - const testPath = path.resolve(__dirname, '__tests-formatter__', 'withoutEditorConfig'); + const testPath = resolve(__dirname, '__tests-formatter__', 'withoutEditorConfig'); describe('prettier-v3', async () => { const prettierV3 = await import('prettier'); @@ -84,7 +84,7 @@ describe('formatter', () => { mockPrettier.version.mockReturnValue(prettierV3.version); mockPrettier.resolveConfig.mockResolvedValue(null); - const filePath = path.resolve(testPath, 'testFile.ts'); + const filePath = resolve(testPath, 'testFile.ts'); const result = await formatFileContent(filePath, dummyContent); diff --git a/code/core/src/common/utils/framework-to-renderer.ts b/code/core/src/common/utils/framework-to-renderer.ts index a34ac765c2c7..63107ad8313b 100644 --- a/code/core/src/common/utils/framework-to-renderer.ts +++ b/code/core/src/common/utils/framework-to-renderer.ts @@ -11,6 +11,7 @@ export const frameworkToRenderer: Record< 'html-vite': 'html', 'html-webpack5': 'html', nextjs: 'react', + 'experimental-nextjs-vite': 'react', 'preact-vite': 'preact', 'preact-webpack5': 'preact', qwik: 'qwik', diff --git a/code/core/src/common/utils/get-story-id.test.ts b/code/core/src/common/utils/get-story-id.test.ts index 7132aeea13ef..e2056119f155 100644 --- a/code/core/src/common/utils/get-story-id.test.ts +++ b/code/core/src/common/utils/get-story-id.test.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; @@ -8,7 +8,7 @@ describe('getStoryId', () => { it('should return the storyId', async () => { const cwd = process.cwd(); const options = { - configDir: path.join(cwd, '.storybook'), + configDir: join(cwd, '.storybook'), presets: { apply: (val: string) => { if (val === 'stories') { @@ -17,7 +17,7 @@ describe('getStoryId', () => { }, }, } as any; - const storyFilePath = path.join(cwd, 'src', 'components', 'stories', 'Page1.stories.ts'); + const storyFilePath = join(cwd, 'src', 'components', 'stories', 'Page1.stories.ts'); const exportedStoryName = 'Default'; const { storyId, kind } = await getStoryId({ storyFilePath, exportedStoryName }, options); @@ -29,7 +29,7 @@ describe('getStoryId', () => { it('should throw an error if the storyId cannot be calculated', async () => { const cwd = process.cwd(); const options = { - configDir: path.join(cwd, '.storybook'), + configDir: join(cwd, '.storybook'), presets: { apply: (val: string) => { if (val === 'stories') { @@ -38,7 +38,7 @@ describe('getStoryId', () => { }, }, } as any; - const storyFilePath = path.join(cwd, 'not-covered-path', 'stories', 'Page1.stories.ts'); + const storyFilePath = join(cwd, 'not-covered-path', 'stories', 'Page1.stories.ts'); const exportedStoryName = 'Default'; await expect(() => diff --git a/code/core/src/common/utils/get-story-id.ts b/code/core/src/common/utils/get-story-id.ts index 3b95683d5812..4ce5abcf4521 100644 --- a/code/core/src/common/utils/get-story-id.ts +++ b/code/core/src/common/utils/get-story-id.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { relative } from 'node:path'; import { normalizeStories, normalizeStoryPath } from '@storybook/core/common'; import type { Options, StoriesEntry } from '@storybook/core/types'; @@ -57,7 +57,7 @@ export function getStoryTitle({ workingDir, }); - const relativePath = path.relative(workingDir, storyFilePath); + const relativePath = relative(workingDir, storyFilePath); const importPath = posix(normalizeStoryPath(relativePath)); return normalizedStories diff --git a/code/core/src/common/utils/get-storybook-info.ts b/code/core/src/common/utils/get-storybook-info.ts index 669b1b2c1d10..e86281caad8e 100644 --- a/code/core/src/common/utils/get-storybook-info.ts +++ b/code/core/src/common/utils/get-storybook-info.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { join } from 'node:path'; import type { SupportedFrameworks } from '@storybook/core/types'; import type { CoreCommon_StorybookInfo, PackageJson } from '@storybook/core/types'; @@ -91,7 +91,7 @@ const getRendererInfo = (packageJson: PackageJson) => { const validConfigExtensions = ['ts', 'js', 'tsx', 'jsx', 'mjs', 'cjs']; export const findConfigFile = (prefix: string, configDir: string) => { - const filePrefix = path.join(configDir, prefix); + const filePrefix = join(configDir, prefix); const extension = validConfigExtensions.find((ext: string) => pathExistsSync(`${filePrefix}.${ext}`) ); diff --git a/code/core/src/common/utils/load-custom-presets.ts b/code/core/src/common/utils/load-custom-presets.ts index 5d8a99ce4c8a..dcb104351703 100644 --- a/code/core/src/common/utils/load-custom-presets.ts +++ b/code/core/src/common/utils/load-custom-presets.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { resolve } from 'node:path'; import type { PresetConfig } from '@storybook/core/types'; @@ -8,11 +8,11 @@ import { validateConfigurationFiles } from './validate-configuration-files'; export function loadCustomPresets({ configDir }: { configDir: string }): PresetConfig[] { validateConfigurationFiles(configDir); - const presets = serverRequire(path.resolve(configDir, 'presets')); - const main = serverRequire(path.resolve(configDir, 'main')); + const presets = serverRequire(resolve(configDir, 'presets')); + const main = serverRequire(resolve(configDir, 'main')); if (main) { - const resolved = serverResolve(path.resolve(configDir, 'main')); + const resolved = serverResolve(resolve(configDir, 'main')); if (resolved) { return [resolved]; } diff --git a/code/core/src/common/utils/load-main-config.ts b/code/core/src/common/utils/load-main-config.ts index 5a3e0925aedd..e594a380d6f8 100644 --- a/code/core/src/common/utils/load-main-config.ts +++ b/code/core/src/common/utils/load-main-config.ts @@ -1,11 +1,10 @@ -import path, { relative } from 'node:path'; +import { readFile } from 'node:fs/promises'; +import { relative, resolve } from 'node:path'; import type { StorybookConfig } from '@storybook/core/types'; import { MainFileESMOnlyImportError, MainFileEvaluationError } from '@storybook/core/server-errors'; -import { readFile } from 'fs/promises'; - import { serverRequire, serverResolve } from './interpret-require'; import { validateConfigurationFiles } from './validate-configuration-files'; @@ -18,7 +17,7 @@ export async function loadMainConfig({ }): Promise { await validateConfigurationFiles(configDir); - const mainJsPath = serverResolve(path.resolve(configDir, 'main')) as string; + const mainJsPath = serverResolve(resolve(configDir, 'main')) as string; if (noCache && mainJsPath && require.cache[mainJsPath]) { delete require.cache[mainJsPath]; diff --git a/code/core/src/common/utils/load-manager-or-addons-file.ts b/code/core/src/common/utils/load-manager-or-addons-file.ts index 2ffe9e476706..a60fd6daf9ec 100644 --- a/code/core/src/common/utils/load-manager-or-addons-file.ts +++ b/code/core/src/common/utils/load-manager-or-addons-file.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { resolve } from 'node:path'; import { logger } from '@storybook/core/node-logger'; @@ -7,8 +7,8 @@ import { dedent } from 'ts-dedent'; import { getInterpretedFile } from './interpret-files'; export function loadManagerOrAddonsFile({ configDir }: { configDir: string }) { - const storybookCustomAddonsPath = getInterpretedFile(path.resolve(configDir, 'addons')); - const storybookCustomManagerPath = getInterpretedFile(path.resolve(configDir, 'manager')); + const storybookCustomAddonsPath = getInterpretedFile(resolve(configDir, 'addons')); + const storybookCustomManagerPath = getInterpretedFile(resolve(configDir, 'manager')); if (storybookCustomAddonsPath || storybookCustomManagerPath) { logger.info('=> Loading custom manager config'); @@ -16,7 +16,7 @@ export function loadManagerOrAddonsFile({ configDir }: { configDir: string }) { if (storybookCustomAddonsPath && storybookCustomManagerPath) { throw new Error(dedent` - You have both a "addons.js" and a "manager.js", remove the "addons.js" file from your configDir (${path.resolve( + You have both a "addons.js" and a "manager.js", remove the "addons.js" file from your configDir (${resolve( configDir, 'addons' )})`); diff --git a/code/core/src/common/utils/load-preview-or-config-file.ts b/code/core/src/common/utils/load-preview-or-config-file.ts index 09124397f860..bc6cadcfbb30 100644 --- a/code/core/src/common/utils/load-preview-or-config-file.ts +++ b/code/core/src/common/utils/load-preview-or-config-file.ts @@ -1,16 +1,16 @@ -import path from 'node:path'; +import { resolve } from 'node:path'; import { dedent } from 'ts-dedent'; import { getInterpretedFile } from './interpret-files'; export function loadPreviewOrConfigFile({ configDir }: { configDir: string }) { - const storybookConfigPath = getInterpretedFile(path.resolve(configDir, 'config')); - const storybookPreviewPath = getInterpretedFile(path.resolve(configDir, 'preview')); + const storybookConfigPath = getInterpretedFile(resolve(configDir, 'config')); + const storybookPreviewPath = getInterpretedFile(resolve(configDir, 'preview')); if (storybookConfigPath && storybookPreviewPath) { throw new Error(dedent` - You have both a "config.js" and a "preview.js", remove the "config.js" file from your configDir (${path.resolve( + You have both a "config.js" and a "preview.js", remove the "config.js" file from your configDir (${resolve( configDir, 'config' )})`); diff --git a/code/core/src/common/utils/normalize-path.ts b/code/core/src/common/utils/normalize-path.ts index 098d8a7a0c58..18c6c00dd157 100644 --- a/code/core/src/common/utils/normalize-path.ts +++ b/code/core/src/common/utils/normalize-path.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { posix } from 'node:path'; /** * Normalize a path to use forward slashes and remove .. and . @@ -10,5 +10,5 @@ import path from 'node:path'; * normalizePath('path\\to\\file') // => 'path/to/file' */ export function normalizePath(p: string) { - return path.posix.normalize(p.replace(/\\/g, '/')); + return posix.normalize(p.replace(/\\/g, '/')); } diff --git a/code/core/src/common/utils/normalize-stories.ts b/code/core/src/common/utils/normalize-stories.ts index 35361e172291..a001209f401b 100644 --- a/code/core/src/common/utils/normalize-stories.ts +++ b/code/core/src/common/utils/normalize-stories.ts @@ -1,5 +1,5 @@ import { lstatSync } from 'node:fs'; -import path from 'node:path'; +import { basename, dirname, relative, resolve } from 'node:path'; import type { NormalizedStoriesSpecifier, StoriesEntry } from '@storybook/core/types'; @@ -16,7 +16,7 @@ const DEFAULT_FILES_PATTERN = '**/*.@(mdx|stories.@(js|jsx|mjs|ts|tsx))'; const isDirectory = (configDir: string, entry: string) => { try { - return lstatSync(path.resolve(configDir, entry)).isDirectory(); + return lstatSync(resolve(configDir, entry)).isDirectory(); } catch (err) { return false; } @@ -27,8 +27,8 @@ export const getDirectoryFromWorkingDir = ({ workingDir, directory, }: NormalizeOptions & { directory: string }) => { - const directoryFromConfig = path.resolve(configDir, directory); - const directoryFromWorking = path.relative(workingDir, directoryFromConfig); + const directoryFromConfig = resolve(configDir, directory); + const directoryFromWorking = relative(workingDir, directoryFromConfig); // relative('/foo', '/foo/src') => 'src' // but we want `./src` to match importPaths @@ -61,8 +61,8 @@ export const normalizeStoriesEntry = ( } else { specifierWithoutMatcher = { titlePrefix: DEFAULT_TITLE_PREFIX, - directory: path.dirname(entry), - files: path.basename(entry), + directory: dirname(entry), + files: basename(entry), }; } } else { diff --git a/code/core/src/common/utils/paths.ts b/code/core/src/common/utils/paths.ts index e92d46d78041..9600f3028e1a 100644 --- a/code/core/src/common/utils/paths.ts +++ b/code/core/src/common/utils/paths.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { join, resolve, sep } from 'node:path'; import { findUpSync } from 'find-up'; @@ -12,7 +12,7 @@ export const getProjectRoot = () => { try { const found = findUpSync('.git', { type: 'directory' }); if (found) { - result = path.join(found, '..'); + result = join(found, '..'); } } catch (e) { // @@ -20,7 +20,7 @@ export const getProjectRoot = () => { try { const found = findUpSync('.svn', { type: 'directory' }); if (found) { - result = result || path.join(found, '..'); + result = result || join(found, '..'); } } catch (e) { // @@ -28,7 +28,7 @@ export const getProjectRoot = () => { try { const found = findUpSync('.hg', { type: 'directory' }); if (found) { - result = result || path.join(found, '..'); + result = result || join(found, '..'); } } catch (e) { // @@ -44,7 +44,7 @@ export const getProjectRoot = () => { try { const found = findUpSync('.yarn', { type: 'directory' }); if (found) { - result = result || path.join(found, '..'); + result = result || join(found, '..'); } } catch (e) { // @@ -57,7 +57,7 @@ export const nodePathsToArray = (nodePath: string) => nodePath .split(process.platform === 'win32' ? ';' : ':') .filter(Boolean) - .map((p) => path.resolve('./', p)); + .map((p) => resolve('./', p)); const relativePattern = /^\.{1,2}([/\\]|$)/; /** @@ -66,5 +66,5 @@ const relativePattern = /^\.{1,2}([/\\]|$)/; export function normalizeStoryPath(filename: string) { if (relativePattern.test(filename)) return filename; - return `.${path.sep}${filename}`; + return `.${sep}${filename}`; } diff --git a/code/core/src/common/utils/posix.ts b/code/core/src/common/utils/posix.ts index d9b8224cdde5..d4fbcc98b3cf 100644 --- a/code/core/src/common/utils/posix.ts +++ b/code/core/src/common/utils/posix.ts @@ -1,7 +1,7 @@ -import path from 'node:path'; +import { posix as posixPath, sep } from 'node:path'; /** * Replaces the path separator with forward slashes */ -export const posix = (localPath: string, sep: string = path.sep) => - localPath.split(sep).filter(Boolean).join(path.posix.sep); +export const posix = (localPath: string, seperator: string = sep) => + localPath.split(seperator).filter(Boolean).join(posixPath.sep); diff --git a/code/core/src/common/utils/resolve-path-in-sb-cache.ts b/code/core/src/common/utils/resolve-path-in-sb-cache.ts index a7df27dc41a8..3bd12c365c2f 100644 --- a/code/core/src/common/utils/resolve-path-in-sb-cache.ts +++ b/code/core/src/common/utils/resolve-path-in-sb-cache.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { join } from 'node:path'; import findCacheDirectory from 'find-cache-dir'; @@ -12,7 +12,7 @@ import findCacheDirectory from 'find-cache-dir'; */ export function resolvePathInStorybookCache(fileOrDirectoryName: string, sub = 'default'): string { let cacheDirectory = findCacheDirectory({ name: 'storybook' }); - cacheDirectory ||= path.join(process.cwd(), '.cache', 'storybook'); + cacheDirectory ||= join(process.cwd(), '.cache', 'storybook'); - return path.join(cacheDirectory, sub, fileOrDirectoryName); + return join(cacheDirectory, sub, fileOrDirectoryName); } diff --git a/code/core/src/common/utils/strip-abs-node-modules-path.ts b/code/core/src/common/utils/strip-abs-node-modules-path.ts index cb0950715e4e..abbb55608a64 100644 --- a/code/core/src/common/utils/strip-abs-node-modules-path.ts +++ b/code/core/src/common/utils/strip-abs-node-modules-path.ts @@ -1,16 +1,16 @@ -import path from 'node:path'; +import { posix, sep } from 'node:path'; import slash from 'slash'; function normalizePath(id: string) { - return path.posix.normalize(slash(id)); + return posix.normalize(slash(id)); } // We need to convert from an absolute path, to a traditional node module import path, // so that vite can correctly pre-bundle/optimize export function stripAbsNodeModulesPath(absPath: string) { // TODO: Evaluate if searching for node_modules in a yarn pnp environment is correct - const splits = absPath.split(`node_modules${path.sep}`); + const splits = absPath.split(`node_modules${sep}`); // Return everything after the final "node_modules/" const module = normalizePath(splits[splits.length - 1]); return module; diff --git a/code/core/src/common/utils/validate-configuration-files.ts b/code/core/src/common/utils/validate-configuration-files.ts index 9bf6a8b68c0a..3e35a6f2bafb 100644 --- a/code/core/src/common/utils/validate-configuration-files.ts +++ b/code/core/src/common/utils/validate-configuration-files.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { resolve } from 'node:path'; import { once } from '@storybook/core/node-logger'; import { MainFileMissingError } from '@storybook/core/server-errors'; @@ -11,13 +11,13 @@ import { boost } from './interpret-files'; export async function validateConfigurationFiles(configDir: string) { const extensionsPattern = `{${Array.from(boost).join(',')}}`; - const mainConfigMatches = await glob(slash(path.resolve(configDir, `main${extensionsPattern}`))); + const mainConfigMatches = await glob(slash(resolve(configDir, `main${extensionsPattern}`))); const [mainConfigPath] = mainConfigMatches; if (mainConfigMatches.length > 1) { once.warn(dedent` - Multiple main files found in your configDir (${path.resolve(configDir)}). + Multiple main files found in your configDir (${resolve(configDir)}). Storybook will use the first one found and ignore the others. Please remove the extra files. `); } diff --git a/code/core/src/common/versions.ts b/code/core/src/common/versions.ts index bd9aa7ebeb1c..605128015d31 100644 --- a/code/core/src/common/versions.ts +++ b/code/core/src/common/versions.ts @@ -42,6 +42,7 @@ export default { '@storybook/types': '8.3.0-alpha.4', '@storybook/angular': '8.3.0-alpha.4', '@storybook/ember': '8.3.0-alpha.4', + '@storybook/experimental-nextjs-vite': '8.3.0-alpha.4', '@storybook/html-vite': '8.3.0-alpha.4', '@storybook/html-webpack5': '8.3.0-alpha.4', '@storybook/nextjs': '8.3.0-alpha.4', diff --git a/code/core/src/core-server/server-channel/create-new-story-channel.test.ts b/code/core/src/core-server/server-channel/create-new-story-channel.test.ts index a8cecafca561..23fa28dbc870 100644 --- a/code/core/src/core-server/server-channel/create-new-story-channel.test.ts +++ b/code/core/src/core-server/server-channel/create-new-story-channel.test.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { join } from 'node:path'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -56,7 +56,7 @@ describe( initCreateNewStoryChannel( mockChannel, { - configDir: path.join(cwd, '.storybook'), + configDir: join(cwd, '.storybook'), presets: { apply: (val: string) => { if (val === 'framework') { @@ -89,7 +89,7 @@ describe( id: 'components-page--default', payload: { storyId: 'components-page--default', - storyFilePath: path.join('src', 'components', 'Page.stories.jsx'), + storyFilePath: join('src', 'components', 'Page.stories.jsx'), exportedStoryName: 'Default', }, success: true, @@ -107,7 +107,7 @@ describe( initCreateNewStoryChannel( mockChannel, { - configDir: path.join(cwd, '.storybook'), + configDir: join(cwd, '.storybook'), presets: { apply: (val: string) => { if (val === 'framework') { diff --git a/code/core/src/core-server/server-channel/create-new-story-channel.ts b/code/core/src/core-server/server-channel/create-new-story-channel.ts index 0824b12a3e6e..4a77720cc193 100644 --- a/code/core/src/core-server/server-channel/create-new-story-channel.ts +++ b/code/core/src/core-server/server-channel/create-new-story-channel.ts @@ -1,6 +1,6 @@ import { existsSync } from 'node:fs'; import { writeFile } from 'node:fs/promises'; -import path from 'node:path'; +import { relative } from 'node:path'; import type { Channel } from '@storybook/core/channels'; import { getStoryId } from '@storybook/core/common'; @@ -38,7 +38,7 @@ export function initCreateNewStoryChannel( options ); - const relativeStoryFilePath = path.relative(process.cwd(), storyFilePath); + const relativeStoryFilePath = relative(process.cwd(), storyFilePath); const { storyId, kind } = await getStoryId({ storyFilePath, exportedStoryName }, options); @@ -70,7 +70,7 @@ export function initCreateNewStoryChannel( id: data.id, payload: { storyId, - storyFilePath: path.relative(process.cwd(), storyFilePath), + storyFilePath: relative(process.cwd(), storyFilePath), exportedStoryName, }, error: null, diff --git a/code/core/src/core-server/server-channel/file-search-channel.ts b/code/core/src/core-server/server-channel/file-search-channel.ts index f3d0655d8cae..31f62a909e18 100644 --- a/code/core/src/core-server/server-channel/file-search-channel.ts +++ b/code/core/src/core-server/server-channel/file-search-channel.ts @@ -1,4 +1,5 @@ -import path from 'node:path'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; import type { Channel } from '@storybook/core/channels'; import { @@ -20,8 +21,6 @@ import { FILE_COMPONENT_SEARCH_RESPONSE, } from '@storybook/core/core-events'; -import fs from 'fs/promises'; - import { doesStoryFileExist, getStoryMetadata } from '../utils/get-new-story-file'; import { getParser } from '../utils/parser'; import { searchFiles } from '../utils/search-files'; @@ -60,14 +59,11 @@ export async function initFileSearchChannel( const parser = getParser(rendererName); try { - const content = await fs.readFile(path.join(projectRoot, file), 'utf-8'); - const { storyFileName } = getStoryMetadata(path.join(projectRoot, file)); - const dirname = path.dirname(file); - - const storyFileExists = doesStoryFileExist( - path.join(projectRoot, dirname), - storyFileName - ); + const content = await readFile(join(projectRoot, file), 'utf-8'); + const { storyFileName } = getStoryMetadata(join(projectRoot, file)); + const dir = dirname(file); + + const storyFileExists = doesStoryFileExist(join(projectRoot, dir), storyFileName); const info = await parser.parse(content); diff --git a/code/core/src/core-server/utils/StoryIndexGenerator.test.ts b/code/core/src/core-server/utils/StoryIndexGenerator.test.ts index 94a169c2a9f3..f199c05cfe7f 100644 --- a/code/core/src/core-server/utils/StoryIndexGenerator.test.ts +++ b/code/core/src/core-server/utils/StoryIndexGenerator.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-shadow */ -import path from 'node:path'; +import { join } from 'node:path'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -38,8 +38,8 @@ const readCsfMock = vi.mocked(readCsf); const getStorySortParameterMock = vi.mocked(getStorySortParameter); const options: StoryIndexGeneratorOptions = { - configDir: path.join(__dirname, '__mockdata__'), - workingDir: path.join(__dirname, '__mockdata__'), + configDir: join(__dirname, '__mockdata__'), + workingDir: join(__dirname, '__mockdata__'), indexers: [csfIndexer], docs: { defaultName: 'docs', autodocs: false }, }; diff --git a/code/core/src/core-server/utils/StoryIndexGenerator.ts b/code/core/src/core-server/utils/StoryIndexGenerator.ts index cb880449ab59..1a86ac9e3ba7 100644 --- a/code/core/src/core-server/utils/StoryIndexGenerator.ts +++ b/code/core/src/core-server/utils/StoryIndexGenerator.ts @@ -1,5 +1,5 @@ /* eslint-disable no-underscore-dangle */ -import path from 'node:path'; +import { dirname, extname, join, normalize, relative, resolve, sep } from 'node:path'; import { commonGlobOptions, normalizeStoryPath } from '@storybook/core/common'; import type { @@ -72,12 +72,7 @@ export function isMdxEntry({ tags }: DocsIndexEntry) { const makeAbsolute = (otherImport: Path, normalizedPath: Path, workingDir: Path) => otherImport.startsWith('.') - ? slash( - path.resolve( - workingDir, - normalizeStoryPath(path.join(path.dirname(normalizedPath), otherImport)) - ) - ) + ? slash(resolve(workingDir, normalizeStoryPath(join(dirname(normalizedPath), otherImport)))) : otherImport; /** @@ -130,7 +125,7 @@ export class StoryIndexGenerator { this.specifiers.map(async (specifier) => { const pathToSubIndex = {} as SpecifierStoriesCache; - const fullGlob = slash(path.join(specifier.directory, specifier.files)); + const fullGlob = slash(join(specifier.directory, specifier.files)); // Dynamically import globby because it is a pure ESM module const { globby } = await import('globby'); @@ -144,15 +139,15 @@ export class StoryIndexGenerator { if (files.length === 0) { once.warn( `No story files found for the specified pattern: ${chalk.blue( - path.join(specifier.directory, specifier.files) + join(specifier.directory, specifier.files) )}` ); } files.sort().forEach((absolutePath: Path) => { - const ext = path.extname(absolutePath); + const ext = extname(absolutePath); if (ext === '.storyshot') { - const relativePath = path.relative(this.options.workingDir, absolutePath); + const relativePath = relative(this.options.workingDir, absolutePath); logger.info(`Skipping ${ext} file ${relativePath}`); return; } @@ -204,10 +199,7 @@ export class StoryIndexGenerator { try { entry[absolutePath] = await updater(specifier, absolutePath, entry[absolutePath]); } catch (err) { - const relativePath = `.${path.sep}${path.relative( - this.options.workingDir, - absolutePath - )}`; + const relativePath = `.${sep}${relative(this.options.workingDir, absolutePath)}`; entry[absolutePath] = { type: 'error', @@ -301,7 +293,7 @@ export class StoryIndexGenerator { * the same format as `importPath`. Respect tsconfig paths if available. * * If no such file exists, assume that the import is from a package and - * return the raw path. + * return the raw */ resolveComponentPath( rawComponentPath: Path, @@ -313,12 +305,12 @@ export class StoryIndexGenerator { rawPath = matchPath(rawPath) ?? rawPath; } - const absoluteComponentPath = path.resolve(path.dirname(absolutePath), rawPath); + const absoluteComponentPath = resolve(dirname(absolutePath), rawPath); const existing = ['', '.js', '.ts', '.jsx', '.tsx', '.mjs', '.mts'] .map((ext) => `${absoluteComponentPath}${ext}`) .find((candidate) => fs.existsSync(candidate)); if (existing) { - const relativePath = path.relative(this.options.workingDir, existing); + const relativePath = relative(this.options.workingDir, existing); return slash(normalizeStoryPath(relativePath)); } @@ -330,7 +322,7 @@ export class StoryIndexGenerator { absolutePath: Path, projectTags: Tag[] = [] ): Promise { - const relativePath = path.relative(this.options.workingDir, absolutePath); + const relativePath = relative(this.options.workingDir, absolutePath); const importPath = slash(normalizeStoryPath(relativePath)); const defaultMakeTitle = (userTitle?: string) => { const title = userOrAutoTitleFromSpecifier(importPath, specifier, userTitle); @@ -419,7 +411,7 @@ export class StoryIndexGenerator { absolutePath: Path, projectTags: Tag[] = [] ) { - const relativePath = path.relative(this.options.workingDir, absolutePath); + const relativePath = relative(this.options.workingDir, absolutePath); try { const normalizedPath = normalizeStoryPath(relativePath); const importPath = slash(normalizedPath); @@ -455,9 +447,9 @@ export class StoryIndexGenerator { const first = dep.entries.find((e) => e.type !== 'docs') as StoryIndexEntryWithExtra; if ( - path - .normalize(path.resolve(this.options.workingDir, first.importPath)) - .startsWith(path.normalize(absoluteOf)) + normalize(resolve(this.options.workingDir, first.importPath)).startsWith( + normalize(absoluteOf) + ) ) { csfEntry = first; } @@ -685,7 +677,7 @@ export class StoryIndexGenerator { } invalidate(specifier: NormalizedStoriesSpecifier, importPath: Path, removed: boolean) { - const absolutePath = slash(path.resolve(this.options.workingDir, importPath)); + const absolutePath = slash(resolve(this.options.workingDir, importPath)); const cache = this.specifierToCache.get(specifier); invariant( cache, @@ -713,7 +705,7 @@ export class StoryIndexGenerator { if (removed) { if (cacheEntry && cacheEntry.type === 'docs') { const absoluteImports = cacheEntry.storiesImports.map((p) => - path.resolve(this.options.workingDir, p) + resolve(this.options.workingDir, p) ); const dependencies = this.findDependencies(absoluteImports); dependencies.forEach((dep) => @@ -730,7 +722,7 @@ export class StoryIndexGenerator { async getPreviewCode() { const previewFile = ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'mts'] - .map((ext) => path.join(this.options.configDir, `preview.${ext}`)) + .map((ext) => join(this.options.configDir, `preview.${ext}`)) .find((fname) => fs.existsSync(fname)); return previewFile && (await fs.readFile(previewFile, 'utf-8')).toString(); diff --git a/code/core/src/core-server/utils/__tests__/index-extraction.test.ts b/code/core/src/core-server/utils/__tests__/index-extraction.test.ts index beb16cc73a27..8377cdb18e2d 100644 --- a/code/core/src/core-server/utils/__tests__/index-extraction.test.ts +++ b/code/core/src/core-server/utils/__tests__/index-extraction.test.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; @@ -11,8 +11,8 @@ import { AUTODOCS_TAG, StoryIndexGenerator } from '../StoryIndexGenerator'; vi.mock('@storybook/core/node-logger'); const options: StoryIndexGeneratorOptions = { - configDir: path.join(__dirname, '..', '__mockdata__'), - workingDir: path.join(__dirname, '..', '__mockdata__'), + configDir: join(__dirname, '..', '__mockdata__'), + workingDir: join(__dirname, '..', '__mockdata__'), indexers: [], docs: { defaultName: 'docs', autodocs: false }, }; @@ -20,7 +20,7 @@ const options: StoryIndexGeneratorOptions = { describe('story extraction', () => { it('extracts stories from full indexer inputs', async () => { const relativePath = './src/A.stories.js'; - const absolutePath = path.join(options.workingDir, relativePath); + const absolutePath = join(options.workingDir, relativePath); const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(relativePath, options); const generator = new StoryIndexGenerator([specifier], { @@ -99,7 +99,7 @@ describe('story extraction', () => { it('extracts stories from minimal indexer inputs', async () => { const relativePath = './src/first-nested/deeply/F.stories.js'; - const absolutePath = path.join(options.workingDir, relativePath); + const absolutePath = join(options.workingDir, relativePath); const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(relativePath, options); const generator = new StoryIndexGenerator([specifier], { @@ -144,7 +144,7 @@ describe('story extraction', () => { it('auto-generates title from indexer inputs without title', async () => { const relativePath = './src/first-nested/deeply/F.stories.js'; - const absolutePath = path.join(options.workingDir, relativePath); + const absolutePath = join(options.workingDir, relativePath); const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(relativePath, options); const generator = new StoryIndexGenerator([specifier], { @@ -195,7 +195,7 @@ describe('story extraction', () => { it('auto-generates name from indexer inputs without name', async () => { const relativePath = './src/A.stories.js'; - const absolutePath = path.join(options.workingDir, relativePath); + const absolutePath = join(options.workingDir, relativePath); const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(relativePath, options); const generator = new StoryIndexGenerator([specifier], { @@ -246,7 +246,7 @@ describe('story extraction', () => { it('auto-generates id', async () => { const relativePath = './src/A.stories.js'; - const absolutePath = path.join(options.workingDir, relativePath); + const absolutePath = join(options.workingDir, relativePath); const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(relativePath, options); const generator = new StoryIndexGenerator([specifier], { @@ -345,7 +345,7 @@ describe('story extraction', () => { it('auto-generates id, title and name from exportName input', async () => { const relativePath = './src/A.stories.js'; - const absolutePath = path.join(options.workingDir, relativePath); + const absolutePath = join(options.workingDir, relativePath); const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(relativePath, options); const generator = new StoryIndexGenerator([specifier], { @@ -394,7 +394,7 @@ describe('story extraction', () => { describe('docs entries from story extraction', () => { it('adds docs entry when autodocs is globally enabled', async () => { const relativePath = './src/A.stories.js'; - const absolutePath = path.join(options.workingDir, relativePath); + const absolutePath = join(options.workingDir, relativePath); const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(relativePath, options); const generator = new StoryIndexGenerator([specifier], { @@ -445,7 +445,7 @@ describe('docs entries from story extraction', () => { }); it(`adds docs entry when autodocs is "tag" and an entry has the "${AUTODOCS_TAG}" tag`, async () => { const relativePath = './src/A.stories.js'; - const absolutePath = path.join(options.workingDir, relativePath); + const absolutePath = join(options.workingDir, relativePath); const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(relativePath, options); const generator = new StoryIndexGenerator([specifier], { @@ -509,7 +509,7 @@ describe('docs entries from story extraction', () => { }); it(`DOES NOT adds docs entry when autodocs is false and an entry has the "${AUTODOCS_TAG}" tag`, async () => { const relativePath = './src/A.stories.js'; - const absolutePath = path.join(options.workingDir, relativePath); + const absolutePath = join(options.workingDir, relativePath); const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(relativePath, options); const generator = new StoryIndexGenerator([specifier], { 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 c739e4ac5fef..ca532a8b04f9 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,4 +1,4 @@ -import path from 'node:path'; +import { resolve } from 'node:path'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -19,14 +19,14 @@ describe('parseStaticDir', () => { it('returns the static dir/path and default target', async () => { await expect(parseStaticDir('public')).resolves.toEqual({ staticDir: './public', - staticPath: path.resolve('public'), + staticPath: resolve('public'), targetDir: './', targetEndpoint: '/', }); await expect(parseStaticDir('foo/bar')).resolves.toEqual({ staticDir: './foo/bar', - staticPath: path.resolve('foo/bar'), + staticPath: resolve('foo/bar'), targetDir: './', targetEndpoint: '/', }); @@ -35,14 +35,14 @@ describe('parseStaticDir', () => { it('returns the static dir/path and custom target', async () => { await expect(parseStaticDir('public:/custom-endpoint')).resolves.toEqual({ staticDir: './public', - staticPath: path.resolve('public'), + staticPath: resolve('public'), targetDir: './custom-endpoint', targetEndpoint: '/custom-endpoint', }); await expect(parseStaticDir('foo/bar:/custom-endpoint')).resolves.toEqual({ staticDir: './foo/bar', - staticPath: path.resolve('foo/bar'), + staticPath: resolve('foo/bar'), targetDir: './custom-endpoint', targetEndpoint: '/custom-endpoint', }); @@ -59,7 +59,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); - await expect(parseStaticDir('nonexistent')).rejects.toThrow(path.resolve('nonexistent')); + await expect(parseStaticDir('nonexistent')).rejects.toThrow(resolve('nonexistent')); }); skipWindows(() => { @@ -85,8 +85,8 @@ describe('parseStaticDir', () => { onlyWindows(() => { it('supports absolute file paths - windows', async () => { await expect(parseStaticDir('C:\\foo\\bar')).resolves.toEqual({ - staticDir: path.resolve('C:\\foo\\bar'), - staticPath: path.resolve('C:\\foo\\bar'), + staticDir: resolve('C:\\foo\\bar'), + staticPath: resolve('C:\\foo\\bar'), targetDir: './', targetEndpoint: '/', }); @@ -95,14 +95,14 @@ describe('parseStaticDir', () => { it('supports absolute file paths with custom endpoint - windows', async () => { await expect(parseStaticDir('C:\\foo\\bar:/custom-endpoint')).resolves.toEqual({ staticDir: expect.any(String), // can't test this properly on unix - staticPath: path.resolve('C:\\foo\\bar'), + staticPath: resolve('C:\\foo\\bar'), targetDir: './custom-endpoint', targetEndpoint: '/custom-endpoint', }); await expect(parseStaticDir('C:\\foo\\bar:\\custom-endpoint')).resolves.toEqual({ staticDir: expect.any(String), // can't test this properly on unix - staticPath: path.resolve('C:\\foo\\bar'), + staticPath: resolve('C:\\foo\\bar'), targetDir: './custom-endpoint', targetEndpoint: '/custom-endpoint', }); diff --git a/code/core/src/core-server/utils/get-new-story-file.test.ts b/code/core/src/core-server/utils/get-new-story-file.test.ts index 354e4470cfa4..6144927e478e 100644 --- a/code/core/src/core-server/utils/get-new-story-file.test.ts +++ b/code/core/src/core-server/utils/get-new-story-file.test.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; @@ -48,7 +48,7 @@ describe('get-new-story-file', () => { export const Default: Story = {};" `); - expect(storyFilePath).toBe(path.join(__dirname, 'src', 'components', 'Page.stories.tsx')); + expect(storyFilePath).toBe(join(__dirname, 'src', 'components', 'Page.stories.tsx')); }); it('should create a new story file (JavaScript)', async () => { @@ -82,6 +82,6 @@ describe('get-new-story-file', () => { export const Default = {};" `); - expect(storyFilePath).toBe(path.join(__dirname, 'src', 'components', 'Page.stories.jsx')); + expect(storyFilePath).toBe(join(__dirname, 'src', 'components', 'Page.stories.jsx')); }); }); diff --git a/code/core/src/core-server/utils/get-new-story-file.ts b/code/core/src/core-server/utils/get-new-story-file.ts index 969bca510067..570ab8aecf7e 100644 --- a/code/core/src/core-server/utils/get-new-story-file.ts +++ b/code/core/src/core-server/utils/get-new-story-file.ts @@ -1,5 +1,5 @@ -import fs from 'node:fs'; -import path from 'node:path'; +import { existsSync } from 'node:fs'; +import { basename, dirname, extname, join } from 'node:path'; import { extractProperRendererNameFromFramework, @@ -31,10 +31,10 @@ export async function getNewStoryFile( ([, value]) => value === rendererName )?.[0]; - const basename = path.basename(componentFilePath); - const extension = path.extname(componentFilePath); - const basenameWithoutExtension = basename.replace(extension, ''); - const dirname = path.dirname(componentFilePath); + const base = basename(componentFilePath); + const extension = extname(componentFilePath); + const basenameWithoutExtension = base.replace(extension, ''); + const dir = dirname(componentFilePath); const { storyFileName, isTypescript, storyFileExtension } = getStoryMetadata(componentFilePath); const storyFileNameWithExtension = `${storyFileName}.${storyFileExtension}`; @@ -59,18 +59,18 @@ export async function getNewStoryFile( }); const storyFilePath = - doesStoryFileExist(path.join(cwd, dirname), storyFileName) && componentExportCount > 1 - ? path.join(cwd, dirname, alternativeStoryFileNameWithExtension) - : path.join(cwd, dirname, storyFileNameWithExtension); + doesStoryFileExist(join(cwd, dir), storyFileName) && componentExportCount > 1 + ? join(cwd, dir, alternativeStoryFileNameWithExtension) + : join(cwd, dir, storyFileNameWithExtension); return { storyFilePath, exportedStoryName, storyFileContent, dirname }; } export const getStoryMetadata = (componentFilePath: string) => { const isTypescript = /\.(ts|tsx|mts|cts)$/.test(componentFilePath); - const basename = path.basename(componentFilePath); - const extension = path.extname(componentFilePath); - const basenameWithoutExtension = basename.replace(extension, ''); + const base = basename(componentFilePath); + const extension = extname(componentFilePath); + const basenameWithoutExtension = base.replace(extension, ''); const storyFileExtension = isTypescript ? 'tsx' : 'jsx'; return { storyFileName: `${basenameWithoutExtension}.stories`, @@ -81,9 +81,9 @@ export const getStoryMetadata = (componentFilePath: string) => { export const doesStoryFileExist = (parentFolder: string, storyFileName: string) => { return ( - fs.existsSync(path.join(parentFolder, `${storyFileName}.ts`)) || - fs.existsSync(path.join(parentFolder, `${storyFileName}.tsx`)) || - fs.existsSync(path.join(parentFolder, `${storyFileName}.js`)) || - fs.existsSync(path.join(parentFolder, `${storyFileName}.jsx`)) + existsSync(join(parentFolder, `${storyFileName}.ts`)) || + existsSync(join(parentFolder, `${storyFileName}.tsx`)) || + existsSync(join(parentFolder, `${storyFileName}.js`)) || + existsSync(join(parentFolder, `${storyFileName}.jsx`)) ); }; diff --git a/code/core/src/core-server/utils/middleware.ts b/code/core/src/core-server/utils/middleware.ts index a583c5c70297..366f3f096c6c 100644 --- a/code/core/src/core-server/utils/middleware.ts +++ b/code/core/src/core-server/utils/middleware.ts @@ -1,14 +1,14 @@ -import fs from 'node:fs'; -import path from 'node:path'; +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; const fileExists = (basename: string) => ['.js', '.cjs'].reduce((found: string, ext: string) => { const filename = `${basename}${ext}`; - return !found && fs.existsSync(filename) ? filename : found; + return !found && existsSync(filename) ? filename : found; }, ''); export function getMiddleware(configDir: string) { - const middlewarePath = fileExists(path.resolve(configDir, 'middleware')); + const middlewarePath = fileExists(resolve(configDir, 'middleware')); if (middlewarePath) { let middlewareModule = require(middlewarePath); // eslint-disable-next-line no-underscore-dangle diff --git a/code/core/src/core-server/utils/output-stats.ts b/code/core/src/core-server/utils/output-stats.ts index d54bc6938b44..c3a5b9f8aad9 100644 --- a/code/core/src/core-server/utils/output-stats.ts +++ b/code/core/src/core-server/utils/output-stats.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { join } from 'node:path'; import type { Stats } from '@storybook/core/types'; @@ -20,7 +20,7 @@ export async function outputStats(directory: string, previewStats?: any, manager } export const writeStats = async (directory: string, name: string, stats: Stats) => { - const filePath = path.join(directory, `${name}-stats.json`); + const filePath = join(directory, `${name}-stats.json`); const { chunks, ...data } = stats.toJson(); // omit chunks, which is about half of the total data await new Promise((resolve, reject) => { stringifyStream(data, null, 2) diff --git a/code/core/src/core-server/utils/parser/generic-parser.test.ts b/code/core/src/core-server/utils/parser/generic-parser.test.ts index 3a0002dde4a9..e461a9cdbf50 100644 --- a/code/core/src/core-server/utils/parser/generic-parser.test.ts +++ b/code/core/src/core-server/utils/parser/generic-parser.test.ts @@ -1,5 +1,5 @@ -import fs from 'node:fs'; -import path from 'node:path'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; @@ -7,11 +7,11 @@ import { GenericParser } from './generic-parser'; const genericParser = new GenericParser(); -const TEST_DIR = path.join(__dirname, '..', '__search-files-tests__'); +const TEST_DIR = join(__dirname, '..', '__search-files-tests__'); describe('generic-parser', () => { it('should correctly return exports from ES modules', async () => { - const content = fs.readFileSync(path.join(TEST_DIR, 'src', 'es-module.js'), 'utf-8'); + const content = readFileSync(join(TEST_DIR, 'src', 'es-module.js'), 'utf-8'); const { exports } = await genericParser.parse(content); expect(exports).toEqual([ diff --git a/code/core/src/core-server/utils/save-story/duplicate-story-with-new-name.test.ts b/code/core/src/core-server/utils/save-story/duplicate-story-with-new-name.test.ts index 2bdb98544d22..549c74337ca9 100644 --- a/code/core/src/core-server/utils/save-story/duplicate-story-with-new-name.test.ts +++ b/code/core/src/core-server/utils/save-story/duplicate-story-with-new-name.test.ts @@ -1,11 +1,11 @@ /* eslint-disable no-underscore-dangle */ +import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { describe, expect, test } from 'vitest'; import { printCsf, readCsf } from '@storybook/core/csf-tools'; -import { readFile } from 'fs/promises'; import { format } from 'prettier'; import { duplicateStoryWithNewName } from './duplicate-story-with-new-name'; diff --git a/code/core/src/core-server/utils/save-story/save-story.ts b/code/core/src/core-server/utils/save-story/save-story.ts index c912647770db..3113fd78e987 100644 --- a/code/core/src/core-server/utils/save-story/save-story.ts +++ b/code/core/src/core-server/utils/save-story/save-story.ts @@ -1,5 +1,5 @@ /* eslint-disable no-underscore-dangle */ -import fs from 'node:fs/promises'; +import { writeFile } from 'node:fs/promises'; import { basename, join } from 'node:path'; import type { Channel } from '@storybook/core/channels'; @@ -103,7 +103,7 @@ export function initializeSaveStory(channel: Channel, options: Options, coreConf channel.on(STORY_RENDERED, resolve); setTimeout(() => resolve(channel.off(STORY_RENDERED, resolve)), 3000); }), - fs.writeFile(sourceFilePath, code), + writeFile(sourceFilePath, code), ]); channel.emit(SAVE_STORY_RESPONSE, { diff --git a/code/core/src/core-server/utils/save-story/update-args-in-csf-file.test.ts b/code/core/src/core-server/utils/save-story/update-args-in-csf-file.test.ts index 4a87fbeff6c1..43bc049fc58b 100644 --- a/code/core/src/core-server/utils/save-story/update-args-in-csf-file.test.ts +++ b/code/core/src/core-server/utils/save-story/update-args-in-csf-file.test.ts @@ -1,11 +1,11 @@ /* eslint-disable no-underscore-dangle */ +import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { describe, expect, test } from 'vitest'; import { printCsf, readCsf } from '@storybook/core/csf-tools'; -import { readFile } from 'fs/promises'; import { format } from 'prettier'; import { getDiff } from './getDiff'; diff --git a/code/core/src/core-server/utils/search-files.test.ts b/code/core/src/core-server/utils/search-files.test.ts index 81ecfe45079f..c86afac79ac9 100644 --- a/code/core/src/core-server/utils/search-files.test.ts +++ b/code/core/src/core-server/utils/search-files.test.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; @@ -8,7 +8,7 @@ describe('search-files', () => { it('should automatically convert static search to a dynamic glob search', async (t) => { const files = await searchFiles({ searchQuery: 'ommonjs', - cwd: path.join(__dirname, '__search-files-tests__'), + cwd: join(__dirname, '__search-files-tests__'), }); expect(files).toEqual(['src/commonjs-module-default.js', 'src/commonjs-module.js']); @@ -17,7 +17,7 @@ describe('search-files', () => { it('should automatically convert static search to a dynamic glob search (with file extension)', async (t) => { const files = await searchFiles({ searchQuery: 'module.js', - cwd: path.join(__dirname, '__search-files-tests__'), + cwd: join(__dirname, '__search-files-tests__'), }); expect(files).toEqual(['src/commonjs-module.js', 'src/es-module.js']); @@ -26,7 +26,7 @@ describe('search-files', () => { it('should return all files if the search query matches the parent folder', async (t) => { const files = await searchFiles({ searchQuery: 'file-extensions', - cwd: path.join(__dirname, '__search-files-tests__'), + cwd: join(__dirname, '__search-files-tests__'), }); expect(files).toEqual([ @@ -44,7 +44,7 @@ describe('search-files', () => { it('should ignore files that do not have the allowed extensions', async (t) => { const files = await searchFiles({ searchQuery: 'asset', - cwd: path.join(__dirname, '__search-files-tests__'), + cwd: join(__dirname, '__search-files-tests__'), }); expect(files).toEqual([]); @@ -53,7 +53,7 @@ describe('search-files', () => { it('should ignore test files (*.spec.*, *.test.*)', async (t) => { const files = await searchFiles({ searchQuery: 'tests', - cwd: path.join(__dirname, '__search-files-tests__'), + cwd: join(__dirname, '__search-files-tests__'), }); expect(files).toEqual([]); @@ -62,7 +62,7 @@ describe('search-files', () => { it('should work with glob search patterns', async (t) => { const files = await searchFiles({ searchQuery: '**/commonjs-module.js', - cwd: path.join(__dirname, '__search-files-tests__'), + cwd: join(__dirname, '__search-files-tests__'), }); expect(files).toEqual(['src/commonjs-module.js']); @@ -71,7 +71,7 @@ describe('search-files', () => { it('should respect glob but also the allowed file extensions', async (t) => { const files = await searchFiles({ searchQuery: '**/*', - cwd: path.join(__dirname, '__search-files-tests__'), + cwd: join(__dirname, '__search-files-tests__'), }); expect(files).toEqual([ @@ -93,7 +93,7 @@ describe('search-files', () => { it('should ignore node_modules', async (t) => { const files = await searchFiles({ searchQuery: 'file-in-common.js', - cwd: path.join(__dirname, '__search-files-tests__'), + cwd: join(__dirname, '__search-files-tests__'), }); expect(files).toEqual([]); @@ -102,7 +102,7 @@ describe('search-files', () => { it('should ignore story files', async (t) => { const files = await searchFiles({ searchQuery: 'es-module.stories.js', - cwd: path.join(__dirname, '__search-files-tests__'), + cwd: join(__dirname, '__search-files-tests__'), }); expect(files).toEqual([]); @@ -112,7 +112,7 @@ describe('search-files', () => { await expect(() => searchFiles({ searchQuery: '../**/*', - cwd: path.join(__dirname, '__search-files-tests__'), + cwd: join(__dirname, '__search-files-tests__'), }) ).rejects.toThrowError(); }); diff --git a/code/core/src/core-server/utils/server-statics.ts b/code/core/src/core-server/utils/server-statics.ts index d359e13653cd..926fcdb34842 100644 --- a/code/core/src/core-server/utils/server-statics.ts +++ b/code/core/src/core-server/utils/server-statics.ts @@ -1,4 +1,4 @@ -import path, { basename, isAbsolute } from 'node:path'; +import { basename, isAbsolute, posix, resolve, sep, win32 } from 'node:path'; import { getDirectoryFromWorkingDir } from '@storybook/core/common'; import type { Options } from '@storybook/core/types'; @@ -54,16 +54,16 @@ export async function useStatics(router: Router, options: Options) { export const parseStaticDir = async (arg: string) => { // Split on last index of ':', for Windows compatibility (e.g. 'C:\some\dir:\foo') const lastColonIndex = arg.lastIndexOf(':'); - const isWindowsAbsolute = path.win32.isAbsolute(arg); + const isWindowsAbsolute = win32.isAbsolute(arg); const isWindowsRawDirOnly = isWindowsAbsolute && lastColonIndex === 1; // e.g. 'C:\some\dir' const splitIndex = lastColonIndex !== -1 && !isWindowsRawDirOnly ? lastColonIndex : arg.length; const targetRaw = arg.substring(splitIndex + 1) || '/'; - const target = targetRaw.split(path.sep).join(path.posix.sep); // Ensure target has forward-slash path + const target = targetRaw.split(sep).join(posix.sep); // Ensure target has forward-slash path const rawDir = arg.substring(0, splitIndex); - const staticDir = path.isAbsolute(rawDir) ? rawDir : `./${rawDir}`; - const staticPath = path.resolve(staticDir); + const staticDir = isAbsolute(rawDir) ? rawDir : `./${rawDir}`; + const staticPath = resolve(staticDir); const targetDir = target.replace(/^\/?/, './'); const targetEndpoint = targetDir.substring(1); diff --git a/code/core/src/core-server/utils/stories-json.test.ts b/code/core/src/core-server/utils/stories-json.test.ts index b28c2cc95a58..e9f5e73a7378 100644 --- a/code/core/src/core-server/utils/stories-json.test.ts +++ b/code/core/src/core-server/utils/stories-json.test.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { join } from 'node:path'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -20,7 +20,7 @@ vi.mock('watchpack'); vi.mock('lodash/debounce'); vi.mock('@storybook/core/node-logger'); -const workingDir = path.join(__dirname, '__mockdata__'); +const workingDir = join(__dirname, '__mockdata__'); const normalizedStories = [ normalizeStoriesEntry( { diff --git a/code/core/src/core-server/utils/watch-story-specifiers.test.ts b/code/core/src/core-server/utils/watch-story-specifiers.test.ts index 0a9f3f367ff3..9142af5aeb22 100644 --- a/code/core/src/core-server/utils/watch-story-specifiers.test.ts +++ b/code/core/src/core-server/utils/watch-story-specifiers.test.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { join } from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; @@ -11,12 +11,12 @@ import { watchStorySpecifiers } from './watch-story-specifiers'; vi.mock('watchpack'); describe('watchStorySpecifiers', () => { - const workingDir = path.join(__dirname, '__mockdata__'); + const workingDir = join(__dirname, '__mockdata__'); const options = { - configDir: path.join(workingDir, '.storybook'), + configDir: join(workingDir, '.storybook'), workingDir, }; - const abspath = (filename: string) => path.join(workingDir, filename); + const abspath = (filename: string) => join(workingDir, filename); let close: () => void; afterEach(() => close?.()); diff --git a/code/core/src/core-server/utils/watch-story-specifiers.ts b/code/core/src/core-server/utils/watch-story-specifiers.ts index e7fba3290a79..b320520dc9a4 100644 --- a/code/core/src/core-server/utils/watch-story-specifiers.ts +++ b/code/core/src/core-server/utils/watch-story-specifiers.ts @@ -1,5 +1,5 @@ -import fs from 'node:fs'; -import path from 'node:path'; +import { type Dirent, lstatSync, readdirSync } from 'node:fs'; +import { basename, join, relative, resolve } from 'node:path'; import { commonGlobOptions } from '@storybook/core/common'; import type { NormalizedStoriesSpecifier, Path } from '@storybook/core/types'; @@ -9,7 +9,7 @@ import Watchpack from 'watchpack'; const isDirectory = (directory: Path) => { try { - return fs.lstatSync(directory).isDirectory(); + return lstatSync(directory).isDirectory(); } catch (err) { return false; } @@ -25,11 +25,11 @@ function getNestedFilesAndDirectories(directories: Path[]) { if (traversedDirectories.has(directory)) { return; } - fs.readdirSync(directory, { withFileTypes: true }).forEach((ent: fs.Dirent) => { + readdirSync(directory, { withFileTypes: true }).forEach((ent: Dirent) => { if (ent.isDirectory()) { - traverse(path.join(directory, ent.name)); + traverse(join(directory, ent.name)); } else if (ent.isFile()) { - files.add(path.join(directory, ent.name)); + files.add(join(directory, ent.name)); } }); traversedDirectories.add(directory); @@ -46,7 +46,7 @@ export function watchStorySpecifiers( // Watch all nested files and directories up front to avoid this issue: // https://github.com/webpack/watchpack/issues/222 const { files, directories } = getNestedFilesAndDirectories( - specifiers.map((ns) => path.resolve(options.workingDir, ns.directory)) + specifiers.map((ns) => resolve(options.workingDir, ns.directory)) ); // See https://www.npmjs.com/package/watchpack for full options. @@ -59,7 +59,7 @@ export function watchStorySpecifiers( wp.watch({ files, directories }); const toImportPath = (absolutePath: Path) => { - const relativePath = path.relative(options.workingDir, absolutePath); + const relativePath = relative(options.workingDir, absolutePath); return slash(relativePath.startsWith('.') ? relativePath : `./${relativePath}`); }; @@ -88,12 +88,12 @@ export function watchStorySpecifiers( .map(async (specifier) => { // If `./path/to/dir` was added, check all files matching `./path/to/dir/**/*.stories.*` // (where the last bit depends on `files`). - const dirGlob = path.join( + const dirGlob = join( absolutePath, '**', // files can be e.g. '**/foo/*/*.js' so we just want the last bit, // because the directory could already be within the files part (e.g. './x/foo/bar') - path.basename(specifier.files) + basename(specifier.files) ); // Dynamically import globby because it is a pure ESM module diff --git a/code/core/src/docs-tools/argTypes/convert/convert.test.ts b/code/core/src/docs-tools/argTypes/convert/convert.test.ts index 97f79c9bc441..e88f8d00a226 100644 --- a/code/core/src/docs-tools/argTypes/convert/convert.test.ts +++ b/code/core/src/docs-tools/argTypes/convert/convert.test.ts @@ -1,4 +1,4 @@ -import fs from 'node:fs'; +import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; @@ -786,7 +786,7 @@ describe('storybook type system', () => { }); const readFixture = (fixture: string) => - fs.readFileSync(`${__dirname}/__testfixtures__/${fixture}`).toString(); + readFileSync(`${__dirname}/__testfixtures__/${fixture}`).toString(); const transformToModule = (inputCode: string) => { const options = { diff --git a/code/core/src/telemetry/anonymous-id.ts b/code/core/src/telemetry/anonymous-id.ts index 2748a2fd2a8a..c97071c0d127 100644 --- a/code/core/src/telemetry/anonymous-id.ts +++ b/code/core/src/telemetry/anonymous-id.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { relative } from 'node:path'; import { getProjectRoot } from '@storybook/core/common'; @@ -29,7 +29,7 @@ export const getAnonymousProjectId = () => { try { const projectRoot = getProjectRoot(); - const projectRootPath = path.relative(projectRoot, process.cwd()); + const projectRootPath = relative(projectRoot, process.cwd()); const originBuffer = execSync(`git config --local --get remote.origin.url`, { timeout: 1000, diff --git a/code/core/src/telemetry/get-framework-info.test.ts b/code/core/src/telemetry/get-framework-info.test.ts index 6987c32ccc9b..e95e643dc8b1 100644 --- a/code/core/src/telemetry/get-framework-info.test.ts +++ b/code/core/src/telemetry/get-framework-info.test.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { sep } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; @@ -30,7 +30,7 @@ describe('getFrameworkInfo', () => { }); it('should resolve the framework package json correctly and strip project paths in the metadata', async () => { - const packageName = `${process.cwd()}/@storybook/react`.split('/').join(path.sep); + const packageName = `${process.cwd()}/@storybook/react`.split('/').join(sep); const framework = { name: packageName }; const frameworkPackageJson = { name: packageName, @@ -48,7 +48,7 @@ describe('getFrameworkInfo', () => { expect(result).toEqual({ framework: { - name: '$SNIP/@storybook/react'.split('/').join(path.sep), + name: '$SNIP/@storybook/react'.split('/').join(sep), options: undefined, }, builder: '@storybook/builder-vite', diff --git a/code/core/src/telemetry/get-framework-info.ts b/code/core/src/telemetry/get-framework-info.ts index 995777e1b798..d5b3390128ea 100644 --- a/code/core/src/telemetry/get-framework-info.ts +++ b/code/core/src/telemetry/get-framework-info.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { normalize } from 'node:path'; import { frameworkPackages } from '@storybook/core/common'; import type { PackageJson, StorybookConfig } from '@storybook/core/types'; @@ -36,7 +36,7 @@ function findMatchingPackage(packageJson: PackageJson, suffixes: string[]) { } export const getFrameworkPackageName = (packageNameOrPath: string) => { - const normalizedPath = path.normalize(packageNameOrPath).replace(new RegExp(/\\/, 'g'), '/'); + const normalizedPath = normalize(packageNameOrPath).replace(new RegExp(/\\/, 'g'), '/'); const knownFramework = Object.keys(frameworkPackages).find((pkg) => normalizedPath.endsWith(pkg)); diff --git a/code/core/src/telemetry/get-monorepo-type.test.ts b/code/core/src/telemetry/get-monorepo-type.test.ts index 6c5366b8c1aa..530da9f4abc9 100644 --- a/code/core/src/telemetry/get-monorepo-type.test.ts +++ b/code/core/src/telemetry/get-monorepo-type.test.ts @@ -1,5 +1,5 @@ /* eslint-disable no-underscore-dangle */ -import path from 'node:path'; +import { join } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; @@ -18,11 +18,11 @@ vi.mock('@storybook/core/common', async (importOriginal) => { const checkMonorepoType = ({ monorepoConfigFile, isYarnWorkspace = false }: any) => { const mockFiles = { - [path.join('root', 'package.json')]: isYarnWorkspace ? '{ "workspaces": [] }' : '{}', + [join('root', 'package.json')]: isYarnWorkspace ? '{ "workspaces": [] }' : '{}', }; if (monorepoConfigFile) { - mockFiles[path.join('root', monorepoConfigFile)] = '{}'; + mockFiles[join('root', monorepoConfigFile)] = '{}'; } vi.mocked(fsExtra as any).__setMockFiles(mockFiles); diff --git a/code/core/src/telemetry/get-monorepo-type.ts b/code/core/src/telemetry/get-monorepo-type.ts index 0427da90247c..e1602f6cf4b2 100644 --- a/code/core/src/telemetry/get-monorepo-type.ts +++ b/code/core/src/telemetry/get-monorepo-type.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { join } from 'node:path'; import { getProjectRoot } from '@storybook/core/common'; import type { PackageJson } from '@storybook/core/types'; @@ -21,7 +21,7 @@ export const getMonorepoType = (): MonorepoType => { const keys = Object.keys(monorepoConfigs) as (keyof typeof monorepoConfigs)[]; const monorepoType: MonorepoType = keys.find((monorepo) => { - const configFile = path.join(projectRootPath, monorepoConfigs[monorepo]); + const configFile = join(projectRootPath, monorepoConfigs[monorepo]); return existsSync(configFile); }) as MonorepoType; @@ -29,11 +29,11 @@ export const getMonorepoType = (): MonorepoType => { return monorepoType; } - if (!existsSync(path.join(projectRootPath, 'package.json'))) { + if (!existsSync(join(projectRootPath, 'package.json'))) { return undefined; } - const packageJson = readJsonSync(path.join(projectRootPath, 'package.json')) as PackageJson; + const packageJson = readJsonSync(join(projectRootPath, 'package.json')) 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 99c8476b64c0..b88ebc237ddc 100644 --- a/code/core/src/telemetry/package-json.ts +++ b/code/core/src/telemetry/package-json.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { join } from 'node:path'; import { readJson } from 'fs-extra'; @@ -22,7 +22,7 @@ export const getActualPackageVersion = async (packageName: string) => { }; export const getActualPackageJson = async (packageName: string) => { - const resolvedPackageJson = require.resolve(path.join(packageName, 'package.json'), { + const resolvedPackageJson = require.resolve(join(packageName, 'package.json'), { paths: [process.cwd()], }); const packageJson = await readJson(resolvedPackageJson); diff --git a/code/core/src/types/modules/frameworks.ts b/code/core/src/types/modules/frameworks.ts index 9ae2cc538b51..246cea20ff9a 100644 --- a/code/core/src/types/modules/frameworks.ts +++ b/code/core/src/types/modules/frameworks.ts @@ -2,6 +2,7 @@ export type SupportedFrameworks = | 'angular' | 'ember' + | 'experimental-nextjs-vite' | 'html-vite' | 'html-webpack5' | 'nextjs' diff --git a/code/frameworks/angular/src/builders/build-storybook/index.spec.ts b/code/frameworks/angular/src/builders/build-storybook/index.spec.ts index d02f927227cd..3b62deeb8c69 100644 --- a/code/frameworks/angular/src/builders/build-storybook/index.spec.ts +++ b/code/frameworks/angular/src/builders/build-storybook/index.spec.ts @@ -1,7 +1,7 @@ import { Architect, createBuilder } from '@angular-devkit/architect'; import { TestingArchitectHost } from '@angular-devkit/architect/testing'; import { schema } from '@angular-devkit/core'; -import * as path from 'path'; +import * as path from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const buildDevStandaloneMock = vi.fn(); diff --git a/code/frameworks/angular/src/builders/start-storybook/index.spec.ts b/code/frameworks/angular/src/builders/start-storybook/index.spec.ts index e4948d77acf0..cdce7ad01f06 100644 --- a/code/frameworks/angular/src/builders/start-storybook/index.spec.ts +++ b/code/frameworks/angular/src/builders/start-storybook/index.spec.ts @@ -1,7 +1,7 @@ import { Architect, createBuilder } from '@angular-devkit/architect'; import { TestingArchitectHost } from '@angular-devkit/architect/testing'; import { schema } from '@angular-devkit/core'; -import * as path from 'path'; +import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const buildDevStandaloneMock = vi.fn(); @@ -62,7 +62,7 @@ describe.skip('Start Storybook Builder', () => { ); // This will either take a Node package name, or a path to the directory // for the package.json file. - await architectHost.addBuilderFromPackage(path.join(__dirname, '../../..')); + await architectHost.addBuilderFromPackage(join(__dirname, '../../..')); }); beforeEach(() => { diff --git a/code/frameworks/angular/src/builders/utils/run-compodoc.ts b/code/frameworks/angular/src/builders/utils/run-compodoc.ts index bf87f04e83b3..e479212c445a 100644 --- a/code/frameworks/angular/src/builders/utils/run-compodoc.ts +++ b/code/frameworks/angular/src/builders/utils/run-compodoc.ts @@ -1,17 +1,18 @@ +import { isAbsolute, relative } from 'node:path'; + import { JsPackageManagerFactory } from 'storybook/internal/common'; import { BuilderContext } from '@angular-devkit/architect'; -import * as path from 'path'; import { Observable } from 'rxjs'; const hasTsConfigArg = (args: string[]) => args.indexOf('-p') !== -1; const hasOutputArg = (args: string[]) => args.indexOf('-d') !== -1 || args.indexOf('--output') !== -1; -// path.relative is necessary to workaround a compodoc issue with +// relative is necessary to workaround a compodoc issue with // absolute paths on windows machines const toRelativePath = (pathToTsConfig: string) => { - return path.isAbsolute(pathToTsConfig) ? path.relative('.', pathToTsConfig) : pathToTsConfig; + return isAbsolute(pathToTsConfig) ? relative('.', pathToTsConfig) : pathToTsConfig; }; export const runCompodoc = ( diff --git a/code/frameworks/angular/src/client/docs/angular-properties.test.ts b/code/frameworks/angular/src/client/docs/angular-properties.test.ts index 143efa84938b..baa410f34fe4 100644 --- a/code/frameworks/angular/src/client/docs/angular-properties.test.ts +++ b/code/frameworks/angular/src/client/docs/angular-properties.test.ts @@ -1,16 +1,17 @@ -import fs from 'fs'; -import path from 'path'; +import { readdirSync } from 'node:fs'; +import { join } from 'node:path'; + import { describe, expect, it } from 'vitest'; // File hierarchy: __testfixtures__ / some-test-case / input.* const inputRegExp = /^input\..*$/; describe('angular component properties', () => { - const fixturesDir = path.join(__dirname, '__testfixtures__'); - fs.readdirSync(fixturesDir, { withFileTypes: true }).forEach((testEntry) => { + const fixturesDir = join(__dirname, '__testfixtures__'); + readdirSync(fixturesDir, { withFileTypes: true }).forEach((testEntry) => { if (testEntry.isDirectory()) { - const testDir = path.join(fixturesDir, testEntry.name); - const testFile = fs.readdirSync(testDir).find((fileName) => inputRegExp.test(fileName)); + const testDir = join(fixturesDir, testEntry.name); + const testFile = readdirSync(testDir).find((fileName) => inputRegExp.test(fileName)); if (testFile) { // TODO: Remove this as soon as the real test is fixed it('true', () => { @@ -18,19 +19,19 @@ describe('angular component properties', () => { }); // TODO: Fix this test // it(`${testEntry.name}`, () => { - // const inputPath = path.join(testDir, testFile); + // const inputPath = join(testDir, testFile); // // snapshot the output of compodoc // const compodocOutput = runCompodoc(inputPath); // const compodocJson = JSON.parse(compodocOutput); // expect(compodocJson).toMatchFileSnapshot( - // path.join(testDir, `compodoc-${SNAPSHOT_OS}.snapshot`) + // join(testDir, `compodoc-${SNAPSHOT_OS}.snapshot`) // ); // // snapshot the output of addon-docs angular-properties // const componentData = findComponentByName('InputComponent', compodocJson); // const argTypes = extractArgTypesFromData(componentData); - // expect(argTypes).toMatchFileSnapshot(path.join(testDir, 'argtypes.snapshot')); + // expect(argTypes).toMatchFileSnapshot(join(testDir, 'argtypes.snapshot')); // }); } } diff --git a/code/frameworks/angular/src/preset.ts b/code/frameworks/angular/src/preset.ts index 31b86b2616dd..4eb938b1f3d3 100644 --- a/code/frameworks/angular/src/preset.ts +++ b/code/frameworks/angular/src/preset.ts @@ -1,6 +1,6 @@ import { PresetProperty } from 'storybook/internal/types'; -import { dirname, join } from 'path'; +import { dirname, join } from 'node:path'; import { StandaloneOptions } from './builders/utils/standalone-options'; import { StorybookConfig } from './types'; diff --git a/code/frameworks/angular/src/server/framework-preset-angular-ivy.ts b/code/frameworks/angular/src/server/framework-preset-angular-ivy.ts index d5a509e2ff83..3f854666f5ae 100644 --- a/code/frameworks/angular/src/server/framework-preset-angular-ivy.ts +++ b/code/frameworks/angular/src/server/framework-preset-angular-ivy.ts @@ -1,7 +1,8 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + import { Preset } from 'storybook/internal/types'; -import fs from 'fs'; -import * as path from 'path'; import { Configuration } from 'webpack'; import { AngularOptions } from '../types'; @@ -43,7 +44,7 @@ export const runNgcc = async () => { // should be async: true but does not work due to // https://github.com/storybookjs/storybook/pull/11157/files#r615413803 async: false, - basePath: path.join(process.cwd(), 'node_modules'), // absolute path to node_modules + basePath: join(process.cwd(), 'node_modules'), // absolute path to node_modules createNewEntryPointFormats: true, // --create-ivy-entry-points compileAllFormats: false, // --first-only }); @@ -51,7 +52,7 @@ export const runNgcc = async () => { export const webpack = async (webpackConfig: Configuration, options: PresetOptions) => { const packageJsonPath = require.resolve('@angular/core/package.json'); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); const VERSION = packageJson.version; const framework = await options.presets.apply('framework'); const angularOptions = (typeof framework === 'object' ? framework.options : {}) as AngularOptions; diff --git a/code/frameworks/ember/src/preset.ts b/code/frameworks/ember/src/preset.ts index 6b77cd8b7a96..170d864e4f6d 100644 --- a/code/frameworks/ember/src/preset.ts +++ b/code/frameworks/ember/src/preset.ts @@ -1,10 +1,10 @@ +import { dirname, join } from 'node:path'; + import { getProjectRoot, resolvePathInStorybookCache } from 'storybook/internal/common'; import type { PresetProperty } from 'storybook/internal/types'; import { getVirtualModules } from '@storybook/builder-webpack5'; -import { dirname, join } from 'path'; - import type { StorybookConfig } from './types'; const getAbsolutePath = (input: I): I => diff --git a/code/frameworks/ember/src/util.ts b/code/frameworks/ember/src/util.ts index 23e8d2aaa527..78cd557820af 100644 --- a/code/frameworks/ember/src/util.ts +++ b/code/frameworks/ember/src/util.ts @@ -1,12 +1,13 @@ +import { dirname, join } from 'node:path'; + import { sync as findUpSync } from 'find-up'; -import path from 'path'; export const findDistFile = (cwd: string, relativePath: string) => { const nearestPackageJson = findUpSync('package.json', { cwd }); if (!nearestPackageJson) { throw new Error(`Could not find package.json in: ${cwd}`); } - const packageDir = path.dirname(nearestPackageJson); + const packageDir = dirname(nearestPackageJson); - return path.join(packageDir, 'dist', relativePath); + return join(packageDir, 'dist', relativePath); }; diff --git a/code/frameworks/experimental-nextjs-vite/.eslintrc.json b/code/frameworks/experimental-nextjs-vite/.eslintrc.json new file mode 100644 index 000000000000..d76f64f6803d --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "rules": { + "global-require": "off", + "no-param-reassign": "off", + "import/no-dynamic-require": "off", + "import/no-unresolved": "off" + }, + "overrides": [ + { + "files": ["**/*.stories.@(jsx|tsx)"], + "rules": { + "react/no-unknown-property": "off", + "jsx-a11y/anchor-is-valid": "off" + } + }, + { + "files": ["**/*.compat.@(tsx|ts)"], + "rules": { + "local-rules/no-uncategorized-errors": "off" + } + } + ] +} diff --git a/code/frameworks/experimental-nextjs-vite/README.md b/code/frameworks/experimental-nextjs-vite/README.md new file mode 100644 index 000000000000..2ed6aeca8721 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/README.md @@ -0,0 +1,10 @@ +# Storybook for Next.js with Vite Builder + +See [documentation](https://storybook.js.org/docs/get-started/frameworks/nextjs?renderer=react) for installation instructions, usage examples, APIs, and more. + +## Acknowledgements + +This framework borrows heavily from these Storybook addons: + +- [storybook-addon-next](https://github.com/RyanClementsHax/storybook-addon-next) by [RyanClementsHax](https://github.com/RyanClementsHax/) +- [storybook-addon-next-router](https://github.com/lifeiscontent/storybook-addon-next-router) by [lifeiscontent](https://github.com/lifeiscontent) diff --git a/code/frameworks/experimental-nextjs-vite/package.json b/code/frameworks/experimental-nextjs-vite/package.json new file mode 100644 index 000000000000..69325c74045c --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/package.json @@ -0,0 +1,143 @@ +{ + "name": "@storybook/experimental-nextjs-vite", + "version": "8.3.0-alpha.4", + "description": "Storybook for Next.js and Vite", + "keywords": [ + "storybook", + "nextjs", + "vite" + ], + "homepage": "https://github.com/storybookjs/storybook/tree/next/code/frameworks/experimental-nextjs-vite", + "bugs": { + "url": "https://github.com/storybookjs/storybook/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/storybookjs/storybook.git", + "directory": "code/frameworks/nextjs" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "license": "MIT", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "node": "./dist/index.js", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./preset": { + "types": "./dist/preset.d.ts", + "require": "./dist/preset.js" + }, + "./dist/preview.mjs": "./dist/preview.mjs", + "./cache.mock": { + "types": "./dist/export-mocks/cache/index.d.ts", + "import": "./dist/export-mocks/cache/index.mjs", + "require": "./dist/export-mocks/cache/index.js" + }, + "./headers.mock": { + "types": "./dist/export-mocks/headers/index.d.ts", + "import": "./dist/export-mocks/headers/index.mjs", + "require": "./dist/export-mocks/headers/index.js" + }, + "./navigation.mock": { + "types": "./dist/export-mocks/navigation/index.d.ts", + "import": "./dist/export-mocks/navigation/index.mjs", + "require": "./dist/export-mocks/navigation/index.js" + }, + "./router.mock": { + "types": "./dist/export-mocks/router/index.d.ts", + "import": "./dist/export-mocks/router/index.mjs", + "require": "./dist/export-mocks/router/index.js" + }, + "./package.json": "./package.json" + }, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "cache.mock": [ + "dist/export-mocks/cache/index.d.ts" + ], + "headers.mock": [ + "dist/export-mocks/headers/index.d.ts" + ], + "router.mock": [ + "dist/export-mocks/router/index.d.ts" + ], + "navigation.mock": [ + "dist/export-mocks/navigation/index.d.ts" + ] + } + }, + "files": [ + "dist/**/*", + "template/cli/**/*", + "README.md", + "*.js", + "*.d.ts", + "!src/**/*" + ], + "scripts": { + "check": "jiti ../../../scripts/prepare/check.ts", + "prep": "jiti ../../../scripts/prepare/bundle.ts" + }, + "dependencies": { + "@storybook/builder-vite": "workspace:*", + "@storybook/react": "workspace:*", + "@storybook/test": "workspace:*", + "styled-jsx": "5.1.6" + }, + "devDependencies": { + "@types/node": "^18.0.0", + "next": "^14.2.5", + "typescript": "^5.3.2", + "vite-plugin-storybook-nextjs": "^1.0.0" + }, + "peerDependencies": { + "next": "^14.2.5", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "workspace:^", + "vite": "^5.0.0", + "vite-plugin-storybook-nextjs": "^1.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + }, + "optionalDependencies": { + "sharp": "^0.33.3" + }, + "engines": { + "node": ">=18.0.0" + }, + "publishConfig": { + "access": "public" + }, + "bundler": { + "entries": [ + "./src/index.ts", + "./src/preset.ts", + "./src/preview.tsx", + "./src/export-mocks/cache/index.ts", + "./src/export-mocks/headers/index.ts", + "./src/export-mocks/router/index.ts", + "./src/export-mocks/navigation/index.ts", + "./src/images/decorator.tsx" + ], + "externals": [ + "sb-original/image-context" + ], + "platform": "node" + }, + "gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae16" +} diff --git a/code/frameworks/experimental-nextjs-vite/preset.js b/code/frameworks/experimental-nextjs-vite/preset.js new file mode 100644 index 000000000000..a83f95279e7f --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/preset.js @@ -0,0 +1 @@ +module.exports = require('./dist/preset'); diff --git a/code/frameworks/experimental-nextjs-vite/project.json b/code/frameworks/experimental-nextjs-vite/project.json new file mode 100644 index 000000000000..f10ef7bdacfb --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/project.json @@ -0,0 +1,8 @@ +{ + "name": "experimental-nextjs-vite", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "targets": { + "build": {} + } +} diff --git a/code/frameworks/experimental-nextjs-vite/src/config/preview.ts b/code/frameworks/experimental-nextjs-vite/src/config/preview.ts new file mode 100644 index 000000000000..4766f590bcaf --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/config/preview.ts @@ -0,0 +1,4 @@ +import { setConfig } from 'next/config'; + +// eslint-disable-next-line no-underscore-dangle +setConfig(process.env.__NEXT_RUNTIME_CONFIG); diff --git a/code/frameworks/experimental-nextjs-vite/src/export-mocks/cache/index.ts b/code/frameworks/experimental-nextjs-vite/src/export-mocks/cache/index.ts new file mode 100644 index 000000000000..35b74b8cb02f --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/export-mocks/cache/index.ts @@ -0,0 +1,23 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { fn } from '@storybook/test'; + +// biome-ignore lint/suspicious/noExplicitAny: +type Callback = (...args: any[]) => Promise; + +// mock utilities/overrides (as of Next v14.2.0) +const revalidatePath = fn().mockName('next/cache::revalidatePath'); +const revalidateTag = fn().mockName('next/cache::revalidateTag'); +const unstable_cache = fn() + .mockName('next/cache::unstable_cache') + .mockImplementation((cb: Callback) => cb); +const unstable_noStore = fn().mockName('next/cache::unstable_noStore'); + +const cacheExports = { + unstable_cache, + revalidateTag, + revalidatePath, + unstable_noStore, +}; + +export default cacheExports; +export { unstable_cache, revalidateTag, revalidatePath, unstable_noStore }; diff --git a/code/frameworks/experimental-nextjs-vite/src/export-mocks/headers/cookies.ts b/code/frameworks/experimental-nextjs-vite/src/export-mocks/headers/cookies.ts new file mode 100644 index 000000000000..02e335834b8a --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/export-mocks/headers/cookies.ts @@ -0,0 +1,39 @@ +// We need this import to be a singleton, and because it's used in multiple entrypoints +// both in ESM and CJS, importing it via the package name instead of having a local import +// is the only way to achieve it actually being a singleton +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore we must ignore types here as during compilation they are not generated yet +import { headers } from '@storybook/nextjs/headers.mock'; +import { fn } from '@storybook/test'; + +import { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies'; + +class RequestCookiesMock extends RequestCookies { + get = fn(super.get.bind(this)).mockName('next/headers::cookies().get'); + + getAll = fn(super.getAll.bind(this)).mockName('next/headers::cookies().getAll'); + + has = fn(super.has.bind(this)).mockName('next/headers::cookies().has'); + + set = fn(super.set.bind(this)).mockName('next/headers::cookies().set'); + + delete = fn(super.delete.bind(this)).mockName('next/headers::cookies().delete'); +} + +let requestCookiesMock: RequestCookiesMock; + +export const cookies = fn(() => { + if (!requestCookiesMock) { + requestCookiesMock = new RequestCookiesMock(headers()); + } + return requestCookiesMock; +}).mockName('next/headers::cookies()'); + +const originalRestore = cookies.mockRestore.bind(null); + +// will be called automatically by the test loader +cookies.mockRestore = () => { + originalRestore(); + headers.mockRestore(); + requestCookiesMock = new RequestCookiesMock(headers()); +}; diff --git a/code/frameworks/experimental-nextjs-vite/src/export-mocks/headers/headers.ts b/code/frameworks/experimental-nextjs-vite/src/export-mocks/headers/headers.ts new file mode 100644 index 000000000000..d9eb5177b447 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/export-mocks/headers/headers.ts @@ -0,0 +1,39 @@ +import { fn } from '@storybook/test'; + +import { HeadersAdapter } from 'next/dist/server/web/spec-extension/adapters/headers'; + +class HeadersAdapterMock extends HeadersAdapter { + constructor() { + super({}); + } + + append = fn(super.append.bind(this)).mockName('next/headers::headers().append'); + + delete = fn(super.delete.bind(this)).mockName('next/headers::headers().delete'); + + get = fn(super.get.bind(this)).mockName('next/headers::headers().get'); + + has = fn(super.has.bind(this)).mockName('next/headers::headers().has'); + + set = fn(super.set.bind(this)).mockName('next/headers::headers().set'); + + forEach = fn(super.forEach.bind(this)).mockName('next/headers::headers().forEach'); + + entries = fn(super.entries.bind(this)).mockName('next/headers::headers().entries'); + + keys = fn(super.keys.bind(this)).mockName('next/headers::headers().keys'); + + values = fn(super.values.bind(this)).mockName('next/headers::headers().values'); +} + +let headersAdapterMock: HeadersAdapterMock; + +export const headers = () => { + if (!headersAdapterMock) headersAdapterMock = new HeadersAdapterMock(); + return headersAdapterMock; +}; + +// This fn is called by ./cookies to restore the headers in the right order +headers.mockRestore = () => { + headersAdapterMock = new HeadersAdapterMock(); +}; diff --git a/code/frameworks/experimental-nextjs-vite/src/export-mocks/headers/index.ts b/code/frameworks/experimental-nextjs-vite/src/export-mocks/headers/index.ts new file mode 100644 index 000000000000..1797d4ccaf57 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/export-mocks/headers/index.ts @@ -0,0 +1,14 @@ +import { fn } from '@storybook/test'; + +import * as originalHeaders from 'next/dist/client/components/headers'; + +// re-exports of the actual module +export * from 'next/dist/client/components/headers'; + +// mock utilities/overrides (as of Next v14.2.0) +export { headers } from './headers'; +export { cookies } from './cookies'; + +// passthrough mocks - keep original implementation but allow for spying +const draftMode = fn(originalHeaders.draftMode).mockName('draftMode'); +export { draftMode }; diff --git a/code/frameworks/experimental-nextjs-vite/src/export-mocks/navigation/index.ts b/code/frameworks/experimental-nextjs-vite/src/export-mocks/navigation/index.ts new file mode 100644 index 000000000000..60d964147dbb --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/export-mocks/navigation/index.ts @@ -0,0 +1,96 @@ +import { NextjsRouterMocksNotAvailable } from 'storybook/internal/preview-errors'; + +import type { Mock } from '@storybook/test'; +import { fn } from '@storybook/test'; + +import * as actual from 'next/dist/client/components/navigation'; +import { getRedirectError } from 'next/dist/client/components/redirect'; +import { RedirectStatusCode } from 'next/dist/client/components/redirect-status-code'; + +let navigationAPI: { + push: Mock; + replace: Mock; + forward: Mock; + back: Mock; + prefetch: Mock; + refresh: Mock; +}; + +/** + * Creates a next/navigation router API mock. Used internally. + * @ignore + * @internal + * */ +export const createNavigation = (overrides: any) => { + const navigationActions = { + push: fn().mockName('next/navigation::useRouter().push'), + replace: fn().mockName('next/navigation::useRouter().replace'), + forward: fn().mockName('next/navigation::useRouter().forward'), + back: fn().mockName('next/navigation::useRouter().back'), + prefetch: fn().mockName('next/navigation::useRouter().prefetch'), + refresh: fn().mockName('next/navigation::useRouter().refresh'), + }; + + if (overrides) { + Object.keys(navigationActions).forEach((key) => { + if (key in overrides) { + (navigationActions as any)[key] = fn((...args: any[]) => { + return (overrides as any)[key](...args); + }).mockName(`useRouter().${key}`); + } + }); + } + + navigationAPI = navigationActions; + + return navigationAPI; +}; + +export const getRouter = () => { + if (!navigationAPI) { + throw new NextjsRouterMocksNotAvailable({ + importType: 'next/navigation', + }); + } + + return navigationAPI; +}; + +// re-exports of the actual module +export * from 'next/dist/client/components/navigation'; + +// mock utilities/overrides (as of Next v14.2.0) +export const redirect = fn( + (url: string, type: actual.RedirectType = actual.RedirectType.push): never => { + throw getRedirectError(url, type, RedirectStatusCode.SeeOther); + } +).mockName('next/navigation::redirect'); + +export const permanentRedirect = fn( + (url: string, type: actual.RedirectType = actual.RedirectType.push): never => { + throw getRedirectError(url, type, RedirectStatusCode.SeeOther); + } +).mockName('next/navigation::permanentRedirect'); + +// passthrough mocks - keep original implementation but allow for spying +export const useSearchParams = fn(actual.useSearchParams).mockName( + 'next/navigation::useSearchParams' +); +export const usePathname = fn(actual.usePathname).mockName('next/navigation::usePathname'); +export const useSelectedLayoutSegment = fn(actual.useSelectedLayoutSegment).mockName( + 'next/navigation::useSelectedLayoutSegment' +); +export const useSelectedLayoutSegments = fn(actual.useSelectedLayoutSegments).mockName( + 'next/navigation::useSelectedLayoutSegments' +); +export const useRouter = fn(actual.useRouter).mockName('next/navigation::useRouter'); +export const useServerInsertedHTML = fn(actual.useServerInsertedHTML).mockName( + 'next/navigation::useServerInsertedHTML' +); +export const notFound = fn(actual.notFound).mockName('next/navigation::notFound'); + +// Params, not exported by Next.js, is manually declared to avoid inference issues. +interface Params { + [key: string]: string | string[]; +} +export const useParams = fn<[], Params>(actual.useParams).mockName('next/navigation::useParams'); diff --git a/code/frameworks/experimental-nextjs-vite/src/export-mocks/router/index.ts b/code/frameworks/experimental-nextjs-vite/src/export-mocks/router/index.ts new file mode 100644 index 000000000000..6d7dac5ef3bc --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/export-mocks/router/index.ts @@ -0,0 +1,117 @@ +import { NextjsRouterMocksNotAvailable } from 'storybook/internal/preview-errors'; + +import type { Mock } from '@storybook/test'; +import { fn } from '@storybook/test'; + +import singletonRouter, * as originalRouter from 'next/dist/client/router'; +import type { NextRouter, SingletonRouter } from 'next/router'; + +const defaultRouterState = { + route: '/', + asPath: '/', + basePath: '/', + pathname: '/', + query: {}, + isFallback: false, + isLocaleDomain: false, + isReady: true, + isPreview: false, +}; + +let routerAPI: { + push: Mock; + replace: Mock; + reload: Mock; + back: Mock; + forward: Mock; + prefetch: Mock; + beforePopState: Mock; + events: { + on: Mock; + off: Mock; + emit: Mock; + }; +} & typeof defaultRouterState; + +/** + * Creates a next/router router API mock. Used internally. + * @ignore + * @internal + * */ +export const createRouter = (overrides: Partial) => { + const routerActions: Partial = { + push: fn((..._args: any[]) => { + return Promise.resolve(true); + }).mockName('next/router::useRouter().push'), + replace: fn((..._args: any[]) => { + return Promise.resolve(true); + }).mockName('next/router::useRouter().replace'), + reload: fn((..._args: any[]) => {}).mockName('next/router::useRouter().reload'), + back: fn((..._args: any[]) => {}).mockName('next/router::useRouter().back'), + forward: fn(() => {}).mockName('next/router::useRouter().forward'), + prefetch: fn((..._args: any[]) => { + return Promise.resolve(); + }).mockName('next/router::useRouter().prefetch'), + beforePopState: fn((..._args: any[]) => {}).mockName('next/router::useRouter().beforePopState'), + }; + + const routerEvents: NextRouter['events'] = { + on: fn((..._args: any[]) => {}).mockName('next/router::useRouter().events.on'), + off: fn((..._args: any[]) => {}).mockName('next/router::useRouter().events.off'), + emit: fn((..._args: any[]) => {}).mockName('next/router::useRouter().events.emit'), + }; + + if (overrides) { + Object.keys(routerActions).forEach((key) => { + if (key in overrides) { + (routerActions as any)[key] = fn((...args: any[]) => { + return (overrides as any)[key](...args); + }).mockName(`useRouter().${key}`); + } + }); + } + + if (overrides?.events) { + Object.keys(routerEvents).forEach((key) => { + if (key in routerEvents) { + (routerEvents as any)[key] = fn((...args: any[]) => { + return (overrides.events as any)[key](...args); + }).mockName(`useRouter().events.${key}`); + } + }); + } + + routerAPI = { + ...defaultRouterState, + ...overrides, + ...routerActions, + // @ts-expect-error TODO improve typings + events: routerEvents, + }; + + // overwrite the singleton router from next/router + (singletonRouter as unknown as SingletonRouter).router = routerAPI as any; + (singletonRouter as unknown as SingletonRouter).readyCallbacks.forEach((cb) => cb()); + (singletonRouter as unknown as SingletonRouter).readyCallbacks = []; + + return routerAPI as unknown as NextRouter; +}; + +export const getRouter = () => { + if (!routerAPI) { + throw new NextjsRouterMocksNotAvailable({ + importType: 'next/router', + }); + } + + return routerAPI; +}; + +// re-exports of the actual module +export * from 'next/dist/client/router'; +export default singletonRouter; + +// mock utilities/overrides (as of Next v14.2.0) +// passthrough mocks - keep original implementation but allow for spying +export const useRouter = fn(originalRouter.useRouter).mockName('next/router::useRouter'); +export const withRouter = fn(originalRouter.withRouter).mockName('next/router::withRouter'); diff --git a/code/frameworks/experimental-nextjs-vite/src/head-manager/decorator.tsx b/code/frameworks/experimental-nextjs-vite/src/head-manager/decorator.tsx new file mode 100644 index 000000000000..84fd0215df4b --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/head-manager/decorator.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; + +import HeadManagerProvider from './head-manager-provider'; + +export const HeadManagerDecorator = (Story: React.FC): React.ReactNode => { + return ( + + + + ); +}; diff --git a/code/frameworks/experimental-nextjs-vite/src/head-manager/head-manager-provider.tsx b/code/frameworks/experimental-nextjs-vite/src/head-manager/head-manager-provider.tsx new file mode 100644 index 000000000000..69b58866c510 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/head-manager/head-manager-provider.tsx @@ -0,0 +1,24 @@ +import type { PropsWithChildren } from 'react'; +import React, { useMemo } from 'react'; + +import initHeadManager from 'next/dist/client/head-manager'; +import { HeadManagerContext } from 'next/dist/shared/lib/head-manager-context.shared-runtime'; + +type HeadManagerValue = { + updateHead?: ((state: JSX.Element[]) => void) | undefined; + mountedInstances?: Set; + updateScripts?: ((state: any) => void) | undefined; + scripts?: any; + getIsSsr?: () => boolean; + appDir?: boolean | undefined; + nonce?: string | undefined; +}; + +const HeadManagerProvider: React.FC = ({ children }) => { + const headManager: HeadManagerValue = useMemo(initHeadManager, []); + headManager.getIsSsr = () => false; + + return {children}; +}; + +export default HeadManagerProvider; diff --git a/code/frameworks/experimental-nextjs-vite/src/images/decorator.tsx b/code/frameworks/experimental-nextjs-vite/src/images/decorator.tsx new file mode 100644 index 000000000000..6dc34310a95c --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/images/decorator.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; + +import type { Addon_StoryContext } from 'storybook/internal/types'; + +import { ImageContext } from 'sb-original/image-context'; + +export const ImageDecorator = ( + Story: React.FC, + { parameters }: Addon_StoryContext +): React.ReactNode => { + if (!parameters.nextjs?.image) { + return ; + } + + return ( + + + + ); +}; diff --git a/code/frameworks/experimental-nextjs-vite/src/index.ts b/code/frameworks/experimental-nextjs-vite/src/index.ts new file mode 100644 index 000000000000..a904f93ec89d --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './portable-stories'; diff --git a/code/frameworks/experimental-nextjs-vite/src/portable-stories.ts b/code/frameworks/experimental-nextjs-vite/src/portable-stories.ts new file mode 100644 index 000000000000..7ad73e7ce778 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/portable-stories.ts @@ -0,0 +1,132 @@ +import { + composeConfigs, + composeStories as originalComposeStories, + composeStory as originalComposeStory, + setProjectAnnotations as originalSetProjectAnnotations, +} from 'storybook/internal/preview-api'; +import type { + Args, + ComposedStoryFn, + NamedOrDefaultProjectAnnotations, + ProjectAnnotations, + Store_CSFExports, + StoriesWithPartialProps, + StoryAnnotationsOrFn, +} from 'storybook/internal/types'; + +import type { Meta, ReactRenderer } from '@storybook/react'; + +import * as rscAnnotations from '../../../renderers/react/src/entry-preview-rsc'; +// ! ATTENTION: This needs to be a relative import so it gets prebundled. This is to avoid ESM issues in Nextjs + Jest setups +import { INTERNAL_DEFAULT_PROJECT_ANNOTATIONS as reactAnnotations } from '../../../renderers/react/src/portable-stories'; +import * as nextJsAnnotations from './preview'; + +/** Function that sets the globalConfig of your storybook. The global config is the preview module of your .storybook folder. + * + * It should be run a single time, so that your global config (e.g. decorators) is applied to your stories when using `composeStories` or `composeStory`. + * + * Example: + *```jsx + * // setup.js (for jest) + * import { setProjectAnnotations } from '@storybook/nextjs'; + * import projectAnnotations from './.storybook/preview'; + * + * setProjectAnnotations(projectAnnotations); + *``` + * + * @param projectAnnotations - e.g. (import projectAnnotations from '../.storybook/preview') + */ +export function setProjectAnnotations( + projectAnnotations: + | NamedOrDefaultProjectAnnotations + | NamedOrDefaultProjectAnnotations[] +): ProjectAnnotations { + return originalSetProjectAnnotations(projectAnnotations); +} + +// This will not be necessary once we have auto preset loading +const defaultProjectAnnotations: ProjectAnnotations = composeConfigs([ + reactAnnotations, + rscAnnotations, + nextJsAnnotations, +]); + +/** + * Function that will receive a story along with meta (e.g. a default export from a .stories file) + * and optionally projectAnnotations e.g. (import * from '../.storybook/preview) + * and will return a composed component that has all args/parameters/decorators/etc combined and applied to it. + * + * + * It's very useful for reusing a story in scenarios outside of Storybook like unit testing. + * + * Example: + *```jsx + * import { render } from '@testing-library/react'; + * import { composeStory } from '@storybook/nextjs'; + * import Meta, { Primary as PrimaryStory } from './Button.stories'; + * + * const Primary = composeStory(PrimaryStory, Meta); + * + * test('renders primary button with Hello World', () => { + * const { getByText } = render(Hello world); + * expect(getByText(/Hello world/i)).not.toBeNull(); + * }); + *``` + * + * @param story + * @param componentAnnotations - e.g. (import Meta from './Button.stories') + * @param [projectAnnotations] - e.g. (import * as projectAnnotations from '../.storybook/preview') this can be applied automatically if you use `setProjectAnnotations` in your setup files. + * @param [exportsName] - in case your story does not contain a name and you want it to have a name. + */ +export function composeStory( + story: StoryAnnotationsOrFn, + componentAnnotations: Meta, + projectAnnotations?: ProjectAnnotations, + exportsName?: string +): ComposedStoryFn> { + return originalComposeStory( + story as StoryAnnotationsOrFn, + componentAnnotations, + projectAnnotations, + defaultProjectAnnotations, + exportsName + ); +} + +/** + * Function that will receive a stories import (e.g. `import * as stories from './Button.stories'`) + * and optionally projectAnnotations (e.g. `import * from '../.storybook/preview`) + * and will return an object containing all the stories passed, but now as a composed component that has all args/parameters/decorators/etc combined and applied to it. + * + * + * It's very useful for reusing stories in scenarios outside of Storybook like unit testing. + * + * Example: + *```jsx + * import { render } from '@testing-library/react'; + * import { composeStories } from '@storybook/nextjs'; + * import * as stories from './Button.stories'; + * + * const { Primary, Secondary } = composeStories(stories); + * + * test('renders primary button with Hello World', () => { + * const { getByText } = render(Hello world); + * expect(getByText(/Hello world/i)).not.toBeNull(); + * }); + *``` + * + * @param csfExports - e.g. (import * as stories from './Button.stories') + * @param [projectAnnotations] - e.g. (import * as projectAnnotations from '../.storybook/preview') this can be applied automatically if you use `setProjectAnnotations` in your setup files. + */ +export function composeStories>( + csfExports: TModule, + projectAnnotations?: ProjectAnnotations +) { + // @ts-expect-error (Converted from ts-ignore) + const composedStories = originalComposeStories(csfExports, projectAnnotations, composeStory); + + return composedStories as unknown as Omit< + StoriesWithPartialProps, + keyof Store_CSFExports + >; +} diff --git a/code/frameworks/experimental-nextjs-vite/src/preset.ts b/code/frameworks/experimental-nextjs-vite/src/preset.ts new file mode 100644 index 000000000000..af5deabf2b87 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/preset.ts @@ -0,0 +1,47 @@ +// https://storybook.js.org/docs/react/addons/writing-presets +import type { PresetProperty } from 'storybook/internal/types'; + +import type { StorybookConfigVite } from '@storybook/builder-vite'; + +import { dirname, join } from 'path'; +// @ts-expect-error - tsconfig settings have to be moduleResolution=Bundler and module=Preserve +import vitePluginStorybookNextjs from 'vite-plugin-storybook-nextjs'; + +import type { StorybookConfig } from './types'; + +export const core: PresetProperty<'core'> = async (config, options) => { + const framework = await options.presets.apply('framework'); + + return { + ...config, + builder: { + name: dirname( + require.resolve(join('@storybook/builder-vite', 'package.json')) + ) as '@storybook/builder-vite', + options: { + ...(typeof framework === 'string' ? {} : framework.options.builder || {}), + }, + }, + renderer: dirname(require.resolve(join('@storybook/react', 'package.json'))), + }; +}; + +export const previewAnnotations: PresetProperty<'previewAnnotations'> = (entry = []) => { + const nextDir = dirname(require.resolve('@storybook/experimental-nextjs-vite/package.json')); + const result = [...entry, join(nextDir, 'dist/preview.mjs')]; + return result; +}; + +export const viteFinal: StorybookConfigVite['viteFinal'] = async (config, options) => { + config.plugins = config.plugins || []; + const framework = (await options.presets.apply( + 'framework', + {}, + options + )) as StorybookConfig['framework']; + + const nextAppDir = typeof framework !== 'string' ? framework.options.nextAppDir : undefined; + config.plugins.push(vitePluginStorybookNextjs({ dir: nextAppDir })); + + return config; +}; diff --git a/code/frameworks/experimental-nextjs-vite/src/preview.tsx b/code/frameworks/experimental-nextjs-vite/src/preview.tsx new file mode 100644 index 000000000000..9bc6df0f8bfb --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/preview.tsx @@ -0,0 +1,85 @@ +import type { Addon_DecoratorFunction, Addon_LoaderFunction } from 'storybook/internal/types'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore we must ignore types here as during compilation they are not generated yet +import { createNavigation } from '@storybook/experimental-nextjs-vite/navigation.mock'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore we must ignore types here as during compilation they are not generated yet +import { createRouter } from '@storybook/experimental-nextjs-vite/router.mock'; + +import { isNextRouterError } from 'next/dist/client/components/is-next-router-error'; + +import './config/preview'; +import { HeadManagerDecorator } from './head-manager/decorator'; +import { ImageDecorator } from './images/decorator'; +import { RouterDecorator } from './routing/decorator'; +import { StyledJsxDecorator } from './styledJsx/decorator'; + +function addNextHeadCount() { + const meta = document.createElement('meta'); + meta.name = 'next-head-count'; + meta.content = '0'; + document.head.appendChild(meta); +} + +function isAsyncClientComponentError(error: unknown) { + return ( + typeof error === 'string' && + (error.includes('A component was suspended by an uncached promise.') || + error.includes('async/await is not yet supported in Client Components')) + ); +} +addNextHeadCount(); + +// Copying Next patch of console.error: +// https://github.com/vercel/next.js/blob/a74deb63e310df473583ab6f7c1783bc609ca236/packages/next/src/client/app-index.tsx#L15 +const origConsoleError = globalThis.console.error; +globalThis.console.error = (...args: unknown[]) => { + const error = args[0]; + if (isNextRouterError(error) || isAsyncClientComponentError(error)) { + return; + } + origConsoleError.apply(globalThis.console, args); +}; + +globalThis.addEventListener('error', (ev: WindowEventMap['error']): void => { + if (isNextRouterError(ev.error) || isAsyncClientComponentError(ev.error)) { + ev.preventDefault(); + return; + } +}); + +export const decorators: Addon_DecoratorFunction[] = [ + StyledJsxDecorator, + ImageDecorator, + RouterDecorator, + HeadManagerDecorator, +]; + +export const loaders: Addon_LoaderFunction = async ({ globals, parameters }) => { + const { router, appDirectory } = parameters.nextjs ?? {}; + if (appDirectory) { + createNavigation(router); + } else { + createRouter({ + locale: globals.locale, + ...router, + }); + } +}; + +export const parameters = { + docs: { + source: { + excludeDecorators: true, + }, + }, + react: { + rootOptions: { + onCaughtError(error: unknown) { + if (isNextRouterError(error)) return; + console.error(error); + }, + }, + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/src/routing/app-router-provider.tsx b/code/frameworks/experimental-nextjs-vite/src/routing/app-router-provider.tsx new file mode 100644 index 000000000000..807d34920ab1 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/routing/app-router-provider.tsx @@ -0,0 +1,115 @@ +import React, { useMemo } from 'react'; + +// We need this import to be a singleton, and because it's used in multiple entrypoints +// both in ESM and CJS, importing it via the package name instead of having a local import +// is the only way to achieve it actually being a singleton +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore we must ignore types here as during compilation they are not generated yet +import { getRouter } from '@storybook/experimental-nextjs-vite/navigation.mock'; + +import type { FlightRouterState } from 'next/dist/server/app-render/types'; +import { + AppRouterContext, + GlobalLayoutRouterContext, + LayoutRouterContext, +} from 'next/dist/shared/lib/app-router-context.shared-runtime'; +import { + PathParamsContext, + PathnameContext, + SearchParamsContext, +} from 'next/dist/shared/lib/hooks-client-context.shared-runtime'; +import { type Params } from 'next/dist/shared/lib/router/utils/route-matcher'; +import { PAGE_SEGMENT_KEY } from 'next/dist/shared/lib/segment'; + +import type { RouteParams } from './types'; + +type AppRouterProviderProps = { + routeParams: RouteParams; +}; + +// Since Next 14.2.x +// https://github.com/vercel/next.js/pull/60708/files#diff-7b6239af735eba0c401e1a0db1a04dd4575c19a031934f02d128cf3ac813757bR106 +function getSelectedParams(currentTree: FlightRouterState, params: Params = {}): Params { + const parallelRoutes = currentTree[1]; + + for (const parallelRoute of Object.values(parallelRoutes)) { + const segment = parallelRoute[0]; + const isDynamicParameter = Array.isArray(segment); + const segmentValue = isDynamicParameter ? segment[1] : segment; + if (!segmentValue || segmentValue.startsWith(PAGE_SEGMENT_KEY)) continue; + + // Ensure catchAll and optional catchall are turned into an array + const isCatchAll = isDynamicParameter && (segment[2] === 'c' || segment[2] === 'oc'); + + if (isCatchAll) { + params[segment[0]] = segment[1].split('/'); + } else if (isDynamicParameter) { + params[segment[0]] = segment[1]; + } + + params = getSelectedParams(parallelRoute, params); + } + + return params; +} + +const getParallelRoutes = (segmentsList: Array): FlightRouterState => { + const segment = segmentsList.shift(); + + if (segment) { + return [segment, { children: getParallelRoutes(segmentsList) }]; + } + + return [] as any; +}; + +export const AppRouterProvider: React.FC> = ({ + children, + routeParams, +}) => { + const { pathname, query, segments = [] } = routeParams; + + const tree: FlightRouterState = [pathname, { children: getParallelRoutes([...segments]) }]; + const pathParams = useMemo(() => { + return getSelectedParams(tree); + }, [tree]); + + // https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/app-router.tsx#L436 + return ( + + + + + + + {children} + + + + + + + ); +}; diff --git a/code/frameworks/experimental-nextjs-vite/src/routing/decorator.tsx b/code/frameworks/experimental-nextjs-vite/src/routing/decorator.tsx new file mode 100644 index 000000000000..f21819f373a4 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/routing/decorator.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; + +import type { Addon_StoryContext } from 'storybook/internal/types'; + +import { RedirectBoundary } from 'next/dist/client/components/redirect-boundary'; + +import { AppRouterProvider } from './app-router-provider'; +import { PageRouterProvider } from './page-router-provider'; +import type { NextAppDirectory, RouteParams } from './types'; + +const defaultRouterParams: RouteParams = { + pathname: '/', + query: {}, +}; + +export const RouterDecorator = ( + Story: React.FC, + { parameters }: Addon_StoryContext +): React.ReactNode => { + const nextAppDirectory = + (parameters.nextjs?.appDirectory as NextAppDirectory | undefined) ?? false; + + if (nextAppDirectory) { + if (!AppRouterProvider) { + return null; + } + return ( + + {/* + The next.js RedirectBoundary causes flashing UI when used client side. + Possible use the implementation of the PR: https://github.com/vercel/next.js/pull/49439 + Or wait for next to solve this on their side. + */} + + + + + ); + } + + return ( + + + + ); +}; diff --git a/code/frameworks/experimental-nextjs-vite/src/routing/page-router-provider.tsx b/code/frameworks/experimental-nextjs-vite/src/routing/page-router-provider.tsx new file mode 100644 index 000000000000..92fb7fe54826 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/routing/page-router-provider.tsx @@ -0,0 +1,15 @@ +import type { PropsWithChildren } from 'react'; +import React from 'react'; + +// We need this import to be a singleton, and because it's used in multiple entrypoints +// both in ESM and CJS, importing it via the package name instead of having a local import +// is the only way to achieve it actually being a singleton +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore we must ignore types here as during compilation they are not generated yet +import { getRouter } from '@storybook/experimental-nextjs-vite/router.mock'; + +import { RouterContext } from 'next/dist/shared/lib/router-context.shared-runtime'; + +export const PageRouterProvider: React.FC = ({ children }) => ( + {children} +); diff --git a/code/frameworks/experimental-nextjs-vite/src/routing/types.tsx b/code/frameworks/experimental-nextjs-vite/src/routing/types.tsx new file mode 100644 index 000000000000..e80b0413260f --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/routing/types.tsx @@ -0,0 +1,7 @@ +export type RouteParams = { + pathname: string; + query: Record; + [key: string]: any; +}; + +export type NextAppDirectory = boolean; diff --git a/code/frameworks/experimental-nextjs-vite/src/styledJsx/decorator.tsx b/code/frameworks/experimental-nextjs-vite/src/styledJsx/decorator.tsx new file mode 100644 index 000000000000..69d5496283bd --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/styledJsx/decorator.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; + +import { StyleRegistry } from 'styled-jsx'; + +export const StyledJsxDecorator = (Story: React.FC): React.ReactNode => ( + + + +); diff --git a/code/frameworks/experimental-nextjs-vite/src/types.ts b/code/frameworks/experimental-nextjs-vite/src/types.ts new file mode 100644 index 000000000000..8e72784e169c --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/types.ts @@ -0,0 +1,45 @@ +import type { + CompatibleString, + StorybookConfig as StorybookConfigBase, +} from 'storybook/internal/types'; + +import type { BuilderOptions, StorybookConfigVite } from '@storybook/builder-vite'; + +type FrameworkName = CompatibleString<'@storybook/experimental-nextjs-vite'>; +type BuilderName = CompatibleString<'@storybook/builder-vite'>; + +export type FrameworkOptions = { + /** + * The directory where the Next.js app is located. + * @default process.cwd() + */ + nextAppDir?: string; + builder?: BuilderOptions; +}; + +type StorybookConfigFramework = { + framework: + | FrameworkName + | { + name: FrameworkName; + options: FrameworkOptions; + }; + core?: StorybookConfigBase['core'] & { + builder?: + | BuilderName + | { + name: BuilderName; + options: BuilderOptions; + }; + }; +}; + +/** + * The interface for Storybook configuration in `main.ts` files. + */ +export type StorybookConfig = Omit< + StorybookConfigBase, + keyof StorybookConfigVite | keyof StorybookConfigFramework +> & + StorybookConfigVite & + StorybookConfigFramework & {}; diff --git a/code/frameworks/experimental-nextjs-vite/src/typings.d.ts b/code/frameworks/experimental-nextjs-vite/src/typings.d.ts new file mode 100644 index 000000000000..090a63a18725 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/src/typings.d.ts @@ -0,0 +1,32 @@ +declare module 'sb-original/image-context' { + import type { StaticImport } from 'next/dist/shared/lib/get-img-props'; + import type { Context } from 'next/dist/compiled/react'; + import type { ImageProps } from 'next/image'; + import type { ImageProps as LegacyImageProps } from 'next/legacy/image'; + + export const ImageContext: Context< + Partial< + Omit & { + src: string | StaticImport; + } + > & + Omit + >; +} + +declare module 'sb-original/default-loader' { + import type { ImageLoaderProps } from 'next/image'; + + export const defaultLoader: (props: ImageLoaderProps) => string; +} + +declare module 'next/dist/compiled/react' { + import * as React from 'react'; + export default React; + export type Context = React.Context; + export function createContext( + // If you thought this should be optional, see + // https://github.com/DefinitelyTyped/DefinitelyTyped/pull/24509#issuecomment-382213106 + defaultValue: T + ): Context; +} diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/.eslintrc.json b/code/frameworks/experimental-nextjs-vite/template/cli/.eslintrc.json new file mode 100644 index 000000000000..2ce44cb74ab3 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/.eslintrc.json @@ -0,0 +1,7 @@ +{ + "rules": { + "import/no-extraneous-dependencies": "off", + "import/extensions": "off", + "react/no-unknown-property": "off" + } +} diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/js/Button.jsx b/code/frameworks/experimental-nextjs-vite/template/cli/js/Button.jsx new file mode 100644 index 000000000000..8231c774f03b --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/js/Button.jsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import PropTypes from 'prop-types'; + +import './button.css'; + +/** + * Primary UI component for user interaction + */ +export const Button = ({ primary, backgroundColor, size, label, ...props }) => { + const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; + return ( + + ); +}; + +Button.propTypes = { + /** + * Is this the principal call to action on the page? + */ + primary: PropTypes.bool, + /** + * What background color to use + */ + backgroundColor: PropTypes.string, + /** + * How large should the button be? + */ + size: PropTypes.oneOf(['small', 'medium', 'large']), + /** + * Button contents + */ + label: PropTypes.string.isRequired, + /** + * Optional click handler + */ + onClick: PropTypes.func, +}; + +Button.defaultProps = { + backgroundColor: null, + primary: false, + size: 'medium', + onClick: undefined, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/js/Button.stories.js b/code/frameworks/experimental-nextjs-vite/template/cli/js/Button.stories.js new file mode 100644 index 000000000000..045d9c477ab1 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/js/Button.stories.js @@ -0,0 +1,49 @@ +import { fn } from '@storybook/test'; + +import { Button } from './Button'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +export default { + title: 'Example/Button', + component: Button, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onClick: fn() }, +}; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary = { + args: { + primary: true, + label: 'Button', + }, +}; + +export const Secondary = { + args: { + label: 'Button', + }, +}; + +export const Large = { + args: { + size: 'large', + label: 'Button', + }, +}; + +export const Small = { + args: { + size: 'small', + label: 'Button', + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/js/Configure.mdx b/code/frameworks/experimental-nextjs-vite/template/cli/js/Configure.mdx new file mode 100644 index 000000000000..cc3292373f73 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/js/Configure.mdx @@ -0,0 +1,446 @@ +import { Meta } from "@storybook/blocks"; +import Image from "next/image"; + +import Github from "./assets/github.svg"; +import Discord from "./assets/discord.svg"; +import Youtube from "./assets/youtube.svg"; +import Tutorials from "./assets/tutorials.svg"; +import Styling from "./assets/styling.png"; +import Context from "./assets/context.png"; +import Assets from "./assets/assets.png"; +import Docs from "./assets/docs.png"; +import Share from "./assets/share.png"; +import FigmaPlugin from "./assets/figma-plugin.png"; +import Testing from "./assets/testing.png"; +import Accessibility from "./assets/accessibility.png"; +import Theming from "./assets/theming.png"; +import AddonLibrary from "./assets/addon-library.png"; + +export const RightArrow = () => + + + + + +
+
+ # Configure your project + + Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community. +
+
+
+ A wall of logos representing different styling technologies +

Add styling and CSS

+

Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.

+ Learn more +
+
+ An abstraction representing the composition of data for a component +

Provide context and mocking

+

Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.

+ Learn more +
+
+ A representation of typography and image assets +
+

Load assets and resources

+

To link static files (like fonts) to your projects and stories, use the + `staticDirs` configuration option to specify folders to load when + starting Storybook.

+ Learn more +
+
+
+
+
+
+ # Do more with Storybook + + Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs. +
+ +
+
+
+ A screenshot showing the autodocs tag being set, pointing a docs page being generated +

Autodocs

+

Auto-generate living, + interactive reference documentation from your components and stories.

+ Learn more +
+
+ A browser window showing a Storybook being published to a chromatic.com URL +

Publish to Chromatic

+

Publish your Storybook to review and collaborate with your entire team.

+ Learn more +
+
+ Windows showing the Storybook plugin in Figma +

Figma Plugin

+

Embed your stories into Figma to cross-reference the design and live + implementation in one place.

+ Learn more +
+
+ Screenshot of tests passing and failing +

Testing

+

Use stories to test a component in all its variations, no matter how + complex.

+ Learn more +
+
+ Screenshot of accessibility tests passing and failing +

Accessibility

+

Automatically test your components for a11y issues as you develop.

+ Learn more +
+
+ Screenshot of Storybook in light and dark mode +

Theming

+

Theme Storybook's UI to personalize it to your project.

+ Learn more +
+
+
+
+
+
+

Addons

+

Integrate your tools with Storybook to connect workflows.

+ Discover all addons +
+
+ Integrate your tools with Storybook to connect workflows. +
+
+ +
+
+ Github logo + Join our contributors building the future of UI development. + + Star on GitHub +
+
+ Discord logo +
+ Get support and chat with frontend developers. + + Join Discord server +
+
+
+ Youtube logo +
+ Watch tutorials, feature previews and interviews. + + Watch on YouTube +
+
+
+ A book +

Follow guided walkthroughs on for key workflows.

+ + Discover tutorials +
+
+ + diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/js/Header.jsx b/code/frameworks/experimental-nextjs-vite/template/cli/js/Header.jsx new file mode 100644 index 000000000000..38aa4d89af8b --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/js/Header.jsx @@ -0,0 +1,60 @@ +import React from 'react'; + +import PropTypes from 'prop-types'; + +import { Button } from './Button'; +import './header.css'; + +export const Header = ({ user, onLogin, onLogout, onCreateAccount }) => ( +
+
+
+ + + + + + + +

Acme

+
+
+ {user ? ( + <> + + Welcome, {user.name}! + +
+
+
+); + +Header.propTypes = { + user: PropTypes.shape({ + name: PropTypes.string.isRequired, + }), + onLogin: PropTypes.func.isRequired, + onLogout: PropTypes.func.isRequired, + onCreateAccount: PropTypes.func.isRequired, +}; + +Header.defaultProps = { + user: null, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/js/Header.stories.js b/code/frameworks/experimental-nextjs-vite/template/cli/js/Header.stories.js new file mode 100644 index 000000000000..699abab07946 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/js/Header.stories.js @@ -0,0 +1,30 @@ +import { fn } from '@storybook/test'; + +import { Header } from './Header'; + +export default { + title: 'Example/Header', + component: Header, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + layout: 'fullscreen', + }, + args: { + onLogin: fn(), + onLogout: fn(), + onCreateAccount: fn(), + }, +}; +export const LoggedIn = { + args: { + user: { + name: 'Jane Doe', + }, + }, +}; + +export const LoggedOut = { + args: {}, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/js/Page.jsx b/code/frameworks/experimental-nextjs-vite/template/cli/js/Page.jsx new file mode 100644 index 000000000000..6db1e0ac3f36 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/js/Page.jsx @@ -0,0 +1,68 @@ +import React from 'react'; + +import { Header } from './Header'; +import './page.css'; + +export const Page = () => { + const [user, setUser] = React.useState(); + + return ( +
+
setUser({ name: 'Jane Doe' })} + onLogout={() => setUser(undefined)} + onCreateAccount={() => setUser({ name: 'Jane Doe' })} + /> +
+

Pages in Storybook

+

+ We recommend building UIs with a{' '} + + component-driven + {' '} + process starting with atomic components and ending with pages. +

+

+ Render pages with mock data. This makes it easy to build and review page states without + needing to navigate to them in your app. Here are some handy patterns for managing page + data in Storybook: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at{' '} + + Storybook tutorials + + . Read more in the{' '} + + docs + + . +

+
+ Tip Adjust the width of the canvas with the{' '} + + + + + + Viewports addon in the toolbar +
+
+
+ ); +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/js/Page.stories.js b/code/frameworks/experimental-nextjs-vite/template/cli/js/Page.stories.js new file mode 100644 index 000000000000..383fd1ab44e3 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/js/Page.stories.js @@ -0,0 +1,28 @@ +import { expect, userEvent, within } from '@storybook/test'; + +import { Page } from './Page'; + +export default { + title: 'Example/Page', + component: Page, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + layout: 'fullscreen', + }, +}; + +export const LoggedOut = {}; + +// More on interaction testing: https://storybook.js.org/docs/writing-tests/interaction-testing +export const LoggedIn = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const loginButton = canvas.getByRole('button', { name: /Log in/i }); + await expect(loginButton).toBeInTheDocument(); + await userEvent.click(loginButton); + await expect(loginButton).not.toBeInTheDocument(); + + const logoutButton = canvas.getByRole('button', { name: /Log out/i }); + await expect(logoutButton).toBeInTheDocument(); + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Button.stories.ts b/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Button.stories.ts new file mode 100644 index 000000000000..18be3ab1aa1d --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Button.stories.ts @@ -0,0 +1,53 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; + +import { Button } from './Button'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta: Meta = { + title: 'Example/Button', + component: Button, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onClick: fn() }, +}; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + primary: true, + label: 'Button', + }, +}; + +export const Secondary: Story = { + args: { + label: 'Button', + }, +}; + +export const Large: Story = { + args: { + size: 'large', + label: 'Button', + }, +}; + +export const Small: Story = { + args: { + size: 'small', + label: 'Button', + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Button.tsx b/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Button.tsx new file mode 100644 index 000000000000..34d8bcdf1fd8 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Button.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import './button.css'; + +export interface ButtonProps { + /** + * Is this the principal call to action on the page? + */ + primary?: boolean; + /** + * What background color to use + */ + backgroundColor?: string; + /** + * How large should the button be? + */ + size?: 'small' | 'medium' | 'large'; + /** + * Button contents + */ + label: string; + /** + * Optional click handler + */ + onClick?: () => void; +} + +/** + * Primary UI component for user interaction + */ +export const Button = ({ + primary = false, + size = 'medium', + backgroundColor, + label, + ...props +}: ButtonProps) => { + const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; + return ( + + ); +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Configure.mdx b/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Configure.mdx new file mode 100644 index 000000000000..cc3292373f73 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Configure.mdx @@ -0,0 +1,446 @@ +import { Meta } from "@storybook/blocks"; +import Image from "next/image"; + +import Github from "./assets/github.svg"; +import Discord from "./assets/discord.svg"; +import Youtube from "./assets/youtube.svg"; +import Tutorials from "./assets/tutorials.svg"; +import Styling from "./assets/styling.png"; +import Context from "./assets/context.png"; +import Assets from "./assets/assets.png"; +import Docs from "./assets/docs.png"; +import Share from "./assets/share.png"; +import FigmaPlugin from "./assets/figma-plugin.png"; +import Testing from "./assets/testing.png"; +import Accessibility from "./assets/accessibility.png"; +import Theming from "./assets/theming.png"; +import AddonLibrary from "./assets/addon-library.png"; + +export const RightArrow = () => + + + + + +
+
+ # Configure your project + + Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community. +
+
+
+ A wall of logos representing different styling technologies +

Add styling and CSS

+

Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.

+ Learn more +
+
+ An abstraction representing the composition of data for a component +

Provide context and mocking

+

Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.

+ Learn more +
+
+ A representation of typography and image assets +
+

Load assets and resources

+

To link static files (like fonts) to your projects and stories, use the + `staticDirs` configuration option to specify folders to load when + starting Storybook.

+ Learn more +
+
+
+
+
+
+ # Do more with Storybook + + Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs. +
+ +
+
+
+ A screenshot showing the autodocs tag being set, pointing a docs page being generated +

Autodocs

+

Auto-generate living, + interactive reference documentation from your components and stories.

+ Learn more +
+
+ A browser window showing a Storybook being published to a chromatic.com URL +

Publish to Chromatic

+

Publish your Storybook to review and collaborate with your entire team.

+ Learn more +
+
+ Windows showing the Storybook plugin in Figma +

Figma Plugin

+

Embed your stories into Figma to cross-reference the design and live + implementation in one place.

+ Learn more +
+
+ Screenshot of tests passing and failing +

Testing

+

Use stories to test a component in all its variations, no matter how + complex.

+ Learn more +
+
+ Screenshot of accessibility tests passing and failing +

Accessibility

+

Automatically test your components for a11y issues as you develop.

+ Learn more +
+
+ Screenshot of Storybook in light and dark mode +

Theming

+

Theme Storybook's UI to personalize it to your project.

+ Learn more +
+
+
+
+
+
+

Addons

+

Integrate your tools with Storybook to connect workflows.

+ Discover all addons +
+
+ Integrate your tools with Storybook to connect workflows. +
+
+ +
+
+ Github logo + Join our contributors building the future of UI development. + + Star on GitHub +
+
+ Discord logo +
+ Get support and chat with frontend developers. + + Join Discord server +
+
+
+ Youtube logo +
+ Watch tutorials, feature previews and interviews. + + Watch on YouTube +
+
+
+ A book +

Follow guided walkthroughs on for key workflows.

+ + Discover tutorials +
+
+ + diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Header.stories.ts b/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Header.stories.ts new file mode 100644 index 000000000000..feddeae98faf --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Header.stories.ts @@ -0,0 +1,33 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; + +import { Header } from './Header'; + +const meta: Meta = { + title: 'Example/Header', + component: Header, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + layout: 'fullscreen', + }, + args: { + onLogin: fn(), + onLogout: fn(), + onCreateAccount: fn(), + }, +}; + +export default meta; +type Story = StoryObj; + +export const LoggedIn: Story = { + args: { + user: { + name: 'Jane Doe', + }, + }, +}; + +export const LoggedOut: Story = {}; diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Header.tsx b/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Header.tsx new file mode 100644 index 000000000000..1bf981a4251f --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Header.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import { Button } from './Button'; +import './header.css'; + +type User = { + name: string; +}; + +export interface HeaderProps { + user?: User; + onLogin?: () => void; + onLogout?: () => void; + onCreateAccount?: () => void; +} + +export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( +
+
+
+ + + + + + + +

Acme

+
+
+ {user ? ( + <> + + Welcome, {user.name}! + +
+
+
+); diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Page.stories.ts b/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Page.stories.ts new file mode 100644 index 000000000000..7581ed2bee30 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Page.stories.ts @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent, within } from '@storybook/test'; + +import { Page } from './Page'; + +const meta: Meta = { + title: 'Example/Page', + component: Page, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +export const LoggedOut: Story = {}; + +// More on interaction testing: https://storybook.js.org/docs/writing-tests/interaction-testing +export const LoggedIn: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const loginButton = canvas.getByRole('button', { name: /Log in/i }); + await expect(loginButton).toBeInTheDocument(); + await userEvent.click(loginButton); + await expect(loginButton).not.toBeInTheDocument(); + + const logoutButton = canvas.getByRole('button', { name: /Log out/i }); + await expect(logoutButton).toBeInTheDocument(); + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Page.tsx b/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Page.tsx new file mode 100644 index 000000000000..e11748301390 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/ts-3-8/Page.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import { Header } from './Header'; +import './page.css'; + +type User = { + name: string; +}; + +export const Page: React.FC = () => { + const [user, setUser] = React.useState(); + + return ( +
+
setUser({ name: 'Jane Doe' })} + onLogout={() => setUser(undefined)} + onCreateAccount={() => setUser({ name: 'Jane Doe' })} + /> + +
+

Pages in Storybook

+

+ We recommend building UIs with a{' '} + + component-driven + {' '} + process starting with atomic components and ending with pages. +

+

+ Render pages with mock data. This makes it easy to build and review page states without + needing to navigate to them in your app. Here are some handy patterns for managing page + data in Storybook: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at{' '} + + Storybook tutorials + + . Read more in the{' '} + + docs + + . +

+
+ Tip Adjust the width of the canvas with the{' '} + + + + + + Viewports addon in the toolbar +
+
+
+ ); +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Button.stories.ts b/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Button.stories.ts new file mode 100644 index 000000000000..2a05e01b06fe --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Button.stories.ts @@ -0,0 +1,53 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; + +import { Button } from './Button'; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = { + title: 'Example/Button', + component: Button, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'centered', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + argTypes: { + backgroundColor: { control: 'color' }, + }, + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args + args: { onClick: fn() }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + primary: true, + label: 'Button', + }, +}; + +export const Secondary: Story = { + args: { + label: 'Button', + }, +}; + +export const Large: Story = { + args: { + size: 'large', + label: 'Button', + }, +}; + +export const Small: Story = { + args: { + size: 'small', + label: 'Button', + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Button.tsx b/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Button.tsx new file mode 100644 index 000000000000..34d8bcdf1fd8 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Button.tsx @@ -0,0 +1,53 @@ +import React from 'react'; + +import './button.css'; + +export interface ButtonProps { + /** + * Is this the principal call to action on the page? + */ + primary?: boolean; + /** + * What background color to use + */ + backgroundColor?: string; + /** + * How large should the button be? + */ + size?: 'small' | 'medium' | 'large'; + /** + * Button contents + */ + label: string; + /** + * Optional click handler + */ + onClick?: () => void; +} + +/** + * Primary UI component for user interaction + */ +export const Button = ({ + primary = false, + size = 'medium', + backgroundColor, + label, + ...props +}: ButtonProps) => { + const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; + return ( + + ); +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Configure.mdx b/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Configure.mdx new file mode 100644 index 000000000000..cc3292373f73 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Configure.mdx @@ -0,0 +1,446 @@ +import { Meta } from "@storybook/blocks"; +import Image from "next/image"; + +import Github from "./assets/github.svg"; +import Discord from "./assets/discord.svg"; +import Youtube from "./assets/youtube.svg"; +import Tutorials from "./assets/tutorials.svg"; +import Styling from "./assets/styling.png"; +import Context from "./assets/context.png"; +import Assets from "./assets/assets.png"; +import Docs from "./assets/docs.png"; +import Share from "./assets/share.png"; +import FigmaPlugin from "./assets/figma-plugin.png"; +import Testing from "./assets/testing.png"; +import Accessibility from "./assets/accessibility.png"; +import Theming from "./assets/theming.png"; +import AddonLibrary from "./assets/addon-library.png"; + +export const RightArrow = () => + + + + + +
+
+ # Configure your project + + Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community. +
+
+
+ A wall of logos representing different styling technologies +

Add styling and CSS

+

Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.

+ Learn more +
+
+ An abstraction representing the composition of data for a component +

Provide context and mocking

+

Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.

+ Learn more +
+
+ A representation of typography and image assets +
+

Load assets and resources

+

To link static files (like fonts) to your projects and stories, use the + `staticDirs` configuration option to specify folders to load when + starting Storybook.

+ Learn more +
+
+
+
+
+
+ # Do more with Storybook + + Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs. +
+ +
+
+
+ A screenshot showing the autodocs tag being set, pointing a docs page being generated +

Autodocs

+

Auto-generate living, + interactive reference documentation from your components and stories.

+ Learn more +
+
+ A browser window showing a Storybook being published to a chromatic.com URL +

Publish to Chromatic

+

Publish your Storybook to review and collaborate with your entire team.

+ Learn more +
+
+ Windows showing the Storybook plugin in Figma +

Figma Plugin

+

Embed your stories into Figma to cross-reference the design and live + implementation in one place.

+ Learn more +
+
+ Screenshot of tests passing and failing +

Testing

+

Use stories to test a component in all its variations, no matter how + complex.

+ Learn more +
+
+ Screenshot of accessibility tests passing and failing +

Accessibility

+

Automatically test your components for a11y issues as you develop.

+ Learn more +
+
+ Screenshot of Storybook in light and dark mode +

Theming

+

Theme Storybook's UI to personalize it to your project.

+ Learn more +
+
+
+
+
+
+

Addons

+

Integrate your tools with Storybook to connect workflows.

+ Discover all addons +
+
+ Integrate your tools with Storybook to connect workflows. +
+
+ +
+
+ Github logo + Join our contributors building the future of UI development. + + Star on GitHub +
+
+ Discord logo +
+ Get support and chat with frontend developers. + + Join Discord server +
+
+
+ Youtube logo +
+ Watch tutorials, feature previews and interviews. + + Watch on YouTube +
+
+
+ A book +

Follow guided walkthroughs on for key workflows.

+ + Discover tutorials +
+
+ + diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Header.stories.ts b/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Header.stories.ts new file mode 100644 index 000000000000..80c71d0f520e --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Header.stories.ts @@ -0,0 +1,33 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; + +import { Header } from './Header'; + +const meta = { + title: 'Example/Header', + component: Header, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + layout: 'fullscreen', + }, + args: { + onLogin: fn(), + onLogout: fn(), + onCreateAccount: fn(), + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const LoggedIn: Story = { + args: { + user: { + name: 'Jane Doe', + }, + }, +}; + +export const LoggedOut: Story = {}; diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Header.tsx b/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Header.tsx new file mode 100644 index 000000000000..1bf981a4251f --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Header.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import { Button } from './Button'; +import './header.css'; + +type User = { + name: string; +}; + +export interface HeaderProps { + user?: User; + onLogin?: () => void; + onLogout?: () => void; + onCreateAccount?: () => void; +} + +export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( +
+
+
+ + + + + + + +

Acme

+
+
+ {user ? ( + <> + + Welcome, {user.name}! + +
+
+
+); diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Page.stories.ts b/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Page.stories.ts new file mode 100644 index 000000000000..53b9f8fdf9c9 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Page.stories.ts @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent, within } from '@storybook/test'; + +import { Page } from './Page'; + +const meta = { + title: 'Example/Page', + component: Page, + parameters: { + // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const LoggedOut: Story = {}; + +// More on interaction testing: https://storybook.js.org/docs/writing-tests/interaction-testing +export const LoggedIn: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const loginButton = canvas.getByRole('button', { name: /Log in/i }); + await expect(loginButton).toBeInTheDocument(); + await userEvent.click(loginButton); + await expect(loginButton).not.toBeInTheDocument(); + + const logoutButton = canvas.getByRole('button', { name: /Log out/i }); + await expect(logoutButton).toBeInTheDocument(); + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Page.tsx b/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Page.tsx new file mode 100644 index 000000000000..e11748301390 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/cli/ts-4-9/Page.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import { Header } from './Header'; +import './page.css'; + +type User = { + name: string; +}; + +export const Page: React.FC = () => { + const [user, setUser] = React.useState(); + + return ( +
+
setUser({ name: 'Jane Doe' })} + onLogout={() => setUser(undefined)} + onCreateAccount={() => setUser({ name: 'Jane Doe' })} + /> + +
+

Pages in Storybook

+

+ We recommend building UIs with a{' '} + + component-driven + {' '} + process starting with atomic components and ending with pages. +

+

+ Render pages with mock data. This makes it easy to build and review page states without + needing to navigate to them in your app. Here are some handy patterns for managing page + data in Storybook: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at{' '} + + Storybook tutorials + + . Read more in the{' '} + + docs + + . +

+
+ Tip Adjust the width of the canvas with the{' '} + + + + + + Viewports addon in the toolbar +
+
+
+ ); +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/next-env.d.ts b/code/frameworks/experimental-nextjs-vite/template/next-env.d.ts new file mode 100644 index 000000000000..77e567dab0a2 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/next-env.d.ts @@ -0,0 +1,7 @@ +// Reference necessary since Next.js 13.2.0, because types in `next/navigation` are not exported per default, but +// type references are dynamically created during Next.js start up. +// See https://github.com/vercel/next.js/commit/cdf1d52d9aed42d01a46539886a4bda14cb77a99 +// for more insights. + +/// +/// diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/DynamicImport.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/DynamicImport.stories.tsx new file mode 100644 index 000000000000..f6b5e2c99f3b --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/DynamicImport.stories.tsx @@ -0,0 +1,27 @@ +import React, { Suspense } from 'react'; + +import type { Meta, StoryObj } from '@storybook/react'; + +import dynamic from 'next/dynamic'; + +const DynamicComponent = dynamic(() => import('./DynamicImport'), { + ssr: false, +}); + +function Component() { + return ( + + + + ); +} + +const meta = { + component: Component, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/DynamicImport.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/DynamicImport.tsx new file mode 100644 index 000000000000..4863633033f3 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/DynamicImport.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function DynamicComponent() { + return
I am a dynamically loaded component
; +} diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/Font.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/Font.stories.tsx new file mode 100644 index 000000000000..32db81dcb67d --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/Font.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Font from './Font'; + +const meta = { + component: Font, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const WithClassName: Story = { + args: { + variant: 'className', + }, +}; + +export const WithStyle: Story = { + args: { + variant: 'style', + }, +}; + +export const WithVariable: Story = { + args: { + variant: 'variable', + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/Font.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/Font.tsx new file mode 100644 index 000000000000..cd8e83ef3dc5 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/Font.tsx @@ -0,0 +1,67 @@ +import React from 'react'; + +import { Rubik_Puddles } from 'next/font/google'; +import localFont from 'next/font/local'; + +const rubik = Rubik_Puddles({ + subsets: ['latin'], + variable: '--font-latin-rubik', + weight: '400', +}); + +export const localRubikStorm = localFont({ + src: '/fonts/RubikStorm-Regular.ttf', + variable: '--font-rubik-storm', +}); + +type FontProps = { + variant: 'className' | 'style' | 'variable'; +}; + +export default function Font({ variant }: FontProps) { + switch (variant) { + case 'className': + return ( +
+

Google Rubik Puddles

+

Google Local Rubik Storm

+
+ ); + case 'style': + return ( +
+

Google Rubik Puddles

+

Google Local Rubik Storm

+
+ ); + case 'variable': + return ( +
+
+

+ Google Rubik Puddles +

+
+
+

+ Google Local Rubik Storm +

+
+
+ ); + default: + return null; + } +} diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/GetImageProps.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/GetImageProps.stories.tsx new file mode 100644 index 000000000000..d4ad15ab240f --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/GetImageProps.stories.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import type { Meta, StoryObj } from '@storybook/react'; + +import { type ImageProps, getImageProps } from 'next/image'; + +import Accessibility from '../../assets/accessibility.svg'; +import Testing from '../../assets/testing.png'; + +// referenced from https://nextjs.org/docs/pages/api-reference/components/image#theme-detection-picture +const Component = (props: Omit) => { + const { + props: { srcSet: dark }, + } = getImageProps({ src: Accessibility, ...props }); + const { + // capture rest on one to spread to img as default; it doesn't matter which barring art direction + props: { srcSet: light, ...rest }, + } = getImageProps({ src: Testing, ...props }); + + return ( + + + + + + ); +}; + +const meta = { + component: Component, + args: { + alt: 'getImageProps Example', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/Head.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/Head.stories.tsx new file mode 100644 index 000000000000..db1b747bf78d --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/Head.stories.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import type { Meta } from '@storybook/react'; +import type { StoryObj } from '@storybook/react'; +import { expect, waitFor } from '@storybook/test'; + +import Head from 'next/head'; + +function Component() { + return ( +
+ + Next.js Head Title + + + + + +

Hello world!

+
+ ); +} + +const meta = { + component: Component, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + play: async () => { + await waitFor(() => expect(document.title).toEqual('Next.js Head Title')); + await expect(document.querySelectorAll('meta[property="og:title"]')).toHaveLength(1); + await expect((document.querySelector('meta[property="og:title"]') as any).content).toEqual( + 'My new title' + ); + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/Image.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/Image.stories.tsx new file mode 100644 index 000000000000..d9efdacdae80 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/Image.stories.tsx @@ -0,0 +1,110 @@ +import React, { useRef, useState } from 'react'; + +import type { Meta, StoryObj } from '@storybook/react'; + +import Image from 'next/image'; + +import Accessibility from '../../assets/accessibility.svg'; +import AvifImage from '../../assets/avif-test-image.avif'; + +const meta = { + component: Image, + args: { + src: Accessibility, + alt: 'Accessibility', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Avif: Story = { + args: { + src: AvifImage, + alt: 'Avif Test Image', + }, +}; + +export const BlurredPlaceholder: Story = { + args: { + placeholder: 'blur', + }, +}; + +export const BlurredAbsolutePlaceholder: Story = { + args: { + src: 'https://storybook.js.org/images/placeholders/50x50.png', + width: 50, + height: 50, + blurDataURL: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAABP5JREFUWEeNlwtz2zgMhEGKsv/k9XFp82xe11763yOJVGcXC4m2czM3GYa0E2s/LACSTi+vv1czM/7CvPpqxY/ejPeS3khmFiPHOiVLHKaZi4ux4j18GpMlS6cALupEQBCKQM4BdnGzjIcJgs//QBxAPQAem55f3yL4PWJIdyCyhlMfPdYZot0cwj3Ayg/5JwHA13paen7pADphxr/n5VI8JQsHCCGQ3gVGLLsxQ3h/LYSn5383B05rwNOws3Z576LOTOduvwfrOd5FtVat4akx0uPTrw8BNuUz23vLsY7hmg7i4ipqM2saiAdruNuirh4ff0bNdb3Qy3vkvfAQwrkHoDxTTZtDCOKrC1bMEkdnsQh/PLyetOGHkXeRAgQAQ84efQcBdUhvFofoulpdm9WGGTA+AJEe7l+i37a2c371tCFKs5zzJjxQNBMi1im7OCudP2aNghJuzZaGdSMEZjpwf/t0UgNdg9DOyLGLnY0BUHlzwVtNDkgEQhBeKkb1tUDgQrq7frwAiIJi5BKAeIFgHk5mOpPzvgltOfcoK0Rrs7lWHwsgqtXarK3N0u23h5Ne8+3Cqxn5RYSMfHCAMgDAx4CBWlA9RAGw0GA/ol0gvFB4WjAvBAFUa83SzdUdAbYMqp28uHpxCRefxwAYhksAFBlthxCiXig+zT4TYqkC+Hq7OdAfJv8lPpZiZShWBBIuRP+jspDb2lwcDkzz7OLzbO/zvAHAoXTz5eYMQL0t2yHAiCFcfPY1QDwNFylA5bPoFpsV9fsEiMl8dhcc4PP1CYD3drYcBYdIKQrx0cbRxd2JHSDcQ297/vvoZ5smRC+AyV2AQ+nm03evge08Tyy4jGqXzWWEoIvTgXHU38pWiNgH4ixB/ukAcy/xycXfp4kwdAAAt399W+OCgMjxILQacxvRQ3gEwHgKUIr/rz53CuDFNyP/Eob4+/vEWkBq6AAA/HIi62n/Lk67Q7wDYQ0UpQB7hc54T4E6gACLTYxeAwB0YKZL6U4ATEGIBwCs7qPfQJCCHkCnoK50noJKcXcAojsEAJZZKXhgCoziGKxqWV8IMNp4kP2aC+oB0TMFvhGxDQHQfIPhDrilwKOm/YCZASAHfgBABQjr3f7CyAkA0cPB03AQULRhKd4xAIjzHymo2Gp7gN0FAMAVOoA2fPz03a9ssh/RM7Iz8QKIzYF9HyB0XEZ1xJ4DzNoDOAfAslhDDTyjDfv8A2AcBeCiu/jBHQEgxnYW6Kp6BlCVAkQM8VnieF2Xyr0ivXy+XvsCzKOihwNHCCryw8HrQXVB8dgFeRfAVQiXjMbIIgXINQYB2H7Kf5wF/2Ar7h0AgKKGuAP4zOjhzlkLbpcRXKRZhNUjxG6HIQDOjN47gCn4+fWW3xVS9urPESEEwwHMo9IhAGxS2ISiA1iEnQOoA4hXRAwItp7WzL9Ow18ESJaw/ar4NgeOR49cAHCAnaH8swBhv+6CBGjeBSxEOUAI7HyKHkD4O9xKb3/feQouAI4uLBciHRRHmgbfA7h/xFc9AngNBADthvii1sMOiPwDAFeyt6s7FSFS4PmnA1v0vQvqDqQKAAPE/weAUuEgsj8c+H11Twdw/AKANXA82EDr5cJBEEzB3oI4Mb0AdR3nNw8vQnegWuvqAABwJFJEBwDgNdA7IOs3gL0LhuJdwBY8c4BfNnDdVgooHiOqn/b7JoSW/QODjTHXhU7hMQAAAABJRU5ErkJggg==', + placeholder: 'blur', + }, + parameters: { + // ignoring in Chromatic to avoid inconsistent snapshots + // given that the switch from blur to image is quite fast + chromatic: { disableSnapshot: true }, + }, +}; + +export const FilledParent: Story = { + args: { + fill: true, + }, + decorators: [ + (Story) =>
{Story()}
, + ], +}; + +export const Sized: Story = { + args: { + fill: true, + sizes: '(max-width: 600px) 100vw, 600px', + }, + decorators: [ + (Story) =>
{Story()}
, + ], +}; + +export const Lazy: Story = { + args: { + src: 'https://storybook.js.org/images/placeholders/50x50.png', + width: 50, + height: 50, + }, + decorators: [ + (Story) => ( + <> +
+ {Story()} + + ), + ], +}; + +export const Eager: Story = { + ...Lazy, + parameters: { + nextjs: { + image: { + loading: 'eager', + }, + }, + }, +}; + +export const WithRef: Story = { + render() { + const [ref, setRef] = useState(null); + + return ( +
+ Accessibility +

Alt attribute of image: {ref?.alt}

+
+ ); + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/ImageLegacy.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/ImageLegacy.stories.tsx new file mode 100644 index 000000000000..61e61b916cbe --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/ImageLegacy.stories.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import Image from 'next/legacy/image'; + +import Accessibility from '../../assets/accessibility.svg'; + +export default { + component: Image, + args: { + src: Accessibility, + alt: 'Accessibility', + }, +}; + +export const Default = {}; + +export const BlurredPlaceholder = { + args: { + placeholder: 'blur', + }, +}; + +export const BlurredAbsolutePlaceholder = { + args: { + src: 'https://storybook.js.org/images/placeholders/50x50.png', + width: 50, + height: 50, + blurDataURL: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAABP5JREFUWEeNlwtz2zgMhEGKsv/k9XFp82xe11763yOJVGcXC4m2czM3GYa0E2s/LACSTi+vv1czM/7CvPpqxY/ejPeS3khmFiPHOiVLHKaZi4ux4j18GpMlS6cALupEQBCKQM4BdnGzjIcJgs//QBxAPQAem55f3yL4PWJIdyCyhlMfPdYZot0cwj3Ayg/5JwHA13paen7pADphxr/n5VI8JQsHCCGQ3gVGLLsxQ3h/LYSn5383B05rwNOws3Z576LOTOduvwfrOd5FtVat4akx0uPTrw8BNuUz23vLsY7hmg7i4ipqM2saiAdruNuirh4ff0bNdb3Qy3vkvfAQwrkHoDxTTZtDCOKrC1bMEkdnsQh/PLyetOGHkXeRAgQAQ84efQcBdUhvFofoulpdm9WGGTA+AJEe7l+i37a2c371tCFKs5zzJjxQNBMi1im7OCudP2aNghJuzZaGdSMEZjpwf/t0UgNdg9DOyLGLnY0BUHlzwVtNDkgEQhBeKkb1tUDgQrq7frwAiIJi5BKAeIFgHk5mOpPzvgltOfcoK0Rrs7lWHwsgqtXarK3N0u23h5Ne8+3Cqxn5RYSMfHCAMgDAx4CBWlA9RAGw0GA/ol0gvFB4WjAvBAFUa83SzdUdAbYMqp28uHpxCRefxwAYhksAFBlthxCiXig+zT4TYqkC+Hq7OdAfJv8lPpZiZShWBBIuRP+jspDb2lwcDkzz7OLzbO/zvAHAoXTz5eYMQL0t2yHAiCFcfPY1QDwNFylA5bPoFpsV9fsEiMl8dhcc4PP1CYD3drYcBYdIKQrx0cbRxd2JHSDcQ297/vvoZ5smRC+AyV2AQ+nm03evge08Tyy4jGqXzWWEoIvTgXHU38pWiNgH4ixB/ukAcy/xycXfp4kwdAAAt399W+OCgMjxILQacxvRQ3gEwHgKUIr/rz53CuDFNyP/Eob4+/vEWkBq6AAA/HIi62n/Lk67Q7wDYQ0UpQB7hc54T4E6gACLTYxeAwB0YKZL6U4ATEGIBwCs7qPfQJCCHkCnoK50noJKcXcAojsEAJZZKXhgCoziGKxqWV8IMNp4kP2aC+oB0TMFvhGxDQHQfIPhDrilwKOm/YCZASAHfgBABQjr3f7CyAkA0cPB03AQULRhKd4xAIjzHymo2Gp7gN0FAMAVOoA2fPz03a9ssh/RM7Iz8QKIzYF9HyB0XEZ1xJ4DzNoDOAfAslhDDTyjDfv8A2AcBeCiu/jBHQEgxnYW6Kp6BlCVAkQM8VnieF2Xyr0ivXy+XvsCzKOihwNHCCryw8HrQXVB8dgFeRfAVQiXjMbIIgXINQYB2H7Kf5wF/2Ar7h0AgKKGuAP4zOjhzlkLbpcRXKRZhNUjxG6HIQDOjN47gCn4+fWW3xVS9urPESEEwwHMo9IhAGxS2ISiA1iEnQOoA4hXRAwItp7WzL9Ow18ESJaw/ar4NgeOR49cAHCAnaH8swBhv+6CBGjeBSxEOUAI7HyKHkD4O9xKb3/feQouAI4uLBciHRRHmgbfA7h/xFc9AngNBADthvii1sMOiPwDAFeyt6s7FSFS4PmnA1v0vQvqDqQKAAPE/weAUuEgsj8c+H11Twdw/AKANXA82EDr5cJBEEzB3oI4Mb0AdR3nNw8vQnegWuvqAABwJFJEBwDgNdA7IOs3gL0LhuJdwBY8c4BfNnDdVgooHiOqn/b7JoSW/QODjTHXhU7hMQAAAABJRU5ErkJggg==', + placeholder: 'blur', + }, + parameters: { + // ignoring in Chromatic to avoid inconsistent snapshots + // given that the switch from blur to image is quite fast + chromatic: { disableSnapshot: true }, + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/Link.stories.module.css b/code/frameworks/experimental-nextjs-vite/template/stories/Link.stories.module.css new file mode 100644 index 000000000000..9edb616226d0 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/Link.stories.module.css @@ -0,0 +1,3 @@ +.link { + color: green; +} diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/Link.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/Link.stories.tsx new file mode 100644 index 000000000000..7c1aa2073ab6 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/Link.stories.tsx @@ -0,0 +1,87 @@ +import React from 'react'; + +import type { Meta, StoryObj } from '@storybook/react'; + +import Link from 'next/link'; + +import style from './Link.stories.module.css'; + +// `onClick`, `href`, and `ref` need to be passed to the DOM element +// for proper handling +const MyButton = React.forwardRef< + HTMLAnchorElement, + React.DetailedHTMLProps, HTMLAnchorElement> +>(function Button({ onClick, href, children }, ref) { + return ( + + {children} + + ); +}); + +const Component = () => ( +
    +
  • + Normal Link +
  • +
  • + + With URL Object + +
  • +
  • + + Replace the URL instead of push + +
  • +
  • + + Legacy behavior + +
  • +
  • + + child is a functional component + +
  • +
  • + + Disables scrolling to the top + +
  • +
  • + + No Prefetching + +
  • +
  • + + With style + +
  • +
  • + + With className + +
  • +
+); + +export default { + component: Component, +} as Meta; + +export const Default: StoryObj = {}; + +export const InAppDir: StoryObj = { + parameters: { + nextjs: { + appDirectory: true, + }, + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/Navigation.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/Navigation.stories.tsx new file mode 100644 index 000000000000..d50ed5174d25 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/Navigation.stories.tsx @@ -0,0 +1,154 @@ +import React from 'react'; + +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent, within } from '@storybook/test'; + +import { getRouter } from '@storybook/experimental-nextjs-vite/navigation.mock'; + +import { + useParams, + usePathname, + useRouter, + useSearchParams, + useSelectedLayoutSegment, + useSelectedLayoutSegments, +} from 'next/navigation'; + +function Component() { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const params = useParams(); + const segment = useSelectedLayoutSegment(); + const segments = useSelectedLayoutSegments(); + + const searchParamsList = searchParams ? Array.from(searchParams.entries()) : []; + + const routerActions = [ + { + cb: () => router.back(), + name: 'Go back', + }, + { + cb: () => router.forward(), + name: 'Go forward', + }, + { + cb: () => router.prefetch('/prefetched-html'), + name: 'Prefetch', + }, + { + // @ts-expect-error (old API) + cb: () => router.push('/push-html', { forceOptimisticNavigation: true }), + name: 'Push HTML', + }, + { + cb: () => router.refresh(), + name: 'Refresh', + }, + { + // @ts-expect-error (old API) + cb: () => router.replace('/replaced-html', { forceOptimisticNavigation: true }), + name: 'Replace', + }, + ]; + + return ( +
+
pathname: {pathname}
+
segment: {segment}
+
segments: {segments.join(',')}
+
+ searchparams:{' '} +
    + {searchParamsList.map(([key, value]) => ( +
  • + {key}: {value} +
  • + ))} +
+
+
+ params:{' '} +
    + {Object.entries(params).map(([key, value]) => ( +
  • + {key}: {value} +
  • + ))} +
+
+ {routerActions.map(({ cb, name }) => ( +
+ +
+ ))} +
+ ); +} + +type Story = StoryObj; + +export default { + component: Component, + parameters: { + nextjs: { + appDirectory: true, + navigation: { + pathname: '/hello', + query: { + foo: 'bar', + }, + prefetch: () => { + console.log('custom prefetch'); + }, + }, + }, + }, +} as Meta; + +export const Default: StoryObj = { + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + const routerMock = getRouter(); + + await step('Asserts whether forward hook is called', async () => { + const forwardBtn = await canvas.findByText('Go forward'); + await userEvent.click(forwardBtn); + await expect(routerMock.forward).toHaveBeenCalled(); + }); + + await step('Asserts whether custom prefetch hook is called', async () => { + const prefetchBtn = await canvas.findByText('Prefetch'); + await userEvent.click(prefetchBtn); + await expect(routerMock.prefetch).toHaveBeenCalledWith('/prefetched-html'); + }); + }, +}; + +export const WithSegmentDefined: Story = { + parameters: { + nextjs: { + appDirectory: true, + navigation: { + segments: ['dashboard', 'settings'], + }, + }, + }, +}; + +export const WithSegmentDefinedForParams: Story = { + parameters: { + nextjs: { + appDirectory: true, + navigation: { + segments: [ + ['slug', 'hello'], + ['framework', 'nextjs'], + ], + }, + }, + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/NextHeader.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/NextHeader.stories.tsx new file mode 100644 index 000000000000..c0ec7f1bbba9 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/NextHeader.stories.tsx @@ -0,0 +1,48 @@ +import type { Meta } from '@storybook/react'; +import type { StoryObj } from '@storybook/react'; +import { expect, userEvent, within } from '@storybook/test'; + +import { cookies, headers } from '@storybook/experimental-nextjs-vite/headers.mock'; + +import NextHeader from './NextHeader'; + +export default { + component: NextHeader, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + loaders: async () => { + cookies().set('firstName', 'Jane'); + cookies().set({ + name: 'lastName', + value: 'Doe', + }); + headers().set('timezone', 'Central European Summer Time'); + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + const headersMock = headers(); + const cookiesMock = cookies(); + await step('Cookie and header store apis are called upon rendering', async () => { + await expect(cookiesMock.getAll).toHaveBeenCalled(); + await expect(headersMock.entries).toHaveBeenCalled(); + }); + + await step('Upon clicking on submit, the user-id cookie is set', async () => { + const submitButton = await canvas.findByRole('button'); + await userEvent.click(submitButton); + + await expect(cookiesMock.set).toHaveBeenCalledWith('user-id', 'encrypted-id'); + }); + + await step('The user-id cookie is available in cookie and header stores', async () => { + await expect(headersMock.get('cookie')).toContain('user-id=encrypted-id'); + await expect(cookiesMock.get('user-id')).toEqual({ + name: 'user-id', + value: 'encrypted-id', + }); + }); + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/NextHeader.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/NextHeader.tsx new file mode 100644 index 000000000000..6189f84baa62 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/NextHeader.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import { cookies, headers } from 'next/headers'; + +export default async function Component() { + async function handleClick() { + 'use server'; + cookies().set('user-id', 'encrypted-id'); + } + + return ( + <> +

Cookies:

+ {cookies() + .getAll() + .map(({ name, value }) => { + return ( +

+ Name: {name} + Value: {value} +

+ ); + })} + +

Headers:

+ {Array.from(headers().entries()).map(([name, value]: [string, string]) => { + return ( +

+ Name: {name} + Value: {value} +

+ ); + })} + +
+ +
+ + ); +} diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/RSC.jsx b/code/frameworks/experimental-nextjs-vite/template/stories/RSC.jsx new file mode 100644 index 000000000000..a5771a6a9202 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/RSC.jsx @@ -0,0 +1,7 @@ +import React from 'react'; + +import 'server-only'; + +export const RSC = async ({ label }) => <>RSC {label}; + +export const Nested = async ({ children }) => <>Nested {children}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/RSC.stories.jsx b/code/frameworks/experimental-nextjs-vite/template/stories/RSC.stories.jsx new file mode 100644 index 000000000000..1847c024379c --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/RSC.stories.jsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import { Nested, RSC } from './RSC'; + +export default { + component: RSC, + args: { label: 'label' }, +}; + +export const Default = {}; + +export const DisableRSC = { + tags: ['!test'], + parameters: { + chromatic: { disable: true }, + nextjs: { rsc: false }, + }, +}; + +export const Error = { + tags: ['!test'], + parameters: { + chromatic: { disable: true }, + }, + render: () => { + throw new Error('RSC Error'); + }, +}; + +export const NestedRSC = { + render: (args) => ( + + + + ), +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/Redirect.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/Redirect.stories.tsx new file mode 100644 index 000000000000..3c5980b79757 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/Redirect.stories.tsx @@ -0,0 +1,57 @@ +import React from 'react'; + +import type { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/test'; + +import { redirect } from 'next/navigation'; + +let state = 'Bug! Not invalidated'; + +export default { + render() { + return ( +
+
{state}
+
{ + state = 'State is invalidated successfully.'; + redirect('/'); + }} + > + +
+
+ ); + }, + parameters: { + test: { + // This is needed until Next will update to the React 19 beta: https://github.com/vercel/next.js/pull/65058 + // In the React 19 beta ErrorBoundary errors (such as redirect) are only logged, and not thrown. + // We will also suspress console.error logs for re the console.error logs for redirect in the next framework. + // Using the onCaughtError react root option: + // react: { + // rootOptions: { + // onCaughtError(error: unknown) { + // if (isNextRouterError(error)) return; + // console.error(error); + // }, + // }, + // See: code/frameworks/nextjs/src/preview.tsx + dangerouslyIgnoreUnhandledErrors: true, + }, + nextjs: { + appDirectory: true, + navigation: { + pathname: '/', + }, + }, + }, + tags: ['!test'], +} as Meta; + +export const SingletonStateGetsInvalidatedAfterRedirecting: StoryObj = { + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByRole('button')); + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/Router.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/Router.stories.tsx new file mode 100644 index 000000000000..7b1d5b0ec0c9 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/Router.stories.tsx @@ -0,0 +1,111 @@ +import React from 'react'; + +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent, within } from '@storybook/test'; + +import { getRouter } from '@storybook/experimental-nextjs-vite/router.mock'; + +import Router, { useRouter } from 'next/router'; + +function Component() { + const router = useRouter(); + const searchParams = router.query; + + const routerActions = [ + { + cb: () => router.back(), + name: 'Go back', + }, + { + cb: () => router.forward(), + name: 'Go forward', + }, + { + cb: () => router.prefetch('/prefetched-html'), + name: 'Prefetch', + }, + { + // @ts-expect-error (old API) + cb: () => router.push('/push-html', { forceOptimisticNavigation: true }), + name: 'Push HTML', + }, + { + // @ts-expect-error (old API) + cb: () => router.replace('/replaced-html', { forceOptimisticNavigation: true }), + name: 'Replace', + }, + ]; + + return ( +
+
Router pathname: {Router.pathname}
+
pathname: {router.pathname}
+
+ searchparams:{' '} +
    + {Object.entries(searchParams).map(([key, value]) => ( +
  • + {key}: {value} +
  • + ))} +
+
+ {routerActions.map(({ cb, name }) => ( +
+ +
+ ))} +
+ ); +} + +export default { + component: Component, + parameters: { + nextjs: { + router: { + pathname: '/hello', + query: { + foo: 'bar', + }, + prefetch: () => { + console.log('custom prefetch'); + }, + }, + }, + }, +} as Meta; + +export const Default: StoryObj = { + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + const routerMock = getRouter(); + + await step('Router property overrides should be available in useRouter fn', async () => { + await expect(Router.pathname).toBe('/hello'); + await expect(Router.query).toEqual({ foo: 'bar' }); + }); + + await step( + 'Router property overrides should be available in default export from next/router', + async () => { + await expect(Router.pathname).toBe('/hello'); + await expect(Router.query).toEqual({ foo: 'bar' }); + } + ); + + await step('Asserts whether forward hook is called', async () => { + const forwardBtn = await canvas.findByText('Go forward'); + await userEvent.click(forwardBtn); + await expect(routerMock.forward).toHaveBeenCalled(); + }); + + await step('Asserts whether custom prefetch hook is called', async () => { + const prefetchBtn = await canvas.findByText('Prefetch'); + await userEvent.click(prefetchBtn); + await expect(routerMock.prefetch).toHaveBeenCalledWith('/prefetched-html'); + }); + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/ServerActions.stories.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/ServerActions.stories.tsx new file mode 100644 index 000000000000..944bc42d8667 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/ServerActions.stories.tsx @@ -0,0 +1,117 @@ +import React from 'react'; + +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent, waitFor, within } from '@storybook/test'; + +import { revalidatePath } from '@storybook/experimental-nextjs-vite/cache.mock'; +import { cookies } from '@storybook/experimental-nextjs-vite/headers.mock'; +import { getRouter, redirect } from '@storybook/experimental-nextjs-vite/navigation.mock'; + +import { accessRoute, login, logout } from './ServerActions'; + +function Component() { + return ( +
+
+ +
+
+ +
+
+ +
+
+ ); +} + +export default { + component: Component, + tags: ['!test'], + parameters: { + nextjs: { + appDirectory: true, + navigation: { + pathname: '/', + }, + }, + test: { + // This is needed until Next will update to the React 19 beta: https://github.com/vercel/next.js/pull/65058 + // In the React 19 beta ErrorBoundary errors (such as redirect) are only logged, and not thrown. + // We will also suspress console.error logs for re the console.error logs for redirect in the next framework. + // Using the onCaughtError react root option: + // react: { + // rootOptions: { + // onCaughtError(error: unknown) { + // if (isNextRouterError(error)) return; + // console.error(error); + // }, + // }, + // See: code/frameworks/nextjs/src/preview.tsx + dangerouslyIgnoreUnhandledErrors: true, + }, + }, +} as Meta; + +export const ProtectedWhileLoggedOut: StoryObj = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByText('Access protected route')); + + await expect(cookies().get).toHaveBeenCalledWith('user'); + await expect(redirect).toHaveBeenCalledWith('/'); + + await waitFor(() => expect(getRouter().push).toHaveBeenCalled()); + }, +}; + +export const ProtectedWhileLoggedIn: StoryObj = { + beforeEach() { + cookies().set('user', 'storybookjs'); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByText('Access protected route')); + + await expect(cookies().get).toHaveBeenLastCalledWith('user'); + await expect(revalidatePath).toHaveBeenLastCalledWith('/'); + await expect(redirect).toHaveBeenLastCalledWith('/protected'); + + await waitFor(() => expect(getRouter().push).toHaveBeenCalled()); + }, +}; + +export const Logout: StoryObj = { + beforeEach() { + cookies().set('user', 'storybookjs'); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await userEvent.click(canvas.getByText('Logout')); + await expect(cookies().delete).toHaveBeenCalled(); + await expect(revalidatePath).toHaveBeenCalledWith('/'); + await expect(redirect).toHaveBeenCalledWith('/'); + + await waitFor(() => expect(getRouter().push).toHaveBeenCalled()); + }, +}; + +export const Login: StoryObj = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByText('Login')); + + await expect(cookies().set).toHaveBeenCalledWith('user', 'storybookjs'); + await expect(revalidatePath).toHaveBeenCalledWith('/'); + await expect(redirect).toHaveBeenCalledWith('/'); + + await waitFor(() => expect(getRouter().push).toHaveBeenCalled()); + }, +}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/ServerActions.tsx b/code/frameworks/experimental-nextjs-vite/template/stories/ServerActions.tsx new file mode 100644 index 000000000000..5e1b3c7227dc --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/ServerActions.tsx @@ -0,0 +1,28 @@ +'use server'; + +import { revalidatePath } from 'next/cache'; +import { cookies } from 'next/headers'; +import { redirect } from 'next/navigation'; + +export async function accessRoute() { + const user = cookies().get('user'); + + if (!user) { + redirect('/'); + } + + revalidatePath('/'); + redirect(`/protected`); +} + +export async function logout() { + cookies().delete('user'); + revalidatePath('/'); + redirect('/'); +} + +export async function login() { + cookies().set('user', 'storybookjs'); + revalidatePath('/'); + redirect('/'); +} diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/StyledJsx.stories.jsx b/code/frameworks/experimental-nextjs-vite/template/stories/StyledJsx.stories.jsx new file mode 100644 index 000000000000..5a0c586e232c --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/StyledJsx.stories.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const Component = () => ( +
+ +
+

This is styled using Styled JSX

+
+
+); + +export default { + component: Component, +}; + +export const Default = {}; diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/fonts/OFL.txt b/code/frameworks/experimental-nextjs-vite/template/stories/fonts/OFL.txt new file mode 100644 index 000000000000..36d2f6f3febb --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/stories/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Rubik Filtered Project Authors (https://https://github.com/NaN-xyz/Rubik-Filtered) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/code/frameworks/experimental-nextjs-vite/template/stories/fonts/RubikStorm-Regular.ttf b/code/frameworks/experimental-nextjs-vite/template/stories/fonts/RubikStorm-Regular.ttf new file mode 100644 index 000000000000..2304ee84a07b Binary files /dev/null and b/code/frameworks/experimental-nextjs-vite/template/stories/fonts/RubikStorm-Regular.ttf differ diff --git a/code/frameworks/experimental-nextjs-vite/template/typings.d.ts b/code/frameworks/experimental-nextjs-vite/template/typings.d.ts new file mode 100644 index 000000000000..b8b55169fef8 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/template/typings.d.ts @@ -0,0 +1,14 @@ +declare module '*.svg' { + const content: string; + export default content; +} + +declare module '*.avif' { + const content: string; + export default content; +} + +declare module '*.png' { + const content: string; + export default content; +} diff --git a/code/frameworks/experimental-nextjs-vite/tsconfig.json b/code/frameworks/experimental-nextjs-vite/tsconfig.json new file mode 100644 index 000000000000..3b01f80f2c32 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": {}, + "include": ["src/**/*", "template/**/*"] +} diff --git a/code/frameworks/experimental-nextjs-vite/vitest.config.ts b/code/frameworks/experimental-nextjs-vite/vitest.config.ts new file mode 100644 index 000000000000..edf3cc3ea035 --- /dev/null +++ b/code/frameworks/experimental-nextjs-vite/vitest.config.ts @@ -0,0 +1,11 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { defineConfig, mergeConfig } from 'vitest/config'; + +import { vitestCommonConfig } from '../../vitest.workspace'; + +export default mergeConfig( + vitestCommonConfig, + defineConfig({ + // Add custom config here + }) +); diff --git a/code/frameworks/html-vite/src/preset.ts b/code/frameworks/html-vite/src/preset.ts index 710a3f8688df..57c18a236cca 100644 --- a/code/frameworks/html-vite/src/preset.ts +++ b/code/frameworks/html-vite/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { dirname, join } from 'node:path'; -import { dirname, join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; function getAbsolutePath(value: I): I { return dirname(require.resolve(join(value, 'package.json'))) as any; diff --git a/code/frameworks/html-webpack5/src/preset.ts b/code/frameworks/html-webpack5/src/preset.ts index 96a7ace0ae13..d2e3e75d1c18 100644 --- a/code/frameworks/html-webpack5/src/preset.ts +++ b/code/frameworks/html-webpack5/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { dirname, join } from 'node:path'; -import { dirname, join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; const getAbsolutePath = (input: I): I => dirname(require.resolve(join(input, 'package.json'))) as any; diff --git a/code/frameworks/nextjs/src/babel/plugins/react-loadable-plugin.ts b/code/frameworks/nextjs/src/babel/plugins/react-loadable-plugin.ts index 01a0ed704393..3c075d437437 100644 --- a/code/frameworks/nextjs/src/babel/plugins/react-loadable-plugin.ts +++ b/code/frameworks/nextjs/src/babel/plugins/react-loadable-plugin.ts @@ -5,8 +5,9 @@ /** * Source: https://github.com/vercel/next.js/blob/canary/packages/next/src/build/babel/plugins/react-loadable-plugin.ts */ +import { relative as relativePath } from 'node:path'; + import type { types as BabelTypes, NodePath, PluginObj } from '@babel/core'; -import { relative as relativePath } from 'path'; export default function ({ types: t }: { types: typeof BabelTypes }): PluginObj { return { diff --git a/code/frameworks/nextjs/src/babel/preset.ts b/code/frameworks/nextjs/src/babel/preset.ts index 1b48e4a663c0..b4b7a465bacf 100644 --- a/code/frameworks/nextjs/src/babel/preset.ts +++ b/code/frameworks/nextjs/src/babel/preset.ts @@ -1,5 +1,6 @@ +import { dirname } from 'node:path'; + import type { PluginItem } from '@babel/core'; -import { dirname } from 'path'; const isLoadIntentTest = process.env.NODE_ENV === 'test'; const isLoadIntentDevelopment = process.env.NODE_ENV === 'development'; diff --git a/code/frameworks/nextjs/src/export-mocks/webpack.ts b/code/frameworks/nextjs/src/export-mocks/webpack.ts index 7c96a0df7724..3d054d114ed2 100644 --- a/code/frameworks/nextjs/src/export-mocks/webpack.ts +++ b/code/frameworks/nextjs/src/export-mocks/webpack.ts @@ -1,4 +1,5 @@ -import { dirname, join } from 'path'; +import { dirname, join } from 'node:path'; + import type { Configuration as WebpackConfig } from 'webpack'; import { getCompatibilityAliases } from '../compatibility/compatibility-map'; diff --git a/code/frameworks/nextjs/src/font/webpack/loader/local/get-font-face-declarations.ts b/code/frameworks/nextjs/src/font/webpack/loader/local/get-font-face-declarations.ts index fb5fc89cb01e..884602b77e64 100644 --- a/code/frameworks/nextjs/src/font/webpack/loader/local/get-font-face-declarations.ts +++ b/code/frameworks/nextjs/src/font/webpack/loader/local/get-font-face-declarations.ts @@ -1,4 +1,4 @@ -import path from 'node:path'; +import { dirname, join } from 'node:path'; import { getProjectRoot } from 'storybook/internal/common'; @@ -20,8 +20,8 @@ export async function getFontFaceDeclarations( // Parent folder relative to the root context const parentFolder = swcMode - ? path.dirname(path.join(getProjectRoot(), options.filename)).replace(rootContext, '') - : path.dirname(options.filename).replace(rootContext, ''); + ? dirname(join(getProjectRoot(), options.filename)).replace(rootContext, '') + : dirname(options.filename).replace(rootContext, ''); const { weight, @@ -43,7 +43,7 @@ export async function getFontFaceDeclarations( const getFontFaceCSS = () => { if (typeof localFontSrc === 'string') { - const localFontPath = path.join(parentFolder, localFontSrc).replaceAll('\\', '/'); + const localFontPath = join(parentFolder, localFontSrc).replaceAll('\\', '/'); return `@font-face { font-family: ${id}; @@ -53,7 +53,7 @@ export async function getFontFaceDeclarations( } return localFontSrc .map((font) => { - const localFontPath = path.join(parentFolder, font.path).replaceAll('\\', '/'); + const localFontPath = join(parentFolder, font.path).replaceAll('\\', '/'); return `@font-face { font-family: ${id}; diff --git a/code/frameworks/nextjs/src/font/webpack/loader/storybook-nextjs-font-loader.ts b/code/frameworks/nextjs/src/font/webpack/loader/storybook-nextjs-font-loader.ts index 06dcbb73f495..26be6ade3e97 100644 --- a/code/frameworks/nextjs/src/font/webpack/loader/storybook-nextjs-font-loader.ts +++ b/code/frameworks/nextjs/src/font/webpack/loader/storybook-nextjs-font-loader.ts @@ -1,4 +1,4 @@ -import path from 'path'; +import { sep } from 'node:path'; import { getFontFaceDeclarations as getGoogleFontFaceDeclarations } from './google/get-font-face-declarations'; import { getFontFaceDeclarations as getLocalFontFaceDeclarations } from './local/get-font-face-declarations'; @@ -41,7 +41,7 @@ export default async function storybookNextjsFontLoader(this: any) { let fontFaceDeclaration: FontFaceDeclaration | undefined; - const pathSep = path.sep; + const pathSep = sep; if ( options.source.endsWith(`next${pathSep}font${pathSep}google`) || diff --git a/code/frameworks/nextjs/src/images/webpack.ts b/code/frameworks/nextjs/src/images/webpack.ts index 5629a18c0d3b..dfcd1c2de6bf 100644 --- a/code/frameworks/nextjs/src/images/webpack.ts +++ b/code/frameworks/nextjs/src/images/webpack.ts @@ -1,5 +1,6 @@ +import { resolve as resolvePath } from 'node:path'; + import type { NextConfig } from 'next'; -import path from 'path'; import semver from 'semver'; import type { RuleSetRule, Configuration as WebpackConfig } from 'webpack'; @@ -18,14 +19,14 @@ const configureImageDefaults = (baseConfig: WebpackConfig): void => { resolve.alias = { ...resolve.alias, 'sb-original/next/image': require.resolve('next/image'), - 'next/image': path.resolve(__dirname, './images/next-image'), + 'next/image': resolvePath(__dirname, './images/next-image'), }; if (semver.satisfies(version, '>=13.0.0')) { resolve.alias = { ...resolve.alias, 'sb-original/next/legacy/image': require.resolve('next/legacy/image'), - 'next/legacy/image': path.resolve(__dirname, './images/next-legacy-image'), + 'next/legacy/image': resolvePath(__dirname, './images/next-legacy-image'), }; } }; diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index 37f6b702994d..1956b978a37a 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -1,12 +1,13 @@ // https://storybook.js.org/docs/react/addons/writing-presets +import { existsSync } from 'node:fs'; +import { dirname, join } from 'node:path'; + import { getProjectRoot } from 'storybook/internal/common'; import { logger } from 'storybook/internal/node-logger'; import type { PresetProperty } from 'storybook/internal/types'; import type { ConfigItem, PluginItem, TransformOptions } from '@babel/core'; import { loadPartialConfig } from '@babel/core'; -import fs from 'fs'; -import { dirname, join } from 'path'; import semver from 'semver'; import { configureAliases } from './aliases/webpack'; @@ -145,7 +146,7 @@ export const webpackFinal: StorybookConfig['webpackFinal'] = async (baseConfig, const babelRCPath = join(getProjectRoot(), '.babelrc'); const babelConfigPath = join(getProjectRoot(), 'babel.config.js'); - const hasBabelConfig = fs.existsSync(babelRCPath) || fs.existsSync(babelConfigPath); + const hasBabelConfig = existsSync(babelRCPath) || existsSync(babelConfigPath); const nextjsVersion = getNextjsVersion(); const isDevelopment = options.configType !== 'PRODUCTION'; diff --git a/code/frameworks/nextjs/src/swc/loader.ts b/code/frameworks/nextjs/src/swc/loader.ts index 23ce4456fdb2..68a9c05e5029 100644 --- a/code/frameworks/nextjs/src/swc/loader.ts +++ b/code/frameworks/nextjs/src/swc/loader.ts @@ -1,3 +1,5 @@ +import { join } from 'node:path'; + import { getProjectRoot } from 'storybook/internal/common'; import type { Options } from 'storybook/internal/types'; @@ -5,7 +7,6 @@ import { getVirtualModules } from '@storybook/builder-webpack5'; import type { NextConfig } from 'next'; import loadJsConfig from 'next/dist/build/load-jsconfig'; -import path from 'path'; import type { Configuration as WebpackConfig } from 'webpack'; export const configureSWCLoader = async ( @@ -49,7 +50,7 @@ export const configureSWCLoader = async ( dir, isDevelopment ), - swcCacheDir: path.join(dir, nextConfig?.distDir ?? '.next', 'cache', 'swc'), + swcCacheDir: join(dir, nextConfig?.distDir ?? '.next', 'cache', 'swc'), bundleTarget: 'default', }, }, diff --git a/code/frameworks/nextjs/src/swc/next-swc-loader-patch.ts b/code/frameworks/nextjs/src/swc/next-swc-loader-patch.ts index 84ed900ad599..bc7aa6b6a451 100644 --- a/code/frameworks/nextjs/src/swc/next-swc-loader-patch.ts +++ b/code/frameworks/nextjs/src/swc/next-swc-loader-patch.ts @@ -26,10 +26,11 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import { isAbsolute, relative } from 'node:path'; + import type { NextConfig } from 'next'; import { isWasm, transform } from 'next/dist/build/swc'; import { getLoaderSWCOptions } from 'next/dist/build/swc/options'; -import path, { isAbsolute } from 'path'; export interface SWCLoaderOptions { rootDir: string; @@ -71,7 +72,7 @@ async function loaderTransform(this: any, parentTrace: any, source?: string, inp isReactServerLayer, } = loaderOptions; const isPageFile = filename.startsWith(pagesDir); - const relativeFilePathFromRoot = path.relative(rootDir, filename); + const relativeFilePathFromRoot = relative(rootDir, filename); const swcOptions = getLoaderSWCOptions({ pagesDir, diff --git a/code/frameworks/nextjs/src/utils.ts b/code/frameworks/nextjs/src/utils.ts index ec0442ac1a6a..689462b20751 100644 --- a/code/frameworks/nextjs/src/utils.ts +++ b/code/frameworks/nextjs/src/utils.ts @@ -1,9 +1,10 @@ +import { dirname, resolve, sep } from 'node:path'; + import { getProjectRoot } from 'storybook/internal/common'; import type { NextConfig } from 'next'; import { PHASE_DEVELOPMENT_SERVER } from 'next/constants'; import loadConfig from 'next/dist/server/config'; -import path from 'path'; import { DefinePlugin } from 'webpack'; import type { Configuration as WebpackConfig } from 'webpack'; @@ -22,7 +23,7 @@ export const resolveNextConfig = async ({ }: { nextConfigPath?: string; }): Promise => { - const dir = nextConfigPath ? path.dirname(nextConfigPath) : getProjectRoot(); + const dir = nextConfigPath ? dirname(nextConfigPath) : getProjectRoot(); return loadConfig(PHASE_DEVELOPMENT_SERVER, dir, undefined); }; @@ -65,13 +66,13 @@ export const scopedResolve = (id: string): string => { try { // TODO: Remove in next major release (SB 8.0) and use the statement in the catch block per default instead - scopedModulePath = require.resolve(id, { paths: [path.resolve()] }); + scopedModulePath = require.resolve(id, { paths: [resolve()] }); } catch (e) { scopedModulePath = require.resolve(id); } const moduleFolderStrPosition = scopedModulePath.lastIndexOf( - id.replace(/\//g /* all '/' occurances */, path.sep) + id.replace(/\//g /* all '/' occurances */, sep) ); const beginningOfMainScriptPath = moduleFolderStrPosition + id.length; return scopedModulePath.substring(0, beginningOfMainScriptPath); diff --git a/code/frameworks/preact-vite/src/preset.ts b/code/frameworks/preact-vite/src/preset.ts index 13c1205cc211..18d7c604cf87 100644 --- a/code/frameworks/preact-vite/src/preset.ts +++ b/code/frameworks/preact-vite/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { dirname, join } from 'node:path'; -import { dirname, join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; import type { StorybookConfig } from './types'; diff --git a/code/frameworks/preact-webpack5/src/preset.ts b/code/frameworks/preact-webpack5/src/preset.ts index 9b7412622d14..e3c5e48087cc 100644 --- a/code/frameworks/preact-webpack5/src/preset.ts +++ b/code/frameworks/preact-webpack5/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { dirname, join } from 'node:path'; -import { dirname, join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; const getAbsolutePath = (input: I): I => dirname(require.resolve(join(input, 'package.json'))) as any; diff --git a/code/frameworks/react-vite/src/plugins/docgen-resolver.ts b/code/frameworks/react-vite/src/plugins/docgen-resolver.ts index ab0fda807cf8..f4ae37407c09 100644 --- a/code/frameworks/react-vite/src/plugins/docgen-resolver.ts +++ b/code/frameworks/react-vite/src/plugins/docgen-resolver.ts @@ -1,4 +1,5 @@ -import { extname } from 'path'; +import { extname } from 'node:path'; + import resolve from 'resolve'; export class ReactDocgenResolveError extends Error { diff --git a/code/frameworks/react-vite/src/plugins/react-docgen.ts b/code/frameworks/react-vite/src/plugins/react-docgen.ts index b3755064a425..7ec1f0b8df29 100644 --- a/code/frameworks/react-vite/src/plugins/react-docgen.ts +++ b/code/frameworks/react-vite/src/plugins/react-docgen.ts @@ -1,9 +1,10 @@ +import { relative } from 'node:path'; + import { logger } from 'storybook/internal/node-logger'; import { createFilter } from '@rollup/pluginutils'; import findUp from 'find-up'; import MagicString from 'magic-string'; -import path from 'path'; import type { Documentation } from 'react-docgen'; import { ERROR_CODES, @@ -59,7 +60,7 @@ export async function reactDocgen({ name: 'storybook:react-docgen-plugin', enforce: 'pre', async transform(src: string, id: string) { - if (!filter(path.relative(cwd, id))) { + if (!filter(relative(cwd, id))) { return; } diff --git a/code/frameworks/react-vite/src/preset.ts b/code/frameworks/react-vite/src/preset.ts index 29683e26cabc..cef1a270f33b 100644 --- a/code/frameworks/react-vite/src/preset.ts +++ b/code/frameworks/react-vite/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { dirname, join } from 'node:path'; -import { dirname, join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; import type { StorybookConfig } from './types'; diff --git a/code/frameworks/react-vite/src/utils.ts b/code/frameworks/react-vite/src/utils.ts index a33babd89b1a..a65c5103483c 100644 --- a/code/frameworks/react-vite/src/utils.ts +++ b/code/frameworks/react-vite/src/utils.ts @@ -1,12 +1,12 @@ -import fs from 'fs'; -import path from 'path'; +import { existsSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; export function readPackageJson(): Record | false { - const packageJsonPath = path.resolve('package.json'); - if (!fs.existsSync(packageJsonPath)) { + const packageJsonPath = resolve('package.json'); + if (!existsSync(packageJsonPath)) { return false; } - const jsonContent = fs.readFileSync(packageJsonPath, 'utf8'); + const jsonContent = readFileSync(packageJsonPath, 'utf8'); return JSON.parse(jsonContent); } diff --git a/code/frameworks/react-webpack5/src/preset.ts b/code/frameworks/react-webpack5/src/preset.ts index 340c214f3952..9e233459c10b 100644 --- a/code/frameworks/react-webpack5/src/preset.ts +++ b/code/frameworks/react-webpack5/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { dirname, join } from 'node:path'; -import { dirname, join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; import type { StorybookConfig } from './types'; diff --git a/code/frameworks/server-webpack5/src/preset.ts b/code/frameworks/server-webpack5/src/preset.ts index 3f3fc6bb1b6a..689a984336e1 100644 --- a/code/frameworks/server-webpack5/src/preset.ts +++ b/code/frameworks/server-webpack5/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { dirname, join } from 'node:path'; -import { dirname, join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; const getAbsolutePath = (input: I): I => dirname(require.resolve(join(input, 'package.json'))) as any; diff --git a/code/frameworks/svelte-vite/src/plugins/svelte-docgen.ts b/code/frameworks/svelte-vite/src/plugins/svelte-docgen.ts index 84bca0a243fe..7d2ba7141287 100644 --- a/code/frameworks/svelte-vite/src/plugins/svelte-docgen.ts +++ b/code/frameworks/svelte-vite/src/plugins/svelte-docgen.ts @@ -1,8 +1,9 @@ +import { readFileSync } from 'node:fs'; +import { basename, relative } from 'node:path'; + import { logger } from 'storybook/internal/node-logger'; -import fs from 'fs'; import MagicString from 'magic-string'; -import path from 'path'; import { replace, typescript } from 'svelte-preprocess'; import { preprocess } from 'svelte/compiler'; import svelteDoc from 'sveltedoc-parser'; @@ -97,11 +98,11 @@ export async function svelteDocgen(svelteOptions: Record = {}): Pro } } - const resource = path.relative(cwd, id); + const resource = relative(cwd, id); let docOptions; if (docPreprocessOptions) { - const rawSource = fs.readFileSync(resource).toString(); + const rawSource = readFileSync(resource).toString(); const { code: fileContent } = await preprocess(rawSource, docPreprocessOptions, { filename: resource, @@ -133,9 +134,9 @@ export async function svelteDocgen(svelteOptions: Record = {}): Pro } // get filename for source content - const file = path.basename(resource); + const file = basename(resource); - componentDoc.name = path.basename(file); + componentDoc.name = basename(file); const componentName = getNameFromFilename(resource); s.append(`;${componentName}.__docgen = ${JSON.stringify(componentDoc)}`); diff --git a/code/frameworks/svelte-vite/src/preset.ts b/code/frameworks/svelte-vite/src/preset.ts index a3d8547f7bdb..c50f86a5eabd 100644 --- a/code/frameworks/svelte-vite/src/preset.ts +++ b/code/frameworks/svelte-vite/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { dirname, join } from 'node:path'; -import { dirname, join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; import { svelteDocgen } from './plugins/svelte-docgen'; import type { StorybookConfig } from './types'; diff --git a/code/frameworks/svelte-webpack5/src/preset.ts b/code/frameworks/svelte-webpack5/src/preset.ts index a10bb92b8cfe..df46921663ed 100644 --- a/code/frameworks/svelte-webpack5/src/preset.ts +++ b/code/frameworks/svelte-webpack5/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { dirname, join } from 'node:path'; -import { dirname, join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; const getAbsolutePath = (input: I): I => dirname(require.resolve(join(input, 'package.json'))) as any; diff --git a/code/frameworks/sveltekit/src/preset.ts b/code/frameworks/sveltekit/src/preset.ts index 03def41988de..263a6b35429d 100644 --- a/code/frameworks/sveltekit/src/preset.ts +++ b/code/frameworks/sveltekit/src/preset.ts @@ -1,11 +1,11 @@ +import { dirname, join } from 'node:path'; + import type { PresetProperty } from 'storybook/internal/types'; import { withoutVitePlugins } from '@storybook/builder-vite'; // @ts-expect-error -- TS picks up the type from preset.js instead of dist/preset.d.ts import { viteFinal as svelteViteFinal } from '@storybook/svelte-vite/preset'; -import { dirname, join } from 'path'; - import { configOverrides } from './plugins/config-overrides'; import { mockSveltekitStores } from './plugins/mock-sveltekit-stores'; import { type StorybookConfig } from './types'; diff --git a/code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts b/code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts index e3a259c78ad2..fb61f1c1e09f 100644 --- a/code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts +++ b/code/frameworks/vue3-vite/src/plugins/vue-component-meta.ts @@ -1,7 +1,8 @@ +import { readFile, stat } from 'node:fs/promises'; +import { dirname, join, parse, relative, resolve } from 'node:path'; + import findPackageJson from 'find-package-json'; -import fs from 'fs/promises'; import MagicString from 'magic-string'; -import path from 'path'; import type { PluginOption } from 'vite'; import { type ComponentMeta, @@ -151,7 +152,7 @@ async function createVueComponentMetaChecker(tsconfigPath = 'tsconfig.json') { }; const projectRoot = getProjectRoot(); - const projectTsConfigPath = path.join(projectRoot, tsconfigPath); + const projectTsConfigPath = join(projectRoot, tsconfigPath); const defaultChecker = createCheckerByJson(projectRoot, { include: ['**/*'] }, checkerOptions); @@ -174,17 +175,17 @@ async function createVueComponentMetaChecker(tsconfigPath = 'tsconfig.json') { function getProjectRoot() { const projectRoot = findPackageJson().next().value?.path ?? ''; - const currentFileDir = path.dirname(__filename); - const relativePathToProjectRoot = path.relative(currentFileDir, projectRoot); + const currentFileDir = dirname(__filename); + const relativePathToProjectRoot = relative(currentFileDir, projectRoot); - return path.resolve(currentFileDir, relativePathToProjectRoot); + return resolve(currentFileDir, relativePathToProjectRoot); } /** * Gets the filename without file extension. */ function getFilenameWithoutExtension(filename: string) { - return path.parse(filename).name; + return parse(filename).name; } /** @@ -199,7 +200,7 @@ function lowercaseFirstLetter(string: string) { */ async function fileExists(fullPath: string) { try { - await fs.stat(fullPath); + await stat(fullPath); return true; } catch { return false; @@ -250,13 +251,13 @@ async function applyTempFixForEventDescriptions(filename: string, componentMeta: } /** - * Gets a list of tsconfig references for the given tsconfig path. + * Gets a list of tsconfig references for the given tsconfig * This is only needed for the temporary workaround/fix for: * https://github.com/vuejs/language-tools/issues/3896 */ async function getTsConfigReferences(tsConfigPath: string) { try { - const content = JSON.parse(await fs.readFile(tsConfigPath, 'utf-8')); + const content = JSON.parse(await readFile(tsConfigPath, 'utf-8')); if (!('references' in content) || !Array.isArray(content.references)) return []; return content.references as unknown[]; } catch { diff --git a/code/frameworks/vue3-vite/src/preset.ts b/code/frameworks/vue3-vite/src/preset.ts index 0cda6c5324be..bef688db2792 100644 --- a/code/frameworks/vue3-vite/src/preset.ts +++ b/code/frameworks/vue3-vite/src/preset.ts @@ -1,6 +1,7 @@ +import { dirname, join } from 'node:path'; + import type { PresetProperty } from 'storybook/internal/types'; -import { dirname, join } from 'path'; import type { PluginOption } from 'vite'; import { vueComponentMeta } from './plugins/vue-component-meta'; diff --git a/code/frameworks/vue3-webpack5/src/preset.ts b/code/frameworks/vue3-webpack5/src/preset.ts index d0424603ce9a..bbb02cb9029f 100644 --- a/code/frameworks/vue3-webpack5/src/preset.ts +++ b/code/frameworks/vue3-webpack5/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { dirname, join } from 'node:path'; -import { dirname, join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; const getAbsolutePath = (input: I): I => dirname(require.resolve(join(input, 'package.json'))) as any; diff --git a/code/frameworks/web-components-vite/src/preset.ts b/code/frameworks/web-components-vite/src/preset.ts index 5788193fd927..397deddb6e4b 100644 --- a/code/frameworks/web-components-vite/src/preset.ts +++ b/code/frameworks/web-components-vite/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { dirname, join } from 'node:path'; -import { dirname, join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; const getAbsolutePath = (input: I): I => dirname(require.resolve(join(input, 'package.json'))) as any; diff --git a/code/frameworks/web-components-webpack5/src/preset.ts b/code/frameworks/web-components-webpack5/src/preset.ts index dcf3667381e1..b005060fa88c 100644 --- a/code/frameworks/web-components-webpack5/src/preset.ts +++ b/code/frameworks/web-components-webpack5/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { dirname, join } from 'node:path'; -import { dirname, join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; const getAbsolutePath = (input: I): I => dirname(require.resolve(join(input, 'package.json'))) as any; diff --git a/code/lib/cli-storybook/src/add.ts b/code/lib/cli-storybook/src/add.ts index 145a4c225221..bc72ed99e786 100644 --- a/code/lib/cli-storybook/src/add.ts +++ b/code/lib/cli-storybook/src/add.ts @@ -1,3 +1,5 @@ +import { isAbsolute, join } from 'node:path'; + import { JsPackageManagerFactory, type PackageManagerName, @@ -8,7 +10,6 @@ import { } from 'storybook/internal/common'; import { readConfig, writeConfig } from 'storybook/internal/csf-tools'; -import { isAbsolute, join } from 'path'; import SemVer from 'semver'; import { dedent } from 'ts-dedent'; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/__test__/main-config-with-wrappers.js b/code/lib/cli-storybook/src/automigrate/fixes/__test__/main-config-with-wrappers.js index 7e6280732df5..e22c255ac492 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/__test__/main-config-with-wrappers.js +++ b/code/lib/cli-storybook/src/automigrate/fixes/__test__/main-config-with-wrappers.js @@ -1,7 +1,6 @@ -import path from 'path'; +import { dirname, join } from 'node:path'; -const wrapForPnp = (packageName) => - path.dirname(require.resolve(path.join(packageName, 'package.json'))); +const wrapForPnp = (packageName) => dirname(require.resolve(join(packageName, 'package.json'))); const config = { stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 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 ca13716ab640..e74d5ce810da 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,14 +1,15 @@ /* eslint-disable no-underscore-dangle */ +import { join } from 'node:path'; + import { expect, it, vi } from 'vitest'; import * as fsExtra from 'fs-extra'; -import path from 'path'; import { initialGlobals } from './initial-globals'; vi.mock('fs-extra', async () => import('../../../../../__mocks__/fs-extra')); -const previewConfigPath = path.join('.storybook', 'preview.js'); +const previewConfigPath = join('.storybook', 'preview.js'); const check = async (previewContents: string) => { vi.mocked(fsExtra as any).__setMockFiles({ [previewConfigPath]: previewContents, 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 a25fc677bab9..cc4e3871b076 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,6 +1,7 @@ +import { basename } from 'node:path'; + import chalk from 'chalk'; import fse from 'fs-extra'; -import { basename } from 'path'; import { dedent } from 'ts-dedent'; import type { Fix } from '../types'; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/mdx-gfm.ts b/code/lib/cli-storybook/src/automigrate/fixes/mdx-gfm.ts index 24cb5ba171a9..0a96b8f96de1 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/mdx-gfm.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/mdx-gfm.ts @@ -1,7 +1,8 @@ +import { join } from 'node:path'; + import { getStorybookVersionSpecifier } from 'storybook/internal/cli'; import { commonGlobOptions } from 'storybook/internal/common'; -import { join } from 'path'; import slash from 'slash'; import { dedent } from 'ts-dedent'; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/remove-argtypes-regex.ts b/code/lib/cli-storybook/src/automigrate/fixes/remove-argtypes-regex.ts index 443102a8265e..ed39e8e4fc22 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/remove-argtypes-regex.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/remove-argtypes-regex.ts @@ -1,4 +1,4 @@ -import * as fs from 'node:fs/promises'; +import { readFile } from 'node:fs/promises'; import { babelParse } from 'storybook/internal/csf-tools'; @@ -16,7 +16,7 @@ export const removeArgtypesRegex: Fix<{ argTypesRegex: NodePath; previewConfigPa async check({ previewConfigPath }) { if (!previewConfigPath) return null; - const previewFile = await fs.readFile(previewConfigPath, { encoding: 'utf-8' }); + const previewFile = await readFile(previewConfigPath, { encoding: 'utf-8' }); // @ts-expect-error File is not yet exposed, see https://github.com/babel/babel/issues/11350#issuecomment-644118606 const file: BabelFile = new babel.File( 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 086a99ce1bbe..6ae48ad95018 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,10 +1,11 @@ /* eslint-disable no-underscore-dangle */ +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 path from 'path'; import { RemovedAPIs, removedGlobalClientAPIs as migration } from './remove-global-client-apis'; @@ -13,7 +14,7 @@ vi.mock('fs-extra', async () => import('../../../../../__mocks__/fs-extra')); const check = async ({ contents, previewConfigPath }: any) => { if (contents) { vi.mocked(fsExtra as any).__setMockFiles({ - [path.join('.storybook', 'preview.js')]: contents, + [join('.storybook', 'preview.js')]: contents, }); } const packageManager = { @@ -38,7 +39,7 @@ describe('removedGlobalClientAPIs fix', () => { export const parameters = {}; `; await expect( - check({ contents, previewConfigPath: path.join('.storybook', 'preview.js') }) + check({ contents, previewConfigPath: join('.storybook', 'preview.js') }) ).resolves.toBeNull(); }); it('uses 1 removed API', async () => { @@ -47,7 +48,7 @@ describe('removedGlobalClientAPIs fix', () => { addParameters({}); `; await expect( - check({ contents, previewConfigPath: path.join('.storybook', 'preview.js') }) + check({ contents, previewConfigPath: join('.storybook', 'preview.js') }) ).resolves.toEqual( expect.objectContaining({ usedAPIs: [RemovedAPIs.addParameters], @@ -61,7 +62,7 @@ describe('removedGlobalClientAPIs fix', () => { addDecorator((storyFn) => storyFn()); `; await expect( - check({ contents, previewConfigPath: path.join('.storybook', 'preview.js') }) + check({ contents, previewConfigPath: join('.storybook', 'preview.js') }) ).resolves.toEqual( expect.objectContaining({ usedAPIs: expect.arrayContaining([RemovedAPIs.addParameters, RemovedAPIs.addDecorator]), diff --git a/code/lib/cli-storybook/src/automigrate/fixes/vite-config-file.ts b/code/lib/cli-storybook/src/automigrate/fixes/vite-config-file.ts index b58540ff978c..6afc31311663 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/vite-config-file.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/vite-config-file.ts @@ -1,8 +1,9 @@ +import { join } from 'node:path'; + import { frameworkToRenderer } from 'storybook/internal/cli'; import { frameworkPackages } from 'storybook/internal/common'; import findUp from 'find-up'; -import path from 'path'; import { dedent } from 'ts-dedent'; import { getFrameworkPackageName } from '../helpers/mainConfigFile'; @@ -23,7 +24,7 @@ export const viteConfigFile = { async check({ mainConfig, packageManager, mainConfigPath }) { let isViteConfigFileFound = !!(await findUp( ['vite.config.js', 'vite.config.mjs', 'vite.config.cjs', 'vite.config.ts', 'vite.config.mts'], - { cwd: mainConfigPath ? path.join(mainConfigPath, '..') : process.cwd() } + { cwd: mainConfigPath ? join(mainConfigPath, '..') : process.cwd() } )); const rendererToVitePluginMap: Record = { diff --git a/code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts b/code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts index 0845fb2cc0d1..3fb4ce3bfc81 100644 --- a/code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts +++ b/code/lib/cli-storybook/src/automigrate/helpers/mainConfigFile.ts @@ -1,3 +1,5 @@ +import { normalize } from 'node:path'; + import { frameworkToRenderer } from 'storybook/internal/cli'; import { builderPackages, @@ -14,7 +16,6 @@ import { readConfig, writeConfig as writeConfigFile } from 'storybook/internal/c import type { StorybookConfig, StorybookConfigRaw } from 'storybook/internal/types'; import chalk from 'chalk'; -import path from 'path'; import { dedent } from 'ts-dedent'; const logger = console; @@ -76,7 +77,7 @@ export const getBuilderPackageName = (mainConfig?: StorybookConfigRaw) => { return null; } - const normalizedPath = path.normalize(packageNameOrPath).replace(new RegExp(/\\/, 'g'), '/'); + const normalizedPath = normalize(packageNameOrPath).replace(new RegExp(/\\/, 'g'), '/'); return builderPackages.find((pkg) => normalizedPath.endsWith(pkg)) || packageNameOrPath; }; diff --git a/code/lib/cli-storybook/src/automigrate/index.ts b/code/lib/cli-storybook/src/automigrate/index.ts index e101ddd7e89b..690c3d1b15f7 100644 --- a/code/lib/cli-storybook/src/automigrate/index.ts +++ b/code/lib/cli-storybook/src/automigrate/index.ts @@ -1,3 +1,5 @@ +import { join } from 'node:path'; + import { type JsPackageManager, JsPackageManagerFactory, @@ -9,7 +11,6 @@ import { import boxen from 'boxen'; import chalk from 'chalk'; import { createWriteStream, move, remove } from 'fs-extra'; -import { join } from 'path'; import prompts from 'prompts'; import semver from 'semver'; import invariant from 'tiny-invariant'; diff --git a/code/lib/cli-storybook/src/doctor/index.ts b/code/lib/cli-storybook/src/doctor/index.ts index 51aa624af179..08e798288753 100644 --- a/code/lib/cli-storybook/src/doctor/index.ts +++ b/code/lib/cli-storybook/src/doctor/index.ts @@ -1,10 +1,11 @@ +import { join } from 'node:path'; + import { JsPackageManagerFactory, temporaryFile } from 'storybook/internal/common'; import type { PackageManagerName } from 'storybook/internal/common'; import boxen from 'boxen'; import chalk from 'chalk'; import { createWriteStream, move, remove } from 'fs-extra'; -import { join } from 'path'; import { dedent } from 'ts-dedent'; import { cleanLog } from '../automigrate/helpers/cleanLog'; diff --git a/code/lib/cli-storybook/src/link.ts b/code/lib/cli-storybook/src/link.ts index 46cc8f94866f..dbb6b30663ba 100644 --- a/code/lib/cli-storybook/src/link.ts +++ b/code/lib/cli-storybook/src/link.ts @@ -1,9 +1,10 @@ +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'; -import path from 'path'; type ExecOptions = Parameters[2]; @@ -67,21 +68,21 @@ export const link = async ({ target, local, start }: LinkOptions) => { } let reproDir = target; - let reproName = path.basename(target); + let reproName = basename(target); if (!local) { - const reprosDir = path.join(storybookDir, '../storybook-repros'); + const reprosDir = join(storybookDir, '../storybook-repros'); logger.info(`Ensuring directory ${reprosDir}`); await fse.ensureDir(reprosDir); logger.info(`Cloning ${target}`); await exec(`git clone ${target}`, { cwd: reprosDir }); // Extract a repro name from url given as input (take the last part of the path and remove the extension) - reproName = path.basename(target, path.extname(target)); - reproDir = path.join(reprosDir, reproName); + reproName = basename(target, extname(target)); + reproDir = join(reprosDir, reproName); } - const reproPackageJson = await fse.readJSON(path.join(reproDir, 'package.json')); + const reproPackageJson = await fse.readJSON(join(reproDir, 'package.json')); const version = spawnSync('yarn', ['--version'], { cwd: reproDir, diff --git a/code/lib/cli-storybook/src/sandbox-templates.ts b/code/lib/cli-storybook/src/sandbox-templates.ts index e98bda7d692f..abc5aac8ce09 100644 --- a/code/lib/cli-storybook/src/sandbox-templates.ts +++ b/code/lib/cli-storybook/src/sandbox-templates.ts @@ -198,10 +198,35 @@ const baseTemplates = { builder: '@storybook/builder-webpack5', }, modifications: { + extraDependencies: ['server-only'], mainConfig: { features: { experimentalRSC: true }, }, - extraDependencies: ['server-only'], + }, + skipTasks: ['e2e-tests-dev', 'bench'], + }, + 'experimental-nextjs-vite/default-ts': { + name: 'Next.js Latest (Vite | TypeScript)', + script: + 'yarn create next-app {{beforeDir}} --typescript --eslint --tailwind --app --import-alias="@/*" --src-dir', + inDevelopment: true, + expected: { + framework: '@storybook/experimental-nextjs-vite', + renderer: '@storybook/react', + builder: '@storybook/builder-vite', + }, + + modifications: { + mainConfig: { + framework: '@storybook/experimental-nextjs-vite', + features: { experimentalRSC: true }, + }, + extraDependencies: [ + 'server-only', + 'vite-plugin-storybook-nextjs', + '@storybook/experimental-nextjs-vite', + 'vite', + ], }, skipTasks: ['e2e-tests-dev', 'bench'], }, @@ -726,6 +751,7 @@ export const daily: TemplateKey[] = [ ...merged, 'angular-cli/prerelease', 'cra/default-js', + 'experimental-nextjs-vite/default-ts', 'react-vite/default-js', 'react-vite/prerelease-ts', 'react-webpack/prerelease-ts', diff --git a/code/lib/cli-storybook/src/sandbox.ts b/code/lib/cli-storybook/src/sandbox.ts index ffc1ae512b7f..a6b4fe40f043 100644 --- a/code/lib/cli-storybook/src/sandbox.ts +++ b/code/lib/cli-storybook/src/sandbox.ts @@ -1,3 +1,5 @@ +import { isAbsolute, join } from 'node:path'; + import type { PackageManagerName } from 'storybook/internal/common'; import { JsPackageManagerFactory } from 'storybook/internal/common'; import { versions } from 'storybook/internal/common'; @@ -7,7 +9,6 @@ import chalk from 'chalk'; import { initiate } from 'create-storybook'; import { existsSync, readdir } from 'fs-extra'; import { downloadTemplate } from 'giget'; -import path from 'path'; import prompts from 'prompts'; import { lt, prerelease } from 'semver'; import invariant from 'tiny-invariant'; @@ -191,9 +192,9 @@ export const sandbox = async ({ invariant(selectedDirectory); try { - const templateDestination = path.isAbsolute(selectedDirectory) + const templateDestination = isAbsolute(selectedDirectory) ? selectedDirectory - : path.join(process.cwd(), selectedDirectory); + : join(process.cwd(), selectedDirectory); logger.info(`🏃 Adding ${selectedConfig.name} into ${templateDestination}`); diff --git a/code/lib/codemod/src/index.ts b/code/lib/codemod/src/index.ts index 0dcd61c58509..d4dc0dcd5b76 100644 --- a/code/lib/codemod/src/index.ts +++ b/code/lib/codemod/src/index.ts @@ -1,8 +1,9 @@ /* eslint import/prefer-default-export: "off" */ +import { readdirSync } from 'node:fs'; +import { rename as renameAsync } from 'node:fs/promises'; +import { extname } from 'node:path'; + import { sync as spawnSync } from 'cross-spawn'; -import fs from 'fs'; -import path from 'path'; -import { promisify } from 'util'; import { jscodeshiftToPrettierParser } from './lib/utils'; @@ -16,14 +17,11 @@ export { default as updateAddonInfo } from './transforms/update-addon-info'; const TRANSFORM_DIR = `${__dirname}/transforms`; export function listCodemods() { - return fs - .readdirSync(TRANSFORM_DIR) + return readdirSync(TRANSFORM_DIR) .filter((fname) => fname.endsWith('.js')) .map((fname) => fname.slice(0, -3)); } -const renameAsync = promisify(fs.rename); - async function renameFile(file: any, from: any, to: any, { logger }: any) { const newFile = file.replace(from, to); logger.log(`Rename: ${file} ${newFile}`); @@ -58,7 +56,7 @@ export async function runCodemod( let inferredParser = parser; if (!parser) { - const extension = path.extname(glob).slice(1); + const extension = extname(glob).slice(1); const knownParser = jscodeshiftToPrettierParser(extension); if (knownParser !== 'babel') inferredParser = extension; } @@ -67,7 +65,7 @@ export async function runCodemod( const { globby } = await import('globby'); const files = await globby([glob, '!**/node_modules', '!**/dist']); - const extensions = new Set(files.map((file) => path.extname(file).slice(1))); + const extensions = new Set(files.map((file) => extname(file).slice(1))); const commaSeparatedExtensions = Array.from(extensions).join(','); logger.log(`=> Applying ${codemod}: ${files.length} files`); diff --git a/code/lib/codemod/src/transforms/__tests__/transforms.tests.js b/code/lib/codemod/src/transforms/__tests__/transforms.tests.js index 4422705118a4..fc6f71ca1e02 100644 --- a/code/lib/codemod/src/transforms/__tests__/transforms.tests.js +++ b/code/lib/codemod/src/transforms/__tests__/transforms.tests.js @@ -1,27 +1,28 @@ +import { readdirSync } from 'node:fs'; +import { join, resolve } from 'node:path'; + import { describe, expect, it, vi } from 'vitest'; -import fs from 'fs'; import { applyTransform } from 'jscodeshift/dist/testUtils'; -import path from 'path'; vi.mock('@storybook/core/node-logger'); const inputRegExp = /\.input\.js$/; -const fixturesDir = path.resolve(__dirname, '../__testfixtures__'); -fs.readdirSync(fixturesDir).forEach((transformName) => { +const fixturesDir = resolve(__dirname, '../__testfixtures__'); +readdirSync(fixturesDir).forEach((transformName) => { // FIXME: delete after https://github.com/storybookjs/storybook/issues/19497 if (transformName === 'mdx-to-csf') return; - const transformFixturesDir = path.join(fixturesDir, transformName); + const transformFixturesDir = join(fixturesDir, transformName); describe(`${transformName}`, () => { fs.readdirSync(transformFixturesDir) .filter((fileName) => inputRegExp.test(fileName)) .forEach((fileName) => { - const inputPath = path.join(transformFixturesDir, fileName); + const inputPath = join(transformFixturesDir, fileName); it(`transforms correctly using "${fileName}" data`, () => expect( - applyTransform(require(path.join(__dirname, '..', transformName)), null, { + applyTransform(require(join(__dirname, '..', transformName)), null, { path: inputPath, source: fs.readFileSync(inputPath, 'utf8'), }) diff --git a/code/lib/codemod/src/transforms/mdx-to-csf.ts b/code/lib/codemod/src/transforms/mdx-to-csf.ts index b507538e02d2..42d6744b7c02 100644 --- a/code/lib/codemod/src/transforms/mdx-to-csf.ts +++ b/code/lib/codemod/src/transforms/mdx-to-csf.ts @@ -1,6 +1,6 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment,@typescript-eslint/no-shadow */ -import * as fs from 'node:fs'; -import * as path from 'node:path'; +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { existsSync, renameSync, writeFileSync } from 'node:fs'; +import { basename, join, parse } from 'node:path'; import { babelParse, babelParseExpression } from '@storybook/core/csf-tools'; @@ -30,23 +30,23 @@ const renameList: { original: string; baseName: string }[] = []; const brokenList: { original: string; baseName: string }[] = []; export default async function jscodeshift(info: FileInfo) { - const parsed = path.parse(info.path); + const parsed = parse(info.path); - let baseName = path.join( + let baseName = join( parsed.dir, parsed.name.replace('.mdx', '').replace('.stories', '').replace('.story', '') ); // make sure the new csf file we are going to create exists - while (fs.existsSync(`${baseName}.stories.js`)) { + while (existsSync(`${baseName}.stories.js`)) { baseName += '_'; } try { - const { csf, mdx } = await transform(info, path.basename(baseName)); + const { csf, mdx } = await transform(info, basename(baseName)); if (csf != null) { - fs.writeFileSync(`${baseName}.stories.js`, csf); + writeFileSync(`${baseName}.stories.js`, csf); } renameList.push({ original: info.path, baseName }); @@ -62,10 +62,10 @@ export default async function jscodeshift(info: FileInfo) { // This is a workaround to rename the files after the transformation, which we can remove after we switch from jscodeshift to another solution. process.on('exit', () => { renameList.forEach((file) => { - fs.renameSync(file.original, `${file.baseName}.mdx`); + renameSync(file.original, `${file.baseName}.mdx`); }); brokenList.forEach((file) => { - fs.renameSync(file.original, `${file.original}.broken`); + renameSync(file.original, `${file.original}.broken`); }); }); diff --git a/code/lib/core-webpack/src/load-custom-webpack-config.ts b/code/lib/core-webpack/src/load-custom-webpack-config.ts index 83c646146627..021dedff7f3d 100644 --- a/code/lib/core-webpack/src/load-custom-webpack-config.ts +++ b/code/lib/core-webpack/src/load-custom-webpack-config.ts @@ -1,8 +1,8 @@ -import { serverRequire } from 'storybook/internal/common'; +import { resolve } from 'node:path'; -import path from 'path'; +import { serverRequire } from 'storybook/internal/common'; const webpackConfigs = ['webpack.config', 'webpackfile']; export const loadCustomWebpackConfig = (configDir: string) => - serverRequire(webpackConfigs.map((configName) => path.resolve(configDir, configName))); + serverRequire(webpackConfigs.map((configName) => resolve(configDir, configName))); diff --git a/code/lib/core-webpack/src/to-require-context.test.ts b/code/lib/core-webpack/src/to-require-context.test.ts index ddb020166bb1..c8acff09b9d0 100644 --- a/code/lib/core-webpack/src/to-require-context.test.ts +++ b/code/lib/core-webpack/src/to-require-context.test.ts @@ -1,9 +1,9 @@ +import { relative } from 'node:path'; + import { describe, expect, it } from 'vitest'; import { normalizeStoriesEntry } from 'storybook/internal/common'; -import path from 'path'; - import { toRequireContext } from './to-require-context'; const testCases = [ @@ -273,7 +273,7 @@ describe('toRequireContext', () => { const regex = new RegExp(match); function isMatched(filePath: string) { - const relativePath = `./${path.relative(base, filePath)}`; + const relativePath = `./${relative(base, filePath)}`; const baseIncluded = filePath.includes(base); const matched = regex.test(relativePath); diff --git a/code/lib/create-storybook/src/generators/ANGULAR/index.ts b/code/lib/create-storybook/src/generators/ANGULAR/index.ts index 4b43813cccbf..9525b7678986 100644 --- a/code/lib/create-storybook/src/generators/ANGULAR/index.ts +++ b/code/lib/create-storybook/src/generators/ANGULAR/index.ts @@ -1,10 +1,10 @@ +import { join } from 'node:path'; + import { CoreBuilder } from 'storybook/internal/cli'; import { AngularJSON, compoDocPreviewPrefix, promptForCompoDocs } from 'storybook/internal/cli'; import { copyTemplate } from 'storybook/internal/cli'; import { commandLog } from 'storybook/internal/common'; -import { join } from 'path'; - import { baseGenerator, getCliDir } from '../baseGenerator'; import type { Generator } from '../types'; diff --git a/code/lib/create-storybook/src/generators/NEXTJS/index.ts b/code/lib/create-storybook/src/generators/NEXTJS/index.ts index 52a768dd8913..2a0f5a876dd4 100644 --- a/code/lib/create-storybook/src/generators/NEXTJS/index.ts +++ b/code/lib/create-storybook/src/generators/NEXTJS/index.ts @@ -1,7 +1,7 @@ -import { CoreBuilder } from 'storybook/internal/cli'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; -import { existsSync } from 'fs'; -import { join } from 'path'; +import { CoreBuilder } from 'storybook/internal/cli'; import { baseGenerator } from '../baseGenerator'; import type { Generator } from '../types'; diff --git a/code/lib/create-storybook/src/generators/REACT_SCRIPTS/index.ts b/code/lib/create-storybook/src/generators/REACT_SCRIPTS/index.ts index ae9856d6be49..d089a3f6c190 100644 --- a/code/lib/create-storybook/src/generators/REACT_SCRIPTS/index.ts +++ b/code/lib/create-storybook/src/generators/REACT_SCRIPTS/index.ts @@ -1,7 +1,8 @@ +import { existsSync } from 'node:fs'; +import { join, resolve } from 'node:path'; + import { CoreBuilder } from 'storybook/internal/cli'; -import fs from 'fs'; -import path from 'path'; import semver from 'semver'; import { dedent } from 'ts-dedent'; @@ -9,7 +10,7 @@ import { baseGenerator } from '../baseGenerator'; import type { Generator } from '../types'; const generator: Generator = async (packageManager, npmOptions, options) => { - const monorepoRootPath = path.join(__dirname, '..', '..', '..', '..', '..', '..'); + const monorepoRootPath = join(__dirname, '..', '..', '..', '..', '..', '..'); const extraMain = options.linkable ? { webpackFinal: `%%(config) => { @@ -58,7 +59,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => { webpackCompiler: () => undefined, extraAddons, extraPackages, - staticDir: fs.existsSync(path.resolve('./public')) ? 'public' : undefined, + staticDir: existsSync(resolve('./public')) ? 'public' : undefined, extraMain, } ); diff --git a/code/lib/create-storybook/src/generators/baseGenerator.ts b/code/lib/create-storybook/src/generators/baseGenerator.ts index 376440c06ef8..9ebd449228bf 100644 --- a/code/lib/create-storybook/src/generators/baseGenerator.ts +++ b/code/lib/create-storybook/src/generators/baseGenerator.ts @@ -1,3 +1,5 @@ +import { dirname, join } from 'node:path'; + import type { NpmOptions } from 'storybook/internal/cli'; import type { Builder, SupportedRenderers } from 'storybook/internal/cli'; import { SupportedLanguage, externalFrameworks } from 'storybook/internal/cli'; @@ -10,7 +12,6 @@ import type { SupportedFrameworks } from 'storybook/internal/types'; import fse from 'fs-extra'; import ora from 'ora'; -import path, { dirname } from 'path'; import invariant from 'tiny-invariant'; import { dedent } from 'ts-dedent'; @@ -361,7 +362,7 @@ export async function baseGenerator( : addons, extensions, language, - ...(staticDir ? { staticDirs: [path.join('..', staticDir)] } : null), + ...(staticDir ? { staticDirs: [join('..', staticDir)] } : null), ...extraMain, ...(type !== 'framework' ? { @@ -396,7 +397,7 @@ export async function baseGenerator( packageManager, language, destination: componentsDestinationPath, - commonAssetsDir: path.join(getCliDir(), 'rendererAssets', 'common'), + commonAssetsDir: join(getCliDir(), 'rendererAssets', 'common'), }); } } diff --git a/code/lib/create-storybook/src/generators/configure.test.ts b/code/lib/create-storybook/src/generators/configure.test.ts index eed5040dc653..f5903d883637 100644 --- a/code/lib/create-storybook/src/generators/configure.test.ts +++ b/code/lib/create-storybook/src/generators/configure.test.ts @@ -94,7 +94,7 @@ describe('configureMain', () => { expect(mainConfigPath).toEqual('./.storybook/main.js'); expect(mainConfigContent).toMatchInlineSnapshot(` - "import path from 'path'; + "import path from 'node:path'; /** @type { import('@storybook/react-webpack5').StorybookConfig } */ const config = { diff --git a/code/lib/create-storybook/src/generators/configure.ts b/code/lib/create-storybook/src/generators/configure.ts index 375322362619..7fd0728b6128 100644 --- a/code/lib/create-storybook/src/generators/configure.ts +++ b/code/lib/create-storybook/src/generators/configure.ts @@ -1,8 +1,9 @@ +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 path from 'path'; import { dedent } from 'ts-dedent'; interface ConfigureMainOptions { @@ -58,7 +59,7 @@ export async function configureMain({ prefixes = [], ...custom }: ConfigureMainOptions) { - const srcPath = path.resolve(storybookConfigFolder, '../src'); + const srcPath = resolve(storybookConfigFolder, '../src'); const prefix = (await fse.pathExists(srcPath)) ? '../src' : '../stories'; const config = { stories: [`${prefix}/**/*.mdx`, `${prefix}/**/*.stories.@(${extensions.join('|')})`], @@ -87,7 +88,7 @@ export async function configureMain({ const finalPrefixes = [...prefixes]; if (custom.framework?.name.includes('path.dirname(')) { - imports.push(`import path from 'path';`); + imports.push(`import path from 'node:path';`); } if (isTypescript) { diff --git a/code/lib/create-storybook/src/initiate.ts b/code/lib/create-storybook/src/initiate.ts index 471460a57247..787e16288cc2 100644 --- a/code/lib/create-storybook/src/initiate.ts +++ b/code/lib/create-storybook/src/initiate.ts @@ -1,3 +1,5 @@ +import { appendFile, readFile } from 'node:fs/promises'; + import type { Builder, NpmOptions } from 'storybook/internal/cli'; import { ProjectType, installableProjectTypes } from 'storybook/internal/cli'; import { detect, detectLanguage, detectPnp, isStorybookInstantiated } from 'storybook/internal/cli'; @@ -17,7 +19,6 @@ import { telemetry } from 'storybook/internal/telemetry'; import boxen from 'boxen'; import chalk from 'chalk'; import findUp from 'find-up'; -import { appendFile, readFile } from 'fs/promises'; import prompts from 'prompts'; import { lt, prerelease } from 'semver'; import { dedent } from 'ts-dedent'; diff --git a/code/lib/csf-plugin/src/rollup-based-plugin.ts b/code/lib/csf-plugin/src/rollup-based-plugin.ts index 0a5d4ad69824..a2b089694912 100644 --- a/code/lib/csf-plugin/src/rollup-based-plugin.ts +++ b/code/lib/csf-plugin/src/rollup-based-plugin.ts @@ -1,7 +1,8 @@ +import { readFile } from 'node:fs/promises'; + import type { EnrichCsfOptions } from 'storybook/internal/csf-tools'; import { enrichCsf, formatCsf, loadCsf } from 'storybook/internal/csf-tools'; -import fs from 'fs/promises'; import type { RollupPlugin } from 'unplugin'; import { STORIES_REGEX } from './constants'; @@ -16,7 +17,7 @@ export function rollupBasedPlugin(options: EnrichCsfOptions): Partial userTitle || 'default'; const csf = loadCsf(code, { makeTitle }).parse(); diff --git a/code/lib/csf-plugin/src/webpack-loader.ts b/code/lib/csf-plugin/src/webpack-loader.ts index bcd4db05d7f7..3304d35c10bf 100644 --- a/code/lib/csf-plugin/src/webpack-loader.ts +++ b/code/lib/csf-plugin/src/webpack-loader.ts @@ -1,8 +1,8 @@ +import { readFile } from 'node:fs/promises'; + import type { EnrichCsfOptions } from 'storybook/internal/csf-tools'; import { enrichCsf, formatCsf, loadCsf } from 'storybook/internal/csf-tools'; -import fs from 'fs/promises'; - interface LoaderContext { async: () => (err: Error | null, result?: string, map?: any) => void; getOptions: () => EnrichCsfOptions; @@ -14,7 +14,7 @@ async function loader(this: LoaderContext, content: string, map: any) { const options = this.getOptions(); const id = this.resourcePath; - const sourceCode = await fs.readFile(id, 'utf-8'); + const sourceCode = await readFile(id, 'utf-8'); try { const makeTitle = (userTitle: string) => userTitle || 'default'; diff --git a/code/lib/react-dom-shim/src/preset.ts b/code/lib/react-dom-shim/src/preset.ts index cc856c6a8535..1eae332f91ee 100644 --- a/code/lib/react-dom-shim/src/preset.ts +++ b/code/lib/react-dom-shim/src/preset.ts @@ -1,7 +1,7 @@ -import type { Options } from 'storybook/internal/types'; +import { readFile } from 'node:fs/promises'; +import { dirname, isAbsolute, join } from 'node:path'; -import { readFile } from 'fs/promises'; -import { dirname, isAbsolute, join } from 'path'; +import type { Options } from 'storybook/internal/types'; /** * Get react-dom version from the resolvedReact preset, which points to either diff --git a/code/lib/source-loader/src/abstract-syntax-tree/inject-decorator.csf.test.js b/code/lib/source-loader/src/abstract-syntax-tree/inject-decorator.csf.test.js index 6359c2e875a5..9979b921fd5f 100644 --- a/code/lib/source-loader/src/abstract-syntax-tree/inject-decorator.csf.test.js +++ b/code/lib/source-loader/src/abstract-syntax-tree/inject-decorator.csf.test.js @@ -1,7 +1,7 @@ -import { describe, expect, it } from 'vitest'; +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; -import { readFile } from 'fs/promises'; -import path from 'path'; +import { describe, expect, it } from 'vitest'; import injectDecorator from './inject-decorator'; import getParser from './parsers'; @@ -13,7 +13,7 @@ describe('inject-decorator', () => { it('includes storySource parameter in the default exported object', async () => { const mockFilePath = './__mocks__/inject-decorator.ts.csf.txt'; const source = await readFile(mockFilePath, 'utf-8'); - const result = injectDecorator(source, path.resolve(__dirname, mockFilePath), { + const result = injectDecorator(source, resolve(__dirname, mockFilePath), { parser: 'typescript', }); @@ -25,7 +25,7 @@ describe('inject-decorator', () => { it('includes storySource parameter in the default exported variable', async () => { const mockFilePath = './__mocks__/inject-decorator.ts.csf-meta-var.txt'; const source = await readFile(mockFilePath, 'utf-8'); - const result = injectDecorator(source, path.resolve(__dirname, mockFilePath), { + const result = injectDecorator(source, resolve(__dirname, mockFilePath), { parser: 'typescript', }); @@ -37,7 +37,7 @@ describe('inject-decorator', () => { it('includes storySource parameter in CSf3', async () => { const mockFilePath = './__mocks__/inject-decorator.ts.csf3.txt'; const source = await readFile(mockFilePath, 'utf-8'); - const result = injectDecorator(source, path.resolve(__dirname, mockFilePath), { + const result = injectDecorator(source, resolve(__dirname, mockFilePath), { parser: 'typescript', }); @@ -51,7 +51,7 @@ describe('inject-decorator', () => { it('includes storySource parameter in the default exported object', async () => { const mockFilePath = './__mocks__/inject-parameters.ts.csf.txt'; const source = await readFile(mockFilePath, 'utf-8'); - const result = injectDecorator(source, path.resolve(__dirname, mockFilePath), { + const result = injectDecorator(source, resolve(__dirname, mockFilePath), { injectStoryParameters: true, parser: 'typescript', }); diff --git a/code/lib/source-loader/src/abstract-syntax-tree/inject-decorator.test.js b/code/lib/source-loader/src/abstract-syntax-tree/inject-decorator.test.js index 4893f2fc1590..a6e08f54c8f8 100644 --- a/code/lib/source-loader/src/abstract-syntax-tree/inject-decorator.test.js +++ b/code/lib/source-loader/src/abstract-syntax-tree/inject-decorator.test.js @@ -1,15 +1,15 @@ -import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; -import fs from 'fs'; -import path from 'path'; +import { describe, expect, it } from 'vitest'; import injectDecorator from './inject-decorator'; describe('inject-decorator', () => { describe('positive', () => { const mockFilePath = './__mocks__/inject-decorator.stories.txt'; - const source = fs.readFileSync(mockFilePath, 'utf-8'); - const result = injectDecorator(source, path.resolve(__dirname, mockFilePath), { + const source = readFileSync(mockFilePath, 'utf-8'); + const result = injectDecorator(source, resolve(__dirname, mockFilePath), { parser: 'javascript', }); @@ -28,8 +28,8 @@ describe('inject-decorator', () => { describe('positive - angular', () => { const mockFilePath = './__mocks__/inject-decorator.angular-stories.txt'; - const source = fs.readFileSync(mockFilePath, 'utf-8'); - const result = injectDecorator(source, path.resolve(__dirname, mockFilePath), { + const source = readFileSync(mockFilePath, 'utf-8'); + const result = injectDecorator(source, resolve(__dirname, mockFilePath), { parser: 'typescript', }); @@ -48,8 +48,8 @@ describe('inject-decorator', () => { describe('positive - flow', () => { const mockFilePath = './__mocks__/inject-decorator.flow-stories.txt'; - const source = fs.readFileSync(mockFilePath, 'utf-8'); - const result = injectDecorator(source, path.resolve(__dirname, mockFilePath), { + const source = readFileSync(mockFilePath, 'utf-8'); + const result = injectDecorator(source, resolve(__dirname, mockFilePath), { parser: 'flow', }); @@ -68,8 +68,8 @@ describe('inject-decorator', () => { describe('positive - ts', () => { const mockFilePath = './__mocks__/inject-decorator.ts.txt'; - const source = fs.readFileSync(mockFilePath, 'utf-8'); - const result = injectDecorator(source, path.resolve(__dirname, mockFilePath), { + const source = readFileSync(mockFilePath, 'utf-8'); + const result = injectDecorator(source, resolve(__dirname, mockFilePath), { parser: 'typescript', }); @@ -88,8 +88,8 @@ describe('inject-decorator', () => { describe('stories with ugly comments', () => { const mockFilePath = './__mocks__/inject-decorator.ugly-comments-stories.txt'; - const source = fs.readFileSync(mockFilePath, 'utf-8'); - const result = injectDecorator(source, path.resolve(__dirname, mockFilePath), { + const source = readFileSync(mockFilePath, 'utf-8'); + const result = injectDecorator(source, resolve(__dirname, mockFilePath), { parser: 'javascript', }); @@ -100,8 +100,8 @@ describe('inject-decorator', () => { describe('stories with ugly comments in ts', () => { const mockFilePath = './__mocks__/inject-decorator.ts.ugly-comments-stories.txt'; - const source = fs.readFileSync(mockFilePath, 'utf-8'); - const result = injectDecorator(source, path.resolve(__dirname, mockFilePath), { + const source = readFileSync(mockFilePath, 'utf-8'); + const result = injectDecorator(source, resolve(__dirname, mockFilePath), { parser: 'typescript', }); @@ -112,9 +112,9 @@ describe('inject-decorator', () => { it('will not change the source when there are no "storiesOf" functions', () => { const mockFilePath = './__mocks__/inject-decorator.no-stories.txt'; - const source = fs.readFileSync(mockFilePath, 'utf-8'); + const source = readFileSync(mockFilePath, 'utf-8'); - const result = injectDecorator(source, path.resolve(__dirname, mockFilePath)); + const result = injectDecorator(source, resolve(__dirname, mockFilePath)); expect(result.changed).toBeFalsy(); expect(result.addsMap).toEqual({}); @@ -123,8 +123,8 @@ describe('inject-decorator', () => { describe('injectDecorator option is false', () => { const mockFilePath = './__mocks__/inject-decorator.stories.txt'; - const source = fs.readFileSync(mockFilePath, 'utf-8'); - const result = injectDecorator(source, path.resolve(__dirname, mockFilePath), { + const source = readFileSync(mockFilePath, 'utf-8'); + const result = injectDecorator(source, resolve(__dirname, mockFilePath), { injectDecorator: false, parser: 'javascript', }); @@ -136,8 +136,8 @@ describe('inject-decorator', () => { describe('injectDecorator option is false - angular', () => { const mockFilePath = './__mocks__/inject-decorator.angular-stories.txt'; - const source = fs.readFileSync(mockFilePath, 'utf-8'); - const result = injectDecorator(source, path.resolve(__dirname, mockFilePath), { + const source = readFileSync(mockFilePath, 'utf-8'); + const result = injectDecorator(source, resolve(__dirname, mockFilePath), { injectDecorator: false, parser: 'typescript', }); @@ -149,8 +149,8 @@ describe('inject-decorator', () => { describe('injectDecorator option is false - flow', () => { const mockFilePath = './__mocks__/inject-decorator.flow-stories.txt'; - const source = fs.readFileSync(mockFilePath, 'utf-8'); - const result = injectDecorator(source, path.resolve(__dirname, mockFilePath), { + const source = readFileSync(mockFilePath, 'utf-8'); + const result = injectDecorator(source, resolve(__dirname, mockFilePath), { injectDecorator: false, parser: 'flow', }); @@ -162,8 +162,8 @@ describe('inject-decorator', () => { describe('injectDecorator option is false - ts', () => { const mockFilePath = './__mocks__/inject-decorator.ts.txt'; - const source = fs.readFileSync(mockFilePath, 'utf-8'); - const result = injectDecorator(source, path.resolve(__dirname, mockFilePath), { + const source = readFileSync(mockFilePath, 'utf-8'); + const result = injectDecorator(source, resolve(__dirname, mockFilePath), { injectDecorator: false, parser: 'typescript', }); diff --git a/code/lib/source-loader/src/build.js b/code/lib/source-loader/src/build.js index 6833858a7510..a906c9a6c414 100644 --- a/code/lib/source-loader/src/build.js +++ b/code/lib/source-loader/src/build.js @@ -1,4 +1,4 @@ -import { readFile } from 'fs/promises'; +import { readFile } from 'node:fs/promises'; import { sanitizeSource } from './abstract-syntax-tree/generate-helpers'; import { readStory } from './dependencies-lookup/readAsObject'; diff --git a/code/presets/create-react-app/src/helpers/getModulePath.ts b/code/presets/create-react-app/src/helpers/getModulePath.ts index 20c336f248f4..db6ae3c1cf9a 100644 --- a/code/presets/create-react-app/src/helpers/getModulePath.ts +++ b/code/presets/create-react-app/src/helpers/getModulePath.ts @@ -1,5 +1,5 @@ -import { existsSync } from 'fs'; -import { join } from 'path'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; interface PartialTSConfig { compilerOptions: { diff --git a/code/presets/create-react-app/src/helpers/getReactScriptsPath.ts b/code/presets/create-react-app/src/helpers/getReactScriptsPath.ts index dd916b53e522..2a4260afe12a 100644 --- a/code/presets/create-react-app/src/helpers/getReactScriptsPath.ts +++ b/code/presets/create-react-app/src/helpers/getReactScriptsPath.ts @@ -1,5 +1,5 @@ -import { lstatSync, readFileSync, realpathSync } from 'fs'; -import { dirname, join } from 'path'; +import { lstatSync, readFileSync, realpathSync } from 'node:fs'; +import { dirname, join } from 'node:path'; export const getReactScriptsPath = (): string => { const cwd = process.cwd(); diff --git a/code/presets/create-react-app/src/helpers/processCraConfig.ts b/code/presets/create-react-app/src/helpers/processCraConfig.ts index 1d0c2a172812..dacf03d469ac 100644 --- a/code/presets/create-react-app/src/helpers/processCraConfig.ts +++ b/code/presets/create-react-app/src/helpers/processCraConfig.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { resolve } from 'node:path'; + import type { PluginItem, TransformOptions } from '@babel/core'; -import { resolve } from 'path'; import semver from 'semver'; import type { Configuration, RuleSetCondition, RuleSetRule } from 'webpack'; diff --git a/code/presets/create-react-app/src/index.ts b/code/presets/create-react-app/src/index.ts index e84e2b2b4e9f..3dbee4f797e1 100644 --- a/code/presets/create-react-app/src/index.ts +++ b/code/presets/create-react-app/src/index.ts @@ -1,6 +1,7 @@ +import { dirname, join, relative } from 'node:path'; + import { logger } from 'storybook/internal/node-logger'; -import { dirname, join, relative } from 'path'; import PnpWebpackPlugin from 'pnp-webpack-plugin'; import type { Configuration, RuleSetRule, WebpackPluginInstance } from 'webpack'; diff --git a/code/presets/preact-webpack/src/index.ts b/code/presets/preact-webpack/src/index.ts index fbda4f2ac8fc..77023cb0d023 100644 --- a/code/presets/preact-webpack/src/index.ts +++ b/code/presets/preact-webpack/src/index.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { dirname, join } from 'node:path'; -import { dirname, join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; import type { StorybookConfig } from './types'; diff --git a/code/presets/react-webpack/src/cra-config.test.ts b/code/presets/react-webpack/src/cra-config.test.ts index 65097d6b9335..d60947708737 100644 --- a/code/presets/react-webpack/src/cra-config.test.ts +++ b/code/presets/react-webpack/src/cra-config.test.ts @@ -1,7 +1,7 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import fs from 'node:fs'; +import { join, sep } from 'node:path'; -import fs from 'fs'; -import path from 'path'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { getReactScriptsPath } from './cra-config'; @@ -11,7 +11,7 @@ vi.mock('fs', () => ({ existsSync: vi.fn(() => true), })); -const SCRIPT_PATH = path.join('.bin', 'react-scripts'); +const SCRIPT_PATH = join('.bin', 'react-scripts'); describe('cra-config', () => { describe('when used with the default react-scripts package', () => { @@ -23,7 +23,7 @@ describe('cra-config', () => { it('should locate the react-scripts package', () => { expect(getReactScriptsPath({ noCache: true })).toEqual( - path.join(path.sep, 'test-project', 'node_modules', 'react-scripts') + join(sep, 'test-project', 'node_modules', 'react-scripts') ); }); }); @@ -37,7 +37,7 @@ describe('cra-config', () => { it('should locate the react-scripts package', () => { expect(getReactScriptsPath({ noCache: true })).toEqual( - path.join(path.sep, 'test-project', 'node_modules', 'custom-react-scripts') + join(sep, 'test-project', 'node_modules', 'custom-react-scripts') ); }); }); @@ -69,7 +69,7 @@ exit $ret` it('should locate the react-scripts package', () => { expect(getReactScriptsPath({ noCache: true })).toEqual( - path.join(path.sep, 'test-project', 'node_modules', 'custom-react-scripts') + join(sep, 'test-project', 'node_modules', 'custom-react-scripts') ); }); }); diff --git a/code/presets/react-webpack/src/cra-config.ts b/code/presets/react-webpack/src/cra-config.ts index 1b4d9011b882..e764e0918ee7 100644 --- a/code/presets/react-webpack/src/cra-config.ts +++ b/code/presets/react-webpack/src/cra-config.ts @@ -1,19 +1,18 @@ +import { existsSync, readFileSync, realpathSync } from 'node:fs'; +import { join } from 'node:path'; + import { logger } from 'storybook/internal/node-logger'; -import fs from 'fs'; -import path from 'path'; import semver from 'semver'; -const appDirectory = fs.realpathSync(process.cwd()); +const appDirectory = realpathSync(process.cwd()); let reactScriptsPath: string; export function getReactScriptsPath({ noCache }: { noCache?: boolean } = {}) { if (reactScriptsPath && !noCache) return reactScriptsPath; - let reactScriptsScriptPath = fs.realpathSync( - path.join(appDirectory, '/node_modules/.bin/react-scripts') - ); + let reactScriptsScriptPath = realpathSync(join(appDirectory, '/node_modules/.bin/react-scripts')); try { // Note: Since there is no symlink for .bin/react-scripts on Windows @@ -25,27 +24,23 @@ export function getReactScriptsPath({ noCache }: { noCache?: boolean } = {}) { ); if (pathIsNotResolved) { - const content = fs.readFileSync(reactScriptsScriptPath, 'utf8'); + const content = readFileSync(reactScriptsScriptPath, 'utf8'); const packagePathMatch = content.match( /"\$basedir[\\/]([^\s]+?[\\/]bin[\\/]react-scripts\.js")/i ); if (packagePathMatch && packagePathMatch.length > 1) { - reactScriptsScriptPath = path.join( - appDirectory, - '/node_modules/.bin/', - packagePathMatch[1] - ); + reactScriptsScriptPath = join(appDirectory, '/node_modules/.bin/', packagePathMatch[1]); } } } catch (e) { logger.warn(`Error occurred during react-scripts package path resolving: ${e}`); } - reactScriptsPath = path.join(reactScriptsScriptPath, '../..'); - const scriptsPkgJson = path.join(reactScriptsPath, 'package.json'); + reactScriptsPath = join(reactScriptsScriptPath, '../..'); + const scriptsPkgJson = join(reactScriptsPath, 'package.json'); - if (!fs.existsSync(scriptsPkgJson)) { + if (!existsSync(scriptsPkgJson)) { reactScriptsPath = 'react-scripts'; } @@ -54,7 +49,7 @@ export function getReactScriptsPath({ noCache }: { noCache?: boolean } = {}) { export function isReactScriptsInstalled(requiredVersion = '2.0.0') { try { - const reactScriptsJson = require(path.join(getReactScriptsPath(), 'package.json')); + const reactScriptsJson = require(join(getReactScriptsPath(), 'package.json')); return !semver.gtr(requiredVersion, reactScriptsJson.version); } catch (e) { return false; diff --git a/code/presets/react-webpack/src/loaders/docgen-resolver.ts b/code/presets/react-webpack/src/loaders/docgen-resolver.ts index 71e2bde5d742..cc325855426a 100644 --- a/code/presets/react-webpack/src/loaders/docgen-resolver.ts +++ b/code/presets/react-webpack/src/loaders/docgen-resolver.ts @@ -1,4 +1,5 @@ -import { extname } from 'path'; +import { extname } from 'node:path'; + import resolve from 'resolve'; export class ReactDocgenResolveError extends Error { 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 1e8badb75727..0c44665bdc25 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,7 +1,8 @@ +import { join } from 'node:path'; + import { describe, expect, it } from 'vitest'; import fs from 'fs-extra'; -import path from 'path'; import YAML from 'yaml'; import { compileCsfModule } from '.'; @@ -16,12 +17,12 @@ async function generate(filePath: string) { const inputRegExp = new RegExp(`.${fileType}$`); describe(`${fileType}-to-csf-compiler`, () => { - const transformFixturesDir = path.join(__dirname, '__testfixtures__'); + const transformFixturesDir = join(__dirname, '__testfixtures__'); fs.readdirSync(transformFixturesDir) .filter((fileName: string) => inputRegExp.test(fileName)) .forEach((fixtureFile: string) => { it(`${fixtureFile}`, async () => { - const inputPath = path.join(transformFixturesDir, fixtureFile); + const inputPath = join(transformFixturesDir, fixtureFile); const code = await generate(inputPath); expect(code).toMatchFileSnapshot(inputPath.replace(inputRegExp, '.snapshot')); }); diff --git a/code/presets/svelte-webpack/src/svelte-docgen-loader.ts b/code/presets/svelte-webpack/src/svelte-docgen-loader.ts index 233a22ce0fc7..fd3a721e12c3 100644 --- a/code/presets/svelte-webpack/src/svelte-docgen-loader.ts +++ b/code/presets/svelte-webpack/src/svelte-docgen-loader.ts @@ -1,7 +1,8 @@ +import { readFile } from 'node:fs/promises'; +import { basename } from 'node:path'; + import { logger } from 'storybook/internal/node-logger'; -import * as fs from 'fs'; -import * as path from 'path'; import { preprocess } from 'svelte/compiler'; import svelteDoc from 'sveltedoc-parser'; import { dedent } from 'ts-dedent'; @@ -72,7 +73,7 @@ export default async function svelteDocgen(this: any, source: string) { let docOptions; if (preprocessOptions) { - const src = fs.readFileSync(resource).toString(); + const src = await readFile(resource).toString(); const { code: fileContent } = await preprocess(src, preprocessOptions); docOptions = { @@ -103,10 +104,10 @@ export default async function svelteDocgen(this: any, source: string) { } // get filename for source content - const file = path.basename(resource); + const file = basename(resource); // populate filename in docgen - componentDoc.name = path.basename(file); + componentDoc.name = basename(file); const componentName = getNameFromFilename(resource); diff --git a/code/renderers/html/src/preset.ts b/code/renderers/html/src/preset.ts index 892d48c5f077..c6cf333ccf3c 100644 --- a/code/renderers/html/src/preset.ts +++ b/code/renderers/html/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { join } from 'node:path'; -import { join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; export const previewAnnotations: PresetProperty<'previewAnnotations'> = async ( input = [], diff --git a/code/renderers/preact/src/preset.ts b/code/renderers/preact/src/preset.ts index 8e86705b6c21..e58147234dd0 100644 --- a/code/renderers/preact/src/preset.ts +++ b/code/renderers/preact/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { join } from 'node:path'; -import { join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; export const previewAnnotations: PresetProperty<'previewAnnotations'> = async ( input = [], diff --git a/code/renderers/react/src/docs/extractArgTypes.test.ts b/code/renderers/react/src/docs/extractArgTypes.test.ts index 687fb9c0d137..d6f612cbc285 100644 --- a/code/renderers/react/src/docs/extractArgTypes.test.ts +++ b/code/renderers/react/src/docs/extractArgTypes.test.ts @@ -1,3 +1,6 @@ +import { readdirSync } from 'node:fs'; +import { join, resolve } from 'node:path'; + import { describe, expect, it } from 'vitest'; import { normalizeNewlines } from 'storybook/internal/docs-tools'; @@ -5,8 +8,6 @@ import { inferControls } from 'storybook/internal/preview-api'; import type { Renderer } from 'storybook/internal/types'; import { transformFileSync, transformSync } from '@babel/core'; -import fs from 'fs'; -import path from 'path'; // @ts-expect-error (seems broken/missing) import requireFromString from 'require-from-string'; @@ -61,21 +62,21 @@ const skippedTests = [ describe('react component properties', () => { // Fixture files are in template/stories - const fixturesDir = path.resolve(__dirname, '../../template/stories/docgen-components'); - fs.readdirSync(fixturesDir, { withFileTypes: true }).forEach((testEntry) => { + const fixturesDir = resolve(__dirname, '../../template/stories/docgen-components'); + readdirSync(fixturesDir, { withFileTypes: true }).forEach((testEntry) => { if (testEntry.isDirectory()) { - const testDir = path.join(fixturesDir, testEntry.name); - const testFile = fs.readdirSync(testDir).find((fileName) => inputRegExp.test(fileName)); + const testDir = join(fixturesDir, testEntry.name); + const testFile = readdirSync(testDir).find((fileName) => inputRegExp.test(fileName)); if (testFile) { if (skippedTests.includes(testEntry.name)) { it.skip(`${testEntry.name}`, () => {}); } else { it(`${testEntry.name}`, () => { - const inputPath = path.join(testDir, testFile); + const inputPath = join(testDir, testFile); // snapshot the output of babel-plugin-react-docgen const docgenPretty = annotateWithDocgen(inputPath); - expect(docgenPretty).toMatchFileSnapshot(path.join(testDir, 'docgen.snapshot')); + expect(docgenPretty).toMatchFileSnapshot(join(testDir, 'docgen.snapshot')); // transform into an uglier format that's works with require-from-string const docgenModule = transformToModule(docgenPretty); @@ -83,7 +84,7 @@ describe('react component properties', () => { // snapshot the output of component-properties/react const { component } = requireFromString(docgenModule, inputPath); const properties = extractProps(component); - expect(properties).toMatchFileSnapshot(path.join(testDir, 'properties.snapshot')); + expect(properties).toMatchFileSnapshot(join(testDir, 'properties.snapshot')); // snapshot the output of `extractArgTypes` const argTypes = extractArgTypes(component); @@ -92,7 +93,7 @@ describe('react component properties', () => { argTypes, parameters, } as unknown as StoryContext); - expect(rows).toMatchFileSnapshot(path.join(testDir, 'argTypes.snapshot')); + expect(rows).toMatchFileSnapshot(join(testDir, 'argTypes.snapshot')); }); } } diff --git a/code/renderers/react/src/preset.ts b/code/renderers/react/src/preset.ts index fdd5add72485..c93dda141b79 100644 --- a/code/renderers/react/src/preset.ts +++ b/code/renderers/react/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { dirname, join } from 'node:path'; -import { dirname, join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; export const addons: PresetProperty<'addons'> = [ require.resolve('@storybook/react-dom-shim/dist/preset'), diff --git a/code/renderers/server/src/preset.ts b/code/renderers/server/src/preset.ts index 644545c094c1..018f50623eee 100644 --- a/code/renderers/server/src/preset.ts +++ b/code/renderers/server/src/preset.ts @@ -1,7 +1,8 @@ +import { join } from 'node:path'; + import type { ComponentTitle, PresetProperty, StoryName, Tag } from 'storybook/internal/types'; import fs from 'fs-extra'; -import { join } from 'path'; import yaml from 'yaml'; type FileContent = { diff --git a/code/renderers/svelte/src/docs/extractArgTypes.test.ts b/code/renderers/svelte/src/docs/extractArgTypes.test.ts index b30931a1e288..a43bdbd2ed75 100644 --- a/code/renderers/svelte/src/docs/extractArgTypes.test.ts +++ b/code/renderers/svelte/src/docs/extractArgTypes.test.ts @@ -1,11 +1,12 @@ +import { readFileSync } from 'node:fs'; + import { describe, expect, it } from 'vitest'; -import * as fs from 'fs'; import svelteDoc from 'sveltedoc-parser'; import { createArgTypes } from './extractArgTypes'; -const content = fs.readFileSync(`${__dirname}/sample/MockButton.svelte`, 'utf-8'); +const content = readFileSync(`${__dirname}/sample/MockButton.svelte`, 'utf-8'); describe('Extracting Arguments', () => { it('should be svelte', () => { expect(content).toMatchInlineSnapshot(` diff --git a/code/renderers/svelte/src/preset.ts b/code/renderers/svelte/src/preset.ts index 892d48c5f077..c6cf333ccf3c 100644 --- a/code/renderers/svelte/src/preset.ts +++ b/code/renderers/svelte/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { join } from 'node:path'; -import { join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; export const previewAnnotations: PresetProperty<'previewAnnotations'> = async ( input = [], diff --git a/code/renderers/vue3/src/preset.ts b/code/renderers/vue3/src/preset.ts index 892d48c5f077..c6cf333ccf3c 100644 --- a/code/renderers/vue3/src/preset.ts +++ b/code/renderers/vue3/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { join } from 'node:path'; -import { join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; export const previewAnnotations: PresetProperty<'previewAnnotations'> = async ( input = [], diff --git a/code/renderers/web-components/src/docs/web-components-properties.test.ts b/code/renderers/web-components/src/docs/web-components-properties.test.ts index fdb4f96ebc26..2ba3618f5bda 100644 --- a/code/renderers/web-components/src/docs/web-components-properties.test.ts +++ b/code/renderers/web-components/src/docs/web-components-properties.test.ts @@ -1,9 +1,10 @@ // @vitest-environment happy-dom +import { readFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; + import { describe, expect, it, vi } from 'vitest'; import { sync as spawnSync } from 'cross-spawn'; -import fs from 'fs'; -import path from 'path'; import tmp from 'tmp'; import { extractArgTypesFromElements } from './custom-elements'; @@ -16,14 +17,14 @@ const runWebComponentsAnalyzer = (inputPath: string) => { const { name: tmpDir, removeCallback } = tmp.dirSync(); const customElementsFile = `${tmpDir}/custom-elements.json`; spawnSync( - path.join(__dirname, '../../../../node_modules/.bin/wca'), + join(__dirname, '../../../../node_modules/.bin/wca'), ['analyze', inputPath, '--outFile', customElementsFile], { stdio: 'ignore', shell: true, } ); - const output = fs.readFileSync(customElementsFile, 'utf8'); + const output = readFileSync(customElementsFile, 'utf8'); try { removeCallback(); } catch (e) { @@ -39,14 +40,14 @@ describe('web-components component properties', () => { vi.mock('lit', () => ({ default: {} })); vi.mock('lit/directive-helpers.js', () => ({ default: {} })); - const fixturesDir = path.join(__dirname, '__testfixtures__'); - fs.readdirSync(fixturesDir, { withFileTypes: true }).forEach((testEntry) => { + const fixturesDir = join(__dirname, '__testfixtures__'); + readdirSync(fixturesDir, { withFileTypes: true }).forEach((testEntry) => { if (testEntry.isDirectory()) { - const testDir = path.join(fixturesDir, testEntry.name); - const testFile = fs.readdirSync(testDir).find((fileName) => inputRegExp.test(fileName)); + const testDir = join(fixturesDir, testEntry.name); + const testFile = readdirSync(testDir).find((fileName) => inputRegExp.test(fileName)); if (testFile) { it(`${testEntry.name}`, () => { - const inputPath = path.join(testDir, testFile); + const inputPath = join(testDir, testFile); // snapshot the output of wca const customElementsJson = runWebComponentsAnalyzer(inputPath); @@ -54,13 +55,11 @@ describe('web-components component properties', () => { customElements.tags.forEach((tag: any) => { tag.path = 'dummy-path-to-component'; }); - expect(customElements).toMatchFileSnapshot( - path.join(testDir, 'custom-elements.snapshot') - ); + expect(customElements).toMatchFileSnapshot(join(testDir, 'custom-elements.snapshot')); // snapshot the properties const properties = extractArgTypesFromElements('input', customElements); - expect(properties).toMatchFileSnapshot(path.join(testDir, 'properties.snapshot')); + expect(properties).toMatchFileSnapshot(join(testDir, 'properties.snapshot')); }); } } diff --git a/code/renderers/web-components/src/preset.ts b/code/renderers/web-components/src/preset.ts index 892d48c5f077..c6cf333ccf3c 100644 --- a/code/renderers/web-components/src/preset.ts +++ b/code/renderers/web-components/src/preset.ts @@ -1,6 +1,6 @@ -import type { PresetProperty } from 'storybook/internal/types'; +import { join } from 'node:path'; -import { join } from 'path'; +import type { PresetProperty } from 'storybook/internal/types'; export const previewAnnotations: PresetProperty<'previewAnnotations'> = async ( input = [], diff --git a/code/yarn.lock b/code/yarn.lock index 7d6ec8fe0d55..e0858c018dc0 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -2642,6 +2642,15 @@ __metadata: languageName: node linkType: hard +"@emnapi/runtime@npm:^1.1.1": + version: 1.2.0 + resolution: "@emnapi/runtime@npm:1.2.0" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10c0/7005ff8b67724c9e61b6cd79a3decbdb2ce25d24abd4d3d187472f200ee6e573329c30264335125fb136bd813aa9cf9f4f7c9391d04b07dd1e63ce0a3427be57 + languageName: node + linkType: hard + "@emotion/babel-plugin@npm:^11.11.0": version: 11.11.0 resolution: "@emotion/babel-plugin@npm:11.11.0" @@ -3567,6 +3576,18 @@ __metadata: languageName: node linkType: hard +"@img/sharp-darwin-arm64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-darwin-arm64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-darwin-arm64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-darwin-arm64": + optional: true + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@img/sharp-darwin-x64@npm:0.33.3": version: 0.33.3 resolution: "@img/sharp-darwin-x64@npm:0.33.3" @@ -3579,6 +3600,18 @@ __metadata: languageName: node linkType: hard +"@img/sharp-darwin-x64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-darwin-x64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-darwin-x64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-darwin-x64": + optional: true + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@img/sharp-libvips-darwin-arm64@npm:1.0.2": version: 1.0.2 resolution: "@img/sharp-libvips-darwin-arm64@npm:1.0.2" @@ -3647,6 +3680,18 @@ __metadata: languageName: node linkType: hard +"@img/sharp-linux-arm64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linux-arm64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linux-arm64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linux-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@img/sharp-linux-arm@npm:0.33.3": version: 0.33.3 resolution: "@img/sharp-linux-arm@npm:0.33.3" @@ -3659,6 +3704,18 @@ __metadata: languageName: node linkType: hard +"@img/sharp-linux-arm@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linux-arm@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linux-arm": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linux-arm": + optional: true + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + "@img/sharp-linux-s390x@npm:0.33.3": version: 0.33.3 resolution: "@img/sharp-linux-s390x@npm:0.33.3" @@ -3671,6 +3728,18 @@ __metadata: languageName: node linkType: hard +"@img/sharp-linux-s390x@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linux-s390x@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linux-s390x": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linux-s390x": + optional: true + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@img/sharp-linux-x64@npm:0.33.3": version: 0.33.3 resolution: "@img/sharp-linux-x64@npm:0.33.3" @@ -3683,6 +3752,18 @@ __metadata: languageName: node linkType: hard +"@img/sharp-linux-x64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linux-x64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linux-x64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linux-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@img/sharp-linuxmusl-arm64@npm:0.33.3": version: 0.33.3 resolution: "@img/sharp-linuxmusl-arm64@npm:0.33.3" @@ -3695,6 +3776,18 @@ __metadata: languageName: node linkType: hard +"@img/sharp-linuxmusl-arm64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linuxmusl-arm64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@img/sharp-linuxmusl-x64@npm:0.33.3": version: 0.33.3 resolution: "@img/sharp-linuxmusl-x64@npm:0.33.3" @@ -3707,6 +3800,18 @@ __metadata: languageName: node linkType: hard +"@img/sharp-linuxmusl-x64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-linuxmusl-x64@npm:0.33.4" + dependencies: + "@img/sharp-libvips-linuxmusl-x64": "npm:1.0.2" + dependenciesMeta: + "@img/sharp-libvips-linuxmusl-x64": + optional: true + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@img/sharp-wasm32@npm:0.33.3": version: 0.33.3 resolution: "@img/sharp-wasm32@npm:0.33.3" @@ -3716,6 +3821,15 @@ __metadata: languageName: node linkType: hard +"@img/sharp-wasm32@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-wasm32@npm:0.33.4" + dependencies: + "@emnapi/runtime": "npm:^1.1.1" + conditions: cpu=wasm32 + languageName: node + linkType: hard + "@img/sharp-win32-ia32@npm:0.33.3": version: 0.33.3 resolution: "@img/sharp-win32-ia32@npm:0.33.3" @@ -3723,6 +3837,13 @@ __metadata: languageName: node linkType: hard +"@img/sharp-win32-ia32@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-win32-ia32@npm:0.33.4" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@img/sharp-win32-x64@npm:0.33.3": version: 0.33.3 resolution: "@img/sharp-win32-x64@npm:0.33.3" @@ -3730,6 +3851,13 @@ __metadata: languageName: node linkType: hard +"@img/sharp-win32-x64@npm:0.33.4": + version: 0.33.4 + resolution: "@img/sharp-win32-x64@npm:0.33.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@inquirer/confirm@npm:^3.0.0": version: 3.1.20 resolution: "@inquirer/confirm@npm:3.1.20" @@ -4053,6 +4181,13 @@ __metadata: languageName: node linkType: hard +"@next/env@npm:14.2.5, @next/env@npm:^14.2.5": + version: 14.2.5 + resolution: "@next/env@npm:14.2.5" + checksum: 10c0/63d8b88ac450b3c37940a9e2119a63a1074aca89908574ade6157a8aa295275dcb3ac5f69e00883fc55d0f12963b73b74e87ba32a5768a489f9609c6be57b699 + languageName: node + linkType: hard + "@next/swc-darwin-arm64@npm:14.1.0": version: 14.1.0 resolution: "@next/swc-darwin-arm64@npm:14.1.0" @@ -4060,6 +4195,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-darwin-arm64@npm:14.2.5": + version: 14.2.5 + resolution: "@next/swc-darwin-arm64@npm:14.2.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@next/swc-darwin-x64@npm:14.1.0": version: 14.1.0 resolution: "@next/swc-darwin-x64@npm:14.1.0" @@ -4067,6 +4209,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-darwin-x64@npm:14.2.5": + version: 14.2.5 + resolution: "@next/swc-darwin-x64@npm:14.2.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@next/swc-linux-arm64-gnu@npm:14.1.0": version: 14.1.0 resolution: "@next/swc-linux-arm64-gnu@npm:14.1.0" @@ -4074,6 +4223,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-arm64-gnu@npm:14.2.5": + version: 14.2.5 + resolution: "@next/swc-linux-arm64-gnu@npm:14.2.5" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@next/swc-linux-arm64-musl@npm:14.1.0": version: 14.1.0 resolution: "@next/swc-linux-arm64-musl@npm:14.1.0" @@ -4081,6 +4237,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-arm64-musl@npm:14.2.5": + version: 14.2.5 + resolution: "@next/swc-linux-arm64-musl@npm:14.2.5" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@next/swc-linux-x64-gnu@npm:14.1.0": version: 14.1.0 resolution: "@next/swc-linux-x64-gnu@npm:14.1.0" @@ -4088,6 +4251,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-x64-gnu@npm:14.2.5": + version: 14.2.5 + resolution: "@next/swc-linux-x64-gnu@npm:14.2.5" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@next/swc-linux-x64-musl@npm:14.1.0": version: 14.1.0 resolution: "@next/swc-linux-x64-musl@npm:14.1.0" @@ -4095,6 +4265,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-x64-musl@npm:14.2.5": + version: 14.2.5 + resolution: "@next/swc-linux-x64-musl@npm:14.2.5" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@next/swc-win32-arm64-msvc@npm:14.1.0": version: 14.1.0 resolution: "@next/swc-win32-arm64-msvc@npm:14.1.0" @@ -4102,6 +4279,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-win32-arm64-msvc@npm:14.2.5": + version: 14.2.5 + resolution: "@next/swc-win32-arm64-msvc@npm:14.2.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@next/swc-win32-ia32-msvc@npm:14.1.0": version: 14.1.0 resolution: "@next/swc-win32-ia32-msvc@npm:14.1.0" @@ -4109,6 +4293,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-win32-ia32-msvc@npm:14.2.5": + version: 14.2.5 + resolution: "@next/swc-win32-ia32-msvc@npm:14.2.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@next/swc-win32-x64-msvc@npm:14.1.0": version: 14.1.0 resolution: "@next/swc-win32-x64-msvc@npm:14.1.0" @@ -4116,6 +4307,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-win32-x64-msvc@npm:14.2.5": + version: 14.2.5 + resolution: "@next/swc-win32-x64-msvc@npm:14.2.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@ngtools/webpack@npm:17.3.0": version: 17.3.0 resolution: "@ngtools/webpack@npm:17.3.0" @@ -6285,6 +6483,35 @@ __metadata: languageName: unknown linkType: soft +"@storybook/experimental-nextjs-vite@workspace:frameworks/experimental-nextjs-vite": + version: 0.0.0-use.local + resolution: "@storybook/experimental-nextjs-vite@workspace:frameworks/experimental-nextjs-vite" + dependencies: + "@storybook/builder-vite": "workspace:*" + "@storybook/react": "workspace:*" + "@storybook/test": "workspace:*" + "@types/node": "npm:^18.0.0" + next: "npm:^14.2.5" + sharp: "npm:^0.33.3" + styled-jsx: "npm:5.1.6" + typescript: "npm:^5.3.2" + vite-plugin-storybook-nextjs: "npm:^1.0.0" + peerDependencies: + next: ^14.2.5 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: "workspace:^" + vite: ^5.0.0 + vite-plugin-storybook-nextjs: ^1.0.0 + dependenciesMeta: + sharp: + optional: true + peerDependenciesMeta: + typescript: + optional: true + languageName: unknown + linkType: soft + "@storybook/global@npm:^5.0.0": version: 5.0.0 resolution: "@storybook/global@npm:5.0.0" @@ -7298,6 +7525,13 @@ __metadata: languageName: node linkType: hard +"@swc/counter@npm:^0.1.3": + version: 0.1.3 + resolution: "@swc/counter@npm:0.1.3" + checksum: 10c0/8424f60f6bf8694cfd2a9bca45845bce29f26105cda8cf19cdb9fd3e78dc6338699e4db77a89ae449260bafa1cc6bec307e81e7fb96dbf7dcfce0eea55151356 + languageName: node + linkType: hard + "@swc/helpers@npm:0.5.2": version: 0.5.2 resolution: "@swc/helpers@npm:0.5.2" @@ -7307,6 +7541,16 @@ __metadata: languageName: node linkType: hard +"@swc/helpers@npm:0.5.5": + version: 0.5.5 + resolution: "@swc/helpers@npm:0.5.5" + dependencies: + "@swc/counter": "npm:^0.1.3" + tslib: "npm:^2.4.0" + checksum: 10c0/21a9b9cfe7e00865f9c9f3eb4c1cc5b397143464f7abee76a2c5366e591e06b0155b5aac93fe8269ef8d548df253f6fd931e9ddfc0fd12efd405f90f45506e7d + languageName: node + linkType: hard + "@swc/helpers@npm:~0.5.0": version: 0.5.6 resolution: "@swc/helpers@npm:0.5.6" @@ -17260,6 +17504,17 @@ __metadata: languageName: node linkType: hard +"image-size@npm:^1.1.1": + version: 1.1.1 + resolution: "image-size@npm:1.1.1" + dependencies: + queue: "npm:6.0.2" + bin: + image-size: bin/image-size.js + checksum: 10c0/2660470096d12be82195f7e80fe03274689fbd14184afb78eaf66ade7cd06352518325814f88af4bde4b26647889fe49e573129f6e7ba8f5ff5b85cc7f559000 + languageName: node + linkType: hard + "image-size@npm:~0.5.0": version: 0.5.5 resolution: "image-size@npm:0.5.5" @@ -19423,7 +19678,7 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.30.0, magic-string@npm:^0.30.10, magic-string@npm:^0.30.4, magic-string@npm:^0.30.5": +"magic-string@npm:^0.30.0, magic-string@npm:^0.30.10, magic-string@npm:^0.30.11, magic-string@npm:^0.30.4, magic-string@npm:^0.30.5": version: 0.30.11 resolution: "magic-string@npm:0.30.11" dependencies: @@ -21028,6 +21283,13 @@ __metadata: languageName: node linkType: hard +"module-alias@npm:^2.2.3": + version: 2.2.3 + resolution: "module-alias@npm:2.2.3" + checksum: 10c0/47dc5b6d04f6e7df0ff330ca9b2a37c688a682ed661e9432b0b327e1e6c43eedad052151b8d50d6beea8b924828d2a92fa4625c18d651bf2d93d8f03aa0172fa + languageName: node + linkType: hard + "mri@npm:^1.1.0, mri@npm:^1.2.0": version: 1.2.0 resolution: "mri@npm:1.2.0" @@ -21254,6 +21516,64 @@ __metadata: languageName: node linkType: hard +"next@npm:^14.2.5": + version: 14.2.5 + resolution: "next@npm:14.2.5" + dependencies: + "@next/env": "npm:14.2.5" + "@next/swc-darwin-arm64": "npm:14.2.5" + "@next/swc-darwin-x64": "npm:14.2.5" + "@next/swc-linux-arm64-gnu": "npm:14.2.5" + "@next/swc-linux-arm64-musl": "npm:14.2.5" + "@next/swc-linux-x64-gnu": "npm:14.2.5" + "@next/swc-linux-x64-musl": "npm:14.2.5" + "@next/swc-win32-arm64-msvc": "npm:14.2.5" + "@next/swc-win32-ia32-msvc": "npm:14.2.5" + "@next/swc-win32-x64-msvc": "npm:14.2.5" + "@swc/helpers": "npm:0.5.5" + busboy: "npm:1.6.0" + caniuse-lite: "npm:^1.0.30001579" + graceful-fs: "npm:^4.2.11" + postcss: "npm:8.4.31" + styled-jsx: "npm:5.1.1" + peerDependencies: + "@opentelemetry/api": ^1.1.0 + "@playwright/test": ^1.41.2 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + dependenciesMeta: + "@next/swc-darwin-arm64": + optional: true + "@next/swc-darwin-x64": + optional: true + "@next/swc-linux-arm64-gnu": + optional: true + "@next/swc-linux-arm64-musl": + optional: true + "@next/swc-linux-x64-gnu": + optional: true + "@next/swc-linux-x64-musl": + optional: true + "@next/swc-win32-arm64-msvc": + optional: true + "@next/swc-win32-ia32-msvc": + optional: true + "@next/swc-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@opentelemetry/api": + optional: true + "@playwright/test": + optional: true + sass: + optional: true + bin: + next: dist/bin/next + checksum: 10c0/8df7d8ccc1a5bab03fa50dd6656c8a6f3750e81ef0b087dc329fea9346847c3094a933a890a8e87151dc32f0bc55020b8f6386d4565856d83bcc10895d29ec08 + languageName: node + linkType: hard + "nice-napi@npm:^1.0.2": version: 1.0.2 resolution: "nice-napi@npm:1.0.2" @@ -25555,6 +25875,75 @@ __metadata: languageName: node linkType: hard +"sharp@npm:^0.33.4": + version: 0.33.4 + resolution: "sharp@npm:0.33.4" + dependencies: + "@img/sharp-darwin-arm64": "npm:0.33.4" + "@img/sharp-darwin-x64": "npm:0.33.4" + "@img/sharp-libvips-darwin-arm64": "npm:1.0.2" + "@img/sharp-libvips-darwin-x64": "npm:1.0.2" + "@img/sharp-libvips-linux-arm": "npm:1.0.2" + "@img/sharp-libvips-linux-arm64": "npm:1.0.2" + "@img/sharp-libvips-linux-s390x": "npm:1.0.2" + "@img/sharp-libvips-linux-x64": "npm:1.0.2" + "@img/sharp-libvips-linuxmusl-arm64": "npm:1.0.2" + "@img/sharp-libvips-linuxmusl-x64": "npm:1.0.2" + "@img/sharp-linux-arm": "npm:0.33.4" + "@img/sharp-linux-arm64": "npm:0.33.4" + "@img/sharp-linux-s390x": "npm:0.33.4" + "@img/sharp-linux-x64": "npm:0.33.4" + "@img/sharp-linuxmusl-arm64": "npm:0.33.4" + "@img/sharp-linuxmusl-x64": "npm:0.33.4" + "@img/sharp-wasm32": "npm:0.33.4" + "@img/sharp-win32-ia32": "npm:0.33.4" + "@img/sharp-win32-x64": "npm:0.33.4" + color: "npm:^4.2.3" + detect-libc: "npm:^2.0.3" + semver: "npm:^7.6.0" + dependenciesMeta: + "@img/sharp-darwin-arm64": + optional: true + "@img/sharp-darwin-x64": + optional: true + "@img/sharp-libvips-darwin-arm64": + optional: true + "@img/sharp-libvips-darwin-x64": + optional: true + "@img/sharp-libvips-linux-arm": + optional: true + "@img/sharp-libvips-linux-arm64": + optional: true + "@img/sharp-libvips-linux-s390x": + optional: true + "@img/sharp-libvips-linux-x64": + optional: true + "@img/sharp-libvips-linuxmusl-arm64": + optional: true + "@img/sharp-libvips-linuxmusl-x64": + optional: true + "@img/sharp-linux-arm": + optional: true + "@img/sharp-linux-arm64": + optional: true + "@img/sharp-linux-s390x": + optional: true + "@img/sharp-linux-x64": + optional: true + "@img/sharp-linuxmusl-arm64": + optional: true + "@img/sharp-linuxmusl-x64": + optional: true + "@img/sharp-wasm32": + optional: true + "@img/sharp-win32-ia32": + optional: true + "@img/sharp-win32-x64": + optional: true + checksum: 10c0/428c5c6a84ff8968effe50c2de931002f5f30b9f263e1c026d0384e581673c13088a49322f7748114d3d9be4ae9476a74bf003a3af34743e97ef2f880d1cfe45 + languageName: node + linkType: hard + "shebang-command@npm:^1.2.0": version: 1.2.0 resolution: "shebang-command@npm:1.2.0" @@ -26458,6 +26847,22 @@ __metadata: languageName: node linkType: hard +"styled-jsx@npm:5.1.6": + version: 5.1.6 + resolution: "styled-jsx@npm:5.1.6" + dependencies: + client-only: "npm:0.0.1" + peerDependencies: + react: ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + peerDependenciesMeta: + "@babel/core": + optional: true + babel-plugin-macros: + optional: true + checksum: 10c0/ace50e7ea5ae5ae6a3b65a50994c51fca6ae7df9c7ecfd0104c36be0b4b3a9c5c1a2374d16e2a11e256d0b20be6d47256d768ecb4f91ab390f60752a075780f5 + languageName: node + linkType: hard + "stylis@npm:4.2.0": version: 4.2.0 resolution: "stylis@npm:4.2.0" @@ -28387,6 +28792,28 @@ __metadata: languageName: node linkType: hard +"vite-plugin-storybook-nextjs@npm:^1.0.0": + version: 1.0.0 + resolution: "vite-plugin-storybook-nextjs@npm:1.0.0" + dependencies: + "@next/env": "npm:^14.2.5" + image-size: "npm:^1.1.1" + magic-string: "npm:^0.30.11" + module-alias: "npm:^2.2.3" + sharp: "npm:^0.33.4" + ts-dedent: "npm:^2.2.0" + peerDependencies: + "@storybook/test": ^8.3.0-alpha.3 + next: ^14.2.5 + storybook: ^8.3.0-alpha.3 + vite: ^5.0.0 + dependenciesMeta: + sharp: + optional: true + checksum: 10c0/6ca17326e0387044d7bfa4373e6ccb64e8bb5bec1f19898ba9b8338c7817d8bea0fb01169adfb623f652fded5e6f59170129f7c8c4d4c3c54ca3764727e5a195 + languageName: node + linkType: hard + "vite@npm:5.1.5, vite@npm:^5.0.0": version: 5.1.5 resolution: "vite@npm:5.1.5" diff --git a/docs/_snippets/nextjs-vite-add-framework.md b/docs/_snippets/nextjs-vite-add-framework.md new file mode 100644 index 000000000000..ac874f278550 --- /dev/null +++ b/docs/_snippets/nextjs-vite-add-framework.md @@ -0,0 +1,19 @@ +```js filename=".storybook/main.js" renderer="react" language="js" +export default { + // ... + // framework: '@storybook/react-webpack5', 👈 Remove this + framework: '@storybook/experimental-nextjs-vite', // 👈 Add this +}; +``` + +```ts filename=".storybook/main.ts" renderer="react" language="ts" +import { StorybookConfig } from '@storybook/experimental-nextjs-vite'; + +const config: StorybookConfig = { + // ... + // framework: '@storybook/react-webpack5', 👈 Remove this + framework: '@storybook/experimental-nextjs-vite', // 👈 Add this +}; + +export default config; +``` diff --git a/docs/_snippets/nextjs-vite-install.md b/docs/_snippets/nextjs-vite-install.md new file mode 100644 index 000000000000..83568e30cfa4 --- /dev/null +++ b/docs/_snippets/nextjs-vite-install.md @@ -0,0 +1,11 @@ +```shell renderer="react" language="js" packageManager="npm" +npm install --save-dev @storybook/experimental-nextjs-vite +``` + +```shell renderer="react" language="js" packageManager="pnpm" +pnpm add --save-dev @storybook/experimental-nextjs-vite +``` + +```shell renderer="react" language="js" packageManager="yarn" +yarn add --dev @storybook/experimental-nextjs-vite +``` diff --git a/docs/_snippets/nextjs-vite-remove-addons.md b/docs/_snippets/nextjs-vite-remove-addons.md new file mode 100644 index 000000000000..85a01de7df21 --- /dev/null +++ b/docs/_snippets/nextjs-vite-remove-addons.md @@ -0,0 +1,27 @@ +```js filename=".storybook/main.js" renderer="react" language="js" +export default { + // ... + addons: [ + // ... + // 👇 These can both be removed + // 'storybook-addon-next', + // 'storybook-addon-next-router', + ], +}; +``` + +```ts filename=".storybook/main.ts" renderer="react" language="ts" +import { StorybookConfig } from '@storybook/experimental-nextjs-vite'; + +const config: StorybookConfig = { + // ... + addons: [ + // ... + // 👇 These can both be removed + // 'storybook-addon-next', + // 'storybook-addon-next-router', + ], +}; + +export default config; +``` diff --git a/docs/get-started/frameworks/nextjs.mdx b/docs/get-started/frameworks/nextjs.mdx index e6322452d6a2..5a1efc07fc29 100644 --- a/docs/get-started/frameworks/nextjs.mdx +++ b/docs/get-started/frameworks/nextjs.mdx @@ -82,6 +82,38 @@ Storybook for Next.js is a [framework](../../contribute/framework.mdx) that make {/* prettier-ignore-end */} + #### With Vite + + (⚠️ **Experimental**) + + You can use our freshly baked, experimental `@storybook/experimental-nextjs-vite` framework, which is based on Vite and removes the need for Webpack and Babel. It supports all of the features documented here. + + {/* prettier-ignore-start */} + + + + {/* prettier-ignore-end */} + + Then, update your `.storybook/main.js|ts` to change the framework property: + + {/* prettier-ignore-start */} + + + + {/* prettier-ignore-end */} + + + If your Storybook configuration contains custom Webpack operations in [`webpackFinal`](../../api/main-config/main-config-webpack-final.mdx), you will likely need to create equivalents in [`viteFinal`]((../../api/main-config/main-config-vite-final.mdx)). + + + Finally, if you were using Storybook plugins to integrate with Next.js, those are no longer necessary when using this framework and can be removed: + + {/* prettier-ignore-start */} + + + + {/* prettier-ignore-end */} + ## Run the Setup Wizard If all goes well, you should see a setup wizard that will help you get started with Storybook introducing you to the main concepts and features, including how the UI is organized, how to write your first story, and how to test your components' response to various inputs utilizing [controls](../../essentials/controls.mdx). @@ -162,6 +194,12 @@ Storybook for Next.js is a [framework](../../contribute/framework.mdx) that make const localRubikStorm = localFont({ src: './fonts/RubikStorm-Regular.ttf' }); ``` + #### `staticDir` mapping + + + You can safely skip this section if you are using [`@storybook/experimental-nextjs-vite`](#with-vite) instead of `@storybook/nextjs`. The Vite-based framework takes care of the mapping automatically. + + You have to tell Storybook where the `fonts` directory is located, via the [`staticDirs` configuration](../../api/main-config/main-config-static-dirs.mdx#with-configuration-objects). The `from` value is relative to the `.storybook` directory. The `to` value is relative to the execution context of Storybook. Very likely it is the root of your project. {/* prettier-ignore-start */} @@ -714,6 +752,11 @@ Storybook for Next.js is a [framework](../../contribute/framework.mdx) that make ## Custom Webpack config + + You can safely skip this section if you are using `@storybook/experimental-nextjs-vite` instead of `@storybook/nextjs`. + The Vite-based Next.js framework does not support Webpack settings. + + Next.js comes with a lot of things for free out of the box like Sass support, but sometimes you add [custom Webpack config modifications to Next.js](https://nextjs.org/docs/pages/api-reference/next-config-js/webpack). This framework takes care of most of the Webpack modifications you would want to add. If Next.js supports a feature out of the box, then that feature will work out of the box in Storybook. If Next.js doesn't support something out of the box, but makes it easy to configure, then this framework will do the same for that thing for Storybook. Any Webpack modifications desired for Storybook should be made in [`.storybook/main.js|ts`](../../builders/webpack.mdx#extending-storybooks-webpack-config). @@ -860,7 +903,7 @@ Storybook for Next.js is a [framework](../../contribute/framework.mdx) that make ### What if I'm using the Vite builder? - The `@storybook/nextjs` package abstracts the Webpack 5 builder and provides all the necessary Webpack configuration needed (and used internally) by Next.js. Webpack is currently the official builder in Next.js, and Next.js does not support Vite, therefore it is not possible to use Vite with `@storybook/nextjs`. You can use `@storybook/react-vite` framework instead, but at the cost of having a degraded experience, and we won't be able to provide you official support. + We have introduced experimental Vite builder support. Just install the experimental framework package `@storybook/experimental-nextjs-vite` and replace all instances of `@storybook/nextjs` with `@storybook/experimental-nextjs-vite`. ### Error: You are importing avif images, but you don't have sharp installed. You have to install sharp in order to use image optimization features in Next.js. diff --git a/scripts/create-nx-sandbox-projects.ts b/scripts/create-nx-sandbox-projects.ts index e58eabd04710..5de0d60bc2e9 100644 --- a/scripts/create-nx-sandbox-projects.ts +++ b/scripts/create-nx-sandbox-projects.ts @@ -1,5 +1,5 @@ -import * as fs from 'fs'; -import * as path from 'path'; +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; import * as templates from '../code/lib/cli-storybook/src/sandbox-templates'; @@ -31,7 +31,7 @@ const projectJson = (name: string, framework: string, tags: string[]) => ({ }); Object.entries(allTemplates).forEach(([key, value]) => { const p = key.replaceAll('/', '-'); - const full = path.join(process.cwd(), '../code/sandbox', p, 'project.json'); + const full = join(process.cwd(), '../code/sandbox', p, 'project.json'); console.log(full); const framework = value.expected.framework.replace('@storybook/', ''); @@ -44,7 +44,7 @@ Object.entries(allTemplates).forEach(([key, value]) => { ]; ensureDirectoryExistence(full); console.log(full); - fs.writeFileSync( + writeFileSync( full, '// auto-generated from scripts/create-nx-sandbox-projects.ts\n' + JSON.stringify(projectJson(key, framework, tags), null, 2), @@ -55,10 +55,10 @@ Object.entries(allTemplates).forEach(([key, value]) => { }); function ensureDirectoryExistence(filePath: string): void { - const dirname = path.dirname(filePath); - if (fs.existsSync(dirname)) { + const dir = dirname(filePath); + if (existsSync(dir)) { return; } - ensureDirectoryExistence(dirname); - fs.mkdirSync(dirname); + ensureDirectoryExistence(dir); + mkdirSync(dir); } diff --git a/scripts/prepare/addon-bundle.ts b/scripts/prepare/addon-bundle.ts index 820374238c91..ac133caeb148 100755 --- a/scripts/prepare/addon-bundle.ts +++ b/scripts/prepare/addon-bundle.ts @@ -2,7 +2,7 @@ import aliasPlugin from 'esbuild-plugin-alias'; import * as fs from 'fs-extra'; import { glob } from 'glob'; import { builtinModules } from 'node:module'; -import path, { dirname, join, relative } from 'path'; +import { dirname, join, relative } from 'path'; import slash from 'slash'; import { dedent } from 'ts-dedent'; import type { Options } from 'tsup'; @@ -293,11 +293,11 @@ function getESBuildOptions(optimized: boolean) { } async function generateDTSMapperFile(file: string) { - const { name: entryName, dir } = path.parse(file); + const { name: entryName, dir } = parse(file); const pathName = join(process.cwd(), dir.replace('./src', 'dist'), `${entryName}.d.ts`); const srcName = join(process.cwd(), file); - const rel = relative(dirname(pathName), dirname(srcName)).split(path.sep).join(path.posix.sep); + const rel = relative(dirname(pathName), dirname(srcName)).split(sep).join(posix.sep); await fs.ensureFile(pathName); await fs.writeFile( diff --git a/scripts/prepare/bundle.ts b/scripts/prepare/bundle.ts index 9d51b477d997..81a7b301fe41 100755 --- a/scripts/prepare/bundle.ts +++ b/scripts/prepare/bundle.ts @@ -1,7 +1,8 @@ +import { dirname, join, parse, posix, relative, resolve, sep } from 'node:path'; + import aliasPlugin from 'esbuild-plugin-alias'; import * as fs from 'fs-extra'; import { glob } from 'glob'; -import path, { dirname, join, relative } from 'path'; import slash from 'slash'; import { dedent } from 'ts-dedent'; import type { Options } from 'tsup'; @@ -79,7 +80,7 @@ const run = async ({ cwd, flags }: { cwd: string; flags: string[] }) => { * Generating an ESM file for them anyway is problematic because they often have a reference to `require`. * TSUP generated code will then have a `require` polyfill/guard in the ESM files, which causes issues for webpack. */ - const nonPresetEntries = allEntries.filter((f) => !path.parse(f).name.includes('preset')); + const nonPresetEntries = allEntries.filter((f) => !parse(f).name.includes('preset')); const noExternal = [...extraNoExternal]; @@ -104,8 +105,8 @@ const run = async ({ cwd, flags }: { cwd: string; flags: string[] }) => { ? [] : [ aliasPlugin({ - process: path.resolve('../node_modules/process/browser.js'), - util: path.resolve('../node_modules/util/util.js'), + process: resolve('../node_modules/process/browser.js'), + util: resolve('../node_modules/util/util.js'), }), ], external: externals, @@ -207,11 +208,11 @@ function getESBuildOptions(optimized: boolean) { } async function generateDTSMapperFile(file: string) { - const { name: entryName, dir } = path.parse(file); + const { name: entryName, dir } = parse(file); const pathName = join(process.cwd(), dir.replace('./src', 'dist'), `${entryName}.d.ts`); const srcName = join(process.cwd(), file); - const rel = relative(dirname(pathName), dirname(srcName)).split(path.sep).join(path.posix.sep); + const rel = relative(dirname(pathName), dirname(srcName)).split(sep).join(posix.sep); await fs.ensureFile(pathName); await fs.writeFile( diff --git a/scripts/prepare/tsc.ts b/scripts/prepare/tsc.ts index 92669cbd7204..6a6aba2643bd 100755 --- a/scripts/prepare/tsc.ts +++ b/scripts/prepare/tsc.ts @@ -1,4 +1,4 @@ -import fs, { move } from 'fs-extra'; +import { emptyDir, move, readJson } from 'fs-extra'; import { globSync } from 'glob'; import { join } from 'path'; import * as ts from 'typescript'; @@ -10,7 +10,7 @@ const hasFlag = (flags: string[], name: string) => !!flags.find((s) => s.startsW const run = async ({ cwd, flags }: { cwd: string; flags: string[] }) => { const { bundler: { pre, post, tsConfig: tsconfigPath = 'tsconfig.json' }, - } = await fs.readJson(join(cwd, 'package.json')); + } = await readJson(join(cwd, 'package.json')); if (pre) { await exec(`jiti ${pre}`, { cwd }); @@ -21,7 +21,7 @@ const run = async ({ cwd, flags }: { cwd: string; flags: string[] }) => { // const optimized = hasFlag(flags, 'optimized'); if (reset) { - await fs.emptyDir(join(process.cwd(), 'dist')); + await emptyDir(join(process.cwd(), 'dist')); } const content = ts.readJsonConfigFile(tsconfigPath, ts.sys.readFile); diff --git a/scripts/release/__tests__/is-pr-frozen.test.ts b/scripts/release/__tests__/is-pr-frozen.test.ts index 8ecd4e00a522..681069af7256 100644 --- a/scripts/release/__tests__/is-pr-frozen.test.ts +++ b/scripts/release/__tests__/is-pr-frozen.test.ts @@ -1,8 +1,9 @@ /* eslint-disable no-underscore-dangle */ +import { join } from 'node:path'; + import { describe, expect, it, vi } from 'vitest'; import * as fsExtraImp from 'fs-extra'; -import path from 'path'; import * as simpleGitImp from 'simple-git'; import type * as MockedFSExtra from '../../../code/__mocks__/fs-extra'; @@ -18,7 +19,7 @@ vi.mock('fs-extra', async () => import('../../../code/__mocks__/fs-extra')); const fsExtra = fsExtraImp as unknown as typeof MockedFSExtra; const simpleGit = simpleGitImp as unknown as typeof MockedSimpleGit; -const CODE_PACKAGE_JSON_PATH = path.join(CODE_DIRECTORY, 'package.json'); +const CODE_PACKAGE_JSON_PATH = join(CODE_DIRECTORY, 'package.json'); fsExtra.__setMockFiles({ [CODE_PACKAGE_JSON_PATH]: JSON.stringify({ version: '1.0.0' }), diff --git a/scripts/release/__tests__/version.test.ts b/scripts/release/__tests__/version.test.ts index 2ad491704a59..3572e4cb76ee 100644 --- a/scripts/release/__tests__/version.test.ts +++ b/scripts/release/__tests__/version.test.ts @@ -1,9 +1,10 @@ /* eslint-disable no-underscore-dangle */ +import { join } from 'node:path'; + import { describe, expect, it, vi } from 'vitest'; import { execaCommand } from 'execa'; import * as fsExtraImp from 'fs-extra'; -import path from 'path'; import type * as MockedFSToExtra from '../../../code/__mocks__/fs-extra'; import { run as version } from '../version'; @@ -31,17 +32,11 @@ vi.spyOn(console, 'warn').mockImplementation(() => {}); vi.spyOn(console, 'error').mockImplementation(() => {}); describe('Version', () => { - const CODE_DIR_PATH = path.join(__dirname, '..', '..', '..', 'code'); - const CODE_PACKAGE_JSON_PATH = path.join(CODE_DIR_PATH, 'package.json'); - const MANAGER_API_VERSION_PATH = path.join( - CODE_DIR_PATH, - 'core', - 'src', - 'manager-api', - 'version.ts' - ); - const VERSIONS_PATH = path.join(CODE_DIR_PATH, 'core', 'src', 'common', 'versions.ts'); - const A11Y_PACKAGE_JSON_PATH = path.join(CODE_DIR_PATH, 'addons', 'a11y', 'package.json'); + const CODE_DIR_PATH = join(__dirname, '..', '..', '..', 'code'); + const CODE_PACKAGE_JSON_PATH = join(CODE_DIR_PATH, 'package.json'); + const MANAGER_API_VERSION_PATH = join(CODE_DIR_PATH, 'core', 'src', 'manager-api', 'version.ts'); + const VERSIONS_PATH = join(CODE_DIR_PATH, 'core', 'src', 'common', 'versions.ts'); + const A11Y_PACKAGE_JSON_PATH = join(CODE_DIR_PATH, 'addons', 'a11y', 'package.json'); it('should throw when release type is invalid', async () => { fsExtra.__setMockFiles({ @@ -285,7 +280,7 @@ describe('Version', () => { { spaces: 2 } ); expect(execaCommand).toHaveBeenCalledWith('yarn install --mode=update-lockfile', { - cwd: path.join(CODE_DIR_PATH), + cwd: join(CODE_DIR_PATH), cleanup: true, stdio: undefined, }); diff --git a/scripts/release/__tests__/write-changelog.test.ts b/scripts/release/__tests__/write-changelog.test.ts index 9a952bddc7ff..f0346066b26c 100644 --- a/scripts/release/__tests__/write-changelog.test.ts +++ b/scripts/release/__tests__/write-changelog.test.ts @@ -1,8 +1,9 @@ /* eslint-disable no-underscore-dangle */ +import { join } from 'node:path'; + import { beforeEach, describe, expect, it, vi } from 'vitest'; import * as fsExtraImp from 'fs-extra'; -import path from 'path'; import { dedent } from 'ts-dedent'; import type * as MockedFSToExtra from '../../../code/__mocks__/fs-extra'; @@ -29,18 +30,10 @@ beforeEach(() => { }); }); -const STABLE_CHANGELOG_PATH = path.join(__dirname, '..', '..', '..', 'CHANGELOG.md'); -const PRERELEASE_CHANGELOG_PATH = path.join(__dirname, '..', '..', '..', 'CHANGELOG.prerelease.md'); -const LATEST_VERSION_PATH = path.join( - __dirname, - '..', - '..', - '..', - 'docs', - 'versions', - 'latest.json' -); -const NEXT_VERSION_PATH = path.join(__dirname, '..', '..', '..', 'docs', 'versions', 'next.json'); +const STABLE_CHANGELOG_PATH = join(__dirname, '..', '..', '..', 'CHANGELOG.md'); +const PRERELEASE_CHANGELOG_PATH = join(__dirname, '..', '..', '..', 'CHANGELOG.prerelease.md'); +const LATEST_VERSION_PATH = join(__dirname, '..', '..', '..', 'docs', 'versions', 'latest.json'); +const NEXT_VERSION_PATH = join(__dirname, '..', '..', '..', 'docs', 'versions', 'next.json'); const EXISTING_STABLE_CHANGELOG = dedent`## 7.0.0 diff --git a/scripts/release/get-changelog-from-file.ts b/scripts/release/get-changelog-from-file.ts index 85dc1df13e0c..f428310cc656 100644 --- a/scripts/release/get-changelog-from-file.ts +++ b/scripts/release/get-changelog-from-file.ts @@ -1,8 +1,9 @@ +import { join } from 'node:path'; + import { setOutput } from '@actions/core'; import chalk from 'chalk'; import { program } from 'commander'; import { readFile } from 'fs-extra'; -import path from 'path'; import semver from 'semver'; import { dedent } from 'ts-dedent'; @@ -26,7 +27,7 @@ export const getChangelogFromFile = async (args: { const version = args.version || (await getCurrentVersion()); const isPrerelease = semver.prerelease(version) !== null; const changelogFilename = isPrerelease ? 'CHANGELOG.prerelease.md' : 'CHANGELOG.md'; - const changelogPath = path.join(__dirname, '..', '..', changelogFilename); + const changelogPath = join(__dirname, '..', '..', changelogFilename); console.log(`📝 Getting changelog from ${chalk.blue(changelogPath)}`); diff --git a/scripts/release/get-current-version.ts b/scripts/release/get-current-version.ts index 49d99410bfd6..f640ccb5907e 100644 --- a/scripts/release/get-current-version.ts +++ b/scripts/release/get-current-version.ts @@ -1,12 +1,13 @@ +import { join } from 'node:path'; + import { setOutput } from '@actions/core'; import chalk from 'chalk'; import { readJson } from 'fs-extra'; -import path from 'path'; import { esMain } from '../utils/esmain'; -const CODE_DIR_PATH = path.join(__dirname, '..', '..', 'code'); -const CODE_PACKAGE_JSON_PATH = path.join(CODE_DIR_PATH, 'package.json'); +const CODE_DIR_PATH = join(__dirname, '..', '..', 'code'); +const CODE_PACKAGE_JSON_PATH = join(CODE_DIR_PATH, 'package.json'); export const getCurrentVersion = async () => { console.log(`📐 Reading current version of Storybook...`); diff --git a/scripts/release/is-pr-frozen.ts b/scripts/release/is-pr-frozen.ts index fd632e10bce9..d1fdec3a9de8 100644 --- a/scripts/release/is-pr-frozen.ts +++ b/scripts/release/is-pr-frozen.ts @@ -1,8 +1,9 @@ +import { join } from 'node:path'; + import { setOutput } from '@actions/core'; import chalk from 'chalk'; import program from 'commander'; import { readJson } from 'fs-extra'; -import path from 'path'; import { esMain } from '../utils/esmain'; import { getPullInfoFromCommit } from './utils/get-github-info'; @@ -16,8 +17,8 @@ program .option('-H, --patch', 'Look for patch PR instead of next PR', false) .option('-V, --verbose', 'Enable verbose logging', false); -const CODE_DIR_PATH = path.join(__dirname, '..', '..', 'code'); -const CODE_PACKAGE_JSON_PATH = path.join(CODE_DIR_PATH, 'package.json'); +const CODE_DIR_PATH = join(__dirname, '..', '..', 'code'); +const CODE_PACKAGE_JSON_PATH = join(CODE_DIR_PATH, 'package.json'); const getCurrentVersion = async () => { console.log(`📐 Reading current version of Storybook...`); diff --git a/scripts/release/publish.ts b/scripts/release/publish.ts index 0e0136107649..b6c6ecfdec1b 100644 --- a/scripts/release/publish.ts +++ b/scripts/release/publish.ts @@ -1,9 +1,10 @@ +import { join } from 'node:path'; + import chalk from 'chalk'; import program from 'commander'; import { execaCommand } from 'execa'; import { readJson } from 'fs-extra'; import pRetry from 'p-retry'; -import path from 'path'; import semver from 'semver'; import dedent from 'ts-dedent'; import { z } from 'zod'; @@ -37,8 +38,8 @@ type Options = { dryRun?: boolean; }; -const CODE_DIR_PATH = path.join(__dirname, '..', '..', 'code'); -const CODE_PACKAGE_JSON_PATH = path.join(CODE_DIR_PATH, 'package.json'); +const CODE_DIR_PATH = join(__dirname, '..', '..', 'code'); +const CODE_PACKAGE_JSON_PATH = join(CODE_DIR_PATH, 'package.json'); const validateOptions = (options: { [key: string]: any }): options is Options => { optionsSchema.parse(options); diff --git a/scripts/release/version.ts b/scripts/release/version.ts index 6574e4609ec9..b5e775792209 100644 --- a/scripts/release/version.ts +++ b/scripts/release/version.ts @@ -1,9 +1,10 @@ +import { join } from 'node:path'; + import { setOutput } from '@actions/core'; import chalk from 'chalk'; import program from 'commander'; import { execaCommand } from 'execa'; import { readFile, readJson, writeFile, writeJson } from 'fs-extra'; -import path from 'path'; import semver from 'semver'; import { z } from 'zod'; @@ -100,8 +101,8 @@ type ApplyOptions = BaseOptions & { }; type Options = BumpOptions | ExactOptions | ApplyOptions; -const CODE_DIR_PATH = path.join(__dirname, '..', '..', 'code'); -const CODE_PACKAGE_JSON_PATH = path.join(CODE_DIR_PATH, 'package.json'); +const CODE_DIR_PATH = join(__dirname, '..', '..', 'code'); +const CODE_PACKAGE_JSON_PATH = join(CODE_DIR_PATH, 'package.json'); const validateOptions = (options: { [key: string]: any }): options is Options => { optionsSchema.parse(options); @@ -127,8 +128,8 @@ const bumpCodeVersion = async (nextVersion: string) => { const bumpVersionSources = async (currentVersion: string, nextVersion: string) => { const filesToUpdate = [ - path.join(CODE_DIR_PATH, 'core', 'src', 'manager-api', 'version.ts'), - path.join(CODE_DIR_PATH, 'core', 'src', 'common', 'versions.ts'), + join(CODE_DIR_PATH, 'core', 'src', 'manager-api', 'version.ts'), + join(CODE_DIR_PATH, 'core', 'src', 'common', 'versions.ts'), ]; console.log(`🤜 Bumping versions in...:\n ${chalk.cyan(filesToUpdate.join('\n '))}`); @@ -161,7 +162,7 @@ const bumpAllPackageJsons = async ({ await Promise.all( packages.map(async (pkg) => { // 2. get the package.json - const packageJsonPath = path.join(CODE_DIR_PATH, pkg.location, 'package.json'); + const packageJsonPath = join(CODE_DIR_PATH, pkg.location, 'package.json'); const packageJson: { version: string; [key: string]: any; @@ -285,7 +286,7 @@ export const run = async (options: unknown) => { console.log(`⬆️ Updating lock file with ${chalk.blue('yarn install --mode=update-lockfile')}`); await execaCommand(`yarn install --mode=update-lockfile`, { - cwd: path.join(CODE_DIR_PATH), + cwd: join(CODE_DIR_PATH), stdio: verbose ? 'inherit' : undefined, cleanup: true, }); diff --git a/scripts/release/write-changelog.ts b/scripts/release/write-changelog.ts index 43a03c8c497f..630db01f0441 100644 --- a/scripts/release/write-changelog.ts +++ b/scripts/release/write-changelog.ts @@ -1,7 +1,8 @@ +import { join } from 'node:path'; + import chalk from 'chalk'; import program from 'commander'; import { readFile, writeFile, writeJson } from 'fs-extra'; -import path from 'path'; import semver from 'semver'; import { z } from 'zod'; @@ -66,7 +67,7 @@ const writeToChangelogFile = async ({ }) => { const isPrerelease = semver.prerelease(version) !== null; const changelogFilename = isPrerelease ? 'CHANGELOG.prerelease.md' : 'CHANGELOG.md'; - const changelogPath = path.join(__dirname, '..', '..', changelogFilename); + const changelogPath = join(__dirname, '..', '..', changelogFilename); if (verbose) { console.log(`📝 Writing changelog to ${chalk.blue(changelogPath)}`); @@ -89,10 +90,10 @@ const writeToDocsVersionFile = async ({ }) => { const isPrerelease = semver.prerelease(version) !== null; const filename = isPrerelease ? 'next.json' : 'latest.json'; - const filepath = path.join(__dirname, '..', '..', 'docs', 'versions', filename); + const filepath = join(__dirname, '..', '..', 'docs', 'versions', filename); if (verbose) { - console.log(`📝 Writing changelog to ${chalk.blue(path)}`); + console.log(`📝 Writing changelog to ${chalk.blue(filepath)}`); } const textWithoutHeading = changelogText.split('\n').slice(2).join('\n').replaceAll('"', '\\"'); diff --git a/scripts/reset.js b/scripts/reset.js index 5d8c70698792..ed93cbce800b 100644 --- a/scripts/reset.js +++ b/scripts/reset.js @@ -1,11 +1,12 @@ -import { spawn } from 'child_process'; -import fs from 'fs'; +import { spawn } from 'node:child_process'; +import { appendFile, writeFileSync } from 'node:fs'; + import { remove } from 'fs-extra'; import trash from 'trash'; const logger = console; -fs.writeFileSync('reset.log', ''); +writeFileSync('reset.log', ''); const cleaningProcess = spawn('git', [ 'clean', @@ -47,7 +48,7 @@ cleaningProcess.stdout.on('data', (data) => { } }); } - fs.appendFile('reset.log', data, (err) => { + appendFile('reset.log', data, (err) => { if (err) { throw err; } diff --git a/scripts/run-registry.ts b/scripts/run-registry.ts index bb8aa97048fc..4d628856e9d3 100755 --- a/scripts/run-registry.ts +++ b/scripts/run-registry.ts @@ -1,13 +1,14 @@ +import { exec } from 'node:child_process'; +import { mkdir } from 'node:fs/promises'; +import http from 'node:http'; +import type { Server } from 'node:http'; +import { join, resolve as resolvePath } from 'node:path'; + import chalk from 'chalk'; -import { exec } from 'child_process'; import program from 'commander'; import { execa, execaSync } from 'execa'; import { pathExists, readJSON, remove } from 'fs-extra'; -import { mkdir } from 'fs/promises'; -import http from 'http'; -import type { Server } from 'http'; import pLimit from 'p-limit'; -import path from 'path'; import { parseConfigFile, runServer } from 'verdaccio'; import { maxConcurrentTasks } from './utils/concurrency'; @@ -22,7 +23,7 @@ program.parse(process.argv); const logger = console; -const root = path.resolve(__dirname, '..'); +const root = resolvePath(__dirname, '..'); const startVerdaccio = async () => { const ready = { @@ -62,9 +63,9 @@ const startVerdaccio = async () => { resolve(verdaccioApp); } }); - const cache = path.join(__dirname, '..', '.verdaccio-cache'); + const cache = join(__dirname, '..', '.verdaccio-cache'); const config = { - ...parseConfigFile(path.join(__dirname, 'verdaccio.yaml')), + ...parseConfigFile(join(__dirname, 'verdaccio.yaml')), self_path: cache, }; @@ -91,7 +92,7 @@ const startVerdaccio = async () => { }; const currentVersion = async () => { - const { version } = await readJSON(path.join(__dirname, '..', 'code', 'package.json')); + const { version } = await readJSON(join(__dirname, '..', 'code', 'package.json')); return version; }; @@ -122,14 +123,11 @@ const publish = async (packages: { name: string; location: string }[], url: stri () => new Promise((res, rej) => { logger.log( - `🛫 publishing ${name} (${location.replace( - path.resolve(path.join(__dirname, '..')), - '.' - )})` + `🛫 publishing ${name} (${location.replace(resolvePath(join(__dirname, '..')), '.')})` ); const tarballFilename = `${name.replace('@', '').replace('/', '-')}.tgz`; - const command = `cd ${path.resolve( + const command = `cd ${resolvePath( '../code', location )} && yarn pack --out=${PACKS_DIRECTORY}/${tarballFilename} && cd ${PACKS_DIRECTORY} && npm publish ./${tarballFilename} --registry ${url} --force --ignore-scripts`; @@ -156,7 +154,7 @@ const run = async () => { if (!process.env.CI) { // when running e2e locally, clear cache to avoid EPUBLISHCONFLICT errors - const verdaccioCache = path.resolve(__dirname, '..', '.verdaccio-cache'); + const verdaccioCache = resolvePath(__dirname, '..', '.verdaccio-cache'); if (await pathExists(verdaccioCache)) { logger.log(`🗑 cleaning up cache`); await remove(verdaccioCache); diff --git a/scripts/strict-ts.ts b/scripts/strict-ts.ts index ac3d30fdf16c..990cbe866ccf 100644 --- a/scripts/strict-ts.ts +++ b/scripts/strict-ts.ts @@ -1,5 +1,5 @@ -import fsSync from 'node:fs'; -import path from 'node:path'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; import glob from 'fast-glob'; import JSON5 from 'json5'; @@ -13,16 +13,16 @@ const files = glob.sync('**/*/tsconfig.json', { const packages = files .filter((file) => !file.includes('node_modules') && !file.includes('dist')) .map((file) => { - const packageJson = path.join(path.dirname(file), 'package.json'); + const packageJson = join(dirname(file), 'package.json'); let packageName; - if (fsSync.existsSync(packageJson)) { - const json = fsSync.readFileSync(packageJson, { encoding: 'utf-8' }); + if (existsSync(packageJson)) { + const json = readFileSync(packageJson, { encoding: 'utf-8' }); packageName = JSON5.parse(json).name; } let strict; - if (fsSync.existsSync(file)) { - const tsconfig = fsSync.readFileSync(file, { encoding: 'utf-8' }); + if (existsSync(file)) { + const tsconfig = readFileSync(file, { encoding: 'utf-8' }); const tsconfigJson = JSON5.parse(tsconfig); strict = tsconfigJson?.compilerOptions?.strict ?? false; } diff --git a/scripts/tasks/sync-docs.ts b/scripts/tasks/sync-docs.ts index 531c235cfa64..01bb6779c4fa 100644 --- a/scripts/tasks/sync-docs.ts +++ b/scripts/tasks/sync-docs.ts @@ -1,5 +1,15 @@ -import fs from 'fs'; -import path from 'path'; +import { + closeSync, + copyFile, + cpSync, + existsSync, + mkdirSync, + openSync, + rmSync, + unlinkSync, + watch, +} from 'node:fs'; +import { join } from 'node:path'; import type { Task } from '../task'; import { ask } from '../utils/ask'; @@ -13,45 +23,45 @@ export const syncDocs: Task = { return false; }, async run() { - const rootDir = path.join(__dirname, '..', '..'); - const docsDir = path.join(rootDir, 'docs'); + const rootDir = join(__dirname, '..', '..'); + const docsDir = join(rootDir, 'docs'); let frontpageDocsPath = '/src/content/docs'; const frontpagePath = await ask('Provide the frontpage project path:'); - frontpageDocsPath = path.join(rootDir, frontpagePath, frontpageDocsPath); + frontpageDocsPath = join(rootDir, frontpagePath, frontpageDocsPath); - if (!fs.existsSync(frontpageDocsPath)) { - fs.mkdirSync(frontpageDocsPath); + if (!existsSync(frontpageDocsPath)) { + mkdirSync(frontpageDocsPath); } logger.info(`Rebuilding docs at ${frontpageDocsPath}`); - fs.rmSync(frontpageDocsPath, { recursive: true }); - fs.cpSync(docsDir, frontpageDocsPath, { recursive: true }); + rmSync(frontpageDocsPath, { recursive: true }); + cpSync(docsDir, frontpageDocsPath, { recursive: true }); logger.info(`Synchronizing files from: \n${docsDir} \nto: \n${frontpageDocsPath}`); - fs.watch(docsDir, { recursive: true }, (_, filename) => { - const srcFilePath = path.join(docsDir, filename); - const targetFilePath = path.join(frontpageDocsPath, filename); + watch(docsDir, { recursive: true }, (_, filename) => { + const srcFilePath = join(docsDir, filename); + const targetFilePath = join(frontpageDocsPath, filename); const targetDir = targetFilePath.split('/').slice(0, -1).join('/'); // Syncs create file - if (!fs.existsSync(targetFilePath)) { - fs.mkdirSync(targetDir, { recursive: true }); - fs.closeSync(fs.openSync(targetFilePath, 'w')); + if (!existsSync(targetFilePath)) { + mkdirSync(targetDir, { recursive: true }); + closeSync(openSync(targetFilePath, 'w')); logger.info(`Created ${filename}.`); } // Syncs remove file - if (!fs.existsSync(srcFilePath)) { - fs.unlinkSync(targetFilePath); + if (!existsSync(srcFilePath)) { + unlinkSync(targetFilePath); logger.info(`Removed ${filename}.`); return; } // Syncs update file - fs.copyFile(srcFilePath, targetFilePath, (err) => { + copyFile(srcFilePath, targetFilePath, (err) => { logger.info(`Updated ${filename}.`); if (err) throw err; }); diff --git a/scripts/utils/esmain.ts b/scripts/utils/esmain.ts index 1a4c3e1a9236..d74509b8ca01 100644 --- a/scripts/utils/esmain.ts +++ b/scripts/utils/esmain.ts @@ -1,7 +1,7 @@ -import { createRequire } from 'module'; -import path from 'path'; -import process from 'process'; -import { fileURLToPath } from 'url'; +import { createRequire } from 'node:module'; +import { extname } from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; /** * Strip the extension from a filename if it has one. @@ -9,7 +9,7 @@ import { fileURLToPath } from 'url'; * @return {string} The filename without a path. */ export function stripExt(name: string) { - const extension = path.extname(name); + const extension = extname(name); if (!extension) { return name; } @@ -31,7 +31,7 @@ export function esMain(url: string) { const modulePath = fileURLToPath(url); - const extension = path.extname(scriptPath); + const extension = extname(scriptPath); if (extension) { return modulePath === scriptPath; } diff --git a/scripts/utils/filterExistsInCodeDir.ts b/scripts/utils/filterExistsInCodeDir.ts index 93d4c22e7487..7a22fdf8d2f2 100644 --- a/scripts/utils/filterExistsInCodeDir.ts +++ b/scripts/utils/filterExistsInCodeDir.ts @@ -1,5 +1,6 @@ +import { join, resolve } from 'node:path'; + import { pathExists } from 'fs-extra'; -import path from 'path'; import { CODE_DIRECTORY } from './constants'; @@ -9,7 +10,7 @@ export const filterExistsInCodeDir = async (packageDirs: string[], pathToCheck: ( await Promise.all( packageDirs.map(async (p) => - (await pathExists(path.resolve(CODE_DIRECTORY, path.join(p, pathToCheck)))) ? p : null + (await pathExists(resolve(CODE_DIRECTORY, join(p, pathToCheck)))) ? p : null ) ) ).filter(Boolean); diff --git a/scripts/utils/yarn.ts b/scripts/utils/yarn.ts index 9b24030db7df..63dd14f03286 100644 --- a/scripts/utils/yarn.ts +++ b/scripts/utils/yarn.ts @@ -1,5 +1,6 @@ +import { join } from 'node:path'; + import { pathExists, readJSON, writeJSON } from 'fs-extra'; -import path from 'path'; // TODO -- should we generate this file a second time outside of CLI? import storybookVersions from '../../code/core/src/common/versions'; @@ -19,7 +20,7 @@ export const addPackageResolutions = async ({ cwd, dryRun }: YarnOptions) => { logger.info(`🔢 Adding package resolutions:`); if (dryRun) return; - const packageJsonPath = path.join(cwd, 'package.json'); + const packageJsonPath = join(cwd, 'package.json'); const packageJson = await readJSON(packageJsonPath); packageJson.resolutions = { ...packageJson.resolutions, @@ -35,7 +36,7 @@ export const addPackageResolutions = async ({ cwd, dryRun }: YarnOptions) => { }; export const installYarn2 = async ({ cwd, dryRun, debug }: YarnOptions) => { - const pnpApiExists = await pathExists(path.join(cwd, '.pnp.cjs')); + const pnpApiExists = await pathExists(join(cwd, '.pnp.cjs')); const command = [ touch('yarn.lock'), @@ -67,7 +68,7 @@ export const addWorkaroundResolutions = async ({ cwd, dryRun }: YarnOptions) => logger.info(`🔢 Adding resolutions for workarounds`); if (dryRun) return; - const packageJsonPath = path.join(cwd, 'package.json'); + const packageJsonPath = join(cwd, 'package.json'); const packageJson = await readJSON(packageJsonPath); packageJson.resolutions = { ...packageJson.resolutions, diff --git a/scripts/vite-ecosystem-ci/before-test.js b/scripts/vite-ecosystem-ci/before-test.js index 15ff7126b188..8dfd66847557 100644 --- a/scripts/vite-ecosystem-ci/before-test.js +++ b/scripts/vite-ecosystem-ci/before-test.js @@ -3,30 +3,28 @@ * This is necessary because the sandbox package.json is used to run the tests and the resolutions are needed to run the tests. * The vite-ecosystem-ci, though, sets the resolutions in the root package.json. */ -import fs from 'node:fs'; -import path from 'node:path'; +import { dirname, resolve } from 'node:path'; +import { readFile, writeFile } from 'node:fs/promises'; + import { fileURLToPath } from 'node:url'; import { execa, execaCommand } from 'execa'; const filename = fileURLToPath(import.meta.url); -const dirname = path.dirname(filename); +const dirname = dirname(filename); -const rootPackageJsonPath = path.resolve(dirname, '../../package.json'); -const sandboxPackageJsonPath = path.resolve( - dirname, - '../../sandbox/react-vite-default-ts/package.json' -); +const rootPackageJsonPath = resolve(dirname, '../../package.json'); +const sandboxPackageJsonPath = resolve(dirname, '../../sandbox/react-vite-default-ts/package.json'); -const rootPackageJson = JSON.parse(await fs.promises.readFile(rootPackageJsonPath, 'utf-8')); -const sandboxPackageJson = JSON.parse(await fs.promises.readFile(sandboxPackageJsonPath, 'utf-8')); +const rootPackageJson = JSON.parse(await readFile(rootPackageJsonPath, 'utf-8')); +const sandboxPackageJson = JSON.parse(await readFile(sandboxPackageJsonPath, 'utf-8')); sandboxPackageJson.resolutions = { ...(sandboxPackageJson.resolutions ?? {}), ...rootPackageJson.resolutions, }; -await fs.promises.writeFile(sandboxPackageJsonPath, JSON.stringify(sandboxPackageJson, null, 2)); -const sandboxFolder = path.dirname(sandboxPackageJsonPath); +await writeFile(sandboxPackageJsonPath, JSON.stringify(sandboxPackageJson, null, 2)); +const sandboxFolder = dirname(sandboxPackageJsonPath); await execa('yarn add playwright', { cwd: sandboxFolder, shell: true }); await execaCommand('yarn playwright install', { cwd: sandboxFolder, shell: true });