From 43bac1c362f52fd971eca33618f606873f527470 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Mon, 24 Nov 2025 18:43:26 +0000 Subject: [PATCH 1/2] Angular: Migrate from RxJS to async/await in command builders and runCompodoc utility --- .../src/builders/build-storybook/index.ts | 179 +++++++------- .../src/builders/start-storybook/index.ts | 232 +++++++++--------- .../src/builders/utils/run-compodoc.spec.ts | 38 ++- .../src/builders/utils/run-compodoc.ts | 50 ++-- 4 files changed, 236 insertions(+), 263 deletions(-) diff --git a/code/frameworks/angular/src/builders/build-storybook/index.ts b/code/frameworks/angular/src/builders/build-storybook/index.ts index dfc9deb1b438..ddeff53ff64d 100644 --- a/code/frameworks/angular/src/builders/build-storybook/index.ts +++ b/code/frameworks/angular/src/builders/build-storybook/index.ts @@ -10,7 +10,6 @@ import type { BuilderContext, BuilderHandlerFn, BuilderOutput, - BuilderOutputLike, Target, Builder as DevkitBuilder, } from '@angular-devkit/architect'; @@ -27,8 +26,6 @@ import type { import type { JsonObject } from '@angular-devkit/core'; import * as find from 'empathic/find'; import * as pkg from 'empathic/package'; -import { from, of, throwError } from 'rxjs'; -import { catchError, map, mapTo, switchMap } from 'rxjs/operators'; import { errorSummary, printErrorDetails } from '../utils/error-handler'; import { runCompodoc } from '../utils/run-compodoc'; @@ -70,94 +67,85 @@ export type StorybookBuilderOutput = JsonObject & BuilderOutput & { [key: string type StandaloneBuildOptions = StandaloneOptions & { outputDir: string }; -const commandBuilder: BuilderHandlerFn = ( +const commandBuilder: BuilderHandlerFn = async ( options, context -): BuilderOutputLike => { - const builder = from(setup(options, context)).pipe( - switchMap(({ tsConfig }) => { - const docTSConfig = find.up('tsconfig.doc.json', { - cwd: options.configDir, - last: getProjectRoot(), - }); - const runCompodoc$ = options.compodoc - ? runCompodoc( - { compodocArgs: options.compodocArgs, tsconfig: docTSConfig ?? tsConfig }, - context - ).pipe(mapTo({ tsConfig })) - : of({}); - - return runCompodoc$.pipe(mapTo({ tsConfig })); - }), - map(({ tsConfig }) => { - getEnvConfig(options, { - staticDir: 'SBCONFIG_STATIC_DIR', - outputDir: 'SBCONFIG_OUTPUT_DIR', - configDir: 'SBCONFIG_CONFIG_DIR', - }); - - const { - browserTarget, - stylePreprocessorOptions, - styles, - configDir, - docs, - loglevel, - test, - outputDir, - quiet, - enableProdMode = true, - webpackStatsJson, - statsJson, - debugWebpack, - disableTelemetry, - assets, - previewUrl, - sourceMap = false, - preserveSymlinks = false, - experimentalZoneless = !!(VERSION.major && Number(VERSION.major) >= 21), - } = options; - - const packageJsonPath = pkg.up({ cwd: __dirname }); - const packageJson = - packageJsonPath != null ? JSON.parse(readFileSync(packageJsonPath, 'utf8')) : null; - - const standaloneOptions: StandaloneBuildOptions = { - packageJson, - configDir, - ...(docs ? { docs } : {}), - loglevel, - outputDir, - test, - quiet, - enableProdMode, - disableTelemetry, - angularBrowserTarget: browserTarget, - angularBuilderContext: context, - angularBuilderOptions: { - ...(stylePreprocessorOptions ? { stylePreprocessorOptions } : {}), - ...(styles ? { styles } : {}), - ...(assets ? { assets } : {}), - sourceMap, - preserveSymlinks, - experimentalZoneless, - }, - tsConfig, - webpackStatsJson, - statsJson, - debugWebpack, - previewUrl, - }; - - return standaloneOptions; - }), - switchMap((standaloneOptions) => runInstance({ ...standaloneOptions, mode: 'static' })), - map(() => { - return { success: true }; - }) - ); - - return builder as any as BuilderOutput; +): Promise => { + const { tsConfig } = await setup(options, context); + + const docTSConfig = find.up('tsconfig.doc.json', { + cwd: options.configDir, + last: getProjectRoot(), + }); + + if (options.compodoc) { + await runCompodoc( + { compodocArgs: options.compodocArgs, tsconfig: docTSConfig ?? tsConfig }, + context + ); + } + + getEnvConfig(options, { + staticDir: 'SBCONFIG_STATIC_DIR', + outputDir: 'SBCONFIG_OUTPUT_DIR', + configDir: 'SBCONFIG_CONFIG_DIR', + }); + + const { + browserTarget, + stylePreprocessorOptions, + styles, + configDir, + docs, + loglevel, + test, + outputDir, + quiet, + enableProdMode = true, + webpackStatsJson, + statsJson, + debugWebpack, + disableTelemetry, + assets, + previewUrl, + sourceMap = false, + preserveSymlinks = false, + experimentalZoneless = !!(VERSION.major && Number(VERSION.major) >= 21), + } = options; + + const packageJsonPath = pkg.up({ cwd: __dirname }); + const packageJson = + packageJsonPath != null ? JSON.parse(readFileSync(packageJsonPath, 'utf8')) : null; + + const standaloneOptions: StandaloneBuildOptions = { + packageJson, + configDir, + ...(docs ? { docs } : {}), + loglevel, + outputDir, + test, + quiet, + enableProdMode, + disableTelemetry, + angularBrowserTarget: browserTarget, + angularBuilderContext: context, + angularBuilderOptions: { + ...(stylePreprocessorOptions ? { stylePreprocessorOptions } : {}), + ...(styles ? { styles } : {}), + ...(assets ? { assets } : {}), + sourceMap, + preserveSymlinks, + experimentalZoneless, + }, + tsConfig, + webpackStatsJson, + statsJson, + debugWebpack, + previewUrl, + }; + + await runInstance({ ...standaloneOptions, mode: 'static' }); + return { success: true } as BuilderOutput; }; export default createBuilder(commandBuilder) as DevkitBuilder; @@ -182,9 +170,9 @@ async function setup(options: StorybookBuilderOptions, context: BuilderContext) }; } -function runInstance(options: StandaloneBuildOptions) { - return from( - withTelemetry( +async function runInstance(options: StandaloneBuildOptions) { + try { + await withTelemetry( 'build', { cliOptions: options, @@ -197,6 +185,9 @@ function runInstance(options: StandaloneBuildOptions) { logger.outro('Storybook build completed successfully'); return result; } - ) - ).pipe(catchError((error: any) => throwError(errorSummary(error)))); + ); + } catch (error) { + const summary = errorSummary(error); + throw new Error(summary); + } } diff --git a/code/frameworks/angular/src/builders/start-storybook/index.ts b/code/frameworks/angular/src/builders/start-storybook/index.ts index 8695d87662b0..d8ffa023f56c 100644 --- a/code/frameworks/angular/src/builders/start-storybook/index.ts +++ b/code/frameworks/angular/src/builders/start-storybook/index.ts @@ -26,8 +26,6 @@ import type { import type { JsonObject } from '@angular-devkit/core'; import * as find from 'empathic/find'; import * as pkg from 'empathic/package'; -import { Observable, from, of } from 'rxjs'; -import { map, mapTo, switchMap } from 'rxjs/operators'; import { errorSummary, printErrorDetails } from '../utils/error-handler'; import { runCompodoc } from '../utils/run-compodoc'; @@ -74,115 +72,108 @@ export type StorybookBuilderOptions = JsonObject & { export type StorybookBuilderOutput = JsonObject & BuilderOutput & {}; -const commandBuilder: BuilderHandlerFn = (options, context) => { - const builder = from(setup(options, context)).pipe( - switchMap(({ tsConfig }) => { - const docTSConfig = find.up('tsconfig.doc.json', { - cwd: options.configDir, - last: getProjectRoot(), - }); - - const runCompodoc$ = options.compodoc - ? runCompodoc( - { - compodocArgs: [...options.compodocArgs, ...(options.quiet ? ['--silent'] : [])], - tsconfig: docTSConfig ?? tsConfig, - }, - context - ).pipe(mapTo({ tsConfig })) - : of({}); - - return runCompodoc$.pipe(mapTo({ tsConfig })); - }), - map(({ tsConfig }) => { - getEnvConfig(options, { - port: 'SBCONFIG_PORT', - host: 'SBCONFIG_HOSTNAME', - staticDir: 'SBCONFIG_STATIC_DIR', - configDir: 'SBCONFIG_CONFIG_DIR', - ci: 'CI', - }); - - options.port = parseInt(`${options.port}`, 10); - - const { - browserTarget, - stylePreprocessorOptions, - styles, - ci, - configDir, - docs, - host, - https, - port, - quiet, - enableProdMode = false, - smokeTest, - sslCa, - sslCert, - sslKey, - disableTelemetry, - assets, - initialPath, - open, - debugWebpack, - loglevel, - webpackStatsJson, - statsJson, - previewUrl, - sourceMap = false, - preserveSymlinks = false, - experimentalZoneless = !!(VERSION.major && Number(VERSION.major) >= 21), - } = options; - - const packageJsonPath = pkg.up({ cwd: __dirname }); - const packageJson = - packageJsonPath != null ? JSON.parse(readFileSync(packageJsonPath, 'utf8')) : null; - - const standaloneOptions: StandaloneOptions = { - packageJson, - ci, - configDir, - ...(docs ? { docs } : {}), - host, - https, - port, - quiet, - enableProdMode, - smokeTest, - sslCa, - sslCert, - sslKey, - disableTelemetry, - angularBrowserTarget: browserTarget, - angularBuilderContext: context, - angularBuilderOptions: { - ...(stylePreprocessorOptions ? { stylePreprocessorOptions } : {}), - ...(styles ? { styles } : {}), - ...(assets ? { assets } : {}), - preserveSymlinks, - sourceMap, - experimentalZoneless, - }, - tsConfig, - initialPath, - open, - debugWebpack, - webpackStatsJson, - statsJson, - loglevel, - previewUrl, - }; - - return standaloneOptions; - }), - switchMap((standaloneOptions) => runInstance(standaloneOptions)), - map((port: number) => { - return { success: true, info: { port } }; - }) - ); - - return builder as any as BuilderOutput; +const commandBuilder: BuilderHandlerFn = async ( + options, + context +): Promise => { + const { tsConfig } = await setup(options, context); + + const docTSConfig = find.up('tsconfig.doc.json', { + cwd: options.configDir, + last: getProjectRoot(), + }); + + if (options.compodoc) { + await runCompodoc( + { + compodocArgs: [...options.compodocArgs, ...(options.quiet ? ['--silent'] : [])], + tsconfig: docTSConfig ?? tsConfig, + }, + context + ); + } + + getEnvConfig(options, { + port: 'SBCONFIG_PORT', + host: 'SBCONFIG_HOSTNAME', + staticDir: 'SBCONFIG_STATIC_DIR', + configDir: 'SBCONFIG_CONFIG_DIR', + ci: 'CI', + }); + + options.port = parseInt(`${options.port}`, 10); + + const { + browserTarget, + stylePreprocessorOptions, + styles, + ci, + configDir, + docs, + host, + https, + port, + quiet, + enableProdMode = false, + smokeTest, + sslCa, + sslCert, + sslKey, + disableTelemetry, + assets, + initialPath, + open, + debugWebpack, + loglevel, + webpackStatsJson, + statsJson, + previewUrl, + sourceMap = false, + preserveSymlinks = false, + experimentalZoneless = !!(VERSION.major && Number(VERSION.major) >= 21), + } = options; + + const packageJsonPath = pkg.up({ cwd: __dirname }); + const packageJson = + packageJsonPath != null ? JSON.parse(readFileSync(packageJsonPath, 'utf8')) : null; + + const standaloneOptions: StandaloneOptions = { + packageJson, + ci, + configDir, + ...(docs ? { docs } : {}), + host, + https, + port, + quiet, + enableProdMode, + smokeTest, + sslCa, + sslCert, + sslKey, + disableTelemetry, + angularBrowserTarget: browserTarget, + angularBuilderContext: context, + angularBuilderOptions: { + ...(stylePreprocessorOptions ? { stylePreprocessorOptions } : {}), + ...(styles ? { styles } : {}), + ...(assets ? { assets } : {}), + preserveSymlinks, + sourceMap, + experimentalZoneless, + }, + tsConfig, + initialPath, + open, + debugWebpack, + webpackStatsJson, + statsJson, + loglevel, + previewUrl, + }; + + const startedPort = await runInstance(standaloneOptions); + return { success: true, info: { port: startedPort } } as BuilderOutput; }; export default createBuilder(commandBuilder) as DevkitBuilder; @@ -206,10 +197,9 @@ async function setup(options: StorybookBuilderOptions, context: BuilderContext) browserOptions.tsConfig, }; } -function runInstance(options: StandaloneOptions) { - return new Observable((observer) => { - // This Observable intentionally never complete, leaving the process running ;) - withTelemetry( +async function runInstance(options: StandaloneOptions) { + try { + const { port } = await withTelemetry( 'dev', { cliOptions: options, @@ -220,10 +210,10 @@ function runInstance(options: StandaloneOptions) { logger.intro('Starting storybook'); return buildDevStandalone(options); } - ) - .then(({ port }) => observer.next(port)) - .catch((error) => { - observer.error(errorSummary(error)); - }); - }); + ); + return port; + } catch (error) { + const summarized = errorSummary(error); + throw new Error(String(summarized)); + } } diff --git a/code/frameworks/angular/src/builders/utils/run-compodoc.spec.ts b/code/frameworks/angular/src/builders/utils/run-compodoc.spec.ts index ebcf27f4c8e3..7ca3de67f9d4 100644 --- a/code/frameworks/angular/src/builders/utils/run-compodoc.spec.ts +++ b/code/frameworks/angular/src/builders/utils/run-compodoc.spec.ts @@ -1,7 +1,6 @@ import type { BuilderContext } from '@angular-devkit/architect'; // @ts-expect-error (TODO) import type { LoggerApi } from '@angular-devkit/core/src/logger'; -import { take } from 'rxjs/operators'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { runCompodoc } from './run-compodoc'; @@ -15,6 +14,13 @@ vi.mock('storybook/internal/common', () => ({ }), }, })); +vi.mock('storybook/internal/node-logger', () => ({ + prompt: { + executeTaskWithSpinner: async (fn: any) => { + await fn(); + }, + }, +})); const builderContextLoggerMock: LoggerApi = { createChild: vi.fn(), @@ -37,15 +43,13 @@ describe('runCompodoc', () => { } as BuilderContext; it('should run compodoc with tsconfig from context', async () => { - runCompodoc( + await runCompodoc( { compodocArgs: [], tsconfig: 'path/to/tsconfig.json', }, builderContextMock - ) - .pipe(take(1)) - .subscribe(); + ); expect(mockRunScript).toHaveBeenCalledWith({ args: ['compodoc', '-p', 'path/to/tsconfig.json', '-d', 'path/to/project'], @@ -54,15 +58,13 @@ describe('runCompodoc', () => { }); it('should run compodoc with tsconfig from compodocArgs', async () => { - runCompodoc( + await runCompodoc( { compodocArgs: ['-p', 'path/to/tsconfig.stories.json'], tsconfig: 'path/to/tsconfig.json', }, builderContextMock - ) - .pipe(take(1)) - .subscribe(); + ); expect(mockRunScript).toHaveBeenCalledWith({ args: ['compodoc', '-d', 'path/to/project', '-p', 'path/to/tsconfig.stories.json'], @@ -71,15 +73,13 @@ describe('runCompodoc', () => { }); it('should run compodoc with default output folder.', async () => { - runCompodoc( + await runCompodoc( { compodocArgs: [], tsconfig: 'path/to/tsconfig.json', }, builderContextMock - ) - .pipe(take(1)) - .subscribe(); + ); expect(mockRunScript).toHaveBeenCalledWith({ args: ['compodoc', '-p', 'path/to/tsconfig.json', '-d', 'path/to/project'], @@ -88,15 +88,13 @@ describe('runCompodoc', () => { }); it('should run with custom output folder specified with --output compodocArgs', async () => { - runCompodoc( + await runCompodoc( { compodocArgs: ['--output', 'path/to/customFolder'], tsconfig: 'path/to/tsconfig.json', }, builderContextMock - ) - .pipe(take(1)) - .subscribe(); + ); expect(mockRunScript).toHaveBeenCalledWith({ args: ['compodoc', '-p', 'path/to/tsconfig.json', '--output', 'path/to/customFolder'], @@ -105,15 +103,13 @@ describe('runCompodoc', () => { }); it('should run with custom output folder specified with -d compodocArgs', async () => { - runCompodoc( + await runCompodoc( { compodocArgs: ['-d', 'path/to/customFolder'], tsconfig: 'path/to/tsconfig.json', }, builderContextMock - ) - .pipe(take(1)) - .subscribe(); + ); expect(mockRunScript).toHaveBeenCalledWith({ args: ['compodoc', '-p', 'path/to/tsconfig.json', '-d', 'path/to/customFolder'], diff --git a/code/frameworks/angular/src/builders/utils/run-compodoc.ts b/code/frameworks/angular/src/builders/utils/run-compodoc.ts index fd0a6353306a..e93aa9a40711 100644 --- a/code/frameworks/angular/src/builders/utils/run-compodoc.ts +++ b/code/frameworks/angular/src/builders/utils/run-compodoc.ts @@ -3,7 +3,7 @@ import { isAbsolute, relative } from 'node:path'; import { JsPackageManagerFactory } from 'storybook/internal/common'; import type { BuilderContext } from '@angular-devkit/architect'; -import { Observable } from 'rxjs'; +import { prompt } from 'storybook/internal/node-logger'; const hasTsConfigArg = (args: string[]) => args.indexOf('-p') !== -1; const hasOutputArg = (args: string[]) => @@ -15,35 +15,31 @@ const toRelativePath = (pathToTsConfig: string) => { return isAbsolute(pathToTsConfig) ? relative('.', pathToTsConfig) : pathToTsConfig; }; -export const runCompodoc = ( +export const runCompodoc = async ( { compodocArgs, tsconfig }: { compodocArgs: string[]; tsconfig: string }, context: BuilderContext -): Observable => { - return new Observable((observer) => { - const tsConfigPath = toRelativePath(tsconfig); - const finalCompodocArgs = [ - 'compodoc', - ...(hasTsConfigArg(compodocArgs) ? [] : ['-p', tsConfigPath]), - ...(hasOutputArg(compodocArgs) ? [] : ['-d', `${context.workspaceRoot || '.'}`]), - ...compodocArgs, - ]; +): Promise => { + const tsConfigPath = toRelativePath(tsconfig); + const finalCompodocArgs = [ + 'compodoc', + ...(hasTsConfigArg(compodocArgs) ? [] : ['-p', tsConfigPath]), + ...(hasOutputArg(compodocArgs) ? [] : ['-d', `${context.workspaceRoot || '.'}`]), + ...compodocArgs, + ]; - const packageManager = JsPackageManagerFactory.getPackageManager(); + const packageManager = JsPackageManagerFactory.getPackageManager(); - try { - packageManager - .runPackageCommand({ - args: finalCompodocArgs, - cwd: context.workspaceRoot, - }) - .then((result) => { - context.logger.info(result.stdout); - observer.next(); - observer.complete(); - }); - } catch (e) { - context.logger.error(e); - observer.error(); + await prompt.executeTaskWithSpinner( + () => + packageManager.runPackageCommand({ + args: finalCompodocArgs, + cwd: context.workspaceRoot, + }), + { + id: 'compodoc', + intro: 'Generating documentation with Compodoc', + success: 'Compodoc finished successfully', + error: 'Compodoc failed', } - }); + ); }; From ed50d99f8dcd565c9ce978a1462571fbe3171d81 Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Tue, 25 Nov 2025 07:54:30 +0000 Subject: [PATCH 2/2] Enhance logging for Storybook build and start processes --- .../frameworks/angular/src/builders/build-storybook/index.ts | 5 +++-- .../frameworks/angular/src/builders/start-storybook/index.ts | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/code/frameworks/angular/src/builders/build-storybook/index.ts b/code/frameworks/angular/src/builders/build-storybook/index.ts index ddeff53ff64d..191f93a16e31 100644 --- a/code/frameworks/angular/src/builders/build-storybook/index.ts +++ b/code/frameworks/angular/src/builders/build-storybook/index.ts @@ -71,6 +71,8 @@ const commandBuilder: BuilderHandlerFn = async ( options, context ): Promise => { + logger.intro('Building Storybook'); + const { tsConfig } = await setup(options, context); const docTSConfig = find.up('tsconfig.doc.json', { @@ -145,6 +147,7 @@ const commandBuilder: BuilderHandlerFn = async ( }; await runInstance({ ...standaloneOptions, mode: 'static' }); + logger.outro('Storybook build completed successfully'); return { success: true } as BuilderOutput; }; @@ -180,9 +183,7 @@ async function runInstance(options: StandaloneBuildOptions) { printError: printErrorDetails, }, async () => { - logger.intro('Building storybook'); const result = await buildStaticStandalone(options); - logger.outro('Storybook build completed successfully'); return result; } ); diff --git a/code/frameworks/angular/src/builders/start-storybook/index.ts b/code/frameworks/angular/src/builders/start-storybook/index.ts index d8ffa023f56c..e09d6089fb3b 100644 --- a/code/frameworks/angular/src/builders/start-storybook/index.ts +++ b/code/frameworks/angular/src/builders/start-storybook/index.ts @@ -76,6 +76,8 @@ const commandBuilder: BuilderHandlerFn = async ( options, context ): Promise => { + logger.intro('Starting Storybook'); + const { tsConfig } = await setup(options, context); const docTSConfig = find.up('tsconfig.doc.json', { @@ -207,7 +209,6 @@ async function runInstance(options: StandaloneOptions) { printError: printErrorDetails, }, () => { - logger.intro('Starting storybook'); return buildDevStandalone(options); } );