Skip to content

Commit 6c79234

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 5516769 commit 6c79234

File tree

15 files changed

+522
-20
lines changed

15 files changed

+522
-20
lines changed

goldens/public-api/platform-browser/index.api.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ export enum HydrationFeatureKind {
116116
// (undocumented)
117117
IncrementalHydration = 4,
118118
// (undocumented)
119-
NoHttpTransferCache = 0
119+
NoHttpTransferCache = 0,
120+
// (undocumented)
121+
NoIncrementalHydration = 5
120122
}
121123

122124
// @public
@@ -215,6 +217,9 @@ export function withIncrementalHydration(): HydrationFeature<HydrationFeatureKin
215217
// @public
216218
export function withNoHttpTransferCache(): HydrationFeature<HydrationFeatureKind.NoHttpTransferCache>;
217219

220+
// @public
221+
export function withNoIncrementalHydration(): HydrationFeature<HydrationFeatureKind.NoIncrementalHydration>;
222+
218223
// (No @packageDocumentation comment for this package)
219224

220225
```
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"dist/browser/main-[hash].js": 232126,
2+
"dist/browser/main-[hash].js": 240329,
33
"dist/browser/polyfills-[hash].js": 35726,
44
"dist/browser/event-dispatch-contract.min.js": 476
55
}

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": "Adds withNoIncrementalHydration() opt out to provideClientHydration() when incremental hydration is not enabled to retain pre-v22 behavior-.",
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: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 not change anything if withIncrementalHydration() is 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('withNoIncrementalHydration()');
34+
expect(content).toContain('withIncrementalHydration()');
35+
expect(content).toContain('provideClientHydration()');
36+
});
37+
38+
it('should add withNoIncrementalHydration() if withIncrementalHydration is absent', async () => {
39+
const {fs} = await runTsurgeMigration(new IncrementalHydrationMigration(), [
40+
{
41+
name: absoluteFrom('/index.ts'),
42+
isProgramRootFile: true,
43+
contents: `
44+
import { provideClientHydration } from '@angular/platform-browser';
45+
46+
provideClientHydration();
47+
`,
48+
},
49+
]);
50+
51+
const content = fs.readFile(absoluteFrom('/index.ts'));
52+
expect(content).toContain('withNoIncrementalHydration()');
53+
});
54+
});
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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) {
62+
// Add withNoIncrementalHydration()
63+
const withNoIncrementalExpr = importManager.addImport({
64+
exportModuleSpecifier: '@angular/platform-browser',
65+
exportSymbolName: 'withNoIncrementalHydration',
66+
requestedFile: sf,
67+
});
68+
69+
const exprText = printer.printNode(ts.EmitHint.Unspecified, withNoIncrementalExpr, sf);
70+
71+
const insertPos = node.arguments.end;
72+
const toInsert = node.arguments.length > 0 ? `, ${exprText}()` : `${exprText}()`;
73+
74+
replacements.push(
75+
new Replacement(
76+
projectFile(sf, info),
77+
new TextUpdate({
78+
position: insertPos,
79+
end: insertPos,
80+
toInsert: toInsert,
81+
}),
82+
),
83+
);
84+
}
85+
}
86+
ts.forEachChild(node, visit);
87+
});
88+
}
89+
90+
applyImportManagerChanges(importManager, replacements, sourceFiles, info);
91+
92+
return confirmAsSerializable({
93+
replacements,
94+
});
95+
}
96+
97+
override async combine(
98+
unitA: IncrementalHydrationMigrationData,
99+
unitB: IncrementalHydrationMigrationData,
100+
): Promise<Serializable<IncrementalHydrationMigrationData>> {
101+
return confirmAsSerializable({
102+
replacements: [...unitA.replacements, ...unitB.replacements],
103+
});
104+
}
105+
106+
override async globalMeta(
107+
combinedData: IncrementalHydrationMigrationData,
108+
): Promise<Serializable<IncrementalHydrationMigrationData>> {
109+
return confirmAsSerializable(combinedData);
110+
}
111+
112+
override async stats(
113+
globalMetadata: IncrementalHydrationMigrationData,
114+
): Promise<Serializable<unknown>> {
115+
return confirmAsSerializable({});
116+
}
117+
118+
override async migrate(
119+
globalData: IncrementalHydrationMigrationData,
120+
): Promise<{replacements: Replacement[]}> {
121+
return {replacements: globalData.replacements};
122+
}
123+
}

0 commit comments

Comments
 (0)