Skip to content

Conversation

@valentinpalkovic
Copy link
Contributor

@valentinpalkovic valentinpalkovic commented Nov 26, 2025

Closes #

What I did

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>

Summary by CodeRabbit

  • Refactor
    • Refactored Angular Storybook builder's internal control flow mechanism for improved architecture and maintainability. The dev server behavior and watch mode functionality remain unchanged.

✏️ Tip: You can customize this high-level summary in your review settings.

@nx-cloud
Copy link

nx-cloud bot commented Nov 26, 2025

View your CI Pipeline Execution ↗ for commit ef6ccb0

Command Status Duration Result
nx run-many -t build --parallel=3 ✅ Succeeded 45s View ↗

☁️ Nx Cloud last updated this comment at 2025-11-26 11:54:46 UTC

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 26, 2025

📝 Walkthrough

Walkthrough

Converts the commandBuilder function from async Promise-based to Observable-based control flow in the Angular Storybook builder. The function now returns Observable<BuilderOutput> instead of Promise<BuilderOutput>, wraps logic in an Observable constructor, and emits results via observer methods rather than direct returns. Adds rxjs Observable import to support the new pattern.

Changes

Cohort / File(s) Summary
Angular Storybook Builder Start Command
code/frameworks/angular/src/builders/start-storybook/index.ts
Updated commandBuilder function signature from async (options, context): Promise<BuilderOutput> to (options, context): Observable<BuilderOutput>. Wrapped function body in new Observable constructor, replacing direct returns with observer.next() for success and observer.error() for error handling. Added rxjs Observable import.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

  • Single file affected with a focused control flow pattern conversion
  • Method signature change is straightforward (Promise → Observable)
  • Existing logic wrapped in Observable constructor with standard observer emission pattern
  • Areas requiring attention:
    • Verify error handling paths correctly route through observer.error() and terminate subscription appropriately
    • Confirm the Observable lifecycle keeps the dev server running during watch mode as intended
    • Check that observer.complete() is called or not called based on expected behavior (dev server continuous operation)

Possibly related PRs

✨ Finishing touches
  • 📝 Generate docstrings

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa3ef6e and ef6ccb0.

📒 Files selected for processing (1)
  • code/frameworks/angular/src/builders/start-storybook/index.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use camelCase for variable and function names

Files:

  • code/frameworks/angular/src/builders/start-storybook/index.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Enable TypeScript strict mode
Export functions from modules for testing purposes

Files:

  • code/frameworks/angular/src/builders/start-storybook/index.ts
**/*.{ts,tsx,js,jsx,json,html,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx,js,jsx,json,html,mjs}: Use ESLint and Prettier for code style enforcement
Run 'yarn prettier --write ' to format code after making changes
Run 'yarn lint:js:cmd ' to check for ESLint issues after making changes

Files:

  • code/frameworks/angular/src/builders/start-storybook/index.ts
code/**/!(*.test).{ts,tsx,js,mjs}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

code/**/!(*.test).{ts,tsx,js,mjs}: Use 'logger' from 'storybook/internal/node-logger' for server-side (Node.js) logging, not console.log/console.warn/console.error
Use 'logger' from 'storybook/internal/client-logger' for client-side (browser) logging, not console.log/console.warn/console.error
Do not use console.log, console.warn, or console.error directly unless in isolated files where importing loggers would significantly increase bundle size

Files:

  • code/frameworks/angular/src/builders/start-storybook/index.ts
🧬 Code graph analysis (1)
code/frameworks/angular/src/builders/start-storybook/index.ts (3)
code/frameworks/angular/src/builders/utils/run-compodoc.ts (1)
  • runCompodoc (18-45)
code/core/src/common/utils/cli.ts (1)
  • getEnvConfig (69-77)
code/core/src/common/js-package-manager/JsPackageManager.ts (1)
  • packageJson (768-770)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: normal
  • GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (3)
code/frameworks/angular/src/builders/start-storybook/index.ts (3)

27-27: LGTM! Observable import is necessary.

The rxjs import is required for the new Observable-based return type and is correctly placed with other imports.


76-94: Excellent fix! Observable pattern correctly prevents premature termination.

The conversion from async Promise to Observable with an async IIFE is the right approach. This pattern allows the dev server to remain running by keeping the Observable subscription alive, which is exactly what the PR title indicates was the issue.

The structure correctly:

  • Returns Observable<BuilderOutput> as expected by Angular Architect
  • Uses an async IIFE to enable await calls within the Observable
  • Wraps logic in try-catch to route errors via observer.error()

183-188: Perfect! This is the core fix that prevents the dev server from being killed.

The intentional omission of observer.complete() is exactly right. By keeping the Observable alive after emitting success, the Angular Architect subscription remains active, which prevents the dev command from terminating prematurely. The inline comment clearly documents this critical behavior.

This pattern is standard for long-running Angular builders (like dev servers) that need to remain active for watch mode.

Comment on lines +76 to 194
const commandBuilder: BuilderHandlerFn<StorybookBuilderOptions> = (
options,
context
): Promise<BuilderOutput> => {
logger.intro('Starting Storybook');

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',
): Observable<BuilderOutput> => {
return new Observable<BuilderOutput>((observer) => {
(async () => {
try {
logger.intro('Starting Storybook');

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);

// Emit success output - the dev server is now running
observer.next({ success: true, info: { port: startedPort } } as BuilderOutput);

// Don't call observer.complete() - this keeps the Observable alive
// so the dev server continues running. Architect will keep subscribing
// until the Observable completes, which allows watch mode to work.
} catch (error) {
observer.error(error);
}
})();
});

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;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Angular Architect builder Observable lifecycle long-running dev server


<web_search_result>
Short summary — how to implement a long-running dev-server builder with Architect (Observable lifecycle):

  • Return an Observable (not Promise) from your builder so Architect can keep it subscribed for watch-mode or long-running servers. [1]
  • Each build cycle must emit a BuilderOutput object when the run finishes (success/failure). After emitting, the builder may enter a “watching” phase. [1][2]
  • While in watch mode, when an external event restarts a build, call context.reportRunning() so Architect knows the builder is active and won’t stop it. [1][2]
  • Expose teardown/stop logic so Architect can stop your long-running builder: Architect will call BuilderRun.stop() / unsubscribe the Observable and then run your cleanup. Ensure your Observable’s teardown closes server sockets, watchers and other resources. [1][3]
  • Structure your code into three phases: (1) Running — perform the build and emit BuilderOutput; (2) Watching — listen for change events and call reportRunning() before re-entering Running; (3) Completion — respond to stop and clean up. This prevents leaks and makes restarts deterministic. [1][2]
  • Practical tips: replay last output if you expose an Observable output, keep Observable hot while watching, and time out or handle unresponsive stop requests in your teardown to avoid hung runs. Use context.reportStatus()/reportProgress() for status updates. [3][1]

