Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions packages/core/src/defer/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
triggerPrefetching,
triggerResourceLoading,
shouldAttachTrigger,
hasHydrateTriggers,
} from './triggering';
import {formatRuntimeError, RuntimeErrorCode} from '../errors';
import {Console} from '../console';
Expand Down Expand Up @@ -138,8 +139,13 @@ export function ɵɵdefer(
if (tView.firstCreatePass) {
performanceMarkFeature('NgDefer');

if (ngDevMode && typeof ngHmrMode !== 'undefined' && ngHmrMode) {
logHmrWarning(injector);
if (ngDevMode) {
if (typeof ngHmrMode !== 'undefined' && ngHmrMode) {
logHmrWarning(injector);
}
if (hasHydrateTriggers(flags)) {
assertIncrementalHydrationIsConfigured(injector);
}
}

const tDetails: TDeferBlockDetails = {
Expand Down Expand Up @@ -193,8 +199,6 @@ export function ɵɵdefer(

let registry: DehydratedBlockRegistry | null = null;
if (ssrUniqueId !== null) {
ngDevMode && assertIncrementalHydrationIsConfigured(injector);

// Store this defer block in the registry, to have an access to
// internal data structures from hydration runtime code.
registry = injector.get(DEHYDRATED_BLOCK_REGISTRY);
Expand Down
17 changes: 11 additions & 6 deletions packages/core/src/defer/triggering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,14 @@ export function shouldAttachTrigger(triggerType: TriggerType, lView: LView, tNod
return !(typeof ngServerMode !== 'undefined' && ngServerMode);
}

/** Whether a given defer block has `hydrate` triggers. */
export function hasHydrateTriggers(flags: TDeferDetailsFlags | null | undefined): boolean {
return (
flags != null &&
(flags & TDeferDetailsFlags.HasHydrateTriggers) === TDeferDetailsFlags.HasHydrateTriggers
);
}

/**
* Defines whether a regular trigger logic (e.g. "on viewport") should be attached
* to a defer block. This function defines a condition, which mutually excludes
Expand All @@ -606,24 +614,21 @@ function shouldAttachRegularTrigger(lView: LView, tNode: TNode): boolean {

const tDetails = getTDeferBlockDetails(lView[TVIEW], tNode);
const incrementalHydrationEnabled = isIncrementalHydrationEnabled(injector);
const hasHydrateTriggers =
tDetails.flags !== null &&
(tDetails.flags & TDeferDetailsFlags.HasHydrateTriggers) ===
TDeferDetailsFlags.HasHydrateTriggers;
const _hasHydrateTriggers = hasHydrateTriggers(tDetails.flags);

// On the server:
if (typeof ngServerMode !== 'undefined' && ngServerMode) {
// Regular triggers are activated on the server when:
// - Either Incremental Hydration is *not* enabled
// - Or Incremental Hydration is enabled, but a given block doesn't have "hydrate" triggers
return !incrementalHydrationEnabled || !hasHydrateTriggers;
return !incrementalHydrationEnabled || !_hasHydrateTriggers;
}

// On the client:
const lDetails = getLDeferBlockDetails(lView, tNode);
const wasServerSideRendered = lDetails[SSR_UNIQUE_ID] !== null;

if (hasHydrateTriggers && wasServerSideRendered && incrementalHydrationEnabled) {
if (_hasHydrateTriggers && wasServerSideRendered && incrementalHydrationEnabled) {
return false;
}
return true;
Expand Down
65 changes: 65 additions & 0 deletions packages/platform-server/test/incremental_hydration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2693,4 +2693,69 @@ describe('platform-server partial hydration integration', () => {
expect(appHostNode.outerHTML).toContain(`<p id="hydrated">yup</p>`);
});
});

describe('misconfiguration', () => {
it('should throw an error when `withIncrementalHydration()` is missing in SSR setup', async () => {
@Component({
selector: 'app',
template: `
@defer (hydrate never) {
<div>Hydrate never block</div>
}
`,
})
class SimpleComponent {}

const appId = 'custom-app-id';
const providers = [{provide: APP_ID, useValue: appId}];

// Empty list, `withIncrementalHydration()` is not included intentionally.
const hydrationFeatures = () => [];

let producedError;
try {
await ssr(SimpleComponent, {envProviders: providers, hydrationFeatures});
} catch (error: unknown) {
producedError = error;
}
expect((producedError as Error).message).toContain('NG0508');
});

it('should throw an error when `withIncrementalHydration()` is missing in hydration setup', async () => {
@Component({
selector: 'app',
template: `
@defer (hydrate never) {
<div>Hydrate never block</div>
}
`,
})
class SimpleComponent {}

const appId = 'custom-app-id';
const providers = [{provide: APP_ID, useValue: appId}];

const hydrationFeatures = () => [withIncrementalHydration()];

const html = await ssr(SimpleComponent, {envProviders: providers, hydrationFeatures});

// Internal cleanup before we do server->client transition in this test.
resetTViewsFor(SimpleComponent);

////////////////////////////////

let producedError;
try {
const doc = getDocument();
await prepareEnvironmentAndHydrate(doc, html, SimpleComponent, {
envProviders: [...providers, {provide: PLATFORM_ID, useValue: 'browser'}],
// Empty list, `withIncrementalHydration()` is not included intentionally.
hydrationFeatures: () => [],
});
} catch (error: unknown) {
producedError = error;
}
expect((producedError as Error).message).toContain('NG0508');
});
});
});