diff --git a/CHANGELOG.md b/CHANGELOG.md index 691a6d7a1c6d..a38ddb377030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 8.1.8 + +- Automigrations: Make VTA "learn more" link clickable - [#28020](https://github.com/storybookjs/storybook/pull/28020), thanks @deiga! +- CLI: Fix `init --skip-install` - [#28226](https://github.com/storybookjs/storybook/pull/28226), thanks @shilman! + ## 8.1.7 - Addon-actions: Only log spies with names - [#28091](https://github.com/storybookjs/storybook/pull/28091), thanks @kasperpeulen! diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 394483f35848..b86ab524dcf5 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,9 @@ +## 8.2.0-alpha.9 + +- Addon-a11y: Workaround for Vite 5.3.0 compat - [#28241](https://github.com/storybookjs/storybook/pull/28241), thanks @shilman! +- CLI: Fix CLI always asking all automigrations - [#28238](https://github.com/storybookjs/storybook/pull/28238), thanks @ndelangen! +- Core: Fix startup hang caused by watchStorySpecifiers - [#27016](https://github.com/storybookjs/storybook/pull/27016), thanks @heyimalex! + ## 8.2.0-alpha.8 - Automigrations: Make VTA "learn more" link clickable - [#28020](https://github.com/storybookjs/storybook/pull/28020), thanks @deiga! diff --git a/code/addons/a11y/src/a11yRunner.ts b/code/addons/a11y/src/a11yRunner.ts index 82a851b89b05..ecfe36315f43 100644 --- a/code/addons/a11y/src/a11yRunner.ts +++ b/code/addons/a11y/src/a11yRunner.ts @@ -29,7 +29,7 @@ const run = async (storyId: string, input: A11yParameters = defaultParameters) = if (!active) { active = true; channel.emit(EVENTS.RUNNING); - const axe = (await import('axe-core')).default; + const { default: axe } = await import('axe-core'); const { element = '#storybook-root', config, options = {} } = input; const htmlElement = document.querySelector(element as string); diff --git a/code/lib/cli/src/upgrade.ts b/code/lib/cli/src/upgrade.ts index 892c04148001..204e24710c6b 100644 --- a/code/lib/cli/src/upgrade.ts +++ b/code/lib/cli/src/upgrade.ts @@ -130,55 +130,59 @@ export const doUpgrade = async ({ // If we can't determine the existing version fallback to v0.0.0 to not block the upgrade const beforeVersion = (await getInstalledStorybookVersion(packageManager)) ?? '0.0.0'; - const currentVersion = versions['@storybook/cli']; + const currentCLIVersion = versions['@storybook/cli']; const isCanary = - currentVersion.startsWith('0.0.0') || + currentCLIVersion.startsWith('0.0.0') || beforeVersion.startsWith('portal:') || beforeVersion.startsWith('workspace:'); if (!(await hasStorybookDependencies(packageManager))) { throw new UpgradeStorybookInWrongWorkingDirectory(); } - if (!isCanary && lt(currentVersion, beforeVersion)) { - throw new UpgradeStorybookToLowerVersionError({ beforeVersion, currentVersion }); + if (!isCanary && lt(currentCLIVersion, beforeVersion)) { + throw new UpgradeStorybookToLowerVersionError({ + beforeVersion, + currentVersion: currentCLIVersion, + }); } - if (!isCanary && eq(currentVersion, beforeVersion)) { + if (!isCanary && eq(currentCLIVersion, beforeVersion)) { // Not throwing, as the beforeVersion calculation doesn't always work in monorepos. logger.warn(new UpgradeStorybookToSameVersionError({ beforeVersion }).message); } - const [latestVersion, packageJson] = await Promise.all([ - // + const [latestCLIVersionOnNPM, packageJson] = await Promise.all([ packageManager.latestVersion('@storybook/cli'), packageManager.retrievePackageJson(), ]); - const isOutdated = lt(currentVersion, latestVersion); - const isExactLatest = currentVersion === latestVersion; - const isPrerelease = prerelease(currentVersion) !== null; + const isCLIOutdated = lt(currentCLIVersion, latestCLIVersionOnNPM); + const isCLIExactLatest = currentCLIVersion === latestCLIVersionOnNPM; + const isCLIPrerelease = prerelease(currentCLIVersion) !== null; + + const isUpgrade = lt(beforeVersion, currentCLIVersion); - const borderColor = isOutdated ? '#FC521F' : '#F1618C'; + const borderColor = isCLIOutdated ? '#FC521F' : '#F1618C'; const messages = { welcome: `Upgrading Storybook from version ${chalk.bold(beforeVersion)} to version ${chalk.bold( - currentVersion + currentCLIVersion )}..`, notLatest: chalk.red(dedent` - This version is behind the latest release, which is: ${chalk.bold(latestVersion)}! + This version is behind the latest release, which is: ${chalk.bold(latestCLIVersionOnNPM)}! You likely ran the upgrade command through npx, which can use a locally cached version, to upgrade to the latest version please run: ${chalk.bold('npx storybook@latest upgrade')} You may want to CTRL+C to stop, and run with the latest version instead. `), - prelease: chalk.yellow('This is a pre-release version.'), + prerelease: chalk.yellow('This is a pre-release version.'), }; logger.plain( boxen( [messages.welcome] - .concat(isOutdated && !isPrerelease ? [messages.notLatest] : []) - .concat(isPrerelease ? [messages.prelease] : []) + .concat(isCLIOutdated && !isCLIPrerelease ? [messages.notLatest] : []) + .concat(isCLIPrerelease ? [messages.prerelease] : []) .join('\n'), { borderStyle: 'round', padding: 1, borderColor } ) @@ -227,7 +231,7 @@ export const doUpgrade = async ({ }) as Array; return monorepoDependencies.map((dependency) => { let char = '^'; - if (isOutdated) { + if (isCLIOutdated) { char = ''; } if (isCanary) { @@ -268,9 +272,9 @@ export const doUpgrade = async ({ configDir, mainConfigPath, beforeVersion, - storybookVersion: currentVersion, - isUpgrade: isOutdated, - isLatest: isExactLatest, + storybookVersion: currentCLIVersion, + isUpgrade, + isLatest: isCLIExactLatest, }); } @@ -284,7 +288,7 @@ export const doUpgrade = async ({ await telemetry('upgrade', { beforeVersion, - afterVersion: currentVersion, + afterVersion: currentCLIVersion, ...automigrationTelemetry, }); } diff --git a/code/lib/core-server/src/utils/stories-json.test.ts b/code/lib/core-server/src/utils/stories-json.test.ts index 5ba134673c83..f770986cd2f6 100644 --- a/code/lib/core-server/src/utils/stories-json.test.ts +++ b/code/lib/core-server/src/utils/stories-json.test.ts @@ -356,12 +356,17 @@ describe('useStoriesJson', () => { expect(Watchpack).toHaveBeenCalledTimes(1); const watcher = Watchpack.mock.instances[0]; - expect(watcher.watch).toHaveBeenCalledWith({ directories: ['./src'] }); + expect(watcher.watch).toHaveBeenCalledWith( + expect.objectContaining({ + directories: expect.any(Array), + files: expect.any(Array), + }) + ); expect(watcher.on).toHaveBeenCalledTimes(2); const onChange = watcher.on.mock.calls[0][1]; - await onChange('src/nested/Button.stories.ts'); + await onChange(`${workingDir}/src/nested/Button.stories.ts`); expect(mockServerChannel.emit).toHaveBeenCalledTimes(1); expect(mockServerChannel.emit).toHaveBeenCalledWith(STORY_INDEX_INVALIDATED); }); @@ -389,12 +394,17 @@ describe('useStoriesJson', () => { expect(Watchpack).toHaveBeenCalledTimes(1); const watcher = Watchpack.mock.instances[0]; - expect(watcher.watch).toHaveBeenCalledWith({ directories: ['./src'] }); + expect(watcher.watch).toHaveBeenCalledWith( + expect.objectContaining({ + directories: expect.any(Array), + files: expect.any(Array), + }) + ); expect(watcher.on).toHaveBeenCalledTimes(2); const onChange = watcher.on.mock.calls[0][1]; - await onChange('src/nested/Button.stories.ts'); + await onChange(`${workingDir}/src/nested/Button.stories.ts`); expect(mockServerChannel.emit).toHaveBeenCalledTimes(1); expect(mockServerChannel.emit).toHaveBeenCalledWith(STORY_INDEX_INVALIDATED); }); @@ -423,16 +433,21 @@ describe('useStoriesJson', () => { expect(Watchpack).toHaveBeenCalledTimes(1); const watcher = Watchpack.mock.instances[0]; - expect(watcher.watch).toHaveBeenCalledWith({ directories: ['./src'] }); + expect(watcher.watch).toHaveBeenCalledWith( + expect.objectContaining({ + directories: expect.any(Array), + files: expect.any(Array), + }) + ); expect(watcher.on).toHaveBeenCalledTimes(2); const onChange = watcher.on.mock.calls[0][1]; - await onChange('src/nested/Button.stories.ts'); - await onChange('src/nested/Button.stories.ts'); - await onChange('src/nested/Button.stories.ts'); - await onChange('src/nested/Button.stories.ts'); - await onChange('src/nested/Button.stories.ts'); + await onChange(`${workingDir}/src/nested/Button.stories.ts`); + await onChange(`${workingDir}/src/nested/Button.stories.ts`); + await onChange(`${workingDir}/src/nested/Button.stories.ts`); + await onChange(`${workingDir}/src/nested/Button.stories.ts`); + await onChange(`${workingDir}/src/nested/Button.stories.ts`); expect(mockServerChannel.emit).toHaveBeenCalledTimes(1); expect(mockServerChannel.emit).toHaveBeenCalledWith(STORY_INDEX_INVALIDATED); diff --git a/code/lib/core-server/src/utils/watch-story-specifiers.test.ts b/code/lib/core-server/src/utils/watch-story-specifiers.test.ts index 4026de15fa11..4a150cc0e7e2 100644 --- a/code/lib/core-server/src/utils/watch-story-specifiers.test.ts +++ b/code/lib/core-server/src/utils/watch-story-specifiers.test.ts @@ -13,6 +13,7 @@ describe('watchStorySpecifiers', () => { configDir: path.join(workingDir, '.storybook'), workingDir, }; + const abspath = (filename: string) => path.join(workingDir, filename); let close: () => void; afterEach(() => close?.()); @@ -25,11 +26,18 @@ describe('watchStorySpecifiers', () => { expect(Watchpack).toHaveBeenCalledTimes(1); const watcher = Watchpack.mock.instances[0]; - expect(watcher.watch).toHaveBeenCalledWith({ directories: ['./src'] }); + expect(watcher.watch).toHaveBeenCalledWith( + expect.objectContaining({ + directories: expect.any(Array), + files: expect.any(Array), + }) + ); expect(watcher.on).toHaveBeenCalledTimes(2); - const onChange = watcher.on.mock.calls[0][1]; - const onRemove = watcher.on.mock.calls[1][1]; + const baseOnChange = watcher.on.mock.calls[0][1]; + const baseOnRemove = watcher.on.mock.calls[1][1]; + const onChange = (filename: string, ...args: any[]) => baseOnChange(abspath(filename), ...args); + const onRemove = (filename: string, ...args: any[]) => baseOnRemove(abspath(filename), ...args); // File changed, matching onInvalidate.mockClear(); @@ -72,10 +80,16 @@ describe('watchStorySpecifiers', () => { expect(Watchpack).toHaveBeenCalledTimes(1); const watcher = Watchpack.mock.instances[0]; - expect(watcher.watch).toHaveBeenCalledWith({ directories: ['./src'] }); + expect(watcher.watch).toHaveBeenCalledWith( + expect.objectContaining({ + directories: expect.any(Array), + files: expect.any(Array), + }) + ); expect(watcher.on).toHaveBeenCalledTimes(2); - const onChange = watcher.on.mock.calls[0][1]; + const baseOnChange = watcher.on.mock.calls[0][1]; + const onChange = (filename: string, ...args: any[]) => baseOnChange(abspath(filename), ...args); onInvalidate.mockClear(); await onChange('src/nested', 1234); @@ -90,11 +104,18 @@ describe('watchStorySpecifiers', () => { expect(Watchpack).toHaveBeenCalledTimes(1); const watcher = Watchpack.mock.instances[0]; - expect(watcher.watch).toHaveBeenCalledWith({ directories: ['./src/nested'] }); + expect(watcher.watch).toHaveBeenCalledWith( + expect.objectContaining({ + directories: expect.any(Array), + files: expect.any(Array), + }) + ); expect(watcher.on).toHaveBeenCalledTimes(2); - const onChange = watcher.on.mock.calls[0][1]; - const onRemove = watcher.on.mock.calls[1][1]; + const baseOnChange = watcher.on.mock.calls[0][1]; + const baseOnRemove = watcher.on.mock.calls[1][1]; + const onChange = (filename: string, ...args: any[]) => baseOnChange(abspath(filename), ...args); + const onRemove = (filename: string, ...args: any[]) => baseOnRemove(abspath(filename), ...args); // File changed, matching onInvalidate.mockClear(); @@ -131,10 +152,16 @@ describe('watchStorySpecifiers', () => { expect(Watchpack).toHaveBeenCalledTimes(1); const watcher = Watchpack.mock.instances[0]; - expect(watcher.watch).toHaveBeenCalledWith({ directories: ['./src', './src/nested'] }); + expect(watcher.watch).toHaveBeenCalledWith( + expect.objectContaining({ + directories: expect.any(Array), + files: expect.any(Array), + }) + ); expect(watcher.on).toHaveBeenCalledTimes(2); - const onChange = watcher.on.mock.calls[0][1]; + const baseOnChange = watcher.on.mock.calls[0][1]; + const onChange = (filename: string, ...args: any[]) => baseOnChange(abspath(filename), ...args); onInvalidate.mockClear(); await onChange('src/nested/Button.stories.ts', 1234); diff --git a/code/lib/core-server/src/utils/watch-story-specifiers.ts b/code/lib/core-server/src/utils/watch-story-specifiers.ts index 70ec0d4eb7fb..414fd4c87617 100644 --- a/code/lib/core-server/src/utils/watch-story-specifiers.ts +++ b/code/lib/core-server/src/utils/watch-story-specifiers.ts @@ -2,7 +2,6 @@ import Watchpack from 'watchpack'; import slash from 'slash'; import fs from 'fs'; import path from 'path'; -import uniq from 'lodash/uniq.js'; import type { NormalizedStoriesSpecifier, Path } from '@storybook/types'; import { commonGlobOptions } from '@storybook/core-common'; @@ -15,11 +14,27 @@ const isDirectory = (directory: Path) => { } }; -// Watchpack (and path.relative) passes paths either with no leading './' - e.g. `src/Foo.stories.js`, -// or with a leading `../` (etc), e.g. `../src/Foo.stories.js`. -// We want to deal in importPaths relative to the working dir, so we normalize -function toImportPath(relativePath: Path) { - return relativePath.startsWith('.') ? relativePath : `./${relativePath}`; +// Takes an array of absolute paths to directories and synchronously returns +// absolute paths to all existing files and directories nested within those +// directories (including the passed parent directories). +function getNestedFilesAndDirectories(directories: Path[]) { + const traversedDirectories = new Set(); + const files = new Set(); + const traverse = (directory: Path) => { + if (traversedDirectories.has(directory)) { + return; + } + fs.readdirSync(directory, { withFileTypes: true }).forEach((ent: fs.Dirent) => { + if (ent.isDirectory()) { + traverse(path.join(directory, ent.name)); + } else if (ent.isFile()) { + files.add(path.join(directory, ent.name)); + } + }); + traversedDirectories.add(directory); + }; + directories.filter(isDirectory).forEach(traverse); + return { files: Array.from(files), directories: Array.from(traversedDirectories) }; } export function watchStorySpecifiers( @@ -27,6 +42,12 @@ export function watchStorySpecifiers( options: { workingDir: Path }, onInvalidate: (specifier: NormalizedStoriesSpecifier, path: Path, removed: boolean) => void ) { + // 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)) + ); + // See https://www.npmjs.com/package/watchpack for full options. // If you want less traffic, consider using aggregation with some interval const wp = new Watchpack({ @@ -34,15 +55,17 @@ export function watchStorySpecifiers( followSymlinks: false, ignored: ['**/.git', '**/node_modules'], }); - wp.watch({ - directories: uniq(specifiers.map((ns) => ns.directory)), - }); + wp.watch({ files, directories }); + + const toImportPath = (absolutePath: Path) => { + const relativePath = path.relative(options.workingDir, absolutePath); + return slash(relativePath.startsWith('.') ? relativePath : `./${relativePath}`); + }; - async function onChangeOrRemove(watchpackPath: Path, removed: boolean) { - // Watchpack passes paths either with no leading './' - e.g. `src/Foo.stories.js`, - // or with a leading `../` (etc), e.g. `../src/Foo.stories.js`. - // We want to deal in importPaths relative to the working dir, or absolute paths. - const importPath = slash(watchpackPath.startsWith('.') ? watchpackPath : `./${watchpackPath}`); + async function onChangeOrRemove(absolutePath: Path, removed: boolean) { + // Watchpack should return absolute paths, given we passed in absolute paths + // to watch. Convert to an import path so we can run against the specifiers. + const importPath = toImportPath(absolutePath); const matchingSpecifier = specifiers.find((ns) => ns.importPathMatcher.exec(importPath)); if (matchingSpecifier) { @@ -55,7 +78,6 @@ export function watchStorySpecifiers( // However, when a directory is added, it does not fire events for any files *within* the directory, // so we need to scan within that directory for new files. It is tricky to use a glob for this, // so we'll do something a bit more "dumb" for now - const absolutePath = path.join(options.workingDir, importPath); if (!removed && isDirectory(absolutePath)) { await Promise.all( specifiers @@ -66,11 +88,10 @@ export function watchStorySpecifiers( // 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( - options.workingDir, - importPath, + absolutePath, '**', // files can be e.g. '**/foo/*/*.js' so we just want the last bit, - // because the directoru could already be within the files part (e.g. './x/foo/bar') + // because the directory could already be within the files part (e.g. './x/foo/bar') path.basename(specifier.files) ); @@ -78,13 +99,10 @@ export function watchStorySpecifiers( const { globby } = await import('globby'); // glob only supports forward slashes - const files = await globby(slash(dirGlob), commonGlobOptions(dirGlob)); + const addedFiles = await globby(slash(dirGlob), commonGlobOptions(dirGlob)); - files.forEach((filePath) => { - const fileImportPath = toImportPath( - // use posix path separators even on windows - path.relative(options.workingDir, filePath).replace(/\\/g, '/') - ); + addedFiles.forEach((filePath: Path) => { + const fileImportPath = toImportPath(filePath); if (specifier.importPathMatcher.exec(fileImportPath)) { onInvalidate(specifier, fileImportPath, removed); diff --git a/code/package.json b/code/package.json index aa069ef40eaa..107542aa4a3e 100644 --- a/code/package.json +++ b/code/package.json @@ -294,5 +294,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.2.0-alpha.9" } diff --git a/docs/get-started/angular.md b/docs/get-started/angular.md index e5c706acc1df..e3b9540941fa 100644 --- a/docs/get-started/angular.md +++ b/docs/get-started/angular.md @@ -26,7 +26,7 @@ Storybook for Angular is only supported in [Angular](?renderer=angular) projects ## Requirements -- Angular ≥ 15.0 < 18.0 +- Angular ≥ 15.0 < 19.0 - Webpack ≥ 5.0 - Storybook ≥ 8.0 diff --git a/docs/versions/next.json b/docs/versions/next.json index 60426ec0cd6f..7a62528ed897 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"8.2.0-alpha.8","info":{"plain":"- Automigrations: Make VTA \\\"learn more\\\" link clickable - [#28020](https://github.com/storybookjs/storybook/pull/28020), thanks @deiga!\n- CLI: Fix `init --skip-install` - [#28226](https://github.com/storybookjs/storybook/pull/28226), thanks @shilman!\n- CSF: Rename `preview.js` `globals` to `initialGlobals` - [#27517](https://github.com/storybookjs/storybook/pull/27517), thanks @shilman!"}} +{"version":"8.2.0-alpha.9","info":{"plain":"- Addon-a11y: Workaround for Vite 5.3.0 compat - [#28241](https://github.com/storybookjs/storybook/pull/28241), thanks @shilman!\n- CLI: Fix CLI always asking all automigrations - [#28238](https://github.com/storybookjs/storybook/pull/28238), thanks @ndelangen!\n- Core: Fix startup hang caused by watchStorySpecifiers - [#27016](https://github.com/storybookjs/storybook/pull/27016), thanks @heyimalex!"}}