Sources:
[1] Angular CLI Builders (official docs) — watch mode, reportRunning, lifecycle.
[2] Community/docs mirrors explaining watch phases and reportRunning.
[3] @angular-devkit/architect / builder typings (BuilderRun/stop/output fields).
</web_search_result>


Script executed:

#!/bin/bash
# Search for other builder implementations using Observable in the angular framework
fd -t f "builder" code/frameworks/angular --exec grep -l "Observable" {} \;

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 1

</shell_result>


Script executed:

#!/bin/bash
# Check the runInstance function to understand cleanup behavior
find code/frameworks/angular -name "*.ts" -type f | xargs grep -l "runInstance" | head -5

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

stdout:

code/frameworks/angular/src/builders/build-storybook/index.ts
code/frameworks/angular/src/builders/start-storybook/index.ts

</shell_result>


Script executed:

#!/bin/bash
# Look for dispose/cleanup patterns in Storybook's Angular builders
rg -A 10 -B 5 "observer\.complete|subscription|unsubscribe|dispose" code/frameworks/angular --type ts | head -100

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

stdout:

code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-      }
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-    }
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-    ngOnDestroy(): void {
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-      if (this.storyComponentPropsSubscription != null) {
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts:        this.storyComponentPropsSubscription.unsubscribe();
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-      }
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-      if (this.storyWrapperPropsSubscription != null) {
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts:        this.storyWrapperPropsSubscription.unsubscribe();
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-      }
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-    }
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-  }
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-  return StorybookWrapperComponent;
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-};
--
code/frameworks/angular/src/client/angular-beta/StorybookProvider.ts-          ngZone.run(() => subscriber.complete());
code/frameworks/angular/src/client/angular-beta/StorybookProvider.ts-        }
code/frameworks/angular/src/client/angular-beta/StorybookProvider.ts-      );
code/frameworks/angular/src/client/angular-beta/StorybookProvider.ts-
code/frameworks/angular/src/client/angular-beta/StorybookProvider.ts-      return () => {
code/frameworks/angular/src/client/angular-beta/StorybookProvider.ts:        sub.unsubscribe();
code/frameworks/angular/src/client/angular-beta/StorybookProvider.ts-      };
code/frameworks/angular/src/client/angular-beta/StorybookProvider.ts-    });
code/frameworks/angular/src/client/angular-beta/StorybookProvider.ts-}
--
code/frameworks/angular/src/builders/start-storybook/index.ts-        const startedPort = await runInstance(standaloneOptions);
code/frameworks/angular/src/builders/start-storybook/index.ts-
code/frameworks/angular/src/builders/start-storybook/index.ts-        // Emit success output - the dev server is now running
code/frameworks/angular/src/builders/start-storybook/index.ts-        observer.next({ success: true, info: { port: startedPort } } as BuilderOutput);
code/frameworks/angular/src/builders/start-storybook/index.ts-
code/frameworks/angular/src/builders/start-storybook/index.ts:        // Don't call observer.complete() - this keeps the Observable alive
code/frameworks/angular/src/builders/start-storybook/index.ts-        // so the dev server continues running. Architect will keep subscribing
code/frameworks/angular/src/builders/start-storybook/index.ts-        // until the Observable completes, which allows watch mode to work.
code/frameworks/angular/src/builders/start-storybook/index.ts-      } catch (error) {
code/frameworks/angular/src/builders/start-storybook/index.ts-        observer.error(error);
code/frameworks/angular/src/builders/start-storybook/index.ts-      }
code/frameworks/angular/src/builders/start-storybook/index.ts-    })();
code/frameworks/angular/src/builders/start-storybook/index.ts-  });
code/frameworks/angular/src/builders/start-storybook/index.ts-};
code/frameworks/angular/src/builders/start-storybook/index.ts-
code/frameworks/angular/src/builders/start-storybook/index.ts-export default createBuilder(commandBuilder) as DevkitBuilder<StorybookBuilderOptions & JsonObject>;

</shell_result>


Script executed:

#!/bin/bash
# Check if there are any comments or documentation about Observable lifecycle
rg -C 5 "dev server|long.running|never.complet" code/frameworks/angular --type ts

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

stdout:

code/frameworks/angular/src/builders/start-storybook/index.ts-          previewUrl,
code/frameworks/angular/src/builders/start-storybook/index.ts-        };
code/frameworks/angular/src/builders/start-storybook/index.ts-
code/frameworks/angular/src/builders/start-storybook/index.ts-        const startedPort = await runInstance(standaloneOptions);
code/frameworks/angular/src/builders/start-storybook/index.ts-
code/frameworks/angular/src/builders/start-storybook/index.ts:        // Emit success output - the dev server is now running
code/frameworks/angular/src/builders/start-storybook/index.ts-        observer.next({ success: true, info: { port: startedPort } } as BuilderOutput);
code/frameworks/angular/src/builders/start-storybook/index.ts-
code/frameworks/angular/src/builders/start-storybook/index.ts-        // Don't call observer.complete() - this keeps the Observable alive
code/frameworks/angular/src/builders/start-storybook/index.ts:        // so the dev server continues running. Architect will keep subscribing
code/frameworks/angular/src/builders/start-storybook/index.ts-        // until the Observable completes, which allows watch mode to work.
code/frameworks/angular/src/builders/start-storybook/index.ts-      } catch (error) {
code/frameworks/angular/src/builders/start-storybook/index.ts-        observer.error(error);
code/frameworks/angular/src/builders/start-storybook/index.ts-      }
code/frameworks/angular/src/builders/start-storybook/index.ts-    })();

</shell_result>


Script executed:

#!/bin/bash
# Find runInstance implementation
rg -B 5 -A 30 "function runInstance|const runInstance|export.*runInstance" code/frameworks/angular --type ts

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

stdout:

