|
9 | 9 | import ts from 'typescript'; |
10 | 10 |
|
11 | 11 | function insertDebugNameIntoCallExpression( |
12 | | - callExpression: ts.CallExpression, |
| 12 | + node: ts.CallExpression, |
13 | 13 | debugName: string, |
14 | 14 | ): ts.CallExpression { |
15 | | - const signalExpressionIsRequired = isRequiredSignalFunction(callExpression.expression); |
16 | | - let configPosition = signalExpressionIsRequired ? 0 : 1; |
17 | | - |
18 | | - // 1. If the call expression has no arguments, we pretend that the config object is at position 0. |
19 | | - // We do this so that we can insert a spread element at the start of the args list in a way where |
20 | | - // undefined can be the first argument but still get tree-shaken out in production builds. |
21 | | - // or |
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. |
24 | | - const signalExpressionHasNoArguments = callExpression.arguments.length === 0; |
25 | | - const signalWithObjectOnlyDefinition = isSignalWithObjectOnlyDefinition(callExpression); |
26 | | - if (signalExpressionHasNoArguments || signalWithObjectOnlyDefinition) { |
27 | | - configPosition = 0; |
28 | | - } |
29 | | - |
30 | | - const nodeArgs = Array.from(callExpression.arguments); |
31 | | - let existingArgument = nodeArgs[configPosition]; |
32 | | - |
33 | | - if (existingArgument === undefined) { |
34 | | - existingArgument = ts.factory.createObjectLiteralExpression([]); |
35 | | - } |
36 | | - |
37 | | - // Do nothing if an identifier is used as the config object |
38 | | - // Ex: |
39 | | - // const defaultObject = { equals: () => false }; |
40 | | - // signal(123, defaultObject) |
41 | | - if (ts.isIdentifier(existingArgument)) { |
42 | | - return callExpression; |
43 | | - } |
| 15 | + const isRequired = isRequiredSignalFunction(node.expression); |
| 16 | + const hasNoArgs = node.arguments.length === 0; |
| 17 | + const configPosition = hasNoArgs || isSignalWithObjectOnlyDefinition(node) || isRequired ? 0 : 1; |
| 18 | + const existingArg = |
| 19 | + configPosition >= node.arguments.length ? null : node.arguments[configPosition]; |
44 | 20 |
|
45 | | - if (!ts.isObjectLiteralExpression(existingArgument)) { |
46 | | - return callExpression; |
47 | | - } |
48 | | - |
49 | | - // Insert debugName into the existing config object |
50 | | - const properties = Array.from(existingArgument.properties); |
51 | | - const debugNameExists = properties.some( |
52 | | - (prop) => |
53 | | - ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'debugName', |
54 | | - ); |
55 | | - |
56 | | - if (debugNameExists) { |
57 | | - return callExpression; |
| 21 | + // Do nothing if the existing parameter isn't statically analyzable or already has a `debugName`. |
| 22 | + if ( |
| 23 | + existingArg !== null && |
| 24 | + (!ts.isObjectLiteralExpression(existingArg) || |
| 25 | + existingArg.properties.some( |
| 26 | + (prop) => |
| 27 | + ts.isPropertyAssignment(prop) && |
| 28 | + ts.isIdentifier(prop.name) && |
| 29 | + prop.name.text === 'debugName', |
| 30 | + )) |
| 31 | + ) { |
| 32 | + return node; |
58 | 33 | } |
59 | 34 |
|
60 | | - const ngDevModeIdentifier = ts.factory.createIdentifier('ngDevMode'); |
61 | 35 | const debugNameProperty = ts.factory.createPropertyAssignment( |
62 | 36 | 'debugName', |
63 | 37 | ts.factory.createStringLiteral(debugName), |
64 | 38 | ); |
65 | | - const debugNameObject = ts.factory.createObjectLiteralExpression([debugNameProperty]); |
66 | | - const emptyObject = ts.factory.createObjectLiteralExpression(); |
67 | | - |
68 | | - // Create the spread expression: `...(ngDevMode ? { debugName: 'myDebugName' } : {})` |
69 | | - const spreadDebugNameExpression = ts.factory.createSpreadAssignment( |
70 | | - ts.factory.createParenthesizedExpression( |
71 | | - ts.factory.createConditionalExpression( |
72 | | - ngDevModeIdentifier, |
73 | | - undefined, // Question token |
74 | | - debugNameObject, |
75 | | - undefined, // Colon token |
76 | | - emptyObject, |
| 39 | + |
| 40 | + let newArgs: ts.Expression[]; |
| 41 | + |
| 42 | + if (existingArg !== null) { |
| 43 | + // If there's an existing object literal already, we transform it as follows: |
| 44 | + // `signal(0, {equal})` becomes `signal(0, { ...(ngDevMode ? {debugName: "n"} : {}), equal })`. |
| 45 | + // During minification the spread will be removed since it's pointing to an empty object. |
| 46 | + const transformedArg = ts.factory.createObjectLiteralExpression([ |
| 47 | + ts.factory.createSpreadAssignment( |
| 48 | + createNgDevModeConditional( |
| 49 | + ts.factory.createObjectLiteralExpression([debugNameProperty]), |
| 50 | + ts.factory.createObjectLiteralExpression(), |
| 51 | + ), |
77 | 52 | ), |
78 | | - ), |
79 | | - ); |
| 53 | + ...existingArg.properties, |
| 54 | + ]); |
80 | 55 |
|
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]; |
| 56 | + newArgs = node.arguments.map((arg) => (arg === existingArg ? transformedArg : arg)); |
97 | 57 | } else { |
98 | | - transformedSignalArgs = [nodeArgs[0], transformedConfigProperties]; |
| 58 | + // If there's no existing argument, we transform it as follows: |
| 59 | + // `input(0)` becomes `input(0, ...(ngDevMode ? [{debugName: "n"}] : []))` |
| 60 | + // Spreading into an empty literal allows for the array to be dropped during minification. |
| 61 | + const spreadArgs: ts.Expression[] = []; |
| 62 | + |
| 63 | + // If we're adding an argument, but the function requires a first argument (e.g. `input()`), |
| 64 | + // we have to add `undefined` before the debug literal. |
| 65 | + if (hasNoArgs && !isRequired) { |
| 66 | + spreadArgs.push(ts.factory.createIdentifier('undefined')); |
| 67 | + } |
| 68 | + |
| 69 | + spreadArgs.push(ts.factory.createObjectLiteralExpression([debugNameProperty])); |
| 70 | + |
| 71 | + newArgs = [ |
| 72 | + ...node.arguments, |
| 73 | + ts.factory.createSpreadElement( |
| 74 | + createNgDevModeConditional( |
| 75 | + ts.factory.createArrayLiteralExpression(spreadArgs), |
| 76 | + ts.factory.createArrayLiteralExpression(), |
| 77 | + ), |
| 78 | + ), |
| 79 | + ]; |
99 | 80 | } |
100 | 81 |
|
101 | | - return ts.factory.updateCallExpression( |
102 | | - callExpression, |
103 | | - callExpression.expression, |
104 | | - callExpression.typeArguments, |
105 | | - ts.factory.createNodeArray(transformedSignalArgs), |
| 82 | + return ts.factory.updateCallExpression(node, node.expression, node.typeArguments, newArgs); |
| 83 | +} |
| 84 | + |
| 85 | +/** |
| 86 | + * Creates an expression in the form of `(ngDevMode ? <devModeExpression> : <prodModeExpression>)`. |
| 87 | + */ |
| 88 | +function createNgDevModeConditional( |
| 89 | + devModeExpression: ts.Expression, |
| 90 | + prodModeExpression: ts.Expression, |
| 91 | +): ts.ParenthesizedExpression { |
| 92 | + return ts.factory.createParenthesizedExpression( |
| 93 | + ts.factory.createConditionalExpression( |
| 94 | + ts.factory.createIdentifier('ngDevMode'), |
| 95 | + undefined, |
| 96 | + devModeExpression, |
| 97 | + undefined, |
| 98 | + prodModeExpression, |
| 99 | + ), |
106 | 100 | ); |
107 | 101 | } |
108 | 102 |
|
|
0 commit comments