Transform logic to extract signal debug names#57348
Transform logic to extract signal debug names#57348AleksanderBodurri wants to merge 1 commit intoangular:mainfrom
Conversation
c3d0bf5 to
fbbc8c0
Compare
alxhub
left a comment
There was a problem hiding this comment.
First round of comments!
Additionally, in the commit history, use refactor commits for implementation details. A given feature (debug names for example) should have exactly 1 feat commit, which is the commit which actually makes the feature available for end users to use.
There was a problem hiding this comment.
Same comment about ts.factory.update*.
There was a problem hiding this comment.
This is still a bit sketchy to me. This creates a node without any context (e.g. sourcemaps).
Ideally, we wouldn't have to copy the user's code like this, but we should still use ts.update* instead.
There was a problem hiding this comment.
We discussed this briefly in the DevTools sync today. We can definitely update this object literal to just be existingArgument. How should this work for the duplicated node though?
We're effectively converting signal(0, { equals }) to signal(0, ...(ngDevMode ? [{ debugName: 'test', equals }] : [{ equals }])). That moves { equals } to the false case, but also duplicate its in the true case. I presume we just need to ts.factory.create*(...) for the true case and can't update the existing one because we need it for the false case?
There was a problem hiding this comment.
Tested over the weekend with ng new and the compiler cli code with the transform. Source maps are working as expected even with the transformation applied. I think this is working as expected now
252d2cd to
5f59f91
Compare
dc33c27 to
9c8ba7d
Compare
9c8ba7d to
9d25903
Compare
|
I ran a presubmit and got some duplicate message errors. Debugging further, I see a problem with how we're generating this code. ContextInternally, i18n messages outside of Angular templates are defined via Closure's /** @desc Say hello. */
const MSG_HELLO = goog.getMsg('Hello, Alex!');
console.log(MSG_HELLO); // 'Hello, Alex!' (or localized equivalent)It's somewhat reasonable to parameterize such messages in a const name = signal('Alex');
const hello = computed(() => {
/** @desc Say hello. */
const MSG_HELLO = goog.getMsg('Hello, {$name}', {name: name()});
return MSG_HELLO;
});BugRunning the transform on this, the body of the computed gets duplicated like so: const name = signal(...(ngDevMode ? ['Alex', {debugName: 'name'}] : ['Alex']));
const hello = computed(...(ngDevMode ? [
() => {
/** @desc Say hello. */
const MSG_HELLO = goog.getMsg('Hello, {$name}', {name: name()});
return MSG_HELLO;
},
{debugName: 'hello'},
] : [
() => {
/** @desc Say hello. */
const MSG_HELLO = goog.getMsg('Hello, {$name}', {name: name()});
return MSG_HELLO;
},
]);Note that we now have two We could ask Closure Compiler to be a little more lenient here, as the two messages are literally identical, it could probably make this work. However I do expect that erroring on duplicate messages is a generally reasonable thing for the compiler to do and I wouldn't want to turn that off just because of this case. Looking at this code it is also clear that we're duplicating a significant amount of code in the I remember we had some discussion about how safe it was to duplicate the expression in a signal and whether that might produce duplicated side effects. I think we ultimately decided it didn't because the ternary short circuits and only the true branch gets evaluated at runtime. However I think this issue makes it clear that duplicating the code at all, even if never executed, is still a potential issue. Solution 1I think the path forward here is to avoid duplicating the expression by putting it in a variable used in both branches of the const name = signal(...(ngDevMode ? ['Alex', {debugName: 'name'}] : ['Alex']));
const hello = computed(...(
(msg) => ngDevMode
? [msg, {debugName: 'hello'}]
: [msg]
)(() => {
/** @desc Say hello. */
const MSG_HELLO = goog.getMsg('Hello, {$name}', {name: name()});
return MSG_HELLO;
});This keeps a single definition of the function and only one The problem with this is that it might be just a little too complicated an expression for bundlers. Testing this out I see: // Ideally:
computed(() => "Hello, Alex!");
// Closure Compiler:
computed(() => "Hello, Alex!");
// Angular CLI: esbuild + terser
computed(...(t=>[t])(()=>"Hello, Alex!"));So Closure does seem smart enough to make this work, but esbuild and Terser are coming up a little short. I suspect the problem is that esbuild/Terser don't want to inline Solution 2const hello = (() => {
const x = () => {
/** @desc Say hello. */
const MSG_HELLO = goog.getMsg('Hello, Alex!');
return MSG_HELLO;
};
return computed(...(ngDevMode ? [x, {debugName: 'hello'}] : [x]);
})();This outputs: // Closure Compiler:
(() => computed(() => "Hello world"))();
// Angular CLI: esbuild + Terser
computed(()=>"Hello, World!");So now we've got an extra IIFE in Closure which is failing to be inlined. It's a little better than this doesn't have a spread operator in the I'm not sure why Closure Compiler can't inline that IIFE, especially since it inlined a much more complicated IIFE in my previous attempt. I'll ask around and see if we can find a pattern which optimizes ideally for both sets of bundlers. |
|
Hmm, retrying the second solution I'm actually now seeing that it outputs If that's reliable then it seems like that transform would be valid path forward, I'll keep an eye out if I can replicate the extra IIFE I saw previously. |
17ba062 to
216e2c1
Compare
216e2c1 to
c86bb3b
Compare
|
Global presubmit is fully passing 🎉, so I think this output format is looking good. http://test/OCL:728286513:BASE:728422677:1739934312484:4bfbb98b |
43e0707 to
850cea2
Compare
There was a problem hiding this comment.
Question: @alxhub, do we care that this is using the type checker to resolve to the imported symbol?
I know we're trying to avoid the type checker for single-file transpilations, so I'm wondering if there's a better way or if this is ok because we're keeping it within the source file.
There was a problem hiding this comment.
Question: What exactly does it mean to have multiple declarations? Are these shadowed declarations like:
import { signal } from '@angular/core';
function foo() {
const signal = () => {};
console.log(signal); // Has two declarations?
}There was a problem hiding this comment.
No, multiple declarations would be something like:
interface Foo {
bar: string;
}
interface Foo {
baz: number;
}Multiple declarations really only applies to types. When looking for the declaration of a value, you can use symbol.valueDeclaration instead, since a value can only ever be defined once.
There was a problem hiding this comment.
Is it safe to just look at the first declaration? I guess if it is a real signal function then it will always have just one declaration from the import?
|
TGP still looking good: http://test/OCL:758440362:BASE:758744767:1747244383957:e4a093db |
…om signal functions Implements a compiler transform that attempts to statically analyze variable names and apply them to usages of signal functions like signal, computed, effect, etc.
|
This PR was merged into the repository by commit 3a9a70d. The changes were merged into the following branches: main |
|
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
Implements a compiler transform that attempts to statically analyze variable names and apply them to usages of signal functions like signal, computed, effect, etc as debug names.
Related: #57074
Depends on: #57073