code/frameworks/angular/src/builders/start-storybook/index.ts-      options.tsConfig ??
code/frameworks/angular/src/builders/start-storybook/index.ts-      find.up('tsconfig.json', { cwd: options.configDir }) ??
code/frameworks/angular/src/builders/start-storybook/index.ts-      browserOptions.tsConfig,
code/frameworks/angular/src/builders/start-storybook/index.ts-  };
code/frameworks/angular/src/builders/start-storybook/index.ts-}
code/frameworks/angular/src/builders/start-storybook/index.ts:async function runInstance(options: StandaloneOptions) {
code/frameworks/angular/src/builders/start-storybook/index.ts-  try {
code/frameworks/angular/src/builders/start-storybook/index.ts-    const { port } = await withTelemetry(
code/frameworks/angular/src/builders/start-storybook/index.ts-      'dev',
code/frameworks/angular/src/builders/start-storybook/index.ts-      {
code/frameworks/angular/src/builders/start-storybook/index.ts-        cliOptions: options,
code/frameworks/angular/src/builders/start-storybook/index.ts-        presetOptions: { ...options, corePresets: [], overridePresets: [] },
code/frameworks/angular/src/builders/start-storybook/index.ts-        printError: printErrorDetails,
code/frameworks/angular/src/builders/start-storybook/index.ts-      },
code/frameworks/angular/src/builders/start-storybook/index.ts-      () => {
code/frameworks/angular/src/builders/start-storybook/index.ts-        return buildDevStandalone(options);
code/frameworks/angular/src/builders/start-storybook/index.ts-      }
code/frameworks/angular/src/builders/start-storybook/index.ts-    );
code/frameworks/angular/src/builders/start-storybook/index.ts-    return port;
code/frameworks/angular/src/builders/start-storybook/index.ts-  } catch (error) {
code/frameworks/angular/src/builders/start-storybook/index.ts-    const summarized = errorSummary(error);
code/frameworks/angular/src/builders/start-storybook/index.ts-    throw new Error(String(summarized));
code/frameworks/angular/src/builders/start-storybook/index.ts-  }
code/frameworks/angular/src/builders/start-storybook/index.ts-}
--
code/frameworks/angular/src/builders/build-storybook/index.ts-      find.up('tsconfig.json', { cwd: options.configDir, last: getProjectRoot() }) ??
code/frameworks/angular/src/builders/build-storybook/index.ts-      browserOptions.tsConfig,
code/frameworks/angular/src/builders/build-storybook/index.ts-  };
code/frameworks/angular/src/builders/build-storybook/index.ts-}
code/frameworks/angular/src/builders/build-storybook/index.ts-
code/frameworks/angular/src/builders/build-storybook/index.ts:async function runInstance(options: StandaloneBuildOptions) {
code/frameworks/angular/src/builders/build-storybook/index.ts-  try {
code/frameworks/angular/src/builders/build-storybook/index.ts-    await withTelemetry(
code/frameworks/angular/src/builders/build-storybook/index.ts-      'build',
code/frameworks/angular/src/builders/build-storybook/index.ts-      {
code/frameworks/angular/src/builders/build-storybook/index.ts-        cliOptions: options,
code/frameworks/angular/src/builders/build-storybook/index.ts-        presetOptions: { ...options, corePresets: [], overridePresets: [] },
code/frameworks/angular/src/builders/build-storybook/index.ts-        printError: printErrorDetails,
code/frameworks/angular/src/builders/build-storybook/index.ts-      },
code/frameworks/angular/src/builders/build-storybook/index.ts-      async () => {
code/frameworks/angular/src/builders/build-storybook/index.ts-        const result = await buildStaticStandalone(options);
code/frameworks/angular/src/builders/build-storybook/index.ts-        return result;
code/frameworks/angular/src/builders/build-storybook/index.ts-      }
code/frameworks/angular/src/builders/build-storybook/index.ts-    );
code/frameworks/angular/src/builders/build-storybook/index.ts-  } catch (error) {
code/frameworks/angular/src/builders/build-storybook/index.ts-    const summary = errorSummary(error);
code/frameworks/angular/src/builders/build-storybook/index.ts-    throw new Error(summary);
code/frameworks/angular/src/builders/build-storybook/index.ts-  }
code/frameworks/angular/src/builders/build-storybook/index.ts-}

</shell_result>


Script executed:

#!/bin/bash
# Check the build-storybook builder for comparison
cat -n code/frameworks/angular/src/builders/build-storybook/index.ts | head -200

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

