Skip to content

Core: Share the program-backed half of a component-meta project - #35820

Merged
valentinpalkovic merged 6 commits into
nextfrom
valentin/component-meta-program-project
Aug 11, 2026
Merged

valentinpalkovic merged 6 commits into
nextfrom
valentin/component-meta-program-project

Conversation

@valentinpalkovic

Copy link
Copy Markdown
Contributor

What I did

A renderer's component-meta project is two things bolted together. How its TypeScript program gets built, and how metadata is read out of it, differ completely between renderers. Answering the manager's membership and lifecycle questions against that program does not differ at all — but it was written out once per renderer anyway.

This moves the second half into core as ProgramBackedProject, and puts React's project on it.

                     ComponentMetaProjectBase        (the manager's contract)
                                 |
                     ProgramBackedProject<Snapshot>  <-- NEW, in core
                       dispose / ensureFiles / hasSourceFile
                       getSourceFilePaths / onFilesChanged
                                 |
              +------------------+------------------+
              |                                     |
   React ComponentMetaProject             Angular project (next PR in the stack)
     volar host + extractPropsFromStories   hand-written host + extract
     overrides dispose + onFilesChanged     inherits all five
     for its warmup timer

A renderer assigns two fields and gets five methods:

export abstract class ProgramBackedProject<Snapshot> implements ComponentMetaProjectBase {
  protected abstract readonly service: ProgramProvider;
  protected abstract readonly files: ProjectFileTracker<Snapshot>;

  hasSourceFile(fileName: string): boolean {
    return !!this.service.getProgram()?.getSourceFile(normalizePath(fileName));
  }
  // dispose / ensureFiles / getSourceFilePaths / onFilesChanged
}

Fields rather than constructor parameters, because a language service host usually closes over the file tracker — so the service cannot exist before super() runs.

dispose and onFilesChanged stay overridable. React schedules a background warmup re-extract and has to cancel and reschedule around both; it overrides those two and inherits the other three.

Why the contracts are structural

ProgramLike and ProgramProvider describe only the slice of ts.Program / ts.LanguageService this base touches, rather than importing them. Naming the real types would tie every renderer to core's copy of the compiler API — the same reason ProjectCommandLine is not ts.ParsedCommandLine, documented on that type. ts.LanguageService satisfies ProgramProvider structurally.

What it removes

file change
ComponentMetaProject.ts (React) +14 / -36
project.ts (Angular, in the child PR) +11 / -39

normalizePath is exported alongside, so path normalization stops being a private const copied into each project.

Not included, deliberately

importClosure and findDefaultExportedClassName in the Angular project are also generic-looking, and I was going to move them here too. They have exactly one caller each and no second renderer needs them — React resolves components from an explicit batch and Vue from its own checker. Core surface with one consumer is the speculative generality AGENTS.md rules out, so they stay where they are used.

Vue cannot extend this base: vue-component-meta's checker owns its own snapshot cache, so it has no ProjectFileTracker, and exposes clearCache() rather than dispose().

Checklist for contributors

  • Bug fix
  • Refactor
  • Feature
  • Documentation
  • Maintenance
  • Other

Manual testing

Run from the repository root.

  1. yarn nx run-many -t check -p core react — expect no type errors.
  2. yarn vitest run code/core/src/component-meta code/renderers/react/src/componentManifest — expect 279 passing.
  3. To confirm the base is load-bearing rather than decorative, delete hasSourceFile from ProgramBackedProject and re-run step 1. Both React and Angular fail to compile, since neither declares it any more.

Documentation

  • Add or update documentation reflecting your changes
  • No documentation needed — internal contract between core and the renderers, no public API change.

@github-actions

github-actions Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor
Fails
🚫 PR description is missing the mandatory "#### Manual testing" section. Please add it so that reviewers know how to manually test your changes.

Generated by 🚫 dangerJS against 4121107

@valentinpalkovic valentinpalkovic self-assigned this Aug 9, 2026
@valentinpalkovic valentinpalkovic added maintenance User-facing maintenance tasks ci:normal Run our default set of CI jobs (choose this for most PRs). labels Aug 9, 2026
@valentinpalkovic
valentinpalkovic marked this pull request as ready for review August 9, 2026 18:51
@valentinpalkovic
valentinpalkovic force-pushed the valentin/component-meta-program-project branch from eafef19 to 07e3954 Compare August 10, 2026 10:17
@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0ee877a9-8dc4-4f93-9882-458dae944b5b

📥 Commits

Reviewing files that changed from the base of the PR and between c3e9876 and 4121107.

📒 Files selected for processing (2)
  • code/core/src/component-meta/ProgramBackedProject.ts
  • code/core/src/component-meta/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/core/src/component-meta/ProgramBackedProject.ts

Walkthrough

Adds generic program and provider contracts with a reusable ProgramBackedProject base. Refactors ComponentMetaProject to use the base for program access, file tracking, disposal, source-file queries, and file-change handling.

Changes

Program-backed component metadata

Layer / File(s) Summary
Program-backed project foundation
code/core/src/component-meta/ProgramBackedProject.ts, code/core/src/component-meta/index.ts
Adds ProgramLike, ProgramProvider, and the generic ProgramBackedProject base. The base handles disposal, file ensuring, normalized source-file checks, filtered source paths, and batch file changes using one program snapshot. The new contracts and class are exported.
React project integration
code/renderers/react/src/componentManifest/componentMeta/ComponentMetaProject.ts
Makes ComponentMetaProject extend ProgramBackedProject, exposes the language service and tracker to the base, delegates lifecycle operations, updates program access, and simplifies meta-component symbol selection. The documentation describes JSX-first prop resolution and fallback behavior.

Sequence Diagram(s)

sequenceDiagram
  participant ComponentMetaProject
  participant LanguageService
  participant ProgramBackedProject
  participant ProjectFileTracker
  ComponentMetaProject->>LanguageService: create language service and access program
  ComponentMetaProject->>ProgramBackedProject: query source files and ensure files
  ComponentMetaProject->>ProgramBackedProject: forward file changes
  ProgramBackedProject->>ProjectFileTracker: process changes with one program snapshot
  ComponentMetaProject->>LanguageService: extract story and meta symbols
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@storybook-app-bot

storybook-app-bot Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Package Benchmarks

Commit: 4121107, ran on 11 August 2026 at 13:14:29 UTC

No significant changes detected, all good. 👏

Comment thread code/core/src/component-meta/ProgramBackedProject.ts Outdated
@valentinpalkovic
valentinpalkovic force-pushed the valentin/component-meta-program-project branch from 07e3954 to 2981834 Compare August 10, 2026 20:38
@valentinpalkovic
valentinpalkovic force-pushed the valentin/component-meta-program-project branch from 2981834 to fdec7f7 Compare August 11, 2026 10:17
A renderer's component-meta project is two things bolted together: how its
TypeScript program gets built and how metadata is read out of it, which differ
completely, and answering the manager's membership and lifecycle questions
against that program, which does not differ at all. Only the second half was
ever renderer-agnostic, and it was being written out per renderer.

`ProgramBackedProject` owns that half. A renderer assigns `service` and `files`
and gets `dispose`, `ensureFiles`, `hasSourceFile`, `getSourceFilePaths` and
`onFilesChanged`; `dispose` and `onFilesChanged` stay overridable because a
renderer scheduling background work has to cancel and reschedule around them.
React does exactly that for its warmup timer and inherits the rest.

Both hook points are fields rather than constructor parameters: a language
service host usually closes over the file tracker, so the service cannot exist
before `super()` runs.

The contracts stay structural. Naming `ts.Program` here would tie every
renderer to core's copy of the compiler API, which is the same reason
`ProjectCommandLine` is not `ts.ParsedCommandLine`.

`normalizePath` is exported alongside, so path normalization stops being a
private const copied into each project.
The guard twenty lines above returns unless `componentProp.valueDeclaration`
exists and is a property assignment, so re-testing both before picking the
symbol can only ever take the first arm, and `componentType.getSymbol?.()` was
never reachable. The initializer had already been read into
`metaComponentInitializer` and null-checked, too.

  let selectedSymbol = checker.getSymbolAtLocation(metaComponentInitializer);
The header claimed props are read from the story "so a component is only ever
described the way a story actually uses it", which reads as a design principle
and is what it is not. Gating the story-JSX path off and running the suite:

  Tests  2 failed | 146 passed (148)

Of the two, only one is a capability - a generic component falls back from
`string[]` to `T[]`. The other is `forwardRef`, whose sole diff is two enum
members swapping order because resolving the story's JSX interns its literal
first; it resolves identically either way.

So the read exists for type-parameter instantiation, and the comment now says
that, along with the consequence nobody would guess: the first matching JSX
element wins, so the documented type is whichever story pins it earliest.
@valentinpalkovic
valentinpalkovic force-pushed the valentin/component-meta-program-project branch from fdec7f7 to 8effccd Compare August 11, 2026 10:42
Adds a Source type parameter (default unknown) to ProgramLike,
ProgramProvider, and ProgramBackedProject, so a renderer whose program
really is a ts.Program can instantiate it with ts.SourceFile | undefined
without core ever naming the typescript package. React's
ComponentMetaProject now does exactly that.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@code/core/src/component-meta/ProgramBackedProject.ts`:
- Around line 14-16: Update the ProgramLike.getSourceFile method signature to
return Source | undefined instead of Source, matching the nullable result of
TypeScript program APIs while preserving the existing getSourceFiles contract.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: be22bfb4-d060-4bfd-8826-d239d797573d

📥 Commits

Reviewing files that changed from the base of the PR and between 07e3954 and c3e9876.

📒 Files selected for processing (3)
  • code/core/src/component-meta/ProgramBackedProject.ts
  • code/core/src/component-meta/ProjectFileTracker.ts
  • code/renderers/react/src/componentManifest/componentMeta/ComponentMetaProject.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • code/renderers/react/src/componentManifest/componentMeta/ComponentMetaProject.ts
  • code/core/src/component-meta/ProjectFileTracker.ts

Comment thread code/core/src/component-meta/ProgramBackedProject.ts
Base automatically changed from valentin/component-meta-project-tracker to next August 11, 2026 13:00
…ta-program-project

# Conflicts:
#	code/core/src/component-meta/ProjectFileTracker.ts
@valentinpalkovic valentinpalkovic added the qa:skip Pull Requests that do not need any QA. (e.g. documentation) label Aug 11, 2026
@valentinpalkovic
valentinpalkovic merged commit ecb4e21 into next Aug 11, 2026
144 of 147 checks passed
@valentinpalkovic
valentinpalkovic deleted the valentin/component-meta-program-project branch August 11, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:normal Run our default set of CI jobs (choose this for most PRs). maintenance User-facing maintenance tasks qa:skip Pull Requests that do not need any QA. (e.g. documentation)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants