ember-vite-hmr 2.3.0 → 2.7.0

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.
@@ -1,143 +1,27 @@
1
- import * as glimmer from '@glimmer/syntax';
2
- import { ImportUtil } from 'babel-import-util';
3
- class HotAstProcessor {
4
- constructor() {
5
- this.options = {
6
- itsStatic: false,
7
- };
8
- this.counter = 0;
9
- this.meta = {
10
- locals: new Set(),
11
- importVar: null,
12
- babelProgram: undefined,
13
- importBindings: new Set(),
14
- };
15
- this.didCreateImportClass = false;
16
- this.transform = this.transform.bind(this);
17
- }
18
- reset() {
19
- this.meta.importVar = null;
20
- this.meta.babelProgram = undefined;
21
- this.meta.importBindings = new Set();
22
- }
23
- transform(env) {
24
- var _a;
25
- if (process.env.EMBER_VITE_HMR_ENABLED !== 'true') {
26
- return {
27
- visitor: {},
28
- };
29
- }
30
- if ((_a = env.filename) === null || _a === void 0 ? void 0 : _a.includes('node_modules')) {
31
- return {
32
- visitor: {},
33
- };
34
- }
35
- const meta = this.meta;
36
- const importVar = env.meta.jsutils.bindExpression(meta.importVar || 'null', null, {
37
- nameHint: 'template__imports__',
38
- });
39
- meta.importVar = meta.importVar || importVar;
40
- return {
41
- visitor: Object.assign({}, this.buildVisitor({
42
- importVar,
43
- importBindings: meta.importBindings,
44
- babelProgram: meta.babelProgram,
45
- })),
46
- };
47
- }
48
- buildVisitor({ importVar, importBindings, babelProgram, }) {
49
- const findImport = function findImport(specifier) {
50
- return babelProgram.body.find((b) => b.type === 'ImportDeclaration' &&
51
- b.specifiers.some((s) => s.local.name === specifier));
52
- };
53
- const findBlockParams = function (expression, p) {
54
- if (p.node.type === 'Template') {
55
- return false;
56
- }
57
- if (p.node &&
58
- p.node.type === 'BlockStatement' &&
59
- p.node.program.blockParams.includes(expression)) {
60
- return true;
61
- }
62
- const node = p.node;
63
- if (node && node.blockParams && node.blockParams.includes(expression)) {
64
- return true;
65
- }
66
- if (!p.parent)
67
- return false;
68
- return findBlockParams(expression, p.parent);
69
- };
70
- const visitor = {
71
- PathExpression: (node, p) => {
72
- var _a, _b;
73
- if ((((_a = p.parentNode) === null || _a === void 0 ? void 0 : _a.type) === 'SubExpression' ||
74
- ((_b = p.parentNode) === null || _b === void 0 ? void 0 : _b.type) === 'MustacheStatement') &&
75
- p.parentNode.params.includes(node)) {
76
- return;
77
- }
78
- const original = node.original.split('.')[0];
79
- if (original === 'this')
80
- return;
81
- if (original.startsWith('@'))
82
- return;
83
- if (original === 'block')
84
- return;
85
- if (original.startsWith('this.'))
86
- return;
87
- if (findBlockParams(original, p))
88
- return;
89
- if (node.original === 'helper' ||
90
- node.original === 'component' ||
91
- node.original === 'modifier') {
92
- // node.original;
93
- const parent = p.parentNode;
94
- if (typeof parent.params[0].original !==
95
- 'string') {
96
- return;
97
- }
98
- const original = parent.params[0].original.split('.')[0];
99
- if (original && findBlockParams(original, p))
100
- return;
101
- if (original === null || original === void 0 ? void 0 : original.includes('.'))
102
- return;
103
- if (!original)
104
- return;
105
- if (findImport(original)) {
106
- const param = glimmer.builders.path(`${importVar}.${original}`);
107
- parent.params.splice(0, 1, param);
108
- importBindings.add(original);
109
- }
110
- return;
111
- }
112
- if (importVar) {
113
- if (findImport(node.original)) {
114
- node.original = `${importVar}.${node.original}`;
115
- node.parts = node.original.split('.');
116
- importBindings.add(original);
117
- }
118
- return;
119
- }
120
- },
121
- ElementNode: (element, p) => {
122
- const original = element.tag.split('.')[0];
123
- if (findBlockParams(original, p))
124
- return;
125
- if (importVar) {
126
- if (findImport(original)) {
127
- element.tag = `${importVar}.${original}`;
128
- p.node.tag = element.tag;
129
- importBindings.add(original);
130
- }
131
- return;
132
- }
133
- },
134
- };
135
- return visitor;
136
- }
137
- }
138
- export const hotAstProcessor = new HotAstProcessor();
1
+ import { hotAstProcessor, finalizeTemplateImports, hmrImportMetadataCache, normalizeHmrCacheFilename, } from './babel-plugin/template-imports.js';
2
+ import { renamePrivateClassMembers } from './babel-plugin/private-members.js';
3
+ import { transformServiceExport } from './babel-plugin/service-proxy.js';
4
+ // `ember-vite-hmr/lib/babel-plugin` is a published subpath import (see
5
+ // test-app/babel.config.mjs), so this file's module path and its exports
6
+ // (this default export, and the `hotAstProcessor` / `hmrImportMetadataCache`
7
+ // / `normalizeHmrCacheFilename` re-exports below) have to keep resolving
8
+ // exactly as-is -- the actual logic lives in ./babel-plugin/*, split by
9
+ // concern:
10
+ // - template-imports.ts: rewrites template references to imported
11
+ // components/helpers/modifiers through a per-module tracked class, so
12
+ // swapping which module a virtual HMR wrapper resolves to is visible to
13
+ // templates (registered separately as an ember-template-compilation
14
+ // transform via the `hotAstProcessor` export below), and shares the
15
+ // computed import metadata with lib/hmr.ts via `hmrImportMetadataCache`.
16
+ // - private-members.ts: rewrites every native `#private` class member the
17
+ // app's own source declares to a plain, uniquely-named property, so it
18
+ // reads/writes correctly through the service proxy below regardless of
19
+ // what `this` is at the call site.
20
+ // - service-proxy.ts: rewrites `export default class X extends Service`
21
+ // into a generated HMR proxy class that swaps its delegate instance on
22
+ // every accepted update.
23
+ export { hotAstProcessor, hmrImportMetadataCache, normalizeHmrCacheFilename };
139
24
  export default function hotReplaceAst(babel) {
140
- const t = babel.types;
141
25
  return {
142
26
  name: 'a-hot-reload-imports',
143
27
  pre(file) {
@@ -145,267 +29,83 @@ export default function hotReplaceAst(babel) {
145
29
  hotAstProcessor.meta.babelProgram = file.ast.program;
146
30
  },
147
31
  visitor: {
148
- ExportDefaultDeclaration(path, state) {
149
- var _a, _b;
32
+ // Runs on *every* class the app's own source defines, independent of
33
+ // whether it's a service, a service's ancestor, or unrelated to the
34
+ // HMR proxy machinery entirely -- see `renamePrivateClassMembers`'s
35
+ // own doc comment for why this can't be scoped any tighter than that
36
+ // without whole-program analysis. This also visits the `proxyClass`
37
+ // (and, for an inline export, the `originalClass` copy)
38
+ // `transformServiceExport` generates below -- harmless today only
39
+ // because generated proxy classes never declare private members of
40
+ // their own; if that ever changes, this visitor would rename them
41
+ // too.
42
+ Class(path, state) {
43
+ var _a;
150
44
  if (process.env.EMBER_VITE_HMR_ENABLED !== 'true') {
151
45
  return;
152
46
  }
153
47
  if ((_a = state.filename) === null || _a === void 0 ? void 0 : _a.includes('node_modules')) {
154
48
  return;
155
49
  }
156
- // Check if this is a service file (normalize path for cross-platform compatibility)
157
- const normalizedFilename = (_b = state.filename) === null || _b === void 0 ? void 0 : _b.replace(/\\/g, '/');
158
- if (!(normalizedFilename === null || normalizedFilename === void 0 ? void 0 : normalizedFilename.includes('/services/'))) {
159
- return;
160
- }
161
- const declaration = path.node.declaration;
162
- // Handle both inline class declaration and identifier reference
163
- let classDeclaration = null;
164
- let classIdentifier = null;
165
- if (declaration.type === 'ClassDeclaration' && declaration.id) {
166
- // Case 1: export default class MyService extends Service { ... }
167
- classDeclaration = declaration;
168
- classIdentifier = declaration.id;
169
- }
170
- else if (declaration.type === 'Identifier') {
171
- // Case 2: class MyService extends Service { ... } \n export default MyService;
172
- const binding = path.scope.getBinding(declaration.name);
173
- if (binding && binding.path.isClassDeclaration()) {
174
- classDeclaration = binding.path
175
- .node;
176
- classIdentifier = classDeclaration.id;
177
- }
178
- }
179
- if (!classDeclaration || !classIdentifier) {
50
+ const programPath = path.findParent((p) => p.isProgram());
51
+ if (!programPath) {
180
52
  return;
181
53
  }
182
- if (classIdentifier.name.endsWith('HmrProxy')) {
54
+ renamePrivateClassMembers(babel, path, programPath);
55
+ },
56
+ ExportDefaultDeclaration(path, state) {
57
+ var _a, _b;
58
+ if (process.env.EMBER_VITE_HMR_ENABLED !== 'true') {
183
59
  return;
184
60
  }
185
- // Check if it extends Service
186
- const superClass = classDeclaration.superClass;
187
- if (!superClass ||
188
- ((superClass.type !== 'Identifier' ||
189
- superClass.name !== 'Service') &&
190
- superClass.type !== 'MemberExpression')) {
61
+ if ((_a = state.filename) === null || _a === void 0 ? void 0 : _a.includes('node_modules')) {
191
62
  return;
192
63
  }
193
- // Get the program path for ImportUtil
194
- const programPath = path.findParent((p) => p.isProgram());
195
- if (!programPath) {
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/')) {
196
78
  return;
197
79
  }
198
- const util = new ImportUtil(babel, programPath);
199
- const tracked = util.import(programPath, '@glimmer/tracking', 'tracked');
200
- const Service = util.import(programPath, '@ember/service', 'default');
201
- const originalClassName = classIdentifier.name;
202
- const proxyClassName = `${originalClassName}HmrProxy`;
203
- // Keep the original class as-is, just remove it from export
204
- // We'll add it back as a non-exported class
205
- const babelTemplate = babel.template;
206
- // The live proxy instance and the latest accepted implementation must
207
- // live on `import.meta.hot.data`, not in a module-scope `let`: Vite
208
- // guarantees that object (and only that object) survives across a
209
- // module's hot re-evaluations. A component's `@service` injection can
210
- // end up constructing the proxy against a *later* re-evaluation of
211
- // this module than the one whose `import.meta.hot.accept` closure
212
- // captured a module-scope variable — the closure and the constructor
213
- // would then silently operate on two disconnected variable bindings
214
- // (a duplicate-module symptom), and the HMR swap would never reach
215
- // the instance actually rendered.
216
- const ctorBody = babelTemplate.statements(`
217
- super(...args);
218
- this._owner = args[0];
219
- const Impl = (import.meta.hot && import.meta.hot.data._impl) || ${proxyClassName}.Impl;
220
- this._delegate = new Impl(this._owner);
221
- if (import.meta.hot) {
222
- import.meta.hot.data._proxy = this;
223
- }
224
- // Bound methods are cached per-delegate (not per-proxy) so identity
225
- // stays stable across repeated reads of the same delegate (e.g.
226
- // {{on "click" this.svc.handler}} comparing args by reference), while
227
- // naturally invalidating once a delegate is swapped out on HMR accept.
228
- const boundMethods = new WeakMap();
229
- return new Proxy(this, {
230
- get(target, prop, receiver) {
231
- if (prop === '_delegate') {
232
- return target._delegate;
233
- }
234
- // A subclass of this service (see the Reflect.get fallback
235
- // below) may override a method the base delegate class also
236
- // defines. Since 'prop in delegate' walks the delegate's
237
- // *entire* prototype chain, it would match the base
238
- // implementation before the subclass's own override ever
239
- // gets a chance -- so look for an own property between
240
- // target's dynamic prototype and this proxy's own prototype
241
- // first, and prefer it over the delegate.
242
- let proto = Object.getPrototypeOf(target);
243
- while (proto && proto !== ${proxyClassName}.prototype) {
244
- if (Object.prototype.hasOwnProperty.call(proto, prop)) {
245
- return Reflect.get(target, prop, receiver);
246
- }
247
- proto = Object.getPrototypeOf(proto);
248
- }
249
- const delegate = target._delegate;
250
- // Methods invoked via normal member-call syntax (service.method())
251
- // bind 'this' to whatever object the property access happened on
252
- // -- the proxy, here -- not to the delegate. That breaks native
253
- // private fields, whose storage lives only on the concrete
254
- // delegate instance, so delegate-owned methods are explicitly
255
- // bound to the delegate.
256
- if (prop in delegate) {
257
- const value = delegate[prop];
258
- if (typeof value === 'function') {
259
- let cache = boundMethods.get(delegate);
260
- if (!cache) {
261
- cache = new Map();
262
- boundMethods.set(delegate, cache);
263
- }
264
- let bound = cache.get(prop);
265
- if (!bound) {
266
- bound = value.bind(delegate);
267
- cache.set(prop, bound);
268
- }
269
- return bound;
270
- }
271
- return value;
272
- }
273
- // Not on the delegate: fall through to the proxy's own real
274
- // prototype chain, which covers methods declared on a subclass
275
- // of this service (the subclass never went through this babel
276
- // transform itself, since it doesn't extend Service directly).
277
- return Reflect.get(target, prop, receiver);
278
- },
279
- set(target, prop, value) {
280
- target._delegate[prop] = value;
281
- return true;
282
- },
283
- defineProperty(target, prop, descriptor) {
284
- // A subclass's own class fields are installed via
285
- // [[DefineOwnProperty]] against whatever 'this' super() returned
286
- // (this proxy), bypassing the 'set' trap entirely. Route them to
287
- // the delegate too, so subclass state lives alongside the rest
288
- // of the service's state instead of stranded on the proxy target.
289
- return Reflect.defineProperty(target._delegate, prop, descriptor);
290
- },
291
- });
292
- `)();
293
- const willDestroyBody = babelTemplate.statements(`
294
- super.willDestroy();
295
- this._delegate.willDestroy();
296
- if (import.meta.hot && import.meta.hot.data._proxy === this) {
297
- import.meta.hot.data._proxy = undefined;
298
- }
299
- `)();
300
- // Create the HMR proxy service class
301
- const proxyClass = t.classDeclaration(t.identifier(proxyClassName), Service, t.classBody([
302
- // static Impl = OriginalService (reference to the original class)
303
- t.classProperty(t.identifier('Impl'), t.identifier(originalClassName), null, null, false, true),
304
- // @tracked _delegate
305
- t.classProperty(t.identifier('_delegate'), null, null, [t.decorator(tracked)], false, false),
306
- t.classMethod('constructor', t.identifier('constructor'), [t.restElement(t.identifier('args'))], t.blockStatement(ctorBody)),
307
- t.classMethod('method', t.identifier('willDestroy'), [], t.blockStatement(willDestroyBody)),
308
- ]));
309
- // For Case 1 (inline class), we need to keep the class as non-exported
310
- // For Case 2 (separate declaration), the class already exists, so we don't recreate it
311
- if (declaration.type === 'ClassDeclaration') {
312
- // Case 1: Inline class export - create a non-exported version
313
- const originalClass = t.classDeclaration(classIdentifier, classDeclaration.superClass, classDeclaration.body, classDeclaration.decorators || []);
314
- // Replace the export with all the necessary declarations
315
- // IMPORTANT: Class must be declared BEFORE the proxy class that references it
80
+ const t = babel.types;
81
+ const declaration = path.node.declaration;
82
+ // If the declaration is already a named class/function, it becomes a
83
+ // declaration statement in its own right — just export the identifier.
84
+ if ((declaration.type === 'ClassDeclaration' ||
85
+ declaration.type === 'FunctionDeclaration') &&
86
+ declaration.id) {
87
+ const id = declaration.id;
316
88
  path.replaceWithMultiple([
317
- originalClass, // The original class (no longer exported) - MUST BE FIRST
318
- t.exportDefaultDeclaration(proxyClass), // Export the proxy class
89
+ declaration,
90
+ t.variableDeclaration('const', [
91
+ t.variableDeclarator(t.identifier('__hmr_default__'), t.identifier(id.name)),
92
+ ]),
93
+ t.exportNamedDeclaration(null, [
94
+ t.exportSpecifier(t.identifier('__hmr_default__'), t.identifier('default')),
95
+ ]),
319
96
  ]);
320
97
  }
321
98
  else {
322
- // Case 2: Separate declaration - class already exists, just replace the export
99
+ // Anonymous expression (template-only component, arrow fn, etc.)
323
100
  path.replaceWithMultiple([
324
- t.exportDefaultDeclaration(proxyClass), // Export the proxy class
101
+ t.variableDeclaration('const', [
102
+ t.variableDeclarator(t.identifier('__hmr_default__'), declaration),
103
+ ]),
104
+ t.exportNamedDeclaration(null, [
105
+ t.exportSpecifier(t.identifier('__hmr_default__'), t.identifier('default')),
106
+ ]),
325
107
  ]);
326
108
  }
327
- // Add HMR code after the export using babel template
328
- if (programPath) {
329
- const hmrCode = `
330
- if (import.meta.hot) {
331
- import.meta.hot.accept((newModule) => {
332
- const NewImpl = newModule?.default?.Impl;
333
- if (!NewImpl) {
334
- return;
335
- }
336
-
337
- // Remember the latest accepted implementation so a proxy
338
- // constructed *after* this update (its instantiation raced
339
- // behind the HMR swap) still picks up the new class instead
340
- // of the stale one captured at module-evaluation time.
341
- import.meta.hot.data._impl = NewImpl;
342
-
343
- const proxy = import.meta.hot.data._proxy;
344
- if (!proxy) {
345
- // No instance has been created yet; the next construction
346
- // will read import.meta.hot.data._impl above.
347
- return;
348
- }
349
-
350
- const oldDelegate = proxy._delegate;
351
- const newDelegate = new NewImpl(proxy._owner);
352
- proxy._delegate = newDelegate;
353
-
354
- // Sync state from old to new while keeping new implementation defaults
355
- for (const key in oldDelegate) {
356
- const descriptor =
357
- Object.getOwnPropertyDescriptor(oldDelegate, key) ||
358
- Object.getOwnPropertyDescriptor(Object.getPrototypeOf(oldDelegate), key);
359
- const hasOwnDefault = Object.prototype.hasOwnProperty.call(newDelegate, key);
360
- const currentValue = newDelegate[key];
361
- const previousValue = oldDelegate[key];
362
-
363
- // Skip Service instances - they should not be synced
364
- if (previousValue instanceof Service) {
365
- continue;
366
- }
367
-
368
- // Skip function properties that the new implementation also
369
- // declares (a fresh redefinition should win over the old
370
- // one). But a function-valued property the new
371
- // implementation *doesn't* declare at all isn't a stale
372
- // redefinition -- it's state a subclass installed on the
373
- // delegate directly (subclasses of this service never go
374
- // through this transform themselves, since their
375
- // superclass isn't literally named Service; their own
376
- // class fields land on the delegate via the proxy's
377
- // defineProperty trap instead). Dropping it here would
378
- // silently break e.g. an arrow-function class field on a
379
- // subclass across a base-class HMR swap.
380
- if (typeof previousValue === 'function' && hasOwnDefault) {
381
- continue;
382
- }
383
-
384
- const shouldSync =
385
- !!descriptor &&
386
- (descriptor.writable || descriptor.set || Object.prototype.hasOwnProperty.call(oldDelegate, key)) &&
387
- (!hasOwnDefault || currentValue === previousValue);
388
-
389
- if (shouldSync) {
390
- try {
391
- newDelegate[key] = previousValue;
392
- } catch (e) {
393
- // Skip non-writable properties
394
- }
395
- }
396
- }
397
-
398
- newDelegate._hmrAccepted?.(oldDelegate);
399
-
400
- if (oldDelegate.willDestroy) {
401
- oldDelegate.willDestroy();
402
- }
403
- });
404
- }
405
- `;
406
- const template = babel.template.statements(hmrCode);
407
- programPath.pushContainer('body', template());
408
- }
409
109
  },
410
110
  Program(path, state) {
411
111
  var _a;
@@ -415,37 +115,7 @@ export default function hotReplaceAst(babel) {
415
115
  if ((_a = state.filename) === null || _a === void 0 ? void 0 : _a.includes('node_modules')) {
416
116
  return;
417
117
  }
418
- if (!hotAstProcessor.meta.importVar ||
419
- hotAstProcessor.meta.importBindings.size === 0) {
420
- return;
421
- }
422
- const util = new ImportUtil(babel, path);
423
- const tracked = util.import(path, '@glimmer/tracking', 'tracked');
424
- util.import(path, '@glimmer/component', 'default');
425
- const klass = t.classExpression(path.scope.generateUidIdentifier('Imports'), null, t.classBody([]));
426
- const bindings = [...hotAstProcessor.meta.importBindings].sort();
427
- for (const local of bindings) {
428
- klass.body.body.push(t.classProperty(t.identifier(local), t.identifier(local), null, [
429
- t.decorator(tracked),
430
- ]));
431
- }
432
- const newExp = t.newExpression(klass, []);
433
- const assign = t.assignmentExpression('=', t.identifier(hotAstProcessor.meta.importVar), newExp);
434
- const varDeclaration = path.node.body.findIndex((e) => e.type === 'VariableDeclaration' &&
435
- e.declarations[0].id.name ===
436
- hotAstProcessor.meta.importVar) + 1;
437
- const lastImportIndex = path.node.body.findLastIndex((e) => e.type === 'ImportDeclaration') + 1;
438
- path.node.body.splice(Math.max(varDeclaration, lastImportIndex), 0, t.expressionStatement(assign));
439
- // Export metadata about tracked imports for hmr.ts to use
440
- const importMetadata = t.variableDeclaration('const', [
441
- t.variableDeclarator(t.identifier('__hmr_import_metadata__'), t.objectExpression([
442
- t.objectProperty(t.identifier('importVar'), t.stringLiteral(hotAstProcessor.meta.importVar)),
443
- t.objectProperty(t.identifier('bindings'), t.arrayExpression(bindings.map((b) => t.stringLiteral(b)))),
444
- ])),
445
- ]);
446
- const exportMetadata = t.exportNamedDeclaration(importMetadata, []);
447
- path.node.body.push(exportMetadata);
448
- path.scope.crawl();
118
+ finalizeTemplateImports(babel, path, state.filename);
449
119
  },
450
120
  },
451
121
  };
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[];