Skip to content

Commit 3c27c2e

Browse files
feat(platform-browser): make incremental hydration default behavior
This commit updates provideClientHydration to automatically enable incremental hydration by default. It also introduces a new withNoIncrementalHydration feature for opting out, adds conflict safety checks, and includes a schematic migration.
1 parent c70b4fe commit 3c27c2e

File tree

9 files changed

+290
-11
lines changed

9 files changed

+290
-11
lines changed

packages/core/schematics/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ bundle_entrypoints = [
133133
"can-match-snapshot-required",
134134
"packages/core/schematics/migrations/can-match-snapshot-required/index.js",
135135
],
136+
[
137+
"incremental-hydration",
138+
"packages/core/schematics/migrations/incremental-hydration/index.js",
139+
],
136140
]
137141

138142
rollup.rollup(
@@ -147,6 +151,7 @@ rollup.rollup(
147151
"//packages/core/schematics/migrations/can-match-snapshot-required",
148152
"//packages/core/schematics/migrations/change-detection-eager",
149153
"//packages/core/schematics/migrations/http-xhr-backend",
154+
"//packages/core/schematics/migrations/incremental-hydration",
150155
"//packages/core/schematics/migrations/strict-template",
151156
"//packages/core/schematics/ng-generate/cleanup-unused-imports",
152157
"//packages/core/schematics/ng-generate/common-to-standalone-migration",

packages/core/schematics/migrations.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
"version": "22.0.0",
2020
"description": "Adds the required third argument to canMatch callsites.",
2121
"factory": "./bundles/can-match-snapshot-required.cjs#migrate"
22+
},
23+
"incremental-hydration": {
24+
"version": "22.0.0",
25+
"description": "Migrates provideClientHydration to default incremental hydration.",
26+
"factory": "./bundles/incremental-hydration.cjs#migrate"
2227
}
2328
}
2429
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
load("//tools:defaults.bzl", "ts_project")
2+
3+
package(
4+
default_visibility = [
5+
"//packages/core/schematics:__pkg__",
6+
"//packages/core/schematics/test:__pkg__",
7+
],
8+
)
9+
10+
ts_project(
11+
name = "incremental-hydration",
12+
srcs = glob(
13+
["**/*.ts"],
14+
exclude = ["*.spec.ts"],
15+
),
16+
deps = [
17+
"//:node_modules/@angular-devkit/schematics",
18+
"//:node_modules/@types/node",
19+
"//:node_modules/typescript",
20+
"//packages/compiler-cli",
21+
"//packages/compiler-cli/private",
22+
"//packages/core/schematics/utils",
23+
"//packages/core/schematics/utils/tsurge",
24+
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",
25+
"//packages/core/schematics/utils/tsurge/helpers/ast",
26+
],
27+
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {Rule} from '@angular-devkit/schematics';
10+
import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit';
11+
import {IncrementalHydrationMigration} from './migration';
12+
13+
export function migrate(): Rule {
14+
return async (tree, context) => {
15+
await runMigrationInDevkit({
16+
tree,
17+
getMigration: (fs) => new IncrementalHydrationMigration(),
18+
});
19+
};
20+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {absoluteFrom} from '@angular/compiler-cli';
10+
import {initMockFileSystem} from '@angular/compiler-cli/private/testing';
11+
import {runTsurgeMigration} from '../../utils/tsurge/testing';
12+
import {IncrementalHydrationMigration} from './migration';
13+
14+
describe('IncrementalHydration migration', () => {
15+
beforeEach(() => {
16+
initMockFileSystem('Native');
17+
});
18+
19+
it('should remove withIncrementalHydration() if present', async () => {
20+
const {fs} = await runTsurgeMigration(new IncrementalHydrationMigration(), [
21+
{
22+
name: absoluteFrom('/index.ts'),
23+
isProgramRootFile: true,
24+
contents: `
25+
import { provideClientHydration, withIncrementalHydration } from '@angular/platform-browser';
26+
27+
provideClientHydration(withIncrementalHydration());
28+
`,
29+
},
30+
]);
31+
32+
const content = fs.readFile(absoluteFrom('/index.ts'));
33+
expect(content).not.toContain('withIncrementalHydration()');
34+
expect(content).toContain('provideClientHydration()');
35+
});
36+
37+
it('should add withNoIncrementalHydration() if withIncrementalHydration is absent', async () => {
38+
const {fs} = await runTsurgeMigration(new IncrementalHydrationMigration(), [
39+
{
40+
name: absoluteFrom('/index.ts'),
41+
isProgramRootFile: true,
42+
contents: `
43+
import { provideClientHydration } from '@angular/platform-browser';
44+
45+
provideClientHydration();
46+
`,
47+
},
48+
]);
49+
50+
const content = fs.readFile(absoluteFrom('/index.ts'));
51+
expect(content).toContain('withNoIncrementalHydration()');
52+
});
53+
});
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {ImportManager} from '@angular/compiler-cli/private/migrations';
10+
import ts from 'typescript';
11+
import {
12+
confirmAsSerializable,
13+
ProgramInfo,
14+
projectFile,
15+
Replacement,
16+
Serializable,
17+
TextUpdate,
18+
TsurgeFunnelMigration,
19+
} from '../../utils/tsurge';
20+
import {applyImportManagerChanges} from '../../utils/tsurge/helpers/apply_import_manager';
21+
22+
export interface IncrementalHydrationMigrationData {
23+
replacements: Replacement[];
24+
}
25+
26+
export class IncrementalHydrationMigration extends TsurgeFunnelMigration<
27+
IncrementalHydrationMigrationData,
28+
IncrementalHydrationMigrationData
29+
> {
30+
override async analyze(
31+
info: ProgramInfo,
32+
): Promise<Serializable<IncrementalHydrationMigrationData>> {
33+
const {sourceFiles, program} = info;
34+
const typeChecker = program.getTypeChecker();
35+
const replacements: Replacement[] = [];
36+
const importManager = new ImportManager();
37+
const printer = ts.createPrinter();
38+
39+
for (const sf of sourceFiles) {
40+
ts.forEachChild(sf, function visit(node: ts.Node) {
41+
if (
42+
ts.isCallExpression(node) &&
43+
ts.isIdentifier(node.expression) &&
44+
node.expression.text === 'provideClientHydration'
45+
) {
46+
let hasIncremental = false;
47+
let incrementalArgNode: ts.CallExpression | null = null;
48+
49+
for (const arg of node.arguments) {
50+
if (
51+
ts.isCallExpression(arg) &&
52+
ts.isIdentifier(arg.expression) &&
53+
arg.expression.text === 'withIncrementalHydration'
54+
) {
55+
hasIncremental = true;
56+
incrementalArgNode = arg;
57+
break;
58+
}
59+
}
60+
61+
if (hasIncremental && incrementalArgNode) {
62+
// Remove withIncrementalHydration()
63+
replacements.push(
64+
new Replacement(
65+
projectFile(sf, info),
66+
new TextUpdate({
67+
position: incrementalArgNode.getStart(),
68+
end: incrementalArgNode.getEnd(),
69+
toInsert: '',
70+
}),
71+
),
72+
);
73+
} else {
74+
// Add withNoIncrementalHydration()
75+
const withNoIncrementalExpr = importManager.addImport({
76+
exportModuleSpecifier: '@angular/platform-browser',
77+
exportSymbolName: 'withNoIncrementalHydration',
78+
requestedFile: sf,
79+
});
80+
81+
const exprText = printer.printNode(ts.EmitHint.Unspecified, withNoIncrementalExpr, sf);
82+
83+
const insertPos = node.arguments.end;
84+
const toInsert = node.arguments.length > 0 ? `, ${exprText}()` : `${exprText}()`;
85+
86+
replacements.push(
87+
new Replacement(
88+
projectFile(sf, info),
89+
new TextUpdate({
90+
position: insertPos,
91+
end: insertPos,
92+
toInsert: toInsert,
93+
}),
94+
),
95+
);
96+
}
97+
}
98+
ts.forEachChild(node, visit);
99+
});
100+
}
101+
102+
applyImportManagerChanges(importManager, replacements, sourceFiles, info);
103+
104+
return confirmAsSerializable({
105+
replacements,
106+
});
107+
}
108+
109+
override async combine(
110+
unitA: IncrementalHydrationMigrationData,
111+
unitB: IncrementalHydrationMigrationData,
112+
): Promise<Serializable<IncrementalHydrationMigrationData>> {
113+
return confirmAsSerializable({
114+
replacements: [...unitA.replacements, ...unitB.replacements],
115+
});
116+
}
117+
118+
override async globalMeta(
119+
combinedData: IncrementalHydrationMigrationData,
120+
): Promise<Serializable<IncrementalHydrationMigrationData>> {
121+
return confirmAsSerializable(combinedData);
122+
}
123+
124+
override async stats(
125+
globalMetadata: IncrementalHydrationMigrationData,
126+
): Promise<Serializable<unknown>> {
127+
return confirmAsSerializable({});
128+
}
129+
130+
override async migrate(
131+
globalData: IncrementalHydrationMigrationData,
132+
): Promise<{replacements: Replacement[]}> {
133+
return {replacements: globalData.replacements};
134+
}
135+
}

packages/platform-browser/src/hydration.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export enum HydrationFeatureKind {
3838
I18nSupport,
3939
EventReplay,
4040
IncrementalHydration,
41+
NoIncrementalHydration,
4142
}
4243

4344
/**
@@ -144,6 +145,16 @@ export function withIncrementalHydration(): HydrationFeature<HydrationFeatureKin
144145
return hydrationFeature(HydrationFeatureKind.IncrementalHydration, ɵwithIncrementalHydration());
145146
}
146147

148+
/**
149+
* Disables support for incremental hydration (which is enabled by default).
150+
*
151+
* @publicApi 22.0
152+
* @see {@link provideClientHydration}
153+
*/
154+
export function withNoIncrementalHydration(): HydrationFeature<HydrationFeatureKind.NoIncrementalHydration> {
155+
return hydrationFeature(HydrationFeatureKind.NoIncrementalHydration);
156+
}
157+
147158
/**
148159
* Returns an `ENVIRONMENT_INITIALIZER` token setup with a function
149160
* that verifies whether enabledBlocking initial navigation is used in an application
@@ -240,16 +251,22 @@ export function provideClientHydration(
240251
HydrationFeatureKind.HttpTransferCacheOptions,
241252
);
242253

243-
if (
244-
typeof ngDevMode !== 'undefined' &&
245-
ngDevMode &&
246-
featuresKind.has(HydrationFeatureKind.NoHttpTransferCache) &&
247-
hasHttpTransferCacheOptions
248-
) {
249-
throw new RuntimeError(
250-
RuntimeErrorCode.HYDRATION_CONFLICTING_FEATURES,
251-
'Configuration error: found both withHttpTransferCacheOptions() and withNoHttpTransferCache() in the same call to provideClientHydration(), which is a contradiction.',
252-
);
254+
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
255+
if (featuresKind.has(HydrationFeatureKind.NoHttpTransferCache) && hasHttpTransferCacheOptions) {
256+
throw new RuntimeError(
257+
RuntimeErrorCode.HYDRATION_CONFLICTING_FEATURES,
258+
'Configuration error: found both withHttpTransferCacheOptions() and withNoHttpTransferCache() in the same call to provideClientHydration(), which is a contradiction.',
259+
);
260+
}
261+
if (
262+
featuresKind.has(HydrationFeatureKind.IncrementalHydration) &&
263+
featuresKind.has(HydrationFeatureKind.NoIncrementalHydration)
264+
) {
265+
throw new RuntimeError(
266+
RuntimeErrorCode.HYDRATION_CONFLICTING_FEATURES,
267+
'Configuration error: found both withIncrementalHydration() and withNoIncrementalHydration() in the same call to provideClientHydration(), which is a contradiction.',
268+
);
269+
}
253270
}
254271

255272
return makeEnvironmentProviders([
@@ -261,6 +278,9 @@ export function provideClientHydration(
261278
featuresKind.has(HydrationFeatureKind.NoHttpTransferCache) || hasHttpTransferCacheOptions
262279
? []
263280
: ɵwithHttpTransferCache({}),
281+
featuresKind.has(HydrationFeatureKind.NoIncrementalHydration)
282+
? []
283+
: ɵwithIncrementalHydration(),
264284
providers,
265285
]);
266286
}

packages/platform-browser/src/platform-browser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export {
2929
withHttpTransferCacheOptions,
3030
withI18nSupport,
3131
withIncrementalHydration,
32+
withNoIncrementalHydration,
3233
withNoHttpTransferCache,
3334
} from './hydration';
3435
export {

packages/platform-browser/test/hydration_spec.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ import {TestBed} from '@angular/core/testing';
2020
import {withBody} from '@angular/private/testing';
2121
import {BehaviorSubject} from 'rxjs';
2222

23-
import {provideClientHydration, withNoHttpTransferCache} from '../public_api';
23+
import {
24+
provideClientHydration,
25+
withNoHttpTransferCache,
26+
withIncrementalHydration,
27+
withNoIncrementalHydration,
28+
} from '../public_api';
2429
import {withHttpTransferCacheOptions} from '../src/hydration';
2530

2631
describe('provideClientHydration', () => {
@@ -164,4 +169,12 @@ describe('provideClientHydration', () => {
164169
TestBed.inject(HttpTestingController).expectNone(url);
165170
});
166171
});
172+
173+
describe('incremental hydration conflicts', () => {
174+
it('should throw when both withIncrementalHydration and withNoIncrementalHydration are provided', () => {
175+
expect(() => {
176+
provideClientHydration(withIncrementalHydration(), withNoIncrementalHydration());
177+
}).toThrowError(/Configuration error: found both withIncrementalHydration/);
178+
});
179+
});
167180
});

0 commit comments

Comments
 (0)