Skip to content

Commit 997bc42

Browse files
feat: generic template-binding-reorder capability — the Ivy input-order regression fix
Reads a directive class for @input() set accessors that dereference another same-directive @input() in a throwing position (Renderer2 element-method arg0 or .classList), then topologically reorders a call site's directive-input bindings so every dep precedes its dependent; refuses cycles and unmatchable selectors, no-ops the safe/non-input/unprovable/unbound cases. Fires exactly once on the real split shape: [splitPos]…[splitTopEl] → [splitTopEl][splitBottomEl] [splitPos], binding VALUES byte-identical, outputs untouched — the root-cause fix for Ivy applying input setters in template order where View Engine used declaration order. 16 tests (positive + four refusals). Rebuild + behavior-check deferred as its own lane. PM verified full gate: 2066/2066.
1 parent bec590f commit 997bc42

5 files changed

Lines changed: 927 additions & 0 deletions

File tree

packages/cli/src/fixture/angular-super-productivity-lanes-run.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,23 @@ async function styleSheetsBelow(directory: string, root: string): Promise<Worksp
202202
return files;
203203
}
204204

205+
/** Every component template below a directory, as workspace files. */
206+
async function templatesBelow(directory: string, root: string): Promise<WorkspaceFile[]> {
207+
const files: WorkspaceFile[] = [];
208+
for (const entry of (await readdir(directory, { withFileTypes: true })).sort((left, right) =>
209+
left.name < right.name ? -1 : 1,
210+
)) {
211+
const item = path.join(directory, entry.name);
212+
if (entry.isDirectory()) {
213+
files.push(...(await templatesBelow(item, root)));
214+
continue;
215+
}
216+
if (!entry.isFile() || path.extname(entry.name) !== '.html') continue;
217+
files.push({ path: path.relative(root, item), source: await readFile(item, 'utf8') });
218+
}
219+
return files;
220+
}
221+
205222
/** Every workspace-relative path the tree carries, excluding installed packages. */
206223
async function workspacePathsBelow(directory: string, root: string): Promise<string[]> {
207224
const paths: string[] = [];
@@ -247,6 +264,7 @@ export async function composeMigration(tree: string): Promise<AngularMigration>
247264
source: await readFile(path.join(tree, 'tsconfig.json'), 'utf8'),
248265
},
249266
sourceModules,
267+
templates: await templatesBelow(source, tree),
250268
styleSheets: await styleSheetsBelow(source, tree),
251269
workspaceFiles: await workspacePathsBelow(tree, tree),
252270
},

packages/frameworks/angular/src/angular-cli-era-migration.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ import {
3434
removeEntryComponents,
3535
type EntryComponentsChange,
3636
} from './entry-components-removal.ts';
37+
import {
38+
readDirectiveBindingDependencies,
39+
reorderTemplateBindings,
40+
type BindingReorderChange,
41+
type DirectiveBindingReading,
42+
} from './template-binding-reorder.ts';
3743
import {
3844
declareUndeclaredRuntimeDependencies,
3945
undeclaredRuntimeDependencies,
@@ -61,6 +67,15 @@ export type AngularMigrationInput = Readonly<{
6167
tsConfig: WorkspaceFile;
6268
/** Application source modules, as read from the workspace. */
6369
sourceModules: readonly WorkspaceFile[];
70+
/**
71+
* Component templates the application owns, kept apart from
72+
* {@link sourceModules} because they are not modules: no source transform
73+
* parses them, and the only capability that reads them reorders directive
74+
* bindings a component class proves are order-dependent. A tree that supplies
75+
* none has none reordered, which is a different thing from having none to
76+
* reorder.
77+
*/
78+
templates?: readonly WorkspaceFile[];
6479
/**
6580
* Webpack fragments a wrapper builder reads, keyed by the path the workspace
6681
* writes for them. A fragment a target references but that is not supplied
@@ -179,6 +194,13 @@ function describeEntryComponentsChange(change: EntryComponentsChange): string {
179194
return `line ${change.line}: ${change.kind} of ${change.symbols.join(', ')}`;
180195
}
181196

197+
function describeBindingReorderChange(change: BindingReorderChange): string {
198+
return (
199+
`line ${change.line}: ${change.kind} on <${change.element}> (${change.directive}) — ` +
200+
`${change.before.join(', ')} -> ${change.after.join(', ')}, forced by ${change.edges.join('; ')}`
201+
);
202+
}
203+
182204
function file(
183205
input: WorkspaceFile,
184206
source: string,
@@ -358,6 +380,33 @@ export function migrateAngularCliEraWorkspace(
358380
]),
359381
);
360382
}
383+
/**
384+
* Templates are the last application capability, and it is cross-file by
385+
* nature: whether a call site's binding order is safe is a question about the
386+
* directive the element resolves to, read from that directive's own class.
387+
* The readings are taken from the modules as the per-module capabilities left
388+
* them, so a directive whose imports those capabilities rewrote still
389+
* resolves. A template no directive reading touches is carried through
390+
* unchanged and still counted, so the scanned total means what it says.
391+
*/
392+
const directiveReadings: DirectiveBindingReading[] = [];
393+
for (const entry of files)
394+
if (entry.kind === 'application' && entry.path.endsWith('.ts'))
395+
directiveReadings.push(...readDirectiveBindingDependencies(entry.path, entry.source));
396+
for (const template of [...(input.templates ?? [])].sort((left, right) =>
397+
compareStrings(left.path, right.path),
398+
)) {
399+
const reordered = reorderTemplateBindings(template.path, template.source, directiveReadings);
400+
unhandled.push(...reordered.unhandled);
401+
files.push(
402+
file(
403+
template,
404+
reordered.source,
405+
'application',
406+
reordered.changes.map(describeBindingReorderChange),
407+
),
408+
);
409+
}
361410
/**
362411
* Stylesheets are application files too, and a blocked package subpath is an
363412
* application file that no longer builds. Each is offered to every package

packages/frameworks/angular/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ export * from './json-module-named-import.ts';
3838
export * from './stylesheet-url-rebase.ts';
3939
export * from './web-worker-url-specifier.ts';
4040
export * from './forms-legacy-disabled-state.ts';
41+
export * from './template-binding-reorder.ts';

0 commit comments

Comments
 (0)