@alloy-js/core 0.23.0-dev.17 → 0.23.0-dev.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/dev/src/devtools/devtools-server.browser.js +3 -0
  2. package/dist/dev/src/devtools/devtools-server.browser.js.map +1 -1
  3. package/dist/dev/src/devtools/devtools-server.js +15 -1
  4. package/dist/dev/src/devtools/devtools-server.js.map +1 -1
  5. package/dist/dev/src/reactivity.js +44 -34
  6. package/dist/dev/src/reactivity.js.map +1 -1
  7. package/dist/dev/src/symbols/output-symbol.js +77 -7
  8. package/dist/dev/src/symbols/output-symbol.js.map +1 -1
  9. package/dist/dev/src/symbols/symbol-table.js +13 -3
  10. package/dist/dev/src/symbols/symbol-table.js.map +1 -1
  11. package/dist/dev/test/reactivity/shallow-reactive.test.js +4 -0
  12. package/dist/dev/test/reactivity/shallow-reactive.test.js.map +1 -1
  13. package/dist/dev/test/symbols/deconflicted-name.test.js +120 -0
  14. package/dist/dev/test/symbols/deconflicted-name.test.js.map +1 -0
  15. package/dist/dev/test/symbols/output-scope.test.js +41 -0
  16. package/dist/dev/test/symbols/output-scope.test.js.map +1 -1
  17. package/dist/src/devtools/devtools-server.browser.d.ts +1 -0
  18. package/dist/src/devtools/devtools-server.browser.d.ts.map +1 -1
  19. package/dist/src/devtools/devtools-server.browser.js +3 -0
  20. package/dist/src/devtools/devtools-server.browser.js.map +1 -1
  21. package/dist/src/devtools/devtools-server.d.ts +5 -0
  22. package/dist/src/devtools/devtools-server.d.ts.map +1 -1
  23. package/dist/src/devtools/devtools-server.js +15 -1
  24. package/dist/src/devtools/devtools-server.js.map +1 -1
  25. package/dist/src/reactivity.d.ts.map +1 -1
  26. package/dist/src/reactivity.js +44 -34
  27. package/dist/src/reactivity.js.map +1 -1
  28. package/dist/src/symbols/output-symbol.d.ts +35 -0
  29. package/dist/src/symbols/output-symbol.d.ts.map +1 -1
  30. package/dist/src/symbols/output-symbol.js +77 -7
  31. package/dist/src/symbols/output-symbol.js.map +1 -1
  32. package/dist/src/symbols/symbol-table.d.ts.map +1 -1
  33. package/dist/src/symbols/symbol-table.js +13 -3
  34. package/dist/src/symbols/symbol-table.js.map +1 -1
  35. package/dist/test/reactivity/shallow-reactive.test.js +4 -0
  36. package/dist/test/reactivity/shallow-reactive.test.js.map +1 -1
  37. package/dist/test/symbols/deconflicted-name.test.d.ts +2 -0
  38. package/dist/test/symbols/deconflicted-name.test.d.ts.map +1 -0
  39. package/dist/test/symbols/deconflicted-name.test.js +120 -0
  40. package/dist/test/symbols/deconflicted-name.test.js.map +1 -0
  41. package/dist/test/symbols/output-scope.test.js +41 -0
  42. package/dist/test/symbols/output-scope.test.js.map +1 -1
  43. package/dist/tsconfig.tsbuildinfo +1 -1
  44. package/docs/api/types/OutputSymbol.md +42 -40
  45. package/package.json +1 -1
  46. package/src/devtools/devtools-server.browser.ts +4 -0
  47. package/src/devtools/devtools-server.ts +16 -1
  48. package/src/reactivity.ts +53 -40
  49. package/src/symbols/output-symbol.ts +92 -10
  50. package/src/symbols/symbol-table.ts +13 -3
  51. package/temp/api.json +61 -1
  52. package/test/reactivity/shallow-reactive.test.tsx +4 -0
  53. package/test/symbols/deconflicted-name.test.ts +120 -0
  54. package/test/symbols/output-scope.test.ts +38 -0
@@ -4,46 +4,48 @@ An output symbol is a named entity that can be referenced in your output code.
4
4
 
5
5
  ## Members
6
6
 
