ember-vite-hmr 2.4.0 → 2.7.1

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.
@@ -3,6 +3,13 @@ import Service from '@ember/service';
3
3
  import Component from '@glimmer/component';
4
4
  import { getInternalComponentManager } from '@glimmer/manager';
5
5
 
6
+ // Accessor for the HMR runtime helpers exposed on the global by
7
+ // virtual:ember-vite-hmr-runtime. Only set in dev/serve mode (guarded by
8
+ // `if (!import.meta.hot)` at the call site).
9
+
10
+ function hmrRuntime() {
11
+ return globalThis.__ember_vite_hmr;
12
+ }
6
13
  function findPropertyDescriptor(component, key) {
7
14
  let proto = component;
8
15
  while (proto) {
@@ -80,24 +87,18 @@ function getState(component, skip) {
80
87
  }
81
88
  return state;
82
89
  }
83
- function syncState(instance) {
84
- const args = instance.args;
85
- if (args.__hot__) {
86
- const oldInstance = args.__hot__.oldInstance;
87
- const state = instance.__get_hot_state__?.() || args.__hot__.getState?.();
88
- for (const k in state) {
89
- if (instance[k] instanceof Service) {
90
- continue;
91
- }
92
- instance[k] = state[k];
93
- }
94
90
 
95
- // Call _hmrAccepted hook if defined
96
- if (typeof instance._hmrAccepted === 'function') {
97
- instance._hmrAccepted(oldInstance);
91
+ // Apply a stashed old-instance state to a freshly created new instance.
92
+ // Also calls the _hmrAccepted lifecycle hook if defined on the new instance.
93
+ function applySyncState(instance, state, oldInstance) {
94
+ for (const k in state) {
95
+ if (instance[k] instanceof Service) {
96
+ continue;
98
97
  }
99
- args.__hot__.getState = () => getState(instance, ['args']);
100
- args.__hot__.oldInstance = instance;
98
+ instance[k] = state[k];
99
+ }
100
+ if (typeof instance._hmrAccepted === 'function') {
101
+ instance._hmrAccepted(oldInstance);
101
102
  }
102
103
  }
103
104
  function initialize() {
@@ -108,9 +109,53 @@ function initialize() {
108
109
  const proto = Object.getPrototypeOf(ComponentManager);
109
110
  const create = proto.create;
110
111
  proto.create = function (...args) {
111
- const instance = create.call(this, ...args);
112
- setTimeout(() => syncState(instance.component));
113
- return instance;
112
+ const bucket = create.call(this, ...args);
113
+ const component = bucket.component;
114
+
115
+ // Enqueue this instance in the runtime's per-class FIFO queue. The runtime
116
+ // only enqueues when isHmrClass(klass) is true (registered HMR class), and
117
+ // clears the queue at update() time so user-flow stale entries are never
118
+ // present when willDestroy fires during an HMR swap.
119
+ const klass = component.constructor;
120
+ const rt = hmrRuntime();
121
+ if (rt?.isHmrClass(klass)) {
122
+ rt.enqueueInstance(klass, component);
123
+ }
124
+ return bucket;
125
+ };
126
+
127
+ // Patch willDestroy on the Component prototype to detect HMR swaps and
128
+ // transfer state from the old instance to the freshly-created new one.
129
+ //
130
+ // Why willDestroy instead of registerDestructor: Glimmer creates the NEW
131
+ // component instance BEFORE calling willDestroy on the OLD one (willDestroy
132
+ // is scheduled in Ember's "actions" runloop queue, which runs after
133
+ // rendering). This ordering is exactly what we need: by willDestroy time,
134
+ // the new instance is already live in liveInstances.
135
+ const origWillDestroy = Component.prototype.willDestroy;
136
+ Component.prototype.willDestroy = function () {
137
+ const rt = hmrRuntime();
138
+ if (rt) {
139
+ const klass = this.constructor;
140
+ const newClass = rt.current(klass);
141
+ if (newClass !== klass) {
142
+ // The class was swapped via HMR — dequeue the next new instance for
143
+ // the replacement class (FIFO matches DOM/creation order) and
144
+ // schedule a state transfer onto it.
145
+ const newInstance = rt.dequeueInstance(newClass);
146
+ if (newInstance) {
147
+ const state = getState(this, ['args']);
148
+ const oldInstance = this;
149
+ setTimeout(() => applySyncState(newInstance, state, oldInstance));
150
+ }
151
+ } else if (rt.isHmrClass(klass)) {
152
+ // Normal destroy (navigation, timer, conditional) — remove this
153
+ // instance from the live queue so it doesn't accumulate as a stale
154
+ // entry that update() would otherwise have to clear at the next save.
155
+ rt.removeInstance(klass, this);
156
+ }
157
+ }
158
+ return origWillDestroy?.call(this);
114
159
  };
115
160
  const setupController = Route.prototype.setupController;
116
161
  const StateCache = {};
@@ -1 +1 @@
1
- {"version":3,"file":"setup-hmr-manager.js","sources":["../../src/instance-initializers/setup-hmr-manager.ts"],"sourcesContent":["import Route from '@ember/routing/route';\nimport Service from '@ember/service';\nimport Component from '@glimmer/component';\nimport { getInternalComponentManager } from '@glimmer/manager';\n\ninterface HotComponent extends Component<{ __hot__?: unknown }> {\n __get_hot_state__?: () => Record<string, unknown>;\n _hmrAccepted?: (oldInstance?: HotComponent) => void;\n [key: string]: unknown;\n}\n\ntype Mutable<T> = {\n -readonly [P in keyof T]: unknown;\n};\n\nfunction findPropertyDescriptor(\n component: HotComponent | Record<string, unknown>,\n key: string,\n) {\n let proto = component;\n while (proto) {\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (desc) return desc;\n proto = Object.getPrototypeOf(proto);\n }\n}\n\n// The private property key `ember-resources`' `wrapForPlainUsage()` uses on\n// the plain object it wraps in a Proxy for `resource()` (which reactiveweb's\n// `trackedTask` uses internally) - see\n// https://github.com/NullVoxPopuli/ember-resources/blob/main/ember-resources/src/plain/index.ts.\n// Carrying such a proxy's *reference* over onto a new HMR-swapped instance\n// (instead of leaving the new instance's own, correctly-parented resource in\n// place) is unsafe: the proxy's underlying helper cache is lazily created on\n// first property read and permanently bound, at that point, to whichever\n// component instance was passed as `context` when `resource()` was called -\n// for a carried-over proxy that's always the *old*, already-destroyed\n// instance, so the first read after the swap throws \"Attempted to associate\n// a destroyable child with an object that is already destroying or\n// destroyed\" - see issue #563.\n//\n// `in` is used (rather than reading a property, or `Object.keys`/\n// `Object.prototype.toString.call`) because it's the one reflective\n// operation `wrapForPlainUsage`'s Proxy doesn't trap (no `has` handler), so\n// it forwards to a plain `[[HasProperty]]` on the underlying target without\n// ever invoking the lazy getter - it can't accidentally trigger the same\n// crash it's checking for. If `ember-resources` ever renames this key, this\n// check simply stops matching and behavior falls back to today's (already\n// broken, pre-existing) state - it does not introduce a new failure mode.\nconst RESOURCE_INTERMEDIATE_VALUE_KEY = '__Intermediate_Value__';\n\nfunction isUnsafeToCarryOver(value: unknown): boolean {\n return (\n (typeof value === 'object' || typeof value === 'function') &&\n value !== null &&\n RESOURCE_INTERMEDIATE_VALUE_KEY in value\n );\n}\n\nfunction getState(component: HotComponent, skip: string[]) {\n const state: Record<string, unknown> = {};\n if (!component) {\n return state;\n }\n for (const key in component) {\n if (skip.includes(key)) continue;\n const entry = findPropertyDescriptor(component, key);\n const value = component[key as keyof Component];\n \n // Skip Service instances - they should not be synced\n if (value instanceof Service) {\n continue;\n }\n \n // Skip Function properties - they should not be synced\n if (typeof value === 'function') {\n continue;\n }\n\n // Skip resource()-backed proxies - see isUnsafeToCarryOver above.\n if (isUnsafeToCarryOver(value)) {\n continue;\n }\n\n if (entry) {\n // Note: don't probe `entry.value` any further here (e.g. via\n // `Object.prototype.toString.call`) to decide whether it's a plain\n // function - the `typeof value === 'function'` check above already\n // covers that, and a stricter check on `entry.value` (which may be a\n // resource proxy, e.g. reactiveweb's `trackedTask`) can trigger that\n // proxy's `get` trap on first touch, lazily invoking a helper against\n // this component instance - see issue #557.\n if (entry.writable) {\n state[key] = value;\n }\n if (entry.set) {\n state[key] = value;\n }\n }\n }\n return state;\n}\n\nfunction syncState(instance: Mutable<HotComponent>) {\n const args = instance.args as { __hot__?: { getState?: () => Record<string, unknown>; oldInstance?: HotComponent } };\n if (args.__hot__) {\n const oldInstance = args.__hot__.oldInstance;\n const state =\n (instance.__get_hot_state__ as (() => Record<string, unknown>) | undefined)?.() || args.__hot__.getState?.();\n for (const k in state) {\n if (instance[k as keyof HotComponent] instanceof Service) {\n continue;\n }\n instance[k as keyof HotComponent] = state[k];\n }\n \n // Call _hmrAccepted hook if defined\n if (typeof instance._hmrAccepted === 'function') {\n instance._hmrAccepted(oldInstance);\n }\n \n args.__hot__.getState = () => getState(instance as HotComponent, ['args']);\n args.__hot__.oldInstance = instance as HotComponent;\n }\n}\n\nexport function initialize() {\n // HMR-only: in a production build `import.meta.hot` is undefined, so this\n // (and the manager/route monkey-patches below) is dead-code-eliminated.\n if (!import.meta.hot) return;\n\n const ComponentManager = getInternalComponentManager(Component);\n const proto = Object.getPrototypeOf(ComponentManager);\n const create = proto.create;\n proto.create = function (...args: unknown[]) {\n const instance = create.call(this, ...args);\n setTimeout(() => syncState(instance.component));\n return instance;\n };\n\n const setupController = Route.prototype.setupController;\n\n const StateCache: Record<string, unknown> = {};\n\n Route.prototype.setupController = function (...args: Parameters<typeof setupController>) {\n const controller = args[0] as unknown as Record<string, unknown>;\n const r = setupController.call(this, ...args);\n const fullRouteName = this.fullRouteName;\n const state = StateCache[fullRouteName] as { router?: Record<string, unknown>; controller?: Record<string, unknown> } || {};\n const skip = ['_qpDelegate', 'target', 'queryParams'];\n const routerState = getState(state.router as HotComponent, skip);\n for (const k in routerState) {\n (this as unknown as Record<string, unknown>)[k] = routerState[k];\n }\n // Only re-apply controller state onto a *new* instance (an HMR swap). On\n // ordinary navigation it is the same singleton, and re-applying a\n // non-`@tracked` query param onto itself trips Ember's mandatory setter.\n if (state.controller && state.controller !== controller) {\n const controllerState = getState(state.controller as HotComponent, skip);\n for (const k in controllerState) {\n (controller as Record<string, unknown>)[k] = controllerState[k];\n }\n }\n StateCache[fullRouteName] = {\n route: this,\n controller,\n };\n return r;\n };\n}\n\nexport default {\n initialize,\n};\n"],"names":["findPropertyDescriptor","component","key","proto","desc","Object","getOwnPropertyDescriptor","getPrototypeOf","RESOURCE_INTERMEDIATE_VALUE_KEY","isUnsafeToCarryOver","value","getState","skip","state","includes","entry","Service","writable","set","syncState","instance","args","__hot__","oldInstance","__get_hot_state__","k","_hmrAccepted","initialize","import","meta","hot","ComponentManager","getInternalComponentManager","Component","create","call","setTimeout","setupController","Route","prototype","StateCache","controller","r","fullRouteName","routerState","router","controllerState","route"],"mappings":"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"}
1
+ {"version":3,"file":"setup-hmr-manager.js","sources":["../../src/instance-initializers/setup-hmr-manager.ts"],"sourcesContent":["import Route from '@ember/routing/route';\nimport Service from '@ember/service';\nimport Component from '@glimmer/component';\nimport { getInternalComponentManager } from '@glimmer/manager';\n\ninterface HotComponent extends Component {\n __get_hot_state__?: () => Record<string, unknown>;\n _hmrAccepted?: (oldInstance?: HotComponent) => void;\n [key: string]: unknown;\n}\n\ntype Mutable<T> = {\n -readonly [P in keyof T]: unknown;\n};\n\n// Accessor for the HMR runtime helpers exposed on the global by\n// virtual:ember-vite-hmr-runtime. Only set in dev/serve mode (guarded by\n// `if (!import.meta.hot)` at the call site).\ninterface HmrRuntime {\n current: (value: unknown) => unknown;\n isHmrClass: (value: unknown) => boolean;\n enqueueInstance: (klass: unknown, instance: HotComponent) => void;\n dequeueInstance: (klass: unknown) => HotComponent | null;\n removeInstance: (klass: unknown, instance: HotComponent) => void;\n}\n\nfunction hmrRuntime(): HmrRuntime | undefined {\n return (globalThis as unknown as { __ember_vite_hmr?: HmrRuntime }).__ember_vite_hmr;\n}\n\nfunction findPropertyDescriptor(\n component: HotComponent | Record<string, unknown>,\n key: string,\n) {\n let proto = component;\n while (proto) {\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (desc) return desc;\n proto = Object.getPrototypeOf(proto);\n }\n}\n\n// The private property key `ember-resources`' `wrapForPlainUsage()` uses on\n// the plain object it wraps in a Proxy for `resource()` (which reactiveweb's\n// `trackedTask` uses internally) - see\n// https://github.com/NullVoxPopuli/ember-resources/blob/main/ember-resources/src/plain/index.ts.\n// Carrying such a proxy's *reference* over onto a new HMR-swapped instance\n// (instead of leaving the new instance's own, correctly-parented resource in\n// place) is unsafe: the proxy's underlying helper cache is lazily created on\n// first property read and permanently bound, at that point, to whichever\n// component instance was passed as `context` when `resource()` was called -\n// for a carried-over proxy that's always the *old*, already-destroyed\n// instance, so the first read after the swap throws \"Attempted to associate\n// a destroyable child with an object that is already destroying or\n// destroyed\" - see issue #563.\n//\n// `in` is used (rather than reading a property, or `Object.keys`/\n// `Object.prototype.toString.call`) because it's the one reflective\n// operation `wrapForPlainUsage`'s Proxy doesn't trap (no `has` handler), so\n// it forwards to a plain `[[HasProperty]]` on the underlying target without\n// ever invoking the lazy getter - it can't accidentally trigger the same\n// crash it's checking for. If `ember-resources` ever renames this key, this\n// check simply stops matching and behavior falls back to today's (already\n// broken, pre-existing) state - it does not introduce a new failure mode.\nconst RESOURCE_INTERMEDIATE_VALUE_KEY = '__Intermediate_Value__';\n\nfunction isUnsafeToCarryOver(value: unknown): boolean {\n return (\n (typeof value === 'object' || typeof value === 'function') &&\n value !== null &&\n RESOURCE_INTERMEDIATE_VALUE_KEY in value\n );\n}\n\nfunction getState(component: HotComponent, skip: string[]) {\n const state: Record<string, unknown> = {};\n if (!component) {\n return state;\n }\n for (const key in component) {\n if (skip.includes(key)) continue;\n const entry = findPropertyDescriptor(component, key);\n const value = component[key as keyof Component];\n \n // Skip Service instances - they should not be synced\n if (value instanceof Service) {\n continue;\n }\n \n // Skip Function properties - they should not be synced\n if (typeof value === 'function') {\n continue;\n }\n\n // Skip resource()-backed proxies - see isUnsafeToCarryOver above.\n if (isUnsafeToCarryOver(value)) {\n continue;\n }\n\n if (entry) {\n // Note: don't probe `entry.value` any further here (e.g. via\n // `Object.prototype.toString.call`) to decide whether it's a plain\n // function - the `typeof value === 'function'` check above already\n // covers that, and a stricter check on `entry.value` (which may be a\n // resource proxy, e.g. reactiveweb's `trackedTask`) can trigger that\n // proxy's `get` trap on first touch, lazily invoking a helper against\n // this component instance - see issue #557.\n if (entry.writable) {\n state[key] = value;\n }\n if (entry.set) {\n state[key] = value;\n }\n }\n }\n return state;\n}\n\n// Apply a stashed old-instance state to a freshly created new instance.\n// Also calls the _hmrAccepted lifecycle hook if defined on the new instance.\nfunction applySyncState(\n instance: Mutable<HotComponent>,\n state: Record<string, unknown>,\n oldInstance: HotComponent | undefined,\n) {\n for (const k in state) {\n if (instance[k as keyof HotComponent] instanceof Service) {\n continue;\n }\n instance[k as keyof HotComponent] = state[k];\n }\n if (typeof instance._hmrAccepted === 'function') {\n instance._hmrAccepted(oldInstance);\n }\n}\n\nexport function initialize() {\n // HMR-only: in a production build `import.meta.hot` is undefined, so this\n // (and the manager/route monkey-patches below) is dead-code-eliminated.\n if (!import.meta.hot) return;\n\n const ComponentManager = getInternalComponentManager(Component);\n const proto = Object.getPrototypeOf(ComponentManager);\n const create = proto.create;\n proto.create = function (...args: unknown[]) {\n const bucket = create.call(this, ...args);\n const component = bucket.component as HotComponent;\n\n // Enqueue this instance in the runtime's per-class FIFO queue. The runtime\n // only enqueues when isHmrClass(klass) is true (registered HMR class), and\n // clears the queue at update() time so user-flow stale entries are never\n // present when willDestroy fires during an HMR swap.\n const klass = component.constructor as object;\n const rt = hmrRuntime();\n if (rt?.isHmrClass(klass)) {\n rt.enqueueInstance(klass, component);\n }\n\n return bucket;\n };\n\n // Patch willDestroy on the Component prototype to detect HMR swaps and\n // transfer state from the old instance to the freshly-created new one.\n //\n // Why willDestroy instead of registerDestructor: Glimmer creates the NEW\n // component instance BEFORE calling willDestroy on the OLD one (willDestroy\n // is scheduled in Ember's \"actions\" runloop queue, which runs after\n // rendering). This ordering is exactly what we need: by willDestroy time,\n // the new instance is already live in liveInstances.\n const origWillDestroy = Component.prototype.willDestroy;\n Component.prototype.willDestroy = function (this: HotComponent) {\n const rt = hmrRuntime();\n if (rt) {\n const klass = this.constructor as object;\n const newClass = rt.current(klass);\n if (newClass !== klass) {\n // The class was swapped via HMR — dequeue the next new instance for\n // the replacement class (FIFO matches DOM/creation order) and\n // schedule a state transfer onto it.\n const newInstance = rt.dequeueInstance(newClass);\n if (newInstance) {\n const state = getState(this, ['args']);\n const oldInstance = this;\n setTimeout(() => applySyncState(newInstance as Mutable<HotComponent>, state, oldInstance));\n }\n } else if (rt.isHmrClass(klass)) {\n // Normal destroy (navigation, timer, conditional) — remove this\n // instance from the live queue so it doesn't accumulate as a stale\n // entry that update() would otherwise have to clear at the next save.\n rt.removeInstance(klass, this);\n }\n }\n return origWillDestroy?.call(this);\n };\n\n const setupController = Route.prototype.setupController;\n\n const StateCache: Record<string, unknown> = {};\n\n Route.prototype.setupController = function (...args: Parameters<typeof setupController>) {\n const controller = args[0] as unknown as Record<string, unknown>;\n const r = setupController.call(this, ...args);\n const fullRouteName = this.fullRouteName;\n const state = StateCache[fullRouteName] as { router?: Record<string, unknown>; controller?: Record<string, unknown> } || {};\n const skip = ['_qpDelegate', 'target', 'queryParams'];\n const routerState = getState(state.router as HotComponent, skip);\n for (const k in routerState) {\n (this as unknown as Record<string, unknown>)[k] = routerState[k];\n }\n // Only re-apply controller state onto a *new* instance (an HMR swap). On\n // ordinary navigation it is the same singleton, and re-applying a\n // non-`@tracked` query param onto itself trips Ember's mandatory setter.\n if (state.controller && state.controller !== controller) {\n const controllerState = getState(state.controller as HotComponent, skip);\n for (const k in controllerState) {\n (controller as Record<string, unknown>)[k] = controllerState[k];\n }\n }\n StateCache[fullRouteName] = {\n route: this,\n controller,\n };\n return r;\n };\n}\n\nexport default {\n initialize,\n};\n"],"names":["hmrRuntime","globalThis","__ember_vite_hmr","findPropertyDescriptor","component","key","proto","desc","Object","getOwnPropertyDescriptor","getPrototypeOf","RESOURCE_INTERMEDIATE_VALUE_KEY","isUnsafeToCarryOver","value","getState","skip","state","includes","entry","Service","writable","set","applySyncState","instance","oldInstance","k","_hmrAccepted","initialize","import","meta","hot","ComponentManager","getInternalComponentManager","Component","create","args","bucket","call","klass","constructor","rt","isHmrClass","enqueueInstance","origWillDestroy","prototype","willDestroy","newClass","current","newInstance","dequeueInstance","setTimeout","removeInstance","setupController","Route","StateCache","controller","r","fullRouteName","routerState","router","controllerState","route"],"mappings":"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"}
@@ -179,16 +179,18 @@ export function renamePrivateClassMembers(babel, classPath, programPath) {
179
179
  // function did -- left such references as native `#foo` PrivateNames
180
180
  // while the outer declaration was renamed to a Symbol, producing invalid
181
181
  // output ("Private field must be declared in an enclosing class").
182
+ const rewriteMemberReference = (path) => {
183
+ const property = path.node.property;
184
+ if (t.isPrivateName(property) &&
185
+ renamedNames.has(property.id.name) &&
186
+ referenceBelongsToOuterClass(path, property.id.name)) {
187
+ path.node.property = t.identifier(getNewName(property.id.name));
188
+ path.node.computed = false;
189
+ }
190
+ };
182
191
  classPath.traverse({
183
- 'MemberExpression|OptionalMemberExpression'(path) {
184
- const property = path.node.property;
185
- if (t.isPrivateName(property) &&
186
- renamedNames.has(property.id.name) &&
187
- referenceBelongsToOuterClass(path, property.id.name)) {
188
- path.node.property = t.identifier(getNewName(property.id.name));
189
- path.node.computed = false;
190
- }
191
- },
192
+ MemberExpression: rewriteMemberReference,
193
+ OptionalMemberExpression: rewriteMemberReference,
192
194
  BinaryExpression(path) {
193
195
  const left = path.node.left;
194
196
  if (path.node.operator === 'in' &&
@@ -266,9 +268,15 @@ function hideRenamedProperties(babel, classPath, names) {
266
268
  // `this` before `super()` has (unconditionally) run.
267
269
  const superCallStatement = ctorBody
268
270
  .get('body')
269
- .find((statement) => statement.isExpressionStatement() &&
270
- statement.get('expression').isCallExpression() &&
271
- statement.get('expression.callee').isSuper());
271
+ .find((statement) => {
272
+ if (!statement.isExpressionStatement())
273
+ return false;
274
+ const expr = statement.get('expression');
275
+ if (Array.isArray(expr) || !expr.isCallExpression())
276
+ return false;
277
+ const callee = expr.get('callee');
278
+ return !Array.isArray(callee) && callee.isSuper();
279
+ });
272
280
  if (superCallStatement) {
273
281
  superCallStatement.insertAfter(hideStatements);
274
282
  }
@@ -10,7 +10,7 @@ import { ImportUtil } from 'babel-import-util';
10
10
  // `renamePrivateClassMembers` (see ../private-members.ts) before this runs,
11
11
  // so this function doesn't need to special-case them at all.
12
12
  export function transformServiceExport(babel, path, state) {
13
- var _a;
13
+ var _a, _b;
14
14
  const t = babel.types;
15
15
  // Check if this is a service file (normalize path for cross-platform compatibility)
16
16
  const normalizedFilename = (_a = state.filename) === null || _a === void 0 ? void 0 : _a.replace(/\\/g, '/');
@@ -34,7 +34,7 @@ export function transformServiceExport(babel, path, state) {
34
34
  if (binding && binding.path.isClassDeclaration()) {
35
35
  classDeclaration = binding.path
36
36
  .node;
37
- classIdentifier = classDeclaration.id;
37
+ classIdentifier = (_b = classDeclaration.id) !== null && _b !== void 0 ? _b : null;
38
38
  classPath =
39
39
  binding.path;
40
40
  }
@@ -206,14 +206,25 @@ export function finalizeTemplateImports(babel, path, filename) {
206
206
  return;
207
207
  }
208
208
  const util = new ImportUtil(babel, path);
209
- const tracked = util.import(path, '@glimmer/tracking', 'tracked');
210
- util.import(path, '@glimmer/component', 'default');
209
+ // `current` is imported from the HMR runtime virtual module. The runtime
210
+ // reads `entry.current` (a @tracked cell) and returns the latest class.
211
+ // Using a getter here (rather than a @tracked field that gets reassigned)
212
+ // means Glimmer only invalidates the scope that contains the `current()`
213
+ // read — not the parent scope that holds the `template__imports__` object.
214
+ // This keeps any outer {{#let}} wrappers (e.g. for stable per-invocation
215
+ // ids) alive across HMR swaps of the child component.
216
+ const currentFn = util.import(path, 'virtual:ember-vite-hmr-runtime', 'current');
211
217
  const klass = t.classExpression(path.scope.generateUidIdentifier('Imports'), null, t.classBody([]));
212
218
  const bindings = [...hotAstProcessor.meta.importBindings].sort();
213
219
  for (const local of bindings) {
214
- klass.body.body.push(t.classProperty(t.identifier(local), t.identifier(local), null, [
215
- t.decorator(tracked),
216
- ]));
220
+ // Emit: get Local() { return current(LocalBinding); }
221
+ // The getter closes over `LocalBinding` (the initial import value).
222
+ // `current()` reads `entry.current` which is @tracked in the runtime,
223
+ // so only the template expression that calls this getter is invalidated
224
+ // when the binding is updated via update(old, new).
225
+ klass.body.body.push(t.classMethod('get', t.identifier(local), [], t.blockStatement([
226
+ t.returnStatement(t.callExpression(currentFn, [t.identifier(local)])),
227
+ ])));
217
228
  }
218
229
  const newExp = t.newExpression(klass, []);
219
230
  const assign = t.assignmentExpression('=', t.identifier(hotAstProcessor.meta.importVar), newExp);
@@ -54,7 +54,7 @@ export default function hotReplaceAst(babel) {
54
54
  renamePrivateClassMembers(babel, path, programPath);
55
55
  },
56
56
  ExportDefaultDeclaration(path, state) {
57
- var _a;
57
+ var _a, _b;
58
58
  if (process.env.EMBER_VITE_HMR_ENABLED !== 'true') {
59
59
  return;
60
60
  }
@@ -62,6 +62,62 @@ export default function hotReplaceAst(babel) {
62
62
  return;
63
63
  }
64
64
  transformServiceExport(babel, path, state);
65
+ // For every non-service default export, rewrite:
66
+ // export default <expr>
67
+ // into:
68
+ // const __hmr_default__ = <expr>;
69
+ // export { __hmr_default__ as default };
70
+ //
71
+ // This gives lib/hmr.ts's appended HMR code a stable local variable
72
+ // name to pass to register()/update()/used() regardless of whether
73
+ // the export is a named class, an anonymous expression, or a compiled
74
+ // template. Services are excluded because they are already rewritten
75
+ // by transformServiceExport above with their own proxy mechanism.
76
+ const normalizedFilename = (_b = state.filename) === null || _b === void 0 ? void 0 : _b.replace(/\\/g, '/');
77
+ if (normalizedFilename === null || normalizedFilename === void 0 ? void 0 : normalizedFilename.includes('/services/')) {
78
+ return;
79
+ }
80
+ const t = babel.types;
81
+ const declaration = path.node.declaration;
82
+ const exportDefault = t.exportNamedDeclaration(null, [
83
+ t.exportSpecifier(t.identifier('__hmr_default__'), t.identifier('default')),
84
+ ]);
85
+ const isClassOrFunction = declaration.type === 'ClassDeclaration' ||
86
+ declaration.type === 'FunctionDeclaration';
87
+ let newPaths;
88
+ if (isClassOrFunction && declaration.id) {
89
+ // A named class/function becomes a declaration statement in its own
90
+ // right — just alias the identifier.
91
+ newPaths = path.replaceWithMultiple([
92
+ declaration,
93
+ t.variableDeclaration('const', [
94
+ t.variableDeclarator(t.identifier('__hmr_default__'), t.identifier(declaration.id.name)),
95
+ ]),
96
+ exportDefault,
97
+ ]);
98
+ }
99
+ else {
100
+ // Anonymous expression (template-only component, arrow fn, etc.).
101
+ // An anonymous `class extends X {}` / `function () {}` is parsed as
102
+ // a declaration node and must be converted to an expression to be
103
+ // a valid initializer.
104
+ const init = isClassOrFunction
105
+ ? t.toExpression(declaration)
106
+ : declaration;
107
+ newPaths = path.replaceWithMultiple([
108
+ t.variableDeclaration('const', [
109
+ t.variableDeclarator(t.identifier('__hmr_default__'), init),
110
+ ]),
111
+ exportDefault,
112
+ ]);
113
+ }
114
+ // Register the new `__hmr_default__` binding so later plugins (e.g.
115
+ // @babel/plugin-transform-typescript) can resolve the export.
116
+ for (const newPath of newPaths) {
117
+ if (newPath.isVariableDeclaration()) {
118
+ newPath.scope.registerDeclaration(newPath);
119
+ }
120
+ }
65
121
  },
66
122
  Program(path, state) {
67
123
  var _a;
package/dist/lib/hmr.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  import { Plugin } from 'vite';
2
- export declare function hmr(enableViteHmrForModes?: string[]): Plugin;
2
+ export declare const hmrRuntimeId = "virtual:ember-vite-hmr-runtime";
3
+ export declare function hmrRuntime(): Plugin;
4
+ export declare function hmr(enableViteHmrForModes?: string[]): Plugin[];