Skip to content

Commit dad7c2d

Browse files
janechuCopilot
andcommitted
fix: address PR review round 3
- Move hydrationEnabled guard above TSDoc so API Extractor attaches docs to enableHydration() correctly - Make enableHydration() merge-safe — hook installs once, subsequent calls chain their options into the shared HydrationTracker via mergeOptions() - Add ExecutionContext, SourceLifetime type exports to styles.js - Add Notifier type export to arrays.js Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d08a3af commit dad7c2d

14 files changed

Lines changed: 203 additions & 26 deletions

packages/fast-element/SIZES.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Bundle sizes for `@microsoft/fast-element` exports.
44

55
| Export | Minified | Gzip | Brotli |
66
|--------|----------|------|--------|
7-
| CDN Rollup Bundle | 64.79 KB | 19.25 KB | 17.21 KB |
7+
| CDN Rollup Bundle | 65.04 KB | 19.35 KB | 17.30 KB |
88
| FASTElement | 23.71 KB | 7.37 KB | 6.63 KB |
99
| Updates | 473 B | 337 B | 287 B |
1010
| Observable | 6.70 KB | 2.49 KB | 2.22 KB |
@@ -19,6 +19,6 @@ Bundle sizes for `@microsoft/fast-element` exports.
1919
| repeat | 29.57 KB | 9.42 KB | 8.47 KB |
2020
| css | 2.43 KB | 1.00 KB | 911 B |
2121
| ElementStyles | 1.65 KB | 729 B | 623 B |
22-
| enableHydration | 42.96 KB | 13.12 KB | 11.80 KB |
22+
| enableHydration | 43.21 KB | 13.20 KB | 11.88 KB |
2323
| ArrayObserver | 12.51 KB | 4.45 KB | 4.01 KB |
24-
| declarativeTemplate | 85.40 KB | 26.14 KB | 23.03 KB |
24+
| declarativeTemplate | 85.65 KB | 26.21 KB | 23.10 KB |