7
- | | | |
8
- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
- | \_\_v\_skip | boolean | |
10
- | \[inspect.custom] | () => string | |
11
- | constructor | (name: string \| Namekey, spaces: OutputSpace\[] \| OutputSpace \| undefined, options: OutputSymbolOptions) | Constructs a new instance of the [`OutputSymbol`](OutputSymbol.md) class |
12
- | aliasTarget | [OutputSymbol](../outputsymbol/) \| undefined | The symbol that this symbol is an alias for. |
13
- | binder | [Binder](../binder/) \| undefined | The binder that is tracking this symbol. |
14
- | copy | () => OutputSymbol | Create a clone of this symbol whose name and flags reactively track the original. |
15
- | copyMembersTo | (targetSymbol: OutputSymbol) => void | Copy the members of this symbol to the target symbol. This is reactive - whenever a member is added to this symbol, it will be copied to the target symbol. |
16
- | copyToSpace | (space: OutputSpace) => OutputSymbol | Copy this symbol into the given space. Calls \[unresolved link] and places the result in `space`, then returns the copy. |
17
- | dealias | () => OutputSymbol | If this symbol is an alias for another symbol, return the the aliased symbol. Otherwise, return this symbol. |
18
- | debugInfo | Record\<string, unknown> | |
19
- | delete | () => void | |
20
- | protected getCopyOptions | () => { binder: Binder \| undefined; aliasTarget: OutputSymbol \| undefined; metadata: Record\<string, unknown>; transient: boolean; } | |
21
- | hasTypeSymbol | boolean | Whether this symbol has its symbol representing its type available. |
22
- | id | number | The unique id of this symbol. |
23
- | ignoreNameConflict | boolean | Whether the name of this symbol bypasses the active name conflict resolution. When true, the name of this symbol will be fixed, though it may conflict with other symbols which are also ignoring name conflict resolution. |
24
- | ignoreNamePolicy | boolean | Whether the name of this symbol bypasses the active name policy. When true, the name of this symbol will be fixed, though it may conflict with other symbols which are also ignoring the name policy. |
25
- | protected initializeCopy | (copy: OutputSymbol) => void | Wires up reactive member-space copying and name tracking from this symbol to its `copy`. |
26
- | isAlias | boolean | Whether this symbol is an alias for another symbol. |
27
- | isMemberSymbol | boolean | Whether this symbol is a member of another symbol. |
28
- | isMoved | boolean | Whether this symbol’s members have been moved to another symbol. |
29
- | isTransient | boolean | Whether this symbol is a transient symbol. Transient symbols cannot be referenced and are meant to be combined with other symbols. |
30
- | isTyped | boolean | Whether this symbol’s members are provided by a type symbol. The `typeSymbol` property is this symbol. It may not be available yet, so check `hasTypeSymbol`. |
31
- | memberSpaceFor | (spaceKey: string) => OutputMemberSpace \| undefined | Get the member space for the given key. |
32
- | memberSpaces | [OutputMemberSpace](../outputmemberspace/)\[] | The member spaces of this symbol. |
33
- | memberSpaces | Readonly\<string\[]> | The member space keys for this symbol type. Subclasses override this to declare which member spaces are created on construction (e.g., `["static", "instance"]`). |
34
- | metadata | Record\<string, unknown> | An arbitrary bag of metadata for this symbol. This property is read only, but the metadata is a reactive object. |
35
- | movedTo | [OutputSymbol](../outputsymbol/) \| undefined | The symbol that this symbol’s members have been moved to. |
36
- | moveMembersTo | (targetSymbol: OutputSymbol) => void | Move member symbols from this transient symbol to the target symbol. This is reactive - whenever a member is added to this symbol, it will be moved to the target symbol. |
37
- | name | string | The name of this symbol. Assigning to this property applies the active name policy (unless `ignoreNamePolicy` is true) before storing the value. |
38
- | namePolicy | [NamePolicyGetter](../namepolicygetter/) \| undefined | |
39
- | originalName | string | Read only. The requested name of this symbol. The symbol’s actual name may be different depending on naming policy or conflicts with other symbols. |
40
- | ownerSymbol | [OutputSymbol](../outputsymbol/) \| undefined | When this is a member symbol, this returns the symbol that this is symbol is a member of. |
41
- | refkeys | [Refkey](../refkey/)\[] | The refkeys for this symbol. |
42
- | resolveMemberByName | (name: string) => OutputSymbol \| undefined | Get a member symbol by name from this symbol’s member spaces. Checks member spaces in order until it finds a member with that name. |
43
- | scope | import(”./output-scope.js”).[OutputScope](../outputscope/) \| undefined | The scope this symbol is in. When this symbol is a member symbol, this will return undefined. |
44
- | spaces | [OutputSpace](../outputspace/)\[] | The declaration or member spaces this symbol belongs to. |
45
- | toString | () => string | |
46
- | type | [OutputSymbol](../outputsymbol/) \| undefined | The symbol which defines the type of this symbol. The type symbol provides information about the value this symbol contains, such as what members it has. |
7
+ | | | |
8
+ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | \_\_v\_skip | boolean | |
10
+ | \[inspect.custom] | () => string | |
11
+ | constructor | (name: string \| Namekey, spaces: OutputSpace\[] \| OutputSpace \| undefined, options: OutputSymbolOptions) | Constructs a new instance of the [`OutputSymbol`](OutputSymbol.md) class |
12
+ | aliasTarget | [OutputSymbol](../outputsymbol/) \| undefined | The symbol that this symbol is an alias for. |
13
+ | binder | [Binder](../binder/) \| undefined | The binder that is tracking this symbol. |
14
+ | canonicalName | string | The canonical requested name for this symbol: the result of applying the symbol’s name policy to its originalName, or the original name itself when no policy applies. This is the name the symbol would carry if there were no conflicts, and is stable across the symbol’s lifetime (it depends only on the immutable `originalName` and the name policy). Used by [SymbolTable](../symboltable/) as the grouping key for name-conflict resolution, so that symbols whose original names normalize to the same policy-applied name (e.g. `foo_bar` and `fooBar` under camelCase) are recognized as conflicting. |
15
+ | copy | () => OutputSymbol | Create a clone of this symbol whose name and flags reactively track the original. |
16
+ | copyMembersTo | (targetSymbol: OutputSymbol) => void | Copy the members of this symbol to the target symbol. This is reactive - whenever a member is added to this symbol, it will be copied to the target symbol. |
17
+ | copyToSpace | (space: OutputSpace) => OutputSymbol | Copy this symbol into the given space. Calls \[unresolved link] and places the result in `space`, then returns the copy. |
18
+ | dealias | () => OutputSymbol | If this symbol is an alias for another symbol, return the the aliased symbol. Otherwise, return this symbol. |
19
+ | debugInfo | Record\<string, unknown> | |
20
+ | deconflictedName | string \| undefined | The name assigned by a name-conflict resolver, or `undefined` when the symbol is not currently renamed by conflict resolution. Resolvers should assign to this slot (rather than `name`) to record that a rename exists only because of a conflict. On re-deconfliction (e.g. after a conflicting symbol is removed), resolvers clear this slot by assigning `undefined`; the effective name then falls back to the user-assigned name, which in turn falls back to the original name. Name policy is applied to values written here (unless `ignoreNamePolicy` is true), matching `name`‘s behavior. |
21
+ | delete | () => void | |
22
+ | protected getCopyOptions | () => { binder: Binder \| undefined; aliasTarget: OutputSymbol \| undefined; metadata: Record\<string, unknown>; transient: boolean; } | |
23
+ | hasTypeSymbol | boolean | Whether this symbol has its symbol representing its type available. |
24
+ | id | number | The unique id of this symbol. |
25
+ | ignoreNameConflict | boolean | Whether the name of this symbol bypasses the active name conflict resolution. When true, the name of this symbol will be fixed, though it may conflict with other symbols which are also ignoring name conflict resolution. |
26
+ | ignoreNamePolicy | boolean | Whether the name of this symbol bypasses the active name policy. When true, the name of this symbol will be fixed, though it may conflict with other symbols which are also ignoring the name policy. |
27
+ | protected initializeCopy | (copy: OutputSymbol) => void | Wires up reactive member-space copying and name tracking from this symbol to its `copy`. |
28
+ | isAlias | boolean | Whether this symbol is an alias for another symbol. |
29
+ | isMemberSymbol | boolean | Whether this symbol is a member of another symbol. |
30
+ | isMoved | boolean | Whether this symbol’s members have been moved to another symbol. |
31
+ | isTransient | boolean | Whether this symbol is a transient symbol. Transient symbols cannot be referenced and are meant to be combined with other symbols. |
32
+ | isTyped | boolean | Whether this symbol’s members are provided by a type symbol. The `typeSymbol` property is this symbol. It may not be available yet, so check `hasTypeSymbol`. |
33
+ | memberSpaceFor | (spaceKey: string) => OutputMemberSpace \| undefined | Get the member space for the given key. |
34
+ | memberSpaces | [OutputMemberSpace](../outputmemberspace/)\[] | The member spaces of this symbol. |
35
+ | memberSpaces | Readonly\<string\[]> | The member space keys for this symbol type. Subclasses override this to declare which member spaces are created on construction (e.g., `["static", "instance"]`). |
36
+ | metadata | Record\<string, unknown> | An arbitrary bag of metadata for this symbol. This property is read only, but the metadata is a reactive object. |
37
+ | movedTo | [OutputSymbol](../outputsymbol/) \| undefined | The symbol that this symbol’s members have been moved to. |
38
+ | moveMembersTo | (targetSymbol: OutputSymbol) => void | Move member symbols from this transient symbol to the target symbol. This is reactive - whenever a member is added to this symbol, it will be moved to the target symbol. |
39
+ | name | string | The name of this symbol. Assigning to this property applies the active name policy (unless `ignoreNamePolicy` is true) before storing the value. The effective name is computed as `deconflictedName ?? userName`, so if a name-conflict resolver has assigned a deconflictedName, that value is returned here; otherwise the value most recently assigned to `name` is returned. |
40
+ | namePolicy | [NamePolicyGetter](../namepolicygetter/) \| undefined | |
41
+ | originalName | string | Read only. The requested name of this symbol. The symbol’s actual name may be different depending on naming policy or conflicts with other symbols. |
42
+ | ownerSymbol | [OutputSymbol](../outputsymbol/) \| undefined | When this is a member symbol, this returns the symbol that this is symbol is a member of. |
43
+ | refkeys | [Refkey](../refkey/)\[] | The refkeys for this symbol. |
44
+ | resolveMemberByName | (name: string) => OutputSymbol \| undefined | Get a member symbol by name from this symbol’s member spaces. Checks member spaces in order until it finds a member with that name. |
45
+ | scope | import(”./output-scope.js”).[OutputScope](../outputscope/) \| undefined | The scope this symbol is in. When this symbol is a member symbol, this will return undefined. |
46
+ | spaces | [OutputSpace](../outputspace/)\[] | The declaration or member spaces this symbol belongs to. |
47
+ | toString | () => string | |
48
+ | type | [OutputSymbol](../outputsymbol/) \| undefined | The symbol which defines the type of this symbol. The type symbol provides information about the value this symbol contains, such as what members it has. |
47
49
 
