From 341c124f4fbc8f3ff113f419f6e4f6090aae4f51 Mon Sep 17 00:00:00 2001 From: P4 <969645+P4@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:22:56 +0200 Subject: [PATCH] fix(compiler-cli): apply debugName transform to required signal queries Transform assumed `.required` functions always take options as the first argument. This is true for `input` and `model`, but not for `viewChild` and `contentChild`, which take the same arguments as non-required versions. Change the code to put options for signal queries in the right position, causing debugName to be correctly generated for signal queries. --- .../implicit_signal_debug_name_transform.ts | 14 +- .../test/ngtsc/debug_transform_spec.ts | 266 ++++++++++++++++++ 2 files changed, 274 insertions(+), 6 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/transform/src/implicit_signal_debug_name_transform.ts b/packages/compiler-cli/src/ngtsc/transform/src/implicit_signal_debug_name_transform.ts index 9fe48a2e082a..a3a30c025945 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/implicit_signal_debug_name_transform.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/implicit_signal_debug_name_transform.ts @@ -12,9 +12,10 @@ function insertDebugNameIntoCallExpression( node: ts.CallExpression, debugName: string, ): ts.CallExpression { - const isRequired = isRequiredSignalFunction(node.expression); + const isRequiredInput = isRequiredInputFunction(node.expression); const hasNoArgs = node.arguments.length === 0; - const configPosition = hasNoArgs || isSignalWithObjectOnlyDefinition(node) || isRequired ? 0 : 1; + const configPosition = + hasNoArgs || isSignalWithObjectOnlyDefinition(node) || isRequiredInput ? 0 : 1; const existingArg = configPosition >= node.arguments.length ? null : node.arguments[configPosition]; @@ -62,7 +63,7 @@ function insertDebugNameIntoCallExpression( // If we're adding an argument, but the function requires a first argument (e.g. `input()`), // we have to add `undefined` before the debug literal. - if (hasNoArgs && !isRequired) { + if (hasNoArgs && !isRequiredInput) { spreadArgs.push(ts.factory.createIdentifier('undefined')); } @@ -297,15 +298,16 @@ function isSignalFunction(expression: ts.Identifier): boolean { return signalFunctions.has(text); } -function isRequiredSignalFunction(expression: ts.Expression): boolean { - // Check for a property access expression that uses the 'required' property +function isRequiredInputFunction(expression: ts.Expression): boolean { + // Check for a property access expression that uses the 'required' property on `input` or `model` if ( ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.name) && ts.isIdentifier(expression.expression) ) { const accessName = expression.name.text; - if (accessName === 'required') { + const parentName = expression.expression.text; + if (accessName === 'required' && (parentName === 'input' || parentName === 'model')) { return true; } } diff --git a/packages/compiler-cli/test/ngtsc/debug_transform_spec.ts b/packages/compiler-cli/test/ngtsc/debug_transform_spec.ts index 4fb520c9e37a..15746de9e801 100644 --- a/packages/compiler-cli/test/ngtsc/debug_transform_spec.ts +++ b/packages/compiler-cli/test/ngtsc/debug_transform_spec.ts @@ -1319,6 +1319,139 @@ runInEachFileSystem(() => { `viewChild("foo", { debugName: "testViewChild", read: ElementRef })`, ); }); + + describe('.required', () => { + it('should insert debug info into .required', () => { + env.write( + 'test.ts', + ` + import {viewChild, Component} from '@angular/core'; + + @Component({ + template: '', + }) + class MyComponent { + testViewChild = viewChild.required('foo'); + } + `, + ); + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(cleanNewLines(jsContents)).toContain( + `viewChild.required('foo', /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "testViewChild" }] : /* istanbul ignore next */ []))`, + ); + }); + + it('should insert debug info into .required with existing options', () => { + env.write( + 'test.ts', + ` + import {viewChild, Component, ElementRef} from '@angular/core'; + + @Component({ + template: '' + }) class MyComponent { + testViewChild = viewChild.required('foo', { read: ElementRef }); + } + `, + ); + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(cleanNewLines(jsContents)).toContain( + `viewChild.required('foo', { ...(ngDevMode ? { debugName: "testViewChild" } : /* istanbul ignore next */ {}), read: ElementRef })`, + ); + }); + + it('should tree-shake away debug info if in prod mode', () => async () => { + env.write( + 'test.ts', + ` + import {viewChild, Component} from '@angular/core'; + + @Component({ + template: '' + }) + class MyComponent { + testViewChild = viewChild.required('foo'); + } + `, + ); + env.driveMain(); + + const jsContents = env.getContents('test.js'); + const builtContent = (await esbuild.transform(jsContents, minifiedProdBuildOptions)).code; + expect(builtContent).not.toContain('debugName'); + expect(cleanNewLines(builtContent)).toContain(`viewChild( "foo" )`); + }); + + it('should not tree-shake away debug info if in dev mode', async () => { + env.write( + 'test.ts', + ` + import {viewChild, Component} from '@angular/core'; + + @Component({ + template: '', + }) + class MyComponent { + testViewChild = viewChild.required('foo'); + } + `, + ); + env.driveMain(); + + const jsContents = env.getContents('test.js'); + const builtContent = (await esbuild.transform(jsContents, minifiedDevBuildOptions)).code; + expect(cleanNewLines(builtContent)).toContain( + `viewChild.required( "foo", { debugName: "testViewChild" } )`, + ); + }); + + it('should tree-shake away debug info if in prod mode with custom options', async () => { + env.write( + 'test.ts', + ` + import {viewChild, Component, ElementRef} from '@angular/core'; + + @Component({ + template: '' + }) class MyComponent { + testViewChild = viewChild.required('foo', { read: ElementRef }); + } + `, + ); + env.driveMain(); + + const jsContents = env.getContents('test.js'); + const builtContent = (await esbuild.transform(jsContents, minifiedProdBuildOptions)).code; + expect(builtContent).not.toContain('debugName'); + expect(builtContent).toContain('viewChild.required("foo", { read: ElementRef })'); + }); + + it('should not tree-shake away debug info if in dev mode with custom options', async () => { + env.write( + 'test.ts', + ` + import {viewChild, Component, ElementRef} from '@angular/core'; + + @Component({ + template: '' + }) class MyComponent { + testViewChild = viewChild.required('foo', { read: ElementRef }); + } + `, + ); + env.driveMain(); + + const jsContents = env.getContents('test.js'); + const builtContent = (await esbuild.transform(jsContents, minifiedDevBuildOptions)).code; + expect(cleanNewLines(builtContent)).toContain( + `viewChild.required("foo", { debugName: "testViewChild", read: ElementRef })`, + ); + }); + }); }); describe('viewChildren', () => { @@ -1575,6 +1708,139 @@ runInEachFileSystem(() => { `contentChild("foo", { debugName: "testContentChild", read: ElementRef })`, ); }); + + describe('.required', () => { + it('should insert debug info into .required', () => { + env.write( + 'test.ts', + ` + import {contentChild, Component} from '@angular/core'; + + @Component({ + template: '', + }) + class MyComponent { + testContentChild = contentChild.required('foo'); + } + `, + ); + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(cleanNewLines(jsContents)).toContain( + `contentChild.required('foo', /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "testContentChild" }] : /* istanbul ignore next */ []))`, + ); + }); + + it('should insert debug info into .required with existing options', () => { + env.write( + 'test.ts', + ` + import {contentChild, Component, ElementRef} from '@angular/core'; + + @Component({ + template: '' + }) class MyComponent { + testContentChild = contentChild.required('foo', { read: ElementRef }); + } + `, + ); + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(cleanNewLines(jsContents)).toContain( + `contentChild.required('foo', { ...(ngDevMode ? { debugName: "testContentChild" } : /* istanbul ignore next */ {}), read: ElementRef })`, + ); + }); + + it('should tree-shake away debug info if in prod mode', () => async () => { + env.write( + 'test.ts', + ` + import {contentChild, Component} from '@angular/core'; + + @Component({ + template: '' + }) + class MyComponent { + testContentChild = contentChild.required('foo'); + } + `, + ); + env.driveMain(); + + const jsContents = env.getContents('test.js'); + const builtContent = (await esbuild.transform(jsContents, minifiedProdBuildOptions)).code; + expect(builtContent).not.toContain('debugName'); + expect(cleanNewLines(builtContent)).toContain(`contentChild( "foo" )`); + }); + + it('should not tree-shake away debug info if in dev mode', async () => { + env.write( + 'test.ts', + ` + import {contentChild, Component} from '@angular/core'; + + @Component({ + template: '', + }) + class MyComponent { + testContentChild = contentChild.required('foo'); + } + `, + ); + env.driveMain(); + + const jsContents = env.getContents('test.js'); + const builtContent = (await esbuild.transform(jsContents, minifiedDevBuildOptions)).code; + expect(cleanNewLines(builtContent)).toContain( + `contentChild.required( "foo", { debugName: "testContentChild" } )`, + ); + }); + + it('should tree-shake away debug info if in prod mode with custom options', async () => { + env.write( + 'test.ts', + ` + import {contentChild, Component, ElementRef} from '@angular/core'; + + @Component({ + template: '' + }) class MyComponent { + testContentChild = contentChild.required('foo', { read: ElementRef }); + } + `, + ); + env.driveMain(); + + const jsContents = env.getContents('test.js'); + const builtContent = (await esbuild.transform(jsContents, minifiedProdBuildOptions)).code; + expect(builtContent).not.toContain('debugName'); + expect(builtContent).toContain('contentChild.required("foo", { read: ElementRef })'); + }); + + it('should not tree-shake away debug info if in dev mode with custom options', async () => { + env.write( + 'test.ts', + ` + import {contentChild, Component, ElementRef} from '@angular/core'; + + @Component({ + template: '' + }) class MyComponent { + testContentChild = contentChild.required('foo', { read: ElementRef }); + } + `, + ); + env.driveMain(); + + const jsContents = env.getContents('test.js'); + const builtContent = (await esbuild.transform(jsContents, minifiedDevBuildOptions)).code; + expect(cleanNewLines(builtContent)).toContain( + `contentChild.required("foo", { debugName: "testContentChild", read: ElementRef })`, + ); + }); + }); }); describe('contentChildren', () => {