packages/fast-element/docs/api-report.api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ export interface ElementViewTemplate<TSource = any, TParent = any> {
303303
// @public
304304
export const emptyArray: readonly never[];
305305

306-
// @public (undocumented)
306+
// @public
307307
export function enableHydration(options?: HydrationOptions): void;
308308

309309
// @public
@@ -559,6 +559,7 @@ export interface HydrationOptions {
559559
export class HydrationTracker {
560560
constructor(options: HydrationOptions);
561561
add(element: HTMLElement): void;
562+
mergeOptions(incoming: HydrationOptions): void;
562563
remove(element: HTMLElement): void;
563564
}
564565

packages/fast-element/docs/arrays/api-report.api.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ export interface LengthObserver extends Subscriber {
2929
// @public
3030
export function lengthOf<T>(array: readonly T[]): number;
3131

32+
// @public
33+
export interface Notifier {
34+
notify(args: any): void;
35+
readonly subject: any;
36+
subscribe(subscriber: Subscriber, propertyToWatch?: any): void;
37+
unsubscribe(subscriber: Subscriber, propertyToUnwatch?: any): void;
38+
}
39+
3240
// @public
3341
export class Sort {
3442
constructor(sorted?: number[] | undefined);
@@ -91,8 +99,6 @@ export interface Subscriber {
9199
handleChange(subject: any, args: any): void;
92100
}
93101

94-
// Warning: (ae-forgotten-export) The symbol "Notifier" needs to be exported by the entry point arrays.d.ts
95-
//
96102
// @public
97103
export class SubscriberSet implements Notifier {
98104
constructor(subject: any, initialSubscriber?: Subscriber);

packages/fast-element/docs/hydration/api-report.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// @beta
88
export const deferHydrationAttribute = "defer-hydration";
99

10-
// @public (undocumented)
10+
// @public
1111
export function enableHydration(options?: HydrationOptions): void;
1212

1313
// @public

packages/fast-element/docs/styles/api-report.api.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,37 @@ export class ElementStyles {
6666
withStrategy(Strategy: ConstructibleStyleStrategy): this;
6767
}
6868

69+
// @public
70+
export interface ExecutionContext<TParent = any> {
71+
readonly event: Event;
72+
eventDetail<TDetail>(): TDetail;
73+
eventTarget<TTarget extends EventTarget>(): TTarget;
74+
index: number;
75+
readonly isEven: boolean;
76+
readonly isFirst: boolean;
77+
readonly isInMiddle: boolean;
78+
readonly isLast: boolean;
79+
readonly isOdd: boolean;
80+
length: number;
81+
parent: TParent;
82+
parentContext: ExecutionContext<TParent>;
83+
}
84+
85+
// @public
86+
export const ExecutionContext: Readonly<{
87+
default: ExecutionContext<any>;
88+
getEvent(): Event | null;
89+
setEvent(event: Event | null): void;
90+
}>;
91+
6992
// @public
7093
export interface ExpressionController<TSource = any, TParent = any> {
71-
// Warning: (ae-forgotten-export) The symbol "ExecutionContext" needs to be exported by the entry point styles.d.ts
7294
readonly context: ExecutionContext<TParent>;
7395
readonly isBound: boolean;
7496
onUnbind(behavior: {
7597
unbind(controller: ExpressionController<TSource, TParent>): any;
7698
}): void;
7799
readonly source: TSource;
78-
// Warning: (ae-forgotten-export) The symbol "SourceLifetime" needs to be exported by the entry point styles.d.ts
79100
readonly sourceLifetime?: SourceLifetime;
80101
}
81102

@@ -97,6 +118,15 @@ export interface HostController<TSource = any> extends ExpressionController<TSou
97118
removeStyles(styles: ElementStyles | HTMLStyleElement | null | undefined): void;
98119
}
99120

121+
// @public
122+
export const SourceLifetime: Readonly<{
123+
readonly unknown: undefined;
124+
readonly coupled: 1;
125+
}>;
126+
127+
// @public
128+
export type SourceLifetime = (typeof SourceLifetime)[keyof typeof SourceLifetime];
129+
100130
// @public
101131
export interface StyleStrategy {
102132
addStylesTo(target: StyleTarget): void;

packages/fast-element/src/arrays.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ export {
99
SpliceStrategySupport,
1010
sortedCount,
1111
} from "./observation/arrays.js";
12-
export type { Subscriber, SubscriberSet } from "./observation/notifier.js";
12+
export type { Notifier, Subscriber, SubscriberSet } from "./observation/notifier.js";

packages/fast-element/src/components/enable-hydration.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ export type { HydrationOptions };
1515
*/
1616
function noopAttributeHandler() {}
1717

18+
let tracker: HydrationTracker | null = null;
19+
let hookInstalled = false;
20+
1821
/**
1922
* Enables hydration support for prerendered FAST elements.
2023
*
@@ -23,6 +26,9 @@ function noopAttributeHandler() {}
2326
* logic is not active unless this function is called, keeping
2427
* `FASTElement` lightweight for client-side-only applications.
2528
*
29+
* Safe to call multiple times — the hydration hook is installed once
30+
* and subsequent calls merge their options into the shared tracker.
31+
*
2632
* @example
2733
* ```ts
2834
* import { enableHydration } from "@microsoft/fast-element/hydration.js";
@@ -37,25 +43,22 @@ function noopAttributeHandler() {}
3743
* @param options - Optional callbacks for global hydration events.
3844
* @public
3945
*/
40-
let hydrationEnabled = false;
41-
4246
export function enableHydration(options?: HydrationOptions): void {
43-
if (hydrationEnabled) {
44-
return;
45-
}
46-
hydrationEnabled = true;
47-
4847
ensureHydrationRuntime();
49-
const tracker = new HydrationTracker(options ?? {});
5048

51-
ElementController.installHydrationHook(
49+
if (!hookInstalled) {
50+
tracker = new HydrationTracker(options ?? {});
51+
hookInstalled = true;
52+
const activeTracker = tracker;
53+
54+
ElementController.installHydrationHook(
5255
(controller, template, element, host) => {
5356
if (!isHydratable(template)) {
5457
return false;
5558
}
5659

5760
const callbacks = controller.definition.lifecycleCallbacks;
58-
tracker.add(element);
61+
activeTracker.add(element);
5962

6063
try {
6164
try {
@@ -96,12 +99,16 @@ export function enableHydration(options?: HydrationOptions): void {
9699
// A lifecycle callback must never prevent post-hydration work.
97100
}
98101
} finally {
99-
tracker.remove(element);
102+
activeTracker.remove(element);
100103
}
101104

102105
return true;
103106
},
104107
);
108+
} else if (options && tracker) {
109+
// Merge options into existing tracker for subsequent calls
110+
tracker.mergeOptions(options);
111+
}
105112
}
106113

107114
/**

packages/fast-element/src/components/hydration-tracker.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,34 @@ export class HydrationTracker {
6868
}, 0);
6969
}
7070
}
71+
72+
/**
73+
* Merges additional options into the tracker, chaining
74+
* callbacks so both the original and new callbacks fire.
75+
*/
76+
public mergeOptions(incoming: HydrationOptions): void {
77+
const prev = this.options;
78+
this.options = {
79+
hydrationStarted: chainCallback(
80+
prev.hydrationStarted,
81+
incoming.hydrationStarted,
82+
),
83+
hydrationComplete: chainCallback(
84+
prev.hydrationComplete,
85+
incoming.hydrationComplete,
86+
),
87+
};
88+
}
89+
}
90+
91+
function chainCallback(
92+
first: (() => void) | undefined,
93+
second: (() => void) | undefined,
94+
): (() => void) | undefined {
95+
if (!first) return second;
96+
if (!second) return first;
97+
return () => {
98+
first();
99+
second();
100+
};
71101
}

packages/fast-element/src/styles.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export {
1010
ElementStyles,
1111
} from "./styles/element-styles.js";
1212
export type { Constructable } from "./interfaces.js";
13-
export type { ExpressionController } from "./observation/observable.js";
13+
export type {
14+
ExecutionContext,
15+
ExpressionController,
16+
SourceLifetime,
17+
} from "./observation/observable.js";
1418
export type { HostBehavior, HostController } from "./styles/host.js";
1519
export type { StyleStrategy, StyleTarget } from "./styles/style-strategy.js";

sites/website/src/docs/3.x/api/fast-element.enablehydration.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ navigationOptions:
1515

1616
## enableHydration() function
1717

18+
Enables hydration support for prerendered FAST elements.
19+
1820
**Signature:**
1921

2022
```typescript
@@ -51,7 +53,7 @@ options
5153

5254
</td><td>
5355

54-
_(Optional)_
56+
_(Optional)_ Optional callbacks for global hydration events.
5557

5658

5759
</td></tr>
@@ -60,3 +62,22 @@ _(Optional)_
6062
**Returns:**
6163

6264
void
65+
66+
## Remarks
67+
68+
Call this before any FAST elements connect to the DOM. Hydration logic is not active unless this function is called, keeping `FASTElement` lightweight for client-side-only applications.
69+
70+
Safe to call multiple times — the hydration hook is installed once and subsequent calls merge their options into the shared tracker.
71+
72+
## Example
73+
74+
75+
```ts
76+
import { enableHydration } from "@microsoft/fast-element/hydration.js";
77+
78+
enableHydration({
79+
hydrationComplete() {
80+
console.log("hydration complete");
81+
},
82+
});
83+
```

0 commit comments

Comments
 (0)