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.
@@ -0,0 +1,300 @@
1
+ import { ImportUtil } from 'babel-import-util';
2
+ // Rewrites `export default class MyService extends Service { ... }` (or the
3
+ // equivalent separate-declaration form) into a generated `...HmrProxy` class
4
+ // that's exported in its place. The proxy keeps a stable identity for
5
+ // Ember's container/injections while the actual state-bearing instance --
6
+ // the "delegate" -- gets swapped underneath on every HMR accept, which is
7
+ // what lets a service's field/method edits apply without a full page
8
+ // reload. Private members on the original class (and any of its ancestors
9
+ // in the app's own source) are rewritten separately by
10
+ // `renamePrivateClassMembers` (see ../private-members.ts) before this runs,
11
+ // so this function doesn't need to special-case them at all.
12
+ export function transformServiceExport(babel, path, state) {
13
+ var _a;
14
+ const t = babel.types;
15
+ // Check if this is a service file (normalize path for cross-platform compatibility)
16
+ const normalizedFilename = (_a = state.filename) === null || _a === void 0 ? void 0 : _a.replace(/\\/g, '/');
17
+ if (!(normalizedFilename === null || normalizedFilename === void 0 ? void 0 : normalizedFilename.includes('/services/'))) {
18
+ return;
19
+ }
20
+ const declaration = path.node.declaration;
21
+ // Handle both inline class declaration and identifier reference
22
+ let classDeclaration = null;
23
+ let classIdentifier = null;
24
+ let classPath = null;
25
+ if (declaration.type === 'ClassDeclaration' && declaration.id) {
26
+ // Case 1: export default class MyService extends Service { ... }
27
+ classDeclaration = declaration;
28
+ classIdentifier = declaration.id;
29
+ classPath = path.get('declaration');
30
+ }
31
+ else if (declaration.type === 'Identifier') {
32
+ // Case 2: class MyService extends Service { ... } \n export default MyService;
33
+ const binding = path.scope.getBinding(declaration.name);
34
+ if (binding && binding.path.isClassDeclaration()) {
35
+ classDeclaration = binding.path
36
+ .node;
37
+ classIdentifier = classDeclaration.id;
38
+ classPath =
39
+ binding.path;
40
+ }
41
+ }
42
+ if (!classDeclaration || !classIdentifier || !classPath) {
43
+ return;
44
+ }
45
+ if (classIdentifier.name.endsWith('HmrProxy')) {
46
+ return;
47
+ }
48
+ // Check if it extends Service
49
+ const superClass = classDeclaration.superClass;
50
+ if (!superClass ||
51
+ ((superClass.type !== 'Identifier' || superClass.name !== 'Service') &&
52
+ superClass.type !== 'MemberExpression')) {
53
+ return;
54
+ }
55
+ // Get the program path for ImportUtil
56
+ const programPath = path.findParent((p) => p.isProgram());
57
+ if (!programPath) {
58
+ return;
59
+ }
60
+ // Private members are renamed generically by the `Class` visitor in
61
+ // ../../babel-plugin.ts, not here -- it runs on every class regardless of
62
+ // whether it ends up wrapped by this HMR proxy.
63
+ const util = new ImportUtil(babel, programPath);
64
+ const tracked = util.import(programPath, '@glimmer/tracking', 'tracked');
65
+ const Service = util.import(programPath, '@ember/service', 'default');
66
+ const originalClassName = classIdentifier.name;
67
+ const proxyClassName = `${originalClassName}HmrProxy`;
68
+ // Keep the original class as-is, just remove it from export
69
+ // We'll add it back as a non-exported class
70
+ const babelTemplate = babel.template;
71
+ // The live proxy instance and the latest accepted implementation must
72
+ // live on `import.meta.hot.data`, not in a module-scope `let`: Vite
73
+ // guarantees that object (and only that object) survives across a
74
+ // module's hot re-evaluations. A component's `@service` injection can
75
+ // end up constructing the proxy against a *later* re-evaluation of
76
+ // this module than the one whose `import.meta.hot.accept` closure
77
+ // captured a module-scope variable — the closure and the constructor
78
+ // would then silently operate on two disconnected variable bindings
79
+ // (a duplicate-module symptom), and the HMR swap would never reach
80
+ // the instance actually rendered.
81
+ const ctorBody = babelTemplate.statements(`
82
+ super(...args);
83
+ this._owner = args[0];
84
+ const Impl = (import.meta.hot && import.meta.hot.data._impl) || ${proxyClassName}.Impl;
85
+ this._delegate = new Impl(this._owner);
86
+ if (import.meta.hot) {
87
+ import.meta.hot.data._proxy = this;
88
+ }
89
+ return new Proxy(this, {
90
+ get(target, prop, receiver) {
91
+ if (prop === '_delegate') {
92
+ return target._delegate;
93
+ }
94
+ // A subclass of this service (see the Reflect.get fallback
95
+ // below) may override a method the base delegate class also
96
+ // defines. Since 'prop in delegate' walks the delegate's
97
+ // *entire* prototype chain, it would match the base
98
+ // implementation before the subclass's own override ever
99
+ // gets a chance -- so look for an own property between
100
+ // target's dynamic prototype and this proxy's own prototype
101
+ // first, and prefer it over the delegate.
102
+ let proto = Object.getPrototypeOf(target);
103
+ while (proto && proto !== ${proxyClassName}.prototype) {
104
+ if (Object.prototype.hasOwnProperty.call(proto, prop)) {
105
+ return Reflect.get(target, prop, receiver);
106
+ }
107
+ proto = Object.getPrototypeOf(proto);
108
+ }
109
+ const delegate = target._delegate;
110
+ // Function-valued properties are returned exactly as stored
111
+ // on the delegate, never bound. Binding used to be necessary
112
+ // for prototype methods reading native '#private' fields
113
+ // (private-field access requires 'this' to literally be the
114
+ // declaring instance), but the private-member rewrite below
115
+ // now rewrites every '#private' member the app's own source
116
+ // declares to a plain property, which reads correctly
117
+ // through this same trap regardless of what 'this' is at
118
+ // the call site -- so a bound copy is never needed for that
119
+ // case. Binding would also be actively wrong for
120
+ // identity-sensitive own-property function values
121
+ // (modifier()/helper() results, component classes, etc.),
122
+ // since bind() returns a fresh function object and silently
123
+ // drops any metadata associated with the original one via a
124
+ // WeakMap (see #560/#561).
125
+ //
126
+ // Calling a returned prototype method as 'service.method()'
127
+ // now runs it with 'this' set to the proxy (standard
128
+ // member-call semantics), not the raw delegate -- the same
129
+ // way own-property function values have always run here.
130
+ // Reflection idioms inside such a method (Object.keys(this),
131
+ // 'this instanceof OriginalClass', for-in over this)
132
+ // therefore see the proxy's own shape, not the delegate's --
133
+ // a pre-existing limitation for own-property functions, now
134
+ // also true for prototype methods. Nothing in this repo's
135
+ // services relies on that, and ember-source's own
136
+ // Service/EmberObject/CoreObject chain declares no native
137
+ // '#private' fields, so this is safe for the common case;
138
+ // a third-party base class outside the app's own source that
139
+ // both uses native '#private' fields AND relies on such
140
+ // reflection from a service method would still be affected.
141
+ if (prop in delegate) {
142
+ return delegate[prop];
143
+ }
144
+ // Not on the delegate: fall through to the proxy's own real
145
+ // prototype chain, which covers methods declared on a subclass
146
+ // of this service (the subclass never went through this babel
147
+ // transform itself, since it doesn't extend Service directly).
148
+ return Reflect.get(target, prop, receiver);
149
+ },
150
+ set(target, prop, value) {
151
+ target._delegate[prop] = value;
152
+ return true;
153
+ },
154
+ defineProperty(target, prop, descriptor) {
155
+ // A subclass's own class fields are installed via
156
+ // [[DefineOwnProperty]] against whatever 'this' super() returned
157
+ // (this proxy), bypassing the 'set' trap entirely. Route them to
158
+ // the delegate too, so subclass state lives alongside the rest
159
+ // of the service's state instead of stranded on the proxy target.
160
+ return Reflect.defineProperty(target._delegate, prop, descriptor);
161
+ },
162
+ });
163
+ `)();
164
+ const willDestroyBody = babelTemplate.statements(`
165
+ super.willDestroy();
166
+ this._delegate.willDestroy();
167
+ if (import.meta.hot && import.meta.hot.data._proxy === this) {
168
+ import.meta.hot.data._proxy = undefined;
169
+ }
170
+ `)();
171
+ // Create the HMR proxy service class
172
+ const proxyClass = t.classDeclaration(t.identifier(proxyClassName), Service, t.classBody([
173
+ // static Impl = OriginalService (reference to the original class)
174
+ t.classProperty(t.identifier('Impl'), t.identifier(originalClassName), null, null, false, true),
175
+ // @tracked _delegate
176
+ t.classProperty(t.identifier('_delegate'), null, null, [t.decorator(tracked)], false, false),
177
+ t.classMethod('constructor', t.identifier('constructor'), [t.restElement(t.identifier('args'))], t.blockStatement(ctorBody)),
178
+ t.classMethod('method', t.identifier('willDestroy'), [], t.blockStatement(willDestroyBody)),
179
+ ]));
180
+ // For Case 1 (inline class), we need to keep the class as non-exported
181
+ // For Case 2 (separate declaration), the class already exists, so we don't recreate it
182
+ if (declaration.type === 'ClassDeclaration') {
183
+ // Case 1: Inline class export - create a non-exported version
184
+ const originalClass = t.classDeclaration(classIdentifier, classDeclaration.superClass, classDeclaration.body, classDeclaration.decorators || []);
185
+ // Replace the export with all the necessary declarations
186
+ // IMPORTANT: Class must be declared BEFORE the proxy class that references it
187
+ path.replaceWithMultiple([
188
+ originalClass, // The original class (no longer exported) - MUST BE FIRST
189
+ t.exportDefaultDeclaration(proxyClass), // Export the proxy class
190
+ ]);
191
+ }
192
+ else {
193
+ // Case 2: Separate declaration - class already exists, just replace the export
194
+ path.replaceWithMultiple([
195
+ t.exportDefaultDeclaration(proxyClass), // Export the proxy class
196
+ ]);
197
+ }
198
+ // Add HMR code after the export using babel template
199
+ if (programPath) {
200
+ const hmrCode = `
201
+ if (import.meta.hot) {
202
+ import.meta.hot.accept((newModule) => {
203
+ const NewImpl = newModule?.default?.Impl;
204
+ if (!NewImpl) {
205
+ return;
206
+ }
207
+
208
+ // Remember the latest accepted implementation so a proxy
209
+ // constructed *after* this update (its instantiation raced
210
+ // behind the HMR swap) still picks up the new class instead
211
+ // of the stale one captured at module-evaluation time.
212
+ import.meta.hot.data._impl = NewImpl;
213
+
214
+ const proxy = import.meta.hot.data._proxy;
215
+ if (!proxy) {
216
+ // No instance has been created yet; the next construction
217
+ // will read import.meta.hot.data._impl above.
218
+ return;
219
+ }
220
+
221
+ const oldDelegate = proxy._delegate;
222
+ const newDelegate = new NewImpl(proxy._owner);
223
+ proxy._delegate = newDelegate;
224
+
225
+ // Sync state from old to new while keeping new implementation defaults.
226
+ // Keys come from both 'for...in' (own AND inherited enumerable
227
+ // properties -- this is what picks up a '@tracked' field, which
228
+ // decorator-transforms implements as an *enumerable* accessor pair
229
+ // on the prototype, not an own instance property) and
230
+ // Object.getOwnPropertyNames (own properties regardless of
231
+ // enumerability -- this is what picks up an undecorated private
232
+ // field, which the private-member rewrite in
233
+ // ../private-members.ts's hideRenamedProperties makes a
234
+ // non-enumerable own property specifically so it's invisible to
235
+ // 'for...in', 'Object.keys', etc. everywhere *except* here). Using
236
+ // only 'for...in' would silently drop such a field's live,
237
+ // runtime-mutated value on every hot-reload, resetting it to
238
+ // whatever the new module's field initializer produces.
239
+ const keysToSync = new Set();
240
+ for (const key in oldDelegate) {
241
+ keysToSync.add(key);
242
+ }
243
+ for (const key of Object.getOwnPropertyNames(oldDelegate)) {
244
+ keysToSync.add(key);
245
+ }
246
+ for (const key of keysToSync) {
247
+ const descriptor =
248
+ Object.getOwnPropertyDescriptor(oldDelegate, key) ||
249
+ Object.getOwnPropertyDescriptor(Object.getPrototypeOf(oldDelegate), key);
250
+ const hasOwnDefault = Object.prototype.hasOwnProperty.call(newDelegate, key);
251
+ const currentValue = newDelegate[key];
252
+ const previousValue = oldDelegate[key];
253
+
254
+ // Skip Service instances - they should not be synced
255
+ if (previousValue instanceof Service) {
256
+ continue;
257
+ }
258
+
259
+ // Skip function properties that the new implementation also
260
+ // declares (a fresh redefinition should win over the old
261
+ // one). But a function-valued property the new
262
+ // implementation *doesn't* declare at all isn't a stale
263
+ // redefinition -- it's state a subclass installed on the
264
+ // delegate directly (subclasses of this service never go
265
+ // through this transform themselves, since their
266
+ // superclass isn't literally named Service; their own
267
+ // class fields land on the delegate via the proxy's
268
+ // defineProperty trap instead). Dropping it here would
269
+ // silently break e.g. an arrow-function class field on a
270
+ // subclass across a base-class HMR swap.
271
+ if (typeof previousValue === 'function' && hasOwnDefault) {
272
+ continue;
273
+ }
274
+
275
+ const shouldSync =
276
+ !!descriptor &&
277
+ (descriptor.writable || descriptor.set || Object.prototype.hasOwnProperty.call(oldDelegate, key)) &&
278
+ (!hasOwnDefault || currentValue === previousValue);
279
+
280
+ if (shouldSync) {
281
+ try {
282
+ newDelegate[key] = previousValue;
283
+ } catch (e) {
284
+ // Skip non-writable properties
285
+ }
286
+ }
287
+ }
288
+
289
+ newDelegate._hmrAccepted?.(oldDelegate);
290
+
291
+ if (oldDelegate.willDestroy) {
292
+ oldDelegate.willDestroy();
293
+ }
294
+ });
295
+ }
296
+ `;
297
+ const template = babel.template.statements(hmrCode);
298
+ programPath.pushContainer('body', template());
299
+ }
300
+ }
@@ -0,0 +1,46 @@
1
+ import type * as BabelTypesNamespace from '@babel/types';
2
+ import { Program } from '@babel/types';
3
+ import type * as Babel from '@babel/core';
4
+ import { NodeVisitor } from '@glimmer/syntax';
5
+ interface ASTPluginEnvironment {
6
+ locals: string[];
7
+ filename: string;
8
+ }
9
+ export interface HmrImportStatement {
10
+ local: string;
11
+ source: string;
12
+ specifier: string;
13
+ }
14
+ export interface HmrImportMetadata {
15
+ importVar: string;
16
+ bindings: string[];
17
+ importStatements: HmrImportStatement[];
18
+ }
19
+ export declare const hmrImportMetadataCache: Map<string, HmrImportMetadata>;
20
+ export declare function normalizeHmrCacheFilename(filename: string): string;
21
+ declare class HotAstProcessor {
22
+ options: {
23
+ itsStatic: boolean;
24
+ };
25
+ counter: number;
26
+ meta: {
27
+ locals: Set<string>;
28
+ importVar: string | null;
29
+ importBindings: Set<string>;
30
+ babelProgram?: Program;
31
+ };
32
+ didCreateImportClass: boolean;
33
+ constructor();
34
+ reset(): void;
35
+ transform(env: ASTPluginEnvironment): {
36
+ visitor: Record<string, unknown>;
37
+ };
38
+ buildVisitor({ importVar, importBindings, babelProgram, }: {
39
+ importVar: string;
40
+ importBindings: Set<string>;
41
+ babelProgram: Program;
42
+ }): NodeVisitor;
43
+ }
44
+ export declare const hotAstProcessor: HotAstProcessor;
45
+ export declare function finalizeTemplateImports(babel: typeof Babel, path: Babel.NodePath<BabelTypesNamespace.Program>, filename: string | undefined): void;
46
+ export {};
@@ -0,0 +1,250 @@
1
+ import * as glimmer from '@glimmer/syntax';
2
+ import { ImportUtil } from 'babel-import-util';
3
+ // Populated by finalizeTemplateImports below (once per file, per babel
4
+ // pass) and consumed by lib/hmr.ts's Vite `transform` hook, so that hook
5
+ // doesn't have to re-parse and re-traverse the same file from scratch just
6
+ // to recover metadata this babel pass already computed. Keyed by the file's
7
+ // path, normalized the same way on both sides (see normalizeHmrCacheFilename
8
+ // / lib/hmr.ts's normalizePath) since both run against the same on-disk
9
+ // file within the same Vite transform pipeline pass.
10
+ //
11
+ // This assumes babel never truly interleaves two files' Program visitors in
12
+ // a way that corrupts a single file's own derived data -- see the
13
+ // `hotAstProcessor.meta.babelProgram === path.node` guard at the write site
14
+ // below, which detects (and skips caching for) the case where a concurrent
15
+ // babel pass for a *different* file has reset the shared `hotAstProcessor`
16
+ // singleton in between. On any doubt, lib/hmr.ts falls back to its own
17
+ // from-source parse, which is always correct, just slower.
18
+ export const hmrImportMetadataCache = new Map();
19
+ export function normalizeHmrCacheFilename(filename) {
20
+ return filename.split('?')[0].replace(/\\/g, '/');
21
+ }
22
+ function computeImportStatements(programBody, bindings) {
23
+ const importStatements = [];
24
+ for (const statement of programBody) {
25
+ if (statement.type !== 'ImportDeclaration')
26
+ continue;
27
+ for (const specifier of statement.specifiers) {
28
+ const local = specifier.local.name;
29
+ if (!bindings.has(local))
30
+ continue;
31
+ let specifierName = 'default';
32
+ if (specifier.type === 'ImportSpecifier') {
33
+ const imported = specifier.imported;
34
+ specifierName =
35
+ imported.type === 'Identifier' ? imported.name : imported.value;
36
+ }
37
+ else if (specifier.type === 'ImportNamespaceSpecifier') {
38
+ specifierName = '*';
39
+ }
40
+ importStatements.push({
41
+ local,
42
+ source: statement.source.value,
43
+ specifier: specifierName,
44
+ });
45
+ }
46
+ }
47
+ return importStatements;
48
+ }
49
+ // Rewrites `<Foo />`/`{{foo}}`/`(helper "foo")`-style template references to
50
+ // a statically imported binding through a per-module `template__imports__`
51
+ // class instance instead, so swapping which module a virtual HMR wrapper
52
+ // resolves to (see lib/hmr.ts's `transform` hook) is visible to templates
53
+ // without re-evaluating the whole module. `transform` below is registered
54
+ // as an `ember-template-compilation` transform (see test-app/babel.config.mjs
55
+ // and tests/babel-plugin.test.js), separate from -- and running before --
56
+ // the js-level `hotReplaceAst` babel plugin in ../babel-plugin.ts, which is
57
+ // why the bookkeeping this class accumulates (`meta.importBindings` etc.) is
58
+ // read back later by that plugin's `Program` visitor (see
59
+ // `finalizeTemplateImports` below) to actually emit the `template__imports__`
60
+ // class declaration.
61
+ class HotAstProcessor {
62
+ constructor() {
63
+ this.options = {
64
+ itsStatic: false,
65
+ };
66
+ this.counter = 0;
67
+ this.meta = {
68
+ locals: new Set(),
69
+ importVar: null,
70
+ babelProgram: undefined,
71
+ importBindings: new Set(),
72
+ };
73
+ this.didCreateImportClass = false;
74
+ this.transform = this.transform.bind(this);
75
+ }
76
+ reset() {
77
+ this.meta.importVar = null;
78
+ this.meta.babelProgram = undefined;
79
+ this.meta.importBindings = new Set();
80
+ }
81
+ transform(env) {
82
+ var _a;
83
+ if (process.env.EMBER_VITE_HMR_ENABLED !== 'true') {
84
+ return {
85
+ visitor: {},
86
+ };
87
+ }
88
+ if ((_a = env.filename) === null || _a === void 0 ? void 0 : _a.includes('node_modules')) {
89
+ return {
90
+ visitor: {},
91
+ };
92
+ }
93
+ const meta = this.meta;
94
+ const importVar = env.meta.jsutils.bindExpression(meta.importVar || 'null', null, {
95
+ nameHint: 'template__imports__',
96
+ });
97
+ meta.importVar = meta.importVar || importVar;
98
+ return {
99
+ visitor: Object.assign({}, this.buildVisitor({
100
+ importVar,
101
+ importBindings: meta.importBindings,
102
+ babelProgram: meta.babelProgram,
103
+ })),
104
+ };
105
+ }
106
+ buildVisitor({ importVar, importBindings, babelProgram, }) {
107
+ const findImport = function findImport(specifier) {
108
+ return babelProgram.body.find((b) => b.type === 'ImportDeclaration' &&
109
+ b.specifiers.some((s) => s.local.name === specifier));
110
+ };
111
+ const findBlockParams = function (expression, p) {
112
+ if (p.node.type === 'Template') {
113
+ return false;
114
+ }
115
+ if (p.node &&
116
+ p.node.type === 'BlockStatement' &&
117
+ p.node.program.blockParams.includes(expression)) {
118
+ return true;
119
+ }
120
+ const node = p.node;
121
+ if (node && node.blockParams && node.blockParams.includes(expression)) {
122
+ return true;
123
+ }
124
+ if (!p.parent)
125
+ return false;
126
+ return findBlockParams(expression, p.parent);
127
+ };
128
+ const visitor = {
129
+ PathExpression: (node, p) => {
130
+ var _a, _b;
131
+ if ((((_a = p.parentNode) === null || _a === void 0 ? void 0 : _a.type) === 'SubExpression' ||
132
+ ((_b = p.parentNode) === null || _b === void 0 ? void 0 : _b.type) === 'MustacheStatement') &&
133
+ p.parentNode.params.includes(node)) {
134
+ return;
135
+ }
136
+ const original = node.original.split('.')[0];
137
+ if (original === 'this')
138
+ return;
139
+ if (original.startsWith('@'))
140
+ return;
141
+ if (original === 'block')
142
+ return;
143
+ if (original.startsWith('this.'))
144
+ return;
145
+ if (findBlockParams(original, p))
146
+ return;
147
+ if (node.original === 'helper' ||
148
+ node.original === 'component' ||
149
+ node.original === 'modifier') {
150
+ // node.original;
151
+ const parent = p.parentNode;
152
+ if (typeof parent.params[0].original !==
153
+ 'string') {
154
+ return;
155
+ }
156
+ const original = parent.params[0].original.split('.')[0];
157
+ if (original && findBlockParams(original, p))
158
+ return;
159
+ if (original === null || original === void 0 ? void 0 : original.includes('.'))
160
+ return;
161
+ if (!original)
162
+ return;
163
+ if (findImport(original)) {
164
+ const param = glimmer.builders.path(`${importVar}.${original}`);
165
+ parent.params.splice(0, 1, param);
166
+ importBindings.add(original);
167
+ }
168
+ return;
169
+ }
170
+ if (importVar) {
171
+ if (findImport(node.original)) {
172
+ node.original = `${importVar}.${node.original}`;
173
+ node.parts = node.original.split('.');
174
+ importBindings.add(original);
175
+ }
176
+ return;
177
+ }
178
+ },
179
+ ElementNode: (element, p) => {
180
+ const original = element.tag.split('.')[0];
181
+ if (findBlockParams(original, p))
182
+ return;
183
+ if (importVar) {
184
+ if (findImport(original)) {
185
+ element.tag = `${importVar}.${original}`;
186
+ p.node.tag = element.tag;
187
+ importBindings.add(original);
188
+ }
189
+ return;
190
+ }
191
+ },
192
+ };
193
+ return visitor;
194
+ }
195
+ }
196
+ export const hotAstProcessor = new HotAstProcessor();
197
+ // Emits the `template__imports__` class the templates rewritten by
198
+ // `HotAstProcessor.transform` above reference, once per module -- run from
199
+ // the js babel plugin's own `Program` visitor (not `HotAstProcessor` itself)
200
+ // since it needs `ImportUtil`/babel `types` to generate js AST, whereas the
201
+ // processor above only ever produces hbs AST.
202
+ export function finalizeTemplateImports(babel, path, filename) {
203
+ const t = babel.types;
204
+ if (!hotAstProcessor.meta.importVar ||
205
+ hotAstProcessor.meta.importBindings.size === 0) {
206
+ return;
207
+ }
208
+ const util = new ImportUtil(babel, path);
209
+ const tracked = util.import(path, '@glimmer/tracking', 'tracked');
210
+ util.import(path, '@glimmer/component', 'default');
211
+ const klass = t.classExpression(path.scope.generateUidIdentifier('Imports'), null, t.classBody([]));
212
+ const bindings = [...hotAstProcessor.meta.importBindings].sort();
213
+ for (const local of bindings) {
214
+ klass.body.body.push(t.classProperty(t.identifier(local), t.identifier(local), null, [
215
+ t.decorator(tracked),
216
+ ]));
217
+ }
218
+ const newExp = t.newExpression(klass, []);
219
+ const assign = t.assignmentExpression('=', t.identifier(hotAstProcessor.meta.importVar), newExp);
220
+ const varDeclaration = path.node.body.findIndex((e) => e.type === 'VariableDeclaration' &&
221
+ e.declarations[0].id.name ===
222
+ hotAstProcessor.meta.importVar) + 1;
223
+ const lastImportIndex = path.node.body.findLastIndex((e) => e.type === 'ImportDeclaration') + 1;
224
+ path.node.body.splice(Math.max(varDeclaration, lastImportIndex), 0, t.expressionStatement(assign));
225
+ // Export metadata about tracked imports for hmr.ts to use
226
+ const importMetadata = t.variableDeclaration('const', [
227
+ t.variableDeclarator(t.identifier('__hmr_import_metadata__'), t.objectExpression([
228
+ t.objectProperty(t.identifier('importVar'), t.stringLiteral(hotAstProcessor.meta.importVar)),
229
+ t.objectProperty(t.identifier('bindings'), t.arrayExpression(bindings.map((b) => t.stringLiteral(b)))),
230
+ ])),
231
+ ]);
232
+ const exportMetadata = t.exportNamedDeclaration(importMetadata, []);
233
+ path.node.body.push(exportMetadata);
234
+ // Share the metadata just computed above with lib/hmr.ts via the
235
+ // in-process cache (see hmrImportMetadataCache's own comment). Only trust
236
+ // `hotAstProcessor.meta` here if it's still the same Program node `pre()`
237
+ // set it up for -- if a concurrent babel pass for a *different* file
238
+ // reset the shared singleton in between (see hmrImportMetadataCache's doc
239
+ // comment), `meta.babelProgram` would point at that other file's AST (or
240
+ // be undefined) instead of `path.node`, and this file's data must not be
241
+ // cached under this file's key.
242
+ if (filename && hotAstProcessor.meta.babelProgram === path.node) {
243
+ hmrImportMetadataCache.set(normalizeHmrCacheFilename(filename), {
244
+ importVar: hotAstProcessor.meta.importVar,
245
+ bindings,
246
+ importStatements: computeImportStatements(path.node.body, hotAstProcessor.meta.importBindings),
247
+ });
248
+ }
249
+ path.scope.crawl();
250
+ }
@@ -1,34 +1,6 @@
1
1
  import { PluginObj } from '@babel/core';