48
50
  ## Remarks
49
51
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alloy-js/core",
3
- "version": "0.23.0-dev.17",
3
+ "version": "0.23.0-dev.19",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -60,3 +60,7 @@ export async function enableDevtoolsAndConnect(
60
60
  export async function resetDevtoolsServerForTests(): Promise<void> {
61
61
  // No-op in browser
62
62
  }
63
+
64
+ export function refreshDebugState(): void {
65
+ // No-op in browser
66
+ }
@@ -153,6 +153,19 @@ function isNodeEnvironment() {
153
153
  );
154
154
  }
155
155
 
156
+ // Cached once at module load. Stable for production runs. Tests that modify
157
+ // process.env.ALLOY_DEBUG dynamically must call refreshDebugState() afterward.
158
+ let _envDebugEnabled: boolean =
159
+ isNodeEnvironment() && Boolean(process.env.ALLOY_DEBUG);
160
+
161
+ /**
162
+ * Invalidates the cached env-var result for isDevtoolsEnabled(). Call this in
163
+ * test beforeEach hooks after modifying process.env.ALLOY_DEBUG.
164
+ */
165
+ export function refreshDebugState(): void {
166
+ _envDebugEnabled = isNodeEnvironment() && Boolean(process.env.ALLOY_DEBUG);
167
+ }
168
+
156
169
  function getCwd() {
157
170
  if (!isNodeEnvironment()) return undefined;
158
171
  try {
@@ -177,7 +190,7 @@ function resolveDebugPort() {
177
190
  /** Returns true when devtools are enabled (via env var or explicit call). */
178
191
  export function isDevtoolsEnabled() {
179
192
  if (!isNodeEnvironment()) return false;
180
- return devtoolsExplicitlyEnabled || Boolean(process.env.ALLOY_DEBUG);
193
+ return devtoolsExplicitlyEnabled || _envDebugEnabled;
181
194
  }
182
195
 
183
196
  /** Returns true when a devtools client is currently connected. */
@@ -463,6 +476,8 @@ export async function resetDevtoolsServerForTests() {
463
476
  loggedDevtoolsLinks = false;
464
477
  subscribedPromise = null;
465
478
  resolveSubscribed = null;
479
+ // Re-read the env var in case tests modified process.env.ALLOY_DEBUG
480
+ refreshDebugState();
466
481
  // Close the trace DB so each test starts fresh
467
482
  closeTrace();
468
483
  }
package/src/reactivity.ts CHANGED
@@ -255,13 +255,16 @@ export function effect<T>(
255
255
  };
256
256
 
257
257
  const debugInfo = options?.debug;
258
- const effectId = debug.effect.register({
259
- name: debugInfo?.name ?? fn.name,
260
- type: debugInfo?.type,
261
- createdAt: captureSourceLocation(),
262
- contextId: context.id,
263
- ownerContextId: resolveOwnerEffectContextId(context),
264
- });
258
+ const effectId =
259
+ isDebugEnabled() ?
260
+ debug.effect.register({
261
+ name: debugInfo?.name ?? fn.name,
262
+ type: debugInfo?.type,
263
+ createdAt: captureSourceLocation(),
264
+ contextId: context.id,
265
+ ownerContextId: resolveOwnerEffectContextId(context),
266
+ })
267
+ : -1;
265
268
 
266
269
  if (effectId !== -1) {
267
270
  context.meta ??= {};
@@ -458,13 +461,15 @@ export function ref<T>(
458
461
  options?: { isInfrastructure?: boolean },
459
462
  ): Ref<T> {
460
463
  const result = vueRef(value) as Ref<T>;
461
- debug.effect.registerRef({
462
- id: refId(result),
463
- kind: "ref",
464
- createdAt: captureSourceLocation(),
465
- createdByEffectId: globalContext?.meta?.effectId,
466
- isInfrastructure: options?.isInfrastructure,
467
- });
464
+ if (isDebugEnabled()) {
465
+ debug.effect.registerRef({
466
+ id: refId(result),
467
+ kind: "ref",
468
+ createdAt: captureSourceLocation(),
469
+ createdByEffectId: globalContext?.meta?.effectId,
470
+ isInfrastructure: options?.isInfrastructure,
471
+ });
472
+ }
468
473
  return result;
469
474
  }
470
475
 
@@ -492,24 +497,28 @@ export function shallowReactive<T extends object>(
492
497
 
493
498
  export function shallowRef<T>(value?: T, options?: { label?: string }): Ref<T> {
494
499
  const result = vueShallowRef(value) as Ref<T>;
495
- debug.effect.registerRef({
496
- id: refId(result),
497
- kind: "shallowRef",
498
- label: options?.label,
499
- createdAt: captureSourceLocation(),
500
- createdByEffectId: globalContext?.meta?.effectId,
501
- });
500
+ if (isDebugEnabled()) {
501
+ debug.effect.registerRef({
502
+ id: refId(result),
503
+ kind: "shallowRef",
504
+ label: options?.label,
505
+ createdAt: captureSourceLocation(),
506
+ createdByEffectId: globalContext?.meta?.effectId,
507
+ });
508
+ }
502
509
  return result;
503
510
  }
504
511
 
505
512
  export function computed<T>(getter: () => T): Ref<T> {
506
513
  const result = vueComputed(getter) as Ref<T>;
507
- debug.effect.registerRef({
508
- id: refId(result),
509
- kind: "computed",
510
- createdAt: captureSourceLocation(),
511
- createdByEffectId: globalContext?.meta?.effectId,
512
- });
514
+ if (isDebugEnabled()) {
515
+ debug.effect.registerRef({
516
+ id: refId(result),
517
+ kind: "computed",
518
+ createdAt: captureSourceLocation(),
519
+ createdByEffectId: globalContext?.meta?.effectId,
520
+ });
521
+ }
513
522
  return result;
514
523
  }
515
524
 
@@ -522,12 +531,14 @@ export function toRef<T extends object, K extends keyof T>(
522
531
  defaultValue === undefined ?
523
532
  (vueToRef(object, key) as Ref<T[K]>)
524
533
  : (vueToRef(object, key, defaultValue) as Ref<T[K]>);
525
- debug.effect.registerRef({
526
- id: refId(result),
527
- kind: "toRef",
528
- createdAt: captureSourceLocation(),
529
- createdByEffectId: globalContext?.meta?.effectId,
530
- });
534
+ if (isDebugEnabled()) {
535
+ debug.effect.registerRef({
536
+ id: refId(result),
537
+ kind: "toRef",
538
+ createdAt: captureSourceLocation(),
539
+ createdByEffectId: globalContext?.meta?.effectId,
540
+ });
541
+ }
531
542
  return result;
532
543
  }
533
544
 
@@ -535,13 +546,15 @@ export function toRefs<T extends object>(
535
546
  object: T,
536
547
  ): { [K in keyof T]: Ref<T[K]> } {
537
548
  const result = vueToRefs(object) as { [K in keyof T]: Ref<T[K]> };
538
- for (const refValue of Object.values(result) as Ref<unknown>[]) {
539
- debug.effect.registerRef({
540
- id: refId(refValue),
541
- kind: "toRef",
542
- createdAt: captureSourceLocation(),
543
- createdByEffectId: globalContext?.meta?.effectId,
544
- });
549
+ if (isDebugEnabled()) {
550
+ for (const refValue of Object.values(result) as Ref<unknown>[]) {
551
+ debug.effect.registerRef({
552
+ id: refId(refValue),
553
+ kind: "toRef",
554
+ createdAt: captureSourceLocation(),
555
+ createdByEffectId: globalContext?.meta?.effectId,
556
+ });
557
+ }
545
558
  }
546
559
  return result;
547
560
  }
@@ -178,31 +178,113 @@ export abstract class OutputSymbol {
178
178
  return this.#originalName;
179
179
  }
180
180
 
181
- // this field is set by calling the name accessor.
182
- #name!: string;
181
+ // The user-assigned name (as set by constructor or direct `.name =`
182
+ // assignments). Always defined after construction.
183
+ #userName!: string;
184
+
185
+ // The name assigned by a name-conflict resolver, if any. When present, this
186
+ // takes precedence over `#userName` in the computed `name` getter. Resolvers
187
+ // assign via the `deconflictedName` setter; clearing (setting to undefined)
188
+ // causes the symbol to fall back to its user-assigned name.
189
+ #deconflictedName: string | undefined;
190
+
183
191
  /**
184
192
  * The name of this symbol. Assigning to this property applies the active
185
193
  * name policy (unless `ignoreNamePolicy` is true) before storing the value.
186
194
  *
195
+ * The effective name is computed as `deconflictedName ?? userName`, so if a
196
+ * name-conflict resolver has assigned a {@link OutputSymbol.deconflictedName | deconflictedName}, that value
197
+ * is returned here; otherwise the value most recently assigned to `name` is
198
+ * returned.
199
+ *
187
200
  * @reactive
188
201
  */
189
202
  get name() {
190
203
  track(this, TrackOpTypes.GET, "name");
191
- return this.#name;
204
+ return this.#deconflictedName ?? this.#userName;
192
205
  }
193
206
 
194
207
  set name(name: string) {
195
- const old = this.#name;
208
+ const policyApplied =
209
+ this.#namePolicy && !this.#ignoreNamePolicy ?
210
+ this.#namePolicy(name)
211
+ : name;
196
212
 
197
- if (old === name) {
213
+ if (this.#userName === policyApplied) {
198
214
  return;
199
215
  }
200
216
 
201
- this.#name =
202
- this.#namePolicy && !this.#ignoreNamePolicy ?
203
- this.#namePolicy(name)
204
- : name;
205
- trigger(this, TriggerOpTypes.SET, "name", name, old);
217
+ const old = this.#deconflictedName ?? this.#userName;
218
+ this.#userName = policyApplied;
219
+ const next = this.#deconflictedName ?? this.#userName;
220
+ if (next !== old) {
221
+ trigger(this, TriggerOpTypes.SET, "name", next, old);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * The name assigned by a name-conflict resolver, or `undefined` when the
227
+ * symbol is not currently renamed by conflict resolution.
228
+ *
229
+ * Resolvers should assign to this slot (rather than `name`) to record that a
230
+ * rename exists only because of a conflict. On re-deconfliction (e.g. after
231
+ * a conflicting symbol is removed), resolvers clear this slot by assigning
232
+ * `undefined`; the effective {@link OutputSymbol.name | name} then falls back to the
233
+ * user-assigned name, which in turn falls back to the original name.
234
+ *
235
+ * Name policy is applied to values written here (unless `ignoreNamePolicy`
236
+ * is true), matching `name`'s behavior.
237
+ *
238
+ * @reactive
239
+ */
240
+ get deconflictedName(): string | undefined {
241
+ track(this, TrackOpTypes.GET, "deconflictedName");
242
+ return this.#deconflictedName;
243
+ }
244
+
245
+ set deconflictedName(value: string | undefined) {
246
+ const policyApplied =
247
+ value !== undefined && this.#namePolicy && !this.#ignoreNamePolicy ?
248
+ this.#namePolicy(value)
249
+ : value;
250
+
251
+ if (this.#deconflictedName === policyApplied) {
252
+ return;
253
+ }
254
+
255
+ const oldName = this.#deconflictedName ?? this.#userName;
256
+ const oldDeconflicted = this.#deconflictedName;
257
+ this.#deconflictedName = policyApplied;
258
+ trigger(
259
+ this,
260
+ TriggerOpTypes.SET,
261
+ "deconflictedName",
262
+ policyApplied,
263
+ oldDeconflicted,
264
+ );
265
+ const nextName = this.#deconflictedName ?? this.#userName;
266
+ if (nextName !== oldName) {
267
+ trigger(this, TriggerOpTypes.SET, "name", nextName, oldName);
268
+ }
269
+ }
270
+
271
+ /**
272
+ * The canonical requested name for this symbol: the result of applying the
273
+ * symbol's name policy to its {@link OutputSymbol.originalName | originalName}, or the original name
274
+ * itself when no policy applies. This is the name the symbol would carry if
275
+ * there were no conflicts, and is stable across the symbol's lifetime (it
276
+ * depends only on the immutable `originalName` and the name policy).
277
+ *
278
+ * Used by {@link SymbolTable} as the grouping key for name-conflict
279
+ * resolution, so that symbols whose original names normalize to the same
280
+ * policy-applied name (e.g. `foo_bar` and `fooBar` under camelCase) are
281
+ * recognized as conflicting.
282
+ */
283
+ get canonicalName(): string {
284
+ if (this.#ignoreNamePolicy || !this.#namePolicy) {
285
+ return this.originalName;
286
+ }
287
+ return this.#namePolicy(this.originalName);
206
288
  }
207
289
 
208
290
  #id: number;
@@ -13,7 +13,7 @@ export abstract class SymbolTable extends ReactiveUnionSet<OutputSymbol> {
13
13
  #deconflictNames = () => {
14
14
  for (const name of this.#namesToDeconflict) {
15
15
  const conflictedSymbols = [...this].filter(
16
- (sym) => sym.originalName === name && !sym.ignoreNameConflict,
16
+ (sym) => sym.canonicalName === name && !sym.ignoreNameConflict,
17
17
  );
18
18
  if (this.#nameConflictResolver) {
19
19
  this.#nameConflictResolver(name, conflictedSymbols);
@@ -67,7 +67,7 @@ export abstract class SymbolTable extends ReactiveUnionSet<OutputSymbol> {
67
67
  `${formatSymbolName(symbol)} added to ${formatSymbolTableName(this)}`,
68
68
  );
69
69
 
70
- this.#namesToDeconflict.add(symbol.name);
70
+ this.#namesToDeconflict.add(symbol.canonicalName);
71
71
 
72
72
  queueJob(this.#deconflictNames);
73
73
 
@@ -79,6 +79,12 @@ export abstract class SymbolTable extends ReactiveUnionSet<OutputSymbol> {
79
79
  () =>
80
80
  `${formatSymbolName(symbol)} removed from ${formatSymbolTableName(this)}`,
81
81
  );
82
+ // Re-run conflict resolution for the deleted symbol's canonical name
83
+ // so survivors in the same cohort (those that share the canonical
84
+ // name) can clear any prior `deconflictedName` rename now that the
85
+ // collision is reduced.
86
+ this.#namesToDeconflict.add(symbol.canonicalName);
87
+ queueJob(this.#deconflictNames);
82
88
  },
83
89
  });
84
90
 
@@ -135,8 +141,12 @@ export abstract class SymbolTable extends ReactiveUnionSet<OutputSymbol> {
135
141
  * to have a suffix of _2, _3, etc.
136
142
  */
137
143
  function defaultConflictHandler(_: string, conflictedSymbols: OutputSymbol[]) {
144
+ if (conflictedSymbols.length === 0) return;
145
+ // The first symbol keeps its original name; clear any prior deconflict
146
+ // rename so it reverts after a collision is resolved.
147
+ conflictedSymbols[0].deconflictedName = undefined;
138
148
  for (let i = 1; i < conflictedSymbols.length; i++) {
139
- conflictedSymbols[i].name =
149
+ conflictedSymbols[i].deconflictedName =
140
150
  conflictedSymbols[i].originalName + "_" + (i + 1);
141
151
  }
142
152
  }
package/temp/api.json CHANGED
@@ -17645,6 +17645,36 @@
17645
17645
  "isProtected": false,
17646
17646
  "isAbstract": false
17647
17647
  },
17648
+ {
17649
+ "kind": "Property",
17650
+ "canonicalReference": "@alloy-js/core!OutputSymbol#canonicalName:member",
17651
+ "docComment": "/**\n * The canonical requested name for this symbol: the result of applying the\n * symbol's name policy to its {@link OutputSymbol.originalName | originalName}, or the original name\n * itself when no policy applies. This is the name the symbol would carry if\n * there were no conflicts, and is stable across the symbol's lifetime (it\n * depends only on the immutable `originalName` and the name policy).\n *\n * Used by {@link SymbolTable} as the grouping key for name-conflict\n * resolution, so that symbols whose original names normalize to the same\n * policy-applied name (e.g. `foo_bar` and `fooBar` under camelCase) are\n * recognized as conflicting.\n */\n",
17652
+ "excerptTokens": [
17653
+ {
17654
+ "kind": "Content",
17655
+ "text": "get canonicalName(): "
17656
+ },
17657
+ {
17658
+ "kind": "Content",
17659
+ "text": "string"
17660
+ },
17661
+ {
17662
+ "kind": "Content",
17663
+ "text": ";"
17664
+ }
17665
+ ],
17666
+ "isReadonly": true,
17667
+ "isOptional": false,
17668
+ "releaseTag": "Public",
17669
+ "name": "canonicalName",
17670
+ "propertyTypeTokenRange": {
17671
+ "startIndex": 1,
17672
+ "endIndex": 2
17673
+ },
17674
+ "isStatic": false,
17675
+ "isProtected": false,
17676
+ "isAbstract": false
17677
+ },
17648
17678
  {
17649
17679
  "kind": "Method",
17650
17680
  "canonicalReference": "@alloy-js/core!OutputSymbol#copy:member(1)",
@@ -17843,6 +17873,36 @@
17843
17873
  "isProtected": false,
17844
17874
  "isAbstract": false
17845
17875
  },
17876
+ {
17877
+ "kind": "Property",
17878
+ "canonicalReference": "@alloy-js/core!OutputSymbol#deconflictedName:member",
17879
+ "docComment": "/**\n * The name assigned by a name-conflict resolver, or `undefined` when the\n * symbol is not currently renamed by conflict resolution.\n *\n * Resolvers should assign to this slot (rather than `name`) to record that a\n * rename exists only because of a conflict. On re-deconfliction (e.g. after\n * a conflicting symbol is removed), resolvers clear this slot by assigning\n * `undefined`; the effective {@link OutputSymbol.name | name} then falls back to the\n * user-assigned name, which in turn falls back to the original name.\n *\n * Name policy is applied to values written here (unless `ignoreNamePolicy`\n * is true), matching `name`'s behavior.\n *\n *\n * @reactive\n */\n",
17880
+ "excerptTokens": [
17881
+ {
17882
+ "kind": "Content",
17883
+ "text": "get deconflictedName(): "
17884
+ },
17885
+ {
17886
+ "kind": "Content",
17887
+ "text": "string | undefined"
17888
+ },
17889
+ {
17890
+ "kind": "Content",
17891
+ "text": ";\n\nset deconflictedName(value: string | undefined);"
17892
+ }
17893
+ ],
17894
+ "isReadonly": false,
17895
+ "isOptional": false,
17896
+ "releaseTag": "Public",
17897
+ "name": "deconflictedName",
17898
+ "propertyTypeTokenRange": {
17899
+ "startIndex": 1,
17900
+ "endIndex": 2
17901
+ },
17902
+ "isStatic": false,
17903
+ "isProtected": false,
17904
+ "isAbstract": false
17905
+ },
17846
17906
  {
17847
17907
  "kind": "Method",
17848
17908
  "canonicalReference": "@alloy-js/core!OutputSymbol#delete:member(1)",
@@ -18496,7 +18556,7 @@
18496
18556
  {
18497
18557
  "kind": "Property",
18498
18558
  "canonicalReference": "@alloy-js/core!OutputSymbol#name:member",
18499
- "docComment": "/**\n * The name of this symbol. Assigning to this property applies the active\n * name policy (unless `ignoreNamePolicy` is true) before storing the value.\n *\n *\n * @reactive\n */\n",
18559
+ "docComment": "/**\n * The name of this symbol. Assigning to this property applies the active\n * name policy (unless `ignoreNamePolicy` is true) before storing the value.\n *\n * The effective name is computed as `deconflictedName ?? userName`, so if a\n * name-conflict resolver has assigned a {@link OutputSymbol.deconflictedName | deconflictedName}, that value\n * is returned here; otherwise the value most recently assigned to `name` is\n * returned.\n *\n *\n * @reactive\n */\n",
18500
18560
  "excerptTokens": [
18501
18561
  {
18502
18562
  "kind": "Content",