Skip to content

fix(fast-element): return compose() synchronously unless a template resolver is provided#7601

Open
radium-v wants to merge 8 commits into
releases/fast-element-v3-rcfrom
users/radium-v/v3-compose-overload-async
Open

fix(fast-element): return compose() synchronously unless a template resolver is provided#7601
radium-v wants to merge 8 commits into
releases/fast-element-v3-rcfrom
users/radium-v/v3-compose-overload-async

Conversation

@radium-v

Copy link
Copy Markdown
Collaborator

Pull Request

📖 Description

The v3 RC changed FASTElement.compose() and FASTElementDefinition.compose() to always return Promise<FASTElementDefinition>. Composition does no asynchronous work, so the Promise resolved immediately. That forced every caller to await or .then() a result with nothing to wait for, which broke synchronous call sites.

This restores a synchronous return through method overloads. compose() returns a FASTElementDefinition directly, and returns a Promise only when the definition's template is a resolver, which is the one input that can be asynchronous. Concrete templates, string names, and bare definitions compose synchronously. define() accepts either result and stays the async registration boundary.

🎫 Issues

👩‍💻 Reviewer Notes

  • The discriminator is whether template is a resolver function. The Promise-returning overloads inline PartialFASTElementDefinition<TType> & { template: FASTElementTemplateResolver<TType> } instead of exporting a named type, which keeps the public surface smaller.
  • Overload order matters: the resolver (Promise) overload must come before its synchronous counterpart, because a resolver-bearing definition also satisfies PartialFASTElementDefinition.
  • compose() does not resolve the template. Resolution still happens at define() time, which existing tests confirm by asserting definition.template === undefined after compose.

📑 Test Plan

  • Added overload coverage in fast-definitions.pw.spec.ts and fast-element.pw.spec.ts. The tests verify that compose() returns synchronously for string names and concrete templates, returns a Promise only when given a template resolver, and leaves the template unresolved until define().
  • All existing fast-element Chromium tests pass, and the full repo build succeeds.

✅ Checklist

General

  • I have included a change request file using $ npm run change
  • I have added tests for my changes.
  • I have tested my changes.
  • I have updated the project documentation to reflect my changes.
  • I have read the CONTRIBUTING documentation and followed the standards for this project.

@radium-v radium-v self-assigned this Jun 20, 2026
@radium-v radium-v requested a review from Copilot June 20, 2026 22:32
@radium-v radium-v changed the title Users/radium v/v3 compose overload async fix(fast-element): return compose() synchronously unless a template resolver is provided Jun 20, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Restores ergonomic synchronous usage of FASTElement.compose() / FASTElementDefinition.compose() in the v3 RC by introducing overloads that return a FASTElementDefinition directly for synchronous inputs, while retaining a Promise return for template-resolver scenarios, and keeping define() as the async boundary.

Changes:

  • Added overloads so compose() returns synchronously except when a template resolver is provided.
  • Updated define() to accept either a direct definition or a promise of one.
  • Updated API reports/docs and added Playwright coverage for the new overload behavior.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