2
- import { Program } from '@babel/types';
3
2
  import type * as Babel from '@babel/core';
4
- import { NodeVisitor } from '@glimmer/syntax';
5
- interface ASTPluginEnvironment {
6
- locals: string[];
7
- filename: string;
8
- }
9
- declare class HotAstProcessor {
10
- options: {
11
- itsStatic: boolean;
12
- };
13
- counter: number;
14
- meta: {
15
- locals: Set<string>;
16
- importVar: string | null;
17
- importBindings: Set<string>;
18
- babelProgram?: Program;
19
- };
20
- didCreateImportClass: boolean;
21
- constructor();
22
- reset(): void;
23
- transform(env: ASTPluginEnvironment): {
24
- visitor: Record<string, unknown>;
25
- };
26
- buildVisitor({ importVar, importBindings, babelProgram, }: {
27
- importVar: string;
28
- importBindings: Set<string>;
29
- babelProgram: Program;
30
- }): NodeVisitor;
31
- }
32
- export declare const hotAstProcessor: HotAstProcessor;
3
+ import { hotAstProcessor, hmrImportMetadataCache, normalizeHmrCacheFilename } from './babel-plugin/template-imports.js';
4
+ export type { HmrImportStatement, HmrImportMetadata, } from './babel-plugin/template-imports.js';
5
+ export { hotAstProcessor, hmrImportMetadataCache, normalizeHmrCacheFilename };
33
6
  export default function hotReplaceAst(babel: typeof Babel): PluginObj;
34
- export {};