@@ -15,31 +15,27 @@ function insertDebugNameIntoCallExpression(
1515 const signalExpressionIsRequired = isRequiredSignalFunction ( callExpression . expression ) ;
1616 let configPosition = signalExpressionIsRequired ? 0 : 1 ;
1717
18- const nodeArgs = Array . from ( callExpression . arguments ) ;
19-
2018 // 1. If the call expression has no arguments, we pretend that the config object is at position 0.
2119 // We do this so that we can insert a spread element at the start of the args list in a way where
2220 // undefined can be the first argument but still get tree-shaken out in production builds.
2321 // or
24- // 2. Since `linkedSignal` with computation uses a single object for both computation logic
25- // and options (unlike other signal-based primitives), we set the argument position to 0, i.e.
26- // reusing the computation logic object.
22+ // 2. If the signal has an object-only definition (e.g. `linkedSignal` or `resource`), we set
23+ // the argument position to 0, i.e. reusing the existing object.
2724 const signalExpressionHasNoArguments = callExpression . arguments . length === 0 ;
28- const isLinkedSignal = callExpression . expression . getText ( ) === 'linkedSignal' ;
29- const isComputationLinkedSignal =
30- isLinkedSignal && nodeArgs [ 0 ] . kind === ts . SyntaxKind . ObjectLiteralExpression ;
31- if ( signalExpressionHasNoArguments || isComputationLinkedSignal ) {
25+ const signalWithObjectOnlyDefinition = isSignalWithObjectOnlyDefinition ( callExpression ) ;
26+ if ( signalExpressionHasNoArguments || signalWithObjectOnlyDefinition ) {
3227 configPosition = 0 ;
3328 }
3429
30+ const nodeArgs = Array . from ( callExpression . arguments ) ;
3531 let existingArgument = nodeArgs [ configPosition ] ;
3632
3733 if ( existingArgument === undefined ) {
3834 existingArgument = ts . factory . createObjectLiteralExpression ( [ ] ) ;
3935 }
4036
4137 // Do nothing if an identifier is used as the config object
42- // Ex -
38+ // Ex:
4339 // const defaultObject = { equals: () => false };
4440 // signal(123, defaultObject)
4541 if ( ts . isIdentifier ( existingArgument ) ) {
@@ -50,7 +46,7 @@ function insertDebugNameIntoCallExpression(
5046 return callExpression ;
5147 }
5248
53- // insert debugName into the existing config object
49+ // Insert debugName into the existing config object
5450 const properties = Array . from ( existingArgument . properties ) ;
5551 const debugNameExists = properties . some (
5652 ( prop ) =>
@@ -61,73 +57,52 @@ function insertDebugNameIntoCallExpression(
6157 return callExpression ;
6258 }
6359
64- // We prepend instead of appending so that we don't overwrite an existing debugName Property
65- // `{ foo: 'bar' }` -> `{ debugName: 'myDebugName', foo: 'bar' }`
66- properties . unshift (
67- ts . factory . createPropertyAssignment ( 'debugName' , ts . factory . createStringLiteral ( debugName ) ) ,
68- ) ;
69-
70- const transformedConfigProperties = ts . factory . createObjectLiteralExpression ( properties ) ;
7160 const ngDevModeIdentifier = ts . factory . createIdentifier ( 'ngDevMode' ) ;
61+ const debugNameProperty = ts . factory . createPropertyAssignment (
62+ 'debugName' ,
63+ ts . factory . createStringLiteral ( debugName ) ,
64+ ) ;
65+ const debugNameObject = ts . factory . createObjectLiteralExpression ( [ debugNameProperty ] ) ;
66+ const emptyObject = ts . factory . createObjectLiteralExpression ( ) ;
7267
73- let devModeCase : ts . ArrayLiteralExpression ;
74- // if the signal expression has no arguments and the config object is not required,
75- // we need to add an undefined identifier to the start of the args list so that we can spread the
76- // config object in the right place.
77- if ( signalExpressionHasNoArguments && ! signalExpressionIsRequired ) {
78- devModeCase = ts . factory . createArrayLiteralExpression ( [
79- ts . factory . createIdentifier ( 'undefined' ) ,
80- transformedConfigProperties ,
81- ] ) ;
82- } else {
83- devModeCase = ts . factory . createArrayLiteralExpression ( [
84- transformedConfigProperties ,
85- ...nodeArgs . slice ( configPosition + 1 ) ,
86- ] ) ;
87- }
88-
89- const nonDevModeCase = signalExpressionIsRequired
90- ? ts . factory . createArrayLiteralExpression ( nodeArgs )
91- : ts . factory . createArrayLiteralExpression ( nodeArgs . slice ( configPosition ) ) ;
92-
93- const spreadElementContainingUpdatedOptions = ts . factory . createSpreadElement (
68+ // Create the spread expression: `...(ngDevMode ? { debugName: 'myDebugName' } : {})`
69+ const spreadDebugNameExpression = ts . factory . createSpreadAssignment (
9470 ts . factory . createParenthesizedExpression (
9571 ts . factory . createConditionalExpression (
9672 ngDevModeIdentifier ,
97- /* question token */ undefined ,
98- devModeCase ,
99- /* colon token */ undefined ,
100- nonDevModeCase ,
73+ undefined , // Question token
74+ debugNameObject ,
75+ undefined , // Colon token
76+ emptyObject ,
10177 ) ,
10278 ) ,
10379 ) ;
10480
105- let transformedSignalArgs : ts . NodeArray < ts . Expression > ;
106-
107- if ( signalExpressionIsRequired || signalExpressionHasNoArguments || isComputationLinkedSignal ) {
108- // 1. If the call expression is a required signal function, there is no args other than the config object.
109- // So we just use the spread element as the only argument.
110- // or
111- // 2. If the call expression has no arguments (ex. input(), model(), etc), we already added the undefined
112- // identifier in the spread element above. So we use that spread Element as is.
113- // or
114- // 3. We are transforming a `linkedSignal` with computation (i.e. we have a single object for both
115- // logic and options).
116- transformedSignalArgs = ts . factory . createNodeArray ( [ spreadElementContainingUpdatedOptions ] ) ;
81+ const transformedConfigProperties = ts . factory . createObjectLiteralExpression ( [
82+ spreadDebugNameExpression ,
83+ ...properties ,
84+ ] ) ;
85+
86+ let transformedSignalArgs = [ ] ;
87+
88+ // The following expression handles 3 cases:
89+ // 1. Non-`required` signals without an argument that need to be prepended with `undefined` (e.g. `model()`).
90+ // 2. Signals with object-only definition like `resource` or `linkedSignal` with computation;
91+ // Or `required` signals.
92+ // 3. All remaining cases where we have a signal with an argument (e.g `computed(Fn)` or `signal('foo')`).
93+ if ( signalExpressionHasNoArguments && ! signalExpressionIsRequired ) {
94+ transformedSignalArgs = [ ts . factory . createIdentifier ( 'undefined' ) , transformedConfigProperties ] ;
95+ } else if ( signalWithObjectOnlyDefinition || signalExpressionIsRequired ) {
96+ transformedSignalArgs = [ transformedConfigProperties ] ;
11797 } else {
118- // 3. Signal expression is not required and has arguments.
119- // Here we leave the first argument as is and spread the rest.
120- transformedSignalArgs = ts . factory . createNodeArray ( [
121- nodeArgs [ 0 ] ,
122- spreadElementContainingUpdatedOptions ,
123- ] ) ;
98+ transformedSignalArgs = [ nodeArgs [ 0 ] , transformedConfigProperties ] ;
12499 }
125100
126101 return ts . factory . updateCallExpression (
127102 callExpression ,
128103 callExpression . expression ,
129104 callExpression . typeArguments ,
130- transformedSignalArgs ,
105+ ts . factory . createNodeArray ( transformedSignalArgs ) ,
131106 ) ;
132107}
133108
@@ -234,6 +209,23 @@ function isPropertyDeclarationCase(
234209 return ts . isIdentifier ( expression ) && isSignalFunction ( expression ) ;
235210}
236211
212+ type PackageName = 'core' | 'common' ;
213+
214+ const signalFunctions : ReadonlyMap < string , PackageName > = new Map ( [
215+ [ 'signal' , 'core' ] ,
216+ [ 'computed' , 'core' ] ,
217+ [ 'linkedSignal' , 'core' ] ,
218+ [ 'input' , 'core' ] ,
219+ [ 'model' , 'core' ] ,
220+ [ 'viewChild' , 'core' ] ,
221+ [ 'viewChildren' , 'core' ] ,
222+ [ 'contentChild' , 'core' ] ,
223+ [ 'contentChildren' , 'core' ] ,
224+ [ 'effect' , 'core' ] ,
225+ [ 'resource' , 'core' ] ,
226+ [ 'httpResource' , 'common' ] ,
227+ ] ) ;
228+
237229/**
238230 *
239231 * Determines if a node is an expression that references an @angular/core imported symbol.
@@ -243,7 +235,7 @@ function isPropertyDeclarationCase(
243235 * const mySignal = signal(123); // expressionIsUsingAngularImportedSymbol === true
244236 * ```
245237 */
246- function expressionIsUsingAngularCoreImportedSymbol (
238+ function expressionIsUsingAngularImportedSymbol (
247239 program : ts . Program ,
248240 expression : ts . Expression ,
249241) : boolean {
@@ -282,25 +274,14 @@ function expressionIsUsingAngularCoreImportedSymbol(
282274 }
283275
284276 const specifier = importDeclaration . moduleSpecifier . text ;
277+ const packageName = signalFunctions . get ( expression . getText ( ) ) ;
285278 return (
286279 specifier !== undefined &&
287- ( specifier === '@angular/core' || specifier . startsWith ( '@angular/core/' ) )
280+ packageName !== undefined &&
281+ ( specifier === `@angular/${ packageName } ` || specifier . startsWith ( `@angular/${ packageName } /` ) )
288282 ) ;
289283}
290284
291- const signalFunctions : ReadonlySet < string > = new Set ( [
292- 'signal' ,
293- 'computed' ,
294- 'linkedSignal' ,
295- 'input' ,
296- 'model' ,
297- 'viewChild' ,
298- 'viewChildren' ,
299- 'contentChild' ,
300- 'contentChildren' ,
301- 'effect' ,
302- ] ) ;
303-
304285function isSignalFunction ( expression : ts . Identifier ) : boolean {
305286 const text = expression . text ;
306287
@@ -331,10 +312,10 @@ function transformVariableDeclaration(
331312
332313 const expression = node . initializer . expression ;
333314 if ( ts . isPropertyAccessExpression ( expression ) ) {
334- if ( ! expressionIsUsingAngularCoreImportedSymbol ( program , expression . expression ) ) {
315+ if ( ! expressionIsUsingAngularImportedSymbol ( program , expression . expression ) ) {
335316 return node ;
336317 }
337- } else if ( ! expressionIsUsingAngularCoreImportedSymbol ( program , expression ) ) {
318+ } else if ( ! expressionIsUsingAngularImportedSymbol ( program , expression ) ) {
338319 return node ;
339320 }
340321
@@ -357,15 +338,18 @@ function transformVariableDeclaration(
357338function transformPropertyAssignment (
358339 program : ts . Program ,
359340 node : ts . ExpressionStatement & {
360- expression : ts . BinaryExpression & { right : ts . CallExpression ; left : ts . PropertyAccessExpression } ;
341+ expression : ts . BinaryExpression & {
342+ right : ts . CallExpression ;
343+ left : ts . PropertyAccessExpression ;
344+ } ;
361345 } ,
362346) : ts . ExpressionStatement {
363347 const expression = node . expression . right . expression ;
364348 if ( ts . isPropertyAccessExpression ( expression ) ) {
365- if ( ! expressionIsUsingAngularCoreImportedSymbol ( program , expression . expression ) ) {
349+ if ( ! expressionIsUsingAngularImportedSymbol ( program , expression . expression ) ) {
366350 return node ;
367351 }
368- } else if ( ! expressionIsUsingAngularCoreImportedSymbol ( program , expression ) ) {
352+ } else if ( ! expressionIsUsingAngularImportedSymbol ( program , expression ) ) {
369353 return node ;
370354 }
371355
@@ -387,10 +371,10 @@ function transformPropertyDeclaration(
387371
388372 const expression = node . initializer . expression ;
389373 if ( ts . isPropertyAccessExpression ( expression ) ) {
390- if ( ! expressionIsUsingAngularCoreImportedSymbol ( program , expression . expression ) ) {
374+ if ( ! expressionIsUsingAngularImportedSymbol ( program , expression . expression ) ) {
391375 return node ;
392376 }
393- } else if ( ! expressionIsUsingAngularCoreImportedSymbol ( program , expression ) ) {
377+ } else if ( ! expressionIsUsingAngularImportedSymbol ( program , expression ) ) {
394378 return node ;
395379 }
396380
@@ -410,6 +394,24 @@ function transformPropertyDeclaration(
410394 }
411395}
412396
397+ /**
398+ * The function determines whether the target signal has an object-only definition, that includes
399+ * both the computation logic and the options (unlike other signal-based primitives), or not.
400+ * Ex: `linkedSignal` with computation, `resource`
401+ */
402+ function isSignalWithObjectOnlyDefinition ( callExpression : ts . CallExpression ) : boolean {
403+ const callExpressionText = callExpression . expression . getText ( ) ;
404+ const nodeArgs = Array . from ( callExpression . arguments ) ;
405+
406+ const isLinkedSignal = callExpressionText === 'linkedSignal' ;
407+ const isComputationLinkedSignal =
408+ isLinkedSignal && nodeArgs [ 0 ] . kind === ts . SyntaxKind . ObjectLiteralExpression ;
409+
410+ const isResource = callExpressionText === 'resource' ;
411+
412+ return isComputationLinkedSignal || isResource ;
413+ }
414+
413415/**
414416 *
415417 * This transformer adds a debugName property to the config object of signal functions like
0 commit comments