sites/website/src/docs/3.x/resources/export-sizes.md Updates published bundle size numbers.
sites/website/src/docs/3.x/api/fast-element/declarative/fast-element.fastelementdefinition.md Adds API doc entry for the additional compose overload page.
sites/website/src/docs/3.x/api/fast-element/declarative/fast-element.fastelementdefinition.compose.md Updates declarative API docs to reflect the resolver-specific overload.
sites/website/src/docs/3.x/api/fast-element/declarative/fast-element.fastelementdefinition.compose_1.md New generated doc page for the synchronous compose overload (declarative export).
sites/website/src/docs/3.x/api/fast-element.fastelementdefinition.md Adds API doc entry for the additional compose overload page.
sites/website/src/docs/3.x/api/fast-element.fastelementdefinition.compose.md Updates API docs to reflect the resolver-specific overload.
sites/website/src/docs/3.x/api/fast-element.fastelementdefinition.compose_1.md New generated doc page for the synchronous compose overload.
sites/website/src/docs/3.x/api/fast-element.fastelementconstructor.md Updates API docs to show multiple compose overload entries.
sites/website/src/docs/3.x/api/fast-element.fastelementconstructor.compose.md Updates API docs for the resolver-bearing compose overload.
sites/website/src/docs/3.x/api/fast-element.fastelementconstructor.compose_3.md New generated doc page for the synchronous compose(type, ...) overload.
sites/website/src/docs/3.x/api/fast-element.fastelementconstructor.compose_2.md New generated doc page for the resolver-bearing compose(type, ...) overload.
sites/website/src/docs/3.x/api/fast-element.fastelementconstructor.compose_1.md Updates generated doc page for the synchronous compose(this, ...) overload.
packages/fast-element/src/components/fast-element.ts Implements compose() overloads + adjusts define() to handle sync/async compose results.
packages/fast-element/src/components/fast-element.pw.spec.ts Adds tests validating sync vs async return behavior for FASTElement.compose().
packages/fast-element/src/components/fast-definitions.ts Implements FASTElementDefinition.compose() overloads and conditional Promise return.
packages/fast-element/src/components/fast-definitions.pw.spec.ts Adds tests validating sync vs async return behavior for FASTElementDefinition.compose().
packages/fast-element/SIZES.md Updates package bundle size numbers.
packages/fast-element/docs/declarative/api-report.api.md Updates declarative API report to include compose overload signatures.
packages/fast-element/docs/api-report.api.md Updates main API report to include compose overload signatures.
examples/csr/todo-mobx-app/src/main.ts Updates example to call .define() directly after compose() becomes sync.
change/@microsoft-fast-element-269e7b6d-2eaf-45a5-9643-391c9d9fde68.json Beachball change file describing the API behavior change.

Comment thread packages/fast-element/src/components/fast-definitions.ts Outdated
Comment thread packages/fast-element/src/components/fast-definitions.ts

@janechu janechu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please provide concrete use cases for this change.

@radium-v

Copy link
Copy Markdown
Collaborator Author

@janechu I updated the migration, design, and skill docs to describe the new behavior, and if this goes in I'll update my documentation PR to go into more detail.

I'm not actually sure if we ever even used composeAsync(), but the compose() changes in RC make it viable for use in Fluent for SSR.

By making the compose() method optionally async, via function signature overloading, we can keep existing CSR scenarios fundamentally untouched from an author's perspective, and allow for future async pathways to be built into the function.

In Fluent, we have two side effect-only import pathways: one for CSR and one for SSR. With this change, the CSR pathway can remain untouched:

// accordion.definition.ts
import { tagName } from './accordion.options.js';
import { Accordion } from './accordion.js';
import { styles } from './accordion.styles.js';
import { template } from './accordion.template.js';

export const definition = Accordion.compose({
  name: tagName,
  template, // <--- ViewTemplate object, not async: `definition` becomes a bare `FASTElementDefinition`
  styles,
});
// define.ts
import { FluentDesignSystem } from '../fluent-design-system.js';
import { definition } from './accordion.definition.js';

definition.define(FluentDesignSystem.registry); // <--- no change necessary here

On the SSR side of the coin, since we now have the declarativeTemplate() resolver, the method is typed as returning a promise, so this works as expected with the combined v3 pathway:

// accordion.definition-async.ts
import { declarativeTemplate } from '@microsoft/fast-element/declarative.js';
import { tagName } from './accordion.options.js';
import { Accordion } from './accordion.js';

export const declarativeDefinition = Accordion.compose({ // <-- using compose here is new
  name: tagName,
  template: declarativeTemplate(), // <--- resolver function, `declarativeDefinition` becomes a `Promise<FASTElementDefinition>`
});
// define-async.ts
import { FluentDesignSystem } from '../fluent-design-system.js';
import { declarativeDefinition } from './accordion.definition-async.js';

const definition = await declarativeDefinition; // <-- the returned promise needs to be awaited or chained with `then()`
definition.define(FluentDesignSystem.registry);

This way, when we expand the functionality and feature-set of compose() in the future, we can augment and overload the existing function signature to support those new features as being opt-in without forcing a hard break for the common CSR path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants