ember-vite-hmr 2.2.4 → 2.4.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,202 +29,31 @@ 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) {
180
- return;
181
- }
182
- if (classIdentifier.name.endsWith('HmrProxy')) {
183
- return;
184
- }
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')) {
191
- return;
192
- }
193
- // Get the program path for ImportUtil
194
50
  const programPath = path.findParent((p) => p.isProgram());
195
51
  if (!programPath) {
196
52
  return;
197
53
  }
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
- return new Proxy(this, {
225
- get(target, prop) {
226
- if (prop === '_delegate') {
227
- return target._delegate;
228
- }
229
- return target._delegate[prop];
230
- },
231
- set(target, prop, value) {
232
- target._delegate[prop] = value;
233
- return true;
234
- },
235
- });
236
- `)();
237
- const willDestroyBody = babelTemplate.statements(`
238
- super.willDestroy();
239
- this._delegate.willDestroy();
240
- if (import.meta.hot && import.meta.hot.data._proxy === this) {
241
- import.meta.hot.data._proxy = undefined;
242
- }
243
- `)();
244
- // Create the HMR proxy service class
245
- const proxyClass = t.classDeclaration(t.identifier(proxyClassName), Service, t.classBody([
246
- // static Impl = OriginalService (reference to the original class)
247
- t.classProperty(t.identifier('Impl'), t.identifier(originalClassName), null, null, false, true),
248
- // @tracked _delegate
249
- t.classProperty(t.identifier('_delegate'), null, null, [t.decorator(tracked)], false, false),
250
- t.classMethod('constructor', t.identifier('constructor'), [t.restElement(t.identifier('args'))], t.blockStatement(ctorBody)),
251
- t.classMethod('method', t.identifier('willDestroy'), [], t.blockStatement(willDestroyBody)),
252
- ]));
253
- // For Case 1 (inline class), we need to keep the class as non-exported
254
- // For Case 2 (separate declaration), the class already exists, so we don't recreate it
255
- if (declaration.type === 'ClassDeclaration') {
256
- // Case 1: Inline class export - create a non-exported version
257
- const originalClass = t.classDeclaration(classIdentifier, classDeclaration.superClass, classDeclaration.body, classDeclaration.decorators || []);
258
- // Replace the export with all the necessary declarations
259
- // IMPORTANT: Class must be declared BEFORE the proxy class that references it
260
- path.replaceWithMultiple([
261
- originalClass, // The original class (no longer exported) - MUST BE FIRST
262
- t.exportDefaultDeclaration(proxyClass), // Export the proxy class
263
- ]);
264
- }
265
- else {
266
- // Case 2: Separate declaration - class already exists, just replace the export
267
- path.replaceWithMultiple([
268
- t.exportDefaultDeclaration(proxyClass), // Export the proxy class
269
- ]);
270
- }
271
- // Add HMR code after the export using babel template
272
- if (programPath) {
273
- const hmrCode = `
274
- if (import.meta.hot) {
275
- import.meta.hot.accept((newModule) => {
276
- const NewImpl = newModule?.default?.Impl;
277
- if (!NewImpl) {
278
- return;
279
- }
280
-
281
- // Remember the latest accepted implementation so a proxy
282
- // constructed *after* this update (its instantiation raced
283
- // behind the HMR swap) still picks up the new class instead
284
- // of the stale one captured at module-evaluation time.
285
- import.meta.hot.data._impl = NewImpl;
286
-
287
- const proxy = import.meta.hot.data._proxy;
288
- if (!proxy) {
289
- // No instance has been created yet; the next construction
290
- // will read import.meta.hot.data._impl above.
291
- return;
292
- }
293
-
294
- const oldDelegate = proxy._delegate;
295
- const newDelegate = new NewImpl(proxy._owner);
296
- proxy._delegate = newDelegate;
297
-
298
- // Sync state from old to new while keeping new implementation defaults
299
- for (const key in oldDelegate) {
300
- const descriptor =
301
- Object.getOwnPropertyDescriptor(oldDelegate, key) ||
302
- Object.getOwnPropertyDescriptor(Object.getPrototypeOf(oldDelegate), key);
303
- const hasOwnDefault = Object.prototype.hasOwnProperty.call(newDelegate, key);
304
- const currentValue = newDelegate[key];
305
- const previousValue = oldDelegate[key];
306
-
307
- // Skip Service instances - they should not be synced
308
- if (previousValue instanceof Service) {
309
- continue;
310
- }
311
-
312
- // Skip Function properties - they should not be synced
313
- if (typeof previousValue === 'function') {
314
- continue;
315
- }
316
-
317
- const shouldSync =
318
- !!descriptor &&
319
- (descriptor.writable || descriptor.set || Object.prototype.hasOwnProperty.call(oldDelegate, key)) &&
320
- (!hasOwnDefault || currentValue === previousValue);
321
-
322
- if (shouldSync) {
323
- try {
324
- newDelegate[key] = previousValue;
325
- } catch (e) {
326
- // Skip non-writable properties
327
- }
328
- }
329
- }
330
-
331
- newDelegate._hmrAccepted?.(oldDelegate);
332
-
333
- if (oldDelegate.willDestroy) {
334
- oldDelegate.willDestroy();
335
- }
336
- });
337
- }
338
- `;
339
- const template = babel.template.statements(hmrCode);
340
- programPath.pushContainer('body', template());
341
- }
54
+ renamePrivateClassMembers(babel, path, programPath);
342
55
  },
343
- Program(path, state) {
56
+ ExportDefaultDeclaration(path, state) {
344
57
  var _a;
345
58
  if (process.env.EMBER_VITE_HMR_ENABLED !== 'true') {
346
59
  return;
@@ -348,37 +61,17 @@ export default function hotReplaceAst(babel) {
348
61
  if ((_a = state.filename) === null || _a === void 0 ? void 0 : _a.includes('node_modules')) {
349
62
  return;
350
63
  }
351
- if (!hotAstProcessor.meta.importVar ||
352
- hotAstProcessor.meta.importBindings.size === 0) {
64
+ transformServiceExport(babel, path, state);
65
+ },
66
+ Program(path, state) {
67
+ var _a;
68
+ if (process.env.EMBER_VITE_HMR_ENABLED !== 'true') {
353
69
  return;
354
70
  }
355
- const util = new ImportUtil(babel, path);
356
- const tracked = util.import(path, '@glimmer/tracking', 'tracked');
357
- util.import(path, '@glimmer/component', 'default');
358
- const klass = t.classExpression(path.scope.generateUidIdentifier('Imports'), null, t.classBody([]));
359
- const bindings = [...hotAstProcessor.meta.importBindings].sort();
360
- for (const local of bindings) {
361
- klass.body.body.push(t.classProperty(t.identifier(local), t.identifier(local), null, [
362
- t.decorator(tracked),
363
- ]));
71
+ if ((_a = state.filename) === null || _a === void 0 ? void 0 : _a.includes('node_modules')) {
72
+ return;
364
73
  }
365
- const newExp = t.newExpression(klass, []);
366
- const assign = t.assignmentExpression('=', t.identifier(hotAstProcessor.meta.importVar), newExp);
367
- const varDeclaration = path.node.body.findIndex((e) => e.type === 'VariableDeclaration' &&
368
- e.declarations[0].id.name ===
369
- hotAstProcessor.meta.importVar) + 1;
370
- const lastImportIndex = path.node.body.findLastIndex((e) => e.type === 'ImportDeclaration') + 1;
371
- path.node.body.splice(Math.max(varDeclaration, lastImportIndex), 0, t.expressionStatement(assign));
372
- // Export metadata about tracked imports for hmr.ts to use
373
- const importMetadata = t.variableDeclaration('const', [
374
- t.variableDeclarator(t.identifier('__hmr_import_metadata__'), t.objectExpression([
375
- t.objectProperty(t.identifier('importVar'), t.stringLiteral(hotAstProcessor.meta.importVar)),
376
- t.objectProperty(t.identifier('bindings'), t.arrayExpression(bindings.map((b) => t.stringLiteral(b)))),
377
- ])),
378
- ]);
379
- const exportMetadata = t.exportNamedDeclaration(importMetadata, []);
380
- path.node.body.push(exportMetadata);
381
- path.scope.crawl();
74
+ finalizeTemplateImports(babel, path, state.filename);
382
75
  },
383
76
  },
384
77
  };