stdout:

     1	import { readFileSync } from 'node:fs';
     2	
     3	import { getEnvConfig, getProjectRoot, versions } from 'storybook/internal/common';
     4	import { buildStaticStandalone, withTelemetry } from 'storybook/internal/core-server';
     5	import { addToGlobalContext } from 'storybook/internal/telemetry';
     6	import type { CLIOptions } from 'storybook/internal/types';
     7	import { logger } from 'storybook/internal/node-logger';
     8	
     9	import type {
    10	  BuilderContext,
    11	  BuilderHandlerFn,
    12	  BuilderOutput,
    13	  Target,
    14	  Builder as DevkitBuilder,
    15	} from '@angular-devkit/architect';
    16	import { createBuilder, targetFromTargetString } from '@angular-devkit/architect';
    17	import type {
    18	  BrowserBuilderOptions,
    19	  StylePreprocessorOptions,
    20	} from '@angular-devkit/build-angular';
    21	import type {
    22	  AssetPattern,
    23	  SourceMapUnion,
    24	  StyleElement,
    25	} from '@angular-devkit/build-angular/src/builders/browser/schema';
    26	import type { JsonObject } from '@angular-devkit/core';
    27	import * as find from 'empathic/find';
    28	import * as pkg from 'empathic/package';
    29	
    30	import { errorSummary, printErrorDetails } from '../utils/error-handler';
    31	import { runCompodoc } from '../utils/run-compodoc';
    32	import type { StandaloneOptions } from '../utils/standalone-options';
    33	import { VERSION } from '@angular/core';
    34	
    35	addToGlobalContext('cliVersion', versions.storybook);
    36	
    37	export type StorybookBuilderOptions = JsonObject & {
    38	  browserTarget?: string | null;
    39	  tsConfig?: string;
    40	  test: boolean;
    41	  docs: boolean;
    42	  compodoc: boolean;
    43	  compodocArgs: string[];
    44	  enableProdMode?: boolean;
    45	  styles?: StyleElement[];
    46	  stylePreprocessorOptions?: StylePreprocessorOptions;
    47	  preserveSymlinks?: boolean;
    48	  assets?: AssetPattern[];
    49	  sourceMap?: SourceMapUnion;
    50	  experimentalZoneless?: boolean;
    51	} & Pick<
    52	    // makes sure the option exists
    53	    CLIOptions,
    54	    | 'outputDir'
    55	    | 'configDir'
    56	    | 'loglevel'
    57	    | 'quiet'
    58	    | 'test'
    59	    | 'webpackStatsJson'
    60	    | 'statsJson'
    61	    | 'disableTelemetry'
    62	    | 'debugWebpack'
    63	    | 'previewUrl'
    64	  >;
    65	
    66	export type StorybookBuilderOutput = JsonObject & BuilderOutput & { [key: string]: any };
    67	
    68	type StandaloneBuildOptions = StandaloneOptions & { outputDir: string };
    69	
    70	const commandBuilder: BuilderHandlerFn<StorybookBuilderOptions> = async (
    71	  options,
    72	  context
    73	): Promise<BuilderOutput> => {
    74	  logger.intro('Building Storybook');
    75	
    76	  const { tsConfig } = await setup(options, context);
    77	
    78	  const docTSConfig = find.up('tsconfig.doc.json', {
    79	    cwd: options.configDir,
    80	    last: getProjectRoot(),
    81	  });
    82	
    83	  if (options.compodoc) {
    84	    await runCompodoc(
    85	      { compodocArgs: options.compodocArgs, tsconfig: docTSConfig ?? tsConfig },
    86	      context
    87	    );
    88	  }
    89	
    90	  getEnvConfig(options, {
    91	    staticDir: 'SBCONFIG_STATIC_DIR',
    92	    outputDir: 'SBCONFIG_OUTPUT_DIR',
    93	    configDir: 'SBCONFIG_CONFIG_DIR',
    94	  });
    95	
    96	  const {
    97	    browserTarget,
    98	    stylePreprocessorOptions,
    99	    styles,
   100	    configDir,
   101	    docs,
   102	    loglevel,
   103	    test,
   104	    outputDir,
   105	    quiet,
   106	    enableProdMode = true,
   107	    webpackStatsJson,
   108	    statsJson,
   109	    debugWebpack,
   110	    disableTelemetry,
   111	    assets,
   112	    previewUrl,
   113	    sourceMap = false,
   114	    preserveSymlinks = false,
   115	    experimentalZoneless = !!(VERSION.major && Number(VERSION.major) >= 21),
   116	  } = options;
   117	
   118	  const packageJsonPath = pkg.up({ cwd: __dirname });
   119	  const packageJson =
   120	    packageJsonPath != null ? JSON.parse(readFileSync(packageJsonPath, 'utf8')) : null;
   121	
   122	  const standaloneOptions: StandaloneBuildOptions = {
   123	    packageJson,
   124	    configDir,
   125	    ...(docs ? { docs } : {}),
   126	    loglevel,
   127	    outputDir,
   128	    test,
   129	    quiet,
   130	    enableProdMode,
   131	    disableTelemetry,
   132	    angularBrowserTarget: browserTarget,
   133	    angularBuilderContext: context,
   134	    angularBuilderOptions: {
   135	      ...(stylePreprocessorOptions ? { stylePreprocessorOptions } : {}),
   136	      ...(styles ? { styles } : {}),
   137	      ...(assets ? { assets } : {}),
   138	      sourceMap,
   139	      preserveSymlinks,
   140	      experimentalZoneless,
   141	    },
   142	    tsConfig,
   143	    webpackStatsJson,
   144	    statsJson,
   145	    debugWebpack,
   146	    previewUrl,
   147	  };
   148	
   149	  await runInstance({ ...standaloneOptions, mode: 'static' });
   150	  logger.outro('Storybook build completed successfully');
   151	  return { success: true } as BuilderOutput;
   152	};
   153	
   154	export default createBuilder(commandBuilder) as DevkitBuilder<StorybookBuilderOptions & JsonObject>;
   155	
   156	async function setup(options: StorybookBuilderOptions, context: BuilderContext) {
   157	  let browserOptions: (JsonObject & BrowserBuilderOptions) | undefined;
   158	  let browserTarget: Target | undefined;
   159	
   160	  if (options.browserTarget) {
   161	    browserTarget = targetFromTargetString(options.browserTarget);
   162	    browserOptions = await context.validateOptions<JsonObject & BrowserBuilderOptions>(
   163	      await context.getTargetOptions(browserTarget),
   164	      await context.getBuilderNameForTarget(browserTarget)
   165	    );
   166	  }
   167	
   168	  return {
   169	    tsConfig:
   170	      options.tsConfig ??
   171	      find.up('tsconfig.json', { cwd: options.configDir, last: getProjectRoot() }) ??
   172	      browserOptions.tsConfig,
   173	  };
   174	}
   175	
   176	async function runInstance(options: StandaloneBuildOptions) {
   177	  try {
   178	    await withTelemetry(
   179	      'build',
   180	      {
   181	        cliOptions: options,
   182	        presetOptions: { ...options, corePresets: [], overridePresets: [] },
   183	        printError: printErrorDetails,
   184	      },
   185	      async () => {
   186	        const result = await buildStaticStandalone(options);
   187	        return result;
   188	      }
   189	    );
   190	  } catch (error) {
   191	    const summary = errorSummary(error);
   192	    throw new Error(summary);
   193	  }
   194	}

</shell_result>


Script executed:

#!/bin/bash
# Search for Observable subscription handling and teardown in the builders
rg -B 3 -A 10 "return new Observable" code/frameworks/angular/src/builders --type ts

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

stdout:

code/frameworks/angular/src/builders/start-storybook/index.ts-  options,
code/frameworks/angular/src/builders/start-storybook/index.ts-  context
code/frameworks/angular/src/builders/start-storybook/index.ts-): Observable<BuilderOutput> => {
code/frameworks/angular/src/builders/start-storybook/index.ts:  return new Observable<BuilderOutput>((observer) => {
code/frameworks/angular/src/builders/start-storybook/index.ts-    (async () => {
code/frameworks/angular/src/builders/start-storybook/index.ts-      try {
code/frameworks/angular/src/builders/start-storybook/index.ts-        logger.intro('Starting Storybook');
code/frameworks/angular/src/builders/start-storybook/index.ts-
code/frameworks/angular/src/builders/start-storybook/index.ts-        const { tsConfig } = await setup(options, context);
code/frameworks/angular/src/builders/start-storybook/index.ts-
code/frameworks/angular/src/builders/start-storybook/index.ts-        const docTSConfig = find.up('tsconfig.doc.json', {
code/frameworks/angular/src/builders/start-storybook/index.ts-          cwd: options.configDir,
code/frameworks/angular/src/builders/start-storybook/index.ts-          last: getProjectRoot(),
code/frameworks/angular/src/builders/start-storybook/index.ts-        });

</shell_result>


Script executed:

#!/bin/bash
# Check if there's any cleanup/stop logic in the start-storybook file
cat -n code/frameworks/angular/src/builders/start-storybook/index.ts | tail -50

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

stdout:

   186	        // Don't call observer.complete() - this keeps the Observable alive
   187	        // so the dev server continues running. Architect will keep subscribing
   188	        // until the Observable completes, which allows watch mode to work.
   189	      } catch (error) {
   190	        observer.error(error);
   191	      }
   192	    })();
   193	  });
   194	};
   195	
   196	export default createBuilder(commandBuilder) as DevkitBuilder<StorybookBuilderOptions & JsonObject>;
   197	
   198	async function setup(options: StorybookBuilderOptions, context: BuilderContext) {
   199	  let browserOptions: (JsonObject & BrowserBuilderOptions) | undefined;
   200	  let browserTarget: Target | undefined;
   201	
   202	  if (options.browserTarget) {
   203	    browserTarget = targetFromTargetString(options.browserTarget);
   204	    browserOptions = await context.validateOptions<JsonObject & BrowserBuilderOptions>(
   205	      await context.getTargetOptions(browserTarget),
   206	      await context.getBuilderNameForTarget(browserTarget)
   207	    );
   208	  }
   209	
   210	  return {
   211	    tsConfig:
   212	      options.tsConfig ??
   213	      find.up('tsconfig.json', { cwd: options.configDir }) ??
   214	      browserOptions.tsConfig,
   215	  };
   216	}
   217	async function runInstance(options: StandaloneOptions) {
   218	  try {
   219	    const { port } = await withTelemetry(
   220	      'dev',
   221	      {
   222	        cliOptions: options,
   223	        presetOptions: { ...options, corePresets: [], overridePresets: [] },
   224	        printError: printErrorDetails,
   225	      },
   226	      () => {
   227	        return buildDevStandalone(options);
   228	      }
   229	    );
   230	    return port;
   231	  } catch (error) {
   232	    const summarized = errorSummary(error);
   233	    throw new Error(String(summarized));
   234	  }
   235	}

