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.
- package/README.md +31 -0
- package/dist/instance-initializers/setup-hmr-manager.js +42 -3
- package/dist/instance-initializers/setup-hmr-manager.js.map +1 -1
- package/dist/lib/babel-plugin/private-members.d.ts +3 -0
- package/dist/lib/babel-plugin/private-members.js +291 -0
- package/dist/lib/babel-plugin/service-proxy.d.ts +3 -0
- package/dist/lib/babel-plugin/service-proxy.js +300 -0
- package/dist/lib/babel-plugin/template-imports.d.ts +46 -0
- package/dist/lib/babel-plugin/template-imports.js +250 -0
- package/dist/lib/babel-plugin.d.ts +3 -31
- package/dist/lib/babel-plugin.js +45 -352
- package/dist/lib/hmr.js +337 -124
- package/package.json +6 -5
|
@@ -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 {
|
|
5
|
-
|
|
6
|
-
|
|
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 {};
|