</shell_result>


Script executed:

#!/bin/bash
# Search for buildDevStandalone to understand if it manages its own cleanup
rg -B 5 -A 20 "buildDevStandalone" code --type ts | head -100

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

stdout:

code/frameworks/angular/src/builders/start-storybook/index.ts-import { readFileSync } from 'node:fs';
code/frameworks/angular/src/builders/start-storybook/index.ts-
code/frameworks/angular/src/builders/start-storybook/index.ts-import { getEnvConfig, getProjectRoot, versions } from 'storybook/internal/common';
code/frameworks/angular/src/builders/start-storybook/index.ts:import { buildDevStandalone, withTelemetry } from 'storybook/internal/core-server';
code/frameworks/angular/src/builders/start-storybook/index.ts-import { addToGlobalContext } from 'storybook/internal/telemetry';
code/frameworks/angular/src/builders/start-storybook/index.ts-import type { CLIOptions } from 'storybook/internal/types';
code/frameworks/angular/src/builders/start-storybook/index.ts-import { logger } from 'storybook/internal/node-logger';
code/frameworks/angular/src/builders/start-storybook/index.ts-
code/frameworks/angular/src/builders/start-storybook/index.ts-import type {
code/frameworks/angular/src/builders/start-storybook/index.ts-  BuilderContext,
code/frameworks/angular/src/builders/start-storybook/index.ts-  BuilderHandlerFn,
code/frameworks/angular/src/builders/start-storybook/index.ts-  BuilderOutput,
code/frameworks/angular/src/builders/start-storybook/index.ts-  Target,
code/frameworks/angular/src/builders/start-storybook/index.ts-  Builder as DevkitBuilder,
code/frameworks/angular/src/builders/start-storybook/index.ts-} from '@angular-devkit/architect';
code/frameworks/angular/src/builders/start-storybook/index.ts-import { createBuilder, targetFromTargetString } from '@angular-devkit/architect';
code/frameworks/angular/src/builders/start-storybook/index.ts-import type {
code/frameworks/angular/src/builders/start-storybook/index.ts-  BrowserBuilderOptions,
code/frameworks/angular/src/builders/start-storybook/index.ts-  StylePreprocessorOptions,
code/frameworks/angular/src/builders/start-storybook/index.ts-} from '@angular-devkit/build-angular';
code/frameworks/angular/src/builders/start-storybook/index.ts-import type {
code/frameworks/angular/src/builders/start-storybook/index.ts-  AssetPattern,
code/frameworks/angular/src/builders/start-storybook/index.ts-  SourceMapUnion,
code/frameworks/angular/src/builders/start-storybook/index.ts-  StyleElement,
--
code/frameworks/angular/src/builders/start-storybook/index.ts-        cliOptions: options,
code/frameworks/angular/src/builders/start-storybook/index.ts-        presetOptions: { ...options, corePresets: [], overridePresets: [] },
code/frameworks/angular/src/builders/start-storybook/index.ts-        printError: printErrorDetails,
code/frameworks/angular/src/builders/start-storybook/index.ts-      },
code/frameworks/angular/src/builders/start-storybook/index.ts-      () => {
code/frameworks/angular/src/builders/start-storybook/index.ts:        return buildDevStandalone(options);
code/frameworks/angular/src/builders/start-storybook/index.ts-      }
code/frameworks/angular/src/builders/start-storybook/index.ts-    );
code/frameworks/angular/src/builders/start-storybook/index.ts-    return port;
code/frameworks/angular/src/builders/start-storybook/index.ts-  } catch (error) {
code/frameworks/angular/src/builders/start-storybook/index.ts-    const summarized = errorSummary(error);
code/frameworks/angular/src/builders/start-storybook/index.ts-    throw new Error(String(summarized));
code/frameworks/angular/src/builders/start-storybook/index.ts-  }
code/frameworks/angular/src/builders/start-storybook/index.ts-}
--
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-import { TestingArchitectHost } from '@angular-devkit/architect/testing';
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-import { schema } from '@angular-devkit/core';
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-import { join } from 'node:path';
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-
code/frameworks/angular/src/builders/start-storybook/index.spec.ts:const buildDevStandaloneMock = vi.fn();
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-const buildStaticStandaloneMock = vi.fn();
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-const buildMock = {
code/frameworks/angular/src/builders/start-storybook/index.spec.ts:  buildDevStandalone: buildDevStandaloneMock,
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-  buildStaticStandalone: buildStaticStandaloneMock,
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-  withTelemetry: (_: string, __: any, fn: any) => fn(),
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-};
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-vi.doMock('storybook/internal/core-server', () => buildMock);
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-vi.doMock('empathic/find', () => ({ up: () => './storybook/tsconfig.ts' }));
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-const mockRunScript = vi.fn();
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-vi.mock('storybook/internal/common', () => ({
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-  getEnvConfig: (options: any) => options,
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-  versions: {
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-    storybook: 'x.x.x',
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-  },
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-  JsPackageManagerFactory: {
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-    getPackageManager: () => ({
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-      runPackageCommand: mockRunScript,
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-    }),
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-  },
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-}));
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-
--
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-    // for the package.json file.
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-    await architectHost.addBuilderFromPackage(join(__dirname, '../../..'));
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-  });
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-  beforeEach(() => {
code/frameworks/angular/src/builders/start-storybook/index.spec.ts:    buildDevStandaloneMock.mockImplementation((_options: unknown) => Promise.resolve(_options));
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-  });
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-  afterEach(() => {
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-    vi.clearAllMocks();
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-  });
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-  it('should start storybook with angularBrowserTarget', async () => {
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-    const run = await architect.scheduleBuilder('@storybook/angular:start-storybook', {
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-      browserTarget: 'angular-cli:build-2',
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-      port: 4400,
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-      compodoc: false,
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-    });
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-    const output = await run.result;
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-    await run.stop();
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-    expect(output.success).toBeTruthy();
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-    expect(mockRunScript).not.toHaveBeenCalledWith();
code/frameworks/angular/src/builders/start-storybook/index.spec.ts:    expect(buildDevStandaloneMock).toHaveBeenCalledWith(
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-      expect.objectContaining({
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-        angularBrowserTarget: 'angular-cli:build-2',
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-        angularBuilderContext: expect.any(Object),
code/frameworks/angular/src/builders/start-storybook/index.spec.ts-        ci: false,

</shell_result>


Script executed:

#!/bin/bash
# Check if there are any process signal handlers in the builders
rg "SIGINT|SIGTERM|process\.on|\.catch|\.finally" code/frameworks/angular/src/builders --type ts -C 3

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 1

</shell_result>


Script executed:

#!/bin/bash
# Look for how other Storybook features handle cleanup/teardown
rg -B 3 -A 10 "teardown|cleanup|dispose|kill|terminate" code/frameworks/angular --type ts | head -80

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

</shell_result>


Script executed:

#!/bin/bash
# Check if there's documentation about the dev server lifecycle
find code -name "*.md" -o -name "*.txt" | xargs grep -l "dev.*server\|long.*running" 2>/dev/null | head -5

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

stdout:

code/addons/docs/docs/faq.md
code/builders/builder-vite/README.md
code/core/README.md
code/core/src/core-server/README.md
code/core/src/instrumenter/README.md

</shell_result>


Script executed:

#!/bin/bash
# Look at the complete test to see how stop() is used
cat -n code/frameworks/angular/src/builders/start-storybook/index.spec.ts | sed -n '30,120p'

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

stdout:

    30	
    31	// Randomly fails on CI. TODO: investigate why
    32	describe.skip('Start Storybook Builder', () => {
    33	  let architect: Architect;
    34	  let architectHost: TestingArchitectHost;
    35	
    36	  beforeEach(async () => {
    37	    const registry = new schema.CoreSchemaRegistry();
    38	    registry.addPostTransform(schema.transforms.addUndefinedDefaults);
    39	
    40	    architectHost = new TestingArchitectHost();
    41	    architect = new Architect(architectHost, registry);
    42	
    43	    architectHost.addBuilder(
    44	      '@angular-devkit/build-angular:browser',
    45	      createBuilder(() => {
    46	        return { success: true };
    47	      })
    48	    );
    49	    architectHost.addTarget(
    50	      { project: 'angular-cli', target: 'build-2' },
    51	      '@angular-devkit/build-angular:browser',
    52	      {
    53	        outputPath: 'dist/angular-cli',
    54	        index: 'src/index.html',
    55	        main: 'src/main.ts',
    56	        polyfills: 'src/polyfills.ts',
    57	        tsConfig: 'src/tsconfig.app.json',
    58	        assets: ['src/favicon.ico', 'src/assets'],
    59	        styles: ['src/styles.css'],
    60	        scripts: [],
    61	      }
    62	    );
    63	    // This will either take a Node package name, or a path to the directory
    64	    // for the package.json file.
    65	    await architectHost.addBuilderFromPackage(join(__dirname, '../../..'));
    66	  });
    67	
    68	  beforeEach(() => {
    69	    buildDevStandaloneMock.mockImplementation((_options: unknown) => Promise.resolve(_options));
    70	  });
    71	
    72	  afterEach(() => {
    73	    vi.clearAllMocks();
    74	  });
    75	
    76	  it('should start storybook with angularBrowserTarget', async () => {
    77	    const run = await architect.scheduleBuilder('@storybook/angular:start-storybook', {
    78	      browserTarget: 'angular-cli:build-2',
    79	      port: 4400,
    80	      compodoc: false,
    81	    });
    82	
    83	    const output = await run.result;
    84	
    85	    await run.stop();
    86	
    87	    expect(output.success).toBeTruthy();
    88	    expect(mockRunScript).not.toHaveBeenCalledWith();
    89	    expect(buildDevStandaloneMock).toHaveBeenCalledWith(
    90	      expect.objectContaining({
    91	        angularBrowserTarget: 'angular-cli:build-2',
    92	        angularBuilderContext: expect.any(Object),
    93	        ci: false,
    94	        configDir: '.storybook',
    95	        disableTelemetry: undefined,
    96	        host: 'localhost',
    97	        https: false,
    98	        packageJson: expect.any(Object),
    99	        port: 4400,
   100	        quiet: false,
   101	        smokeTest: false,
   102	        sslCa: undefined,
   103	        sslCert: undefined,
   104	        sslKey: undefined,
   105	        tsConfig: './storybook/tsconfig.ts',
   106	      })
   107	    );
   108	  });
   109	
   110	  it('should start storybook with tsConfig', async () => {
   111	    const run = await architect.scheduleBuilder('@storybook/angular:start-storybook', {
   112	      tsConfig: 'path/to/tsConfig.json',
   113	      port: 4400,
   114	      compodoc: false,
   115	    });
   116	
   117	    const output = await run.result;
   118	
   119	    await run.stop();
   120	

</shell_result>


Script executed:

#!/bin/bash
# Search for Observable subscription handling patterns in Angular Architect examples
rg -B 5 -A 15 "return new Observable.*observer.*=>" code --type ts | head -150

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

stdout:

code/frameworks/angular/src/builders/start-storybook/index.ts-
code/frameworks/angular/src/builders/start-storybook/index.ts-const commandBuilder: BuilderHandlerFn<StorybookBuilderOptions> = (
code/frameworks/angular/src/builders/start-storybook/index.ts-  options,
code/frameworks/angular/src/builders/start-storybook/index.ts-  context
code/frameworks/angular/src/builders/start-storybook/index.ts-): Observable<BuilderOutput> => {
code/frameworks/angular/src/builders/start-storybook/index.ts:  return new Observable<BuilderOutput>((observer) => {
code/frameworks/angular/src/builders/start-storybook/index.ts-    (async () => {
code/frameworks/angular/src/builders/start-storybook/index.ts-      try {
code/frameworks/angular/src/builders/start-storybook/index.ts-        logger.intro('Starting Storybook');
code/frameworks/angular/src/builders/start-storybook/index.ts-
code/frameworks/angular/src/builders/start-storybook/index.ts-        const { tsConfig } = await setup(options, context);
code/frameworks/angular/src/builders/start-storybook/index.ts-
code/frameworks/angular/src/builders/start-storybook/index.ts-        const docTSConfig = find.up('tsconfig.doc.json', {
code/frameworks/angular/src/builders/start-storybook/index.ts-          cwd: options.configDir,
code/frameworks/angular/src/builders/start-storybook/index.ts-          last: getProjectRoot(),
code/frameworks/angular/src/builders/start-storybook/index.ts-        });
code/frameworks/angular/src/builders/start-storybook/index.ts-
code/frameworks/angular/src/builders/start-storybook/index.ts-        if (options.compodoc) {
code/frameworks/angular/src/builders/start-storybook/index.ts-          await runCompodoc(
code/frameworks/angular/src/builders/start-storybook/index.ts-            {
code/frameworks/angular/src/builders/start-storybook/index.ts-              compodocArgs: [...options.compodocArgs, ...(options.quiet ? ['--silent'] : [])],

</shell_result>


Script executed:

#!/bin/bash
# Check if there are any examples of Observable cleanup/unsubscribe in the codebase
rg "subscription|unsubscribe" code/frameworks/angular --type ts -B 3 -A 3

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

stdout:

code/frameworks/angular/src/client/angular-beta/StorybookProvider.ts-      );
code/frameworks/angular/src/client/angular-beta/StorybookProvider.ts-
code/frameworks/angular/src/client/angular-beta/StorybookProvider.ts-      return () => {
code/frameworks/angular/src/client/angular-beta/StorybookProvider.ts:        sub.unsubscribe();
code/frameworks/angular/src/client/angular-beta/StorybookProvider.ts-      };
code/frameworks/angular/src/client/angular-beta/StorybookProvider.ts-    });
code/frameworks/angular/src/client/angular-beta/StorybookProvider.ts-}
--
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-    ngOnDestroy(): void {
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-      if (this.storyComponentPropsSubscription != null) {
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts:        this.storyComponentPropsSubscription.unsubscribe();
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-      }
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-      if (this.storyWrapperPropsSubscription != null) {
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts:        this.storyWrapperPropsSubscription.unsubscribe();
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-      }
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-    }
code/frameworks/angular/src/client/angular-beta/StorybookWrapperComponent.ts-  }

</shell_result>


Angular Architect builder Observable subscription cleanup teardown function


<web_search_result>
Short answer

  • When an Architect (Angular CLI) builder returns an Observable, Architect subscribes to it and will call the Observable’s teardown when the run is stopped (e.g., BuilderRun.stop()). Put any cleanup/unsubscribe logic in the Observable’s teardown function (the function you return from new Observable(...)). [1][2]

Example

  • Minimal pattern to ensure proper cleanup:

    const myBuilder = createBuilder((options, context) => {
    return new Observable(observer => {
    const sub = someHotObservable.subscribe(value => {
    // emit a BuilderOutput when appropriate
    observer.next({ success: true });
    }, err => observer.error(err));

    // when Architect unsubscribes (or the Observable completes/error), this teardown runs
    return () => {
      sub.unsubscribe();
      // other cleanup (close watchers, stop servers, etc.)
      context.logger.info('builder teardown ran');
    };
    

    });
    });

Notes

  • Call context.reportRunning() if your builder enters watch mode between runs so Architect knows it’s still active. Architect will unsubscribe and run your teardown when the run is stopped. [1][2]

Sources
[1] Angular CLI builders (official docs).
[2] createBuilder / CLI builder behavior (Architect teardown description).
</web_search_result>


Script executed:

#!/bin/bash
# Check what buildDevStandalone returns to understand what needs cleanup
rg -B 5 -A 20 "export.*buildDevStandalone|function buildDevStandalone" code/core --type ts | head -60

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

stdout:

code/core/src/core-server/build-dev.ts-import { getServerChannelUrl, getServerPort } from './utils/server-address';
code/core/src/core-server/build-dev.ts-import { updateCheck } from './utils/update-check';
code/core/src/core-server/build-dev.ts-import { warnOnIncompatibleAddons } from './utils/warnOnIncompatibleAddons';
code/core/src/core-server/build-dev.ts-import { warnWhenUsingArgTypesRegex } from './utils/warnWhenUsingArgTypesRegex';
code/core/src/core-server/build-dev.ts-
code/core/src/core-server/build-dev.ts:export async function buildDevStandalone(
code/core/src/core-server/build-dev.ts-  options: CLIOptions &
code/core/src/core-server/build-dev.ts-    LoadOptions &
code/core/src/core-server/build-dev.ts-    BuilderOptions & {
code/core/src/core-server/build-dev.ts-      storybookVersion?: string;
code/core/src/core-server/build-dev.ts-      previewConfigPath?: string;
code/core/src/core-server/build-dev.ts-    }
code/core/src/core-server/build-dev.ts-): Promise<{ port: number; address: string; networkAddress: string }> {
code/core/src/core-server/build-dev.ts-  const { packageJson, versionUpdates } = options;
code/core/src/core-server/build-dev.ts-  let { storybookVersion, previewConfigPath } = options;
code/core/src/core-server/build-dev.ts-  const configDir = resolve(options.configDir);
code/core/src/core-server/build-dev.ts-  if (packageJson) {
code/core/src/core-server/build-dev.ts-    invariant(
code/core/src/core-server/build-dev.ts-      packageJson.version !== undefined,
code/core/src/core-server/build-dev.ts-      `Expected package.json#version to be defined in the "${packageJson.name}" package}`
code/core/src/core-server/build-dev.ts-    );
code/core/src/core-server/build-dev.ts-    storybookVersion = packageJson.version;
code/core/src/core-server/build-dev.ts-    previewConfigPath = getConfigInfo(configDir).previewConfigPath ?? undefined;
code/core/src/core-server/build-dev.ts-  } else {
code/core/src/core-server/build-dev.ts-    if (!storybookVersion) {
code/core/src/core-server/build-dev.ts-      storybookVersion = versions.storybook;

</shell_result>


Script executed:

#!/bin/bash
# Look for how buildDevStandalone handles process termination
cat code/core/src/core-server/build-dev.ts | head -150 | tail -100

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

stdout:

      packageJson.version !== undefined,
      `Expected package.json#version to be defined in the "${packageJson.name}" package}`
    );
    storybookVersion = packageJson.version;
    previewConfigPath = getConfigInfo(configDir).previewConfigPath ?? undefined;
  } else {
    if (!storybookVersion) {
      storybookVersion = versions.storybook;
    }
  }
  // updateInfo are cached, so this is typically pretty fast
  const [port, versionCheck] = await Promise.all([
    getServerPort(options.port, { exactPort: options.exactPort }),
    versionUpdates
      ? updateCheck(storybookVersion)
      : Promise.resolve({ success: false, cached: false, data: {}, time: Date.now() }),
  ]);

  if (!options.ci && !options.smokeTest && options.port != null && port !== options.port) {
    const shouldChangePort = await prompt.confirm({
      message: dedent`
        Port ${options.port} is not available. 
        Would you like to run Storybook on port ${port} instead?
      `,
      initialValue: true,
    });
    if (!shouldChangePort) {
      process.exit(1);
    }
  }

  const cacheKey = oneWayHash(relative(getProjectRoot(), configDir));

  const cacheOutputDir = resolvePathInStorybookCache('public', cacheKey);
  let outputDir = resolve(options.outputDir || cacheOutputDir);
  if (options.smokeTest) {
    outputDir = cacheOutputDir;
  }

  options.port = port;
  options.versionCheck = versionCheck;
  options.configType = 'DEVELOPMENT';
  options.configDir = configDir;
  options.cacheKey = cacheKey;
  options.outputDir = outputDir;
  options.serverChannelUrl = getServerChannelUrl(port, options);

  // TODO: Remove in SB11
  options.pnp = await detectPnp();
  if (options.pnp) {
    deprecate(dedent`
      As of Storybook 10.0, PnP is deprecated.
      If you are using PnP, you can continue to use Storybook 10.0, but we recommend migrating to a different package manager or linker-mode.

      In future versions, PnP compatibility will be removed.
    `);
  }

  const config = await loadMainConfig(options);
  const { framework } = config;
  const corePresets = [];

  let frameworkName = typeof framework === 'string' ? framework : framework?.name;
  if (!options.ignorePreview) {
    validateFrameworkName(frameworkName);
  }
  if (frameworkName) {
    corePresets.push(join(frameworkName, 'preset'));
  }

  frameworkName = frameworkName || 'custom';

  const packageManager = JsPackageManagerFactory.getPackageManager({
    configDir: options.configDir,
  });

  try {
    await warnOnIncompatibleAddons(storybookVersion, packageManager);
  } catch (e) {
    logger.warn('Storybook failed to check addon compatibility');
    logger.debug(`${e instanceof Error ? e.stack : String(e)}`);
  }

  // TODO: Bring back in 9.x when we officialy launch CSF4
  // We need to consider more scenarios in this function, such as removing addons from main.ts
  // try {
  //   await syncStorybookAddons(config, previewConfigPath!);
  // } catch (e) {}

  try {
    await warnWhenUsingArgTypesRegex(previewConfigPath, config);
  } catch (e) {}

  // Load first pass: We need to determine the builder
  // We need to do this because builders might introduce 'overridePresets' which we need to take into account
  // We hope to remove this in SB8
  let presets = await loadAllPresets({
    corePresets,
    overridePresets: [
      import.meta.resolve('storybook/internal/core-server/presets/common-override-preset'),

</shell_result>


Script executed:

#!/bin/bash
# Search for SIGINT or process signal handling in build-dev.ts
rg "SIGINT|SIGTERM|process\.on|\.listen|server\." code/core/src/core-server/build-dev.ts -B 2 -A 5 | head -100

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

</shell_result>


Script executed:

#!/bin/bash
# Check how Express app.listen works in context - does it block or return immediately
rg "app\.listen|server\.listen" code/core/src/core-server --type ts -B 3 -A 10 | head -80

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

stdout:

code/core/src/core-server/dev-server.ts-
code/core/src/core-server/dev-server.ts-  const listening = new Promise<void>((resolve, reject) => {
code/core/src/core-server/dev-server.ts-    server.once('error', reject);
code/core/src/core-server/dev-server.ts:    app.listen({ port, host }, resolve);
code/core/src/core-server/dev-server.ts-  });
code/core/src/core-server/dev-server.ts-
code/core/src/core-server/dev-server.ts-  await Promise.all([initializedStoryIndexGenerator, listening]).then(async ([indexGenerator]) => {
code/core/src/core-server/dev-server.ts-    if (indexGenerator && !options.ci && !options.smokeTest && options.open) {
code/core/src/core-server/dev-server.ts-      const url = host ? networkAddress : address;
code/core/src/core-server/dev-server.ts-      openInBrowser(options.previewOnly ? `${url}iframe.html?navigator=true` : url).catch(() => {
code/core/src/core-server/dev-server.ts-        // the browser window could not be opened, this is non-critical, we just ignore the error
code/core/src/core-server/dev-server.ts-      });
code/core/src/core-server/dev-server.ts-    }
code/core/src/core-server/dev-server.ts-  });

</shell_result>


Script executed:

#!/bin/bash
# Check if there's any process cleanup in the Angular builder or if it relies on signals
rg -B 10 -A 10 "process\.exit|process\.kill|process\.on.*SIGINT" code/core/src/core-server/build-dev.ts

Repository: storybookjs/storybook


<shell_result>
Repository: storybookjs/storybook
Exit code: 0

stdout:


  if (!options.ci && !options.smokeTest && options.port != null && port !== options.port) {
    const shouldChangePort = await prompt.confirm({
      message: dedent`
        Port ${options.port} is not available. 
        Would you like to run Storybook on port ${port} instead?
      `,
      initialValue: true,
    });
    if (!shouldChangePort) {
      process.exit(1);
    }
  }

  const cacheKey = oneWayHash(relative(getProjectRoot(), configDir));

  const cacheOutputDir = resolvePathInStorybookCache('public', cacheKey);
  let outputDir = resolve(options.outputDir || cacheOutputDir);
  if (options.smokeTest) {
    outputDir = cacheOutputDir;
  }
--
    }

    const problems = warnings
      .filter((warning) => !warning.message.includes(`export 'useInsertionEffect'`))
      .filter((warning) => !warning.message.includes(`compilation but it's unused`))
      .filter(
        (warning) => !warning.message.includes(`Conflicting values for 'process.env.NODE_ENV'`)
      );

    logger.log(problems.map((p) => p.stack).join('\n'));
    process.exit(problems.length > 0 ? 1 : 0);
  } else {
    const name =
      frameworkName.split('@storybook/').length > 1
        ? frameworkName.split('@storybook/')[1]
        : frameworkName;

    if (!options.quiet) {
      outputStartupInformation({
        updateInfo: versionCheck,
        version: storybookVersion,

</shell_result>


Add Observable teardown function to gracefully stop the dev server when the builder is stopped.

The implementation starts a dev server via buildDevStandalone() which resolves after the server is listening, then emits a successful BuilderOutput and keeps the Observable alive. However, when Architect calls run.stop() or the Observable is unsubscribed, there is no teardown function to gracefully shut down the server process. This leaves the Node.js dev server running as an orphaned process.

According to Angular Architect's documented patterns, provide a teardown function that stops the dev server:

return new Observable<BuilderOutput>((observer) => {
  let serverProcess: any;
  (async () => {
    try {
      // ... existing setup code ...
      const startedPort = await runInstance(standaloneOptions);
      observer.next({ success: true, info: { port: startedPort } } as BuilderOutput);
    } catch (error) {
      observer.error(error);
    }
  })();

  // Teardown called when run.stop() is invoked by Architect
  return () => {
    // Gracefully terminate the dev server process
    if (serverProcess) {
      serverProcess.kill();
    }
  };
});

Note: buildDevStandalone() needs to be modified to return a handle (e.g., the server instance or process) that can be terminated in the teardown function.

@valentinpalkovic valentinpalkovic merged commit 2972335 into next Nov 26, 2025
67 of 72 checks passed
@valentinpalkovic valentinpalkovic deleted the valentin/fix-dev-of-storybook branch November 26, 2025 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants