ember-vite-hmr 2.4.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/instance-initializers/setup-hmr-manager.js +64 -19
- package/dist/instance-initializers/setup-hmr-manager.js.map +1 -1
- package/dist/lib/babel-plugin/private-members.js +20 -12
- package/dist/lib/babel-plugin/service-proxy.js +2 -2
- package/dist/lib/babel-plugin/template-imports.js +16 -5
- package/dist/lib/babel-plugin.js +57 -1
- package/dist/lib/hmr.d.ts +3 -1
- package/dist/lib/hmr.js +317 -564
- package/package.json +10 -10
package/dist/lib/hmr.js
CHANGED
|
@@ -9,428 +9,198 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
};
|
|
10
10
|
import path from 'path';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
|
-
import { parseSync } from '@babel/core';
|
|
13
|
-
import traverseModule from '@babel/traverse';
|
|
14
|
-
const traverse = traverseModule.default || traverseModule;
|
|
15
12
|
import { readFile } from 'fs/promises';
|
|
16
13
|
import { hmrImportMetadataCache } from './babel-plugin.js';
|
|
17
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
15
|
const __dirname = path.dirname(__filename);
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// (
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return `{{#if (has-block '${y}')}}${generateInvocations(rest, [...selected, y])}{{else}}${generateInvocations(rest, selected)}{{/if}}`;
|
|
53
|
-
}
|
|
54
|
-
function generateContent(yields) {
|
|
55
|
-
if (!yields.includes('default')) {
|
|
56
|
-
yields.push('default');
|
|
57
|
-
}
|
|
58
|
-
if (yields.length > MAX_COMBINATORIAL_YIELDS) {
|
|
59
|
-
let all = yields.map((y) => `(has-block '${y}')`).join(' ');
|
|
60
|
-
return `
|
|
61
|
-
{{#if (notAny ${all})}}
|
|
62
|
-
<this.curried @__hot__={{this.hot}} ...attributes />
|
|
63
|
-
{{else}}
|
|
64
|
-
${invocation(yields)}
|
|
65
|
-
{{/if}}
|
|
66
|
-
`;
|
|
67
|
-
}
|
|
68
|
-
return generateInvocations(yields, []);
|
|
16
|
+
// Embroider's resolver registry. It's the binding the app entry's HMR hook
|
|
17
|
+
// mutates, so we use it to locate that entry. Embroider keeps this as an
|
|
18
|
+
// internal literal (not a public export), so we mirror the string here.
|
|
19
|
+
const compatModulesSpecifier = '@embroider/virtual/compat-modules';
|
|
20
|
+
export const hmrRuntimeId = 'virtual:ember-vite-hmr-runtime';
|
|
21
|
+
const resolvedRuntimeId = '\0' + hmrRuntimeId;
|
|
22
|
+
// This runs in the browser.
|
|
23
|
+
// It has no imports on purpose: a virtual module has no location on disk,
|
|
24
|
+
// so Embroider's resolver has nothing to resolve `@glimmer/tracking` against.
|
|
25
|
+
// The app modules that call `define` can import `tracked` normally and pass
|
|
26
|
+
// it in.
|
|
27
|
+
//
|
|
28
|
+
// `byValue` lets an old reference to a component class find its current
|
|
29
|
+
// registry entry, so modules holding stale bindings still serve the newest
|
|
30
|
+
// version.
|
|
31
|
+
//
|
|
32
|
+
// `consumed` lets a self-accepting module that nobody rendered through
|
|
33
|
+
// fall back to `invalidate()` rather than silently doing nothing.
|
|
34
|
+
//
|
|
35
|
+
// `tracked` is called as a function (not a decorator syntax), so the runtime
|
|
36
|
+
// doesn't depend on the app's Babel decorator config.
|
|
37
|
+
const runtimeSource = `
|
|
38
|
+
const entries = new WeakMap();
|
|
39
|
+
|
|
40
|
+
// Per-class FIFO queues of live component instances, used to pair old instances
|
|
41
|
+
// (being destroyed by an HMR swap) with the new instances that replace them.
|
|
42
|
+
// Keyed by the NEW class (the replacement). Cleared at update() time so that
|
|
43
|
+
// instances created by normal user-flow navigation before the swap are never
|
|
44
|
+
// in the queue — only instances created after the swap callback fires.
|
|
45
|
+
const liveInstanceQueues = new WeakMap();
|
|
46
|
+
|
|
47
|
+
function isRef(value) {
|
|
48
|
+
return value !== null && (typeof value === 'object' || typeof value === 'function');
|
|
69
49
|
}
|
|
70
|
-
const getHotComponent = (imp, specifier, yields) => `
|
|
71
|
-
import { ${specifier} as TargetComponent } from "${imp}";
|
|
72
|
-
import Component from "@glimmer/component";
|
|
73
|
-
import { tracked } from "@glimmer/tracking";
|
|
74
|
-
import { createComputeRef } from "@glimmer/reference";
|
|
75
|
-
import { curry } from '@glimmer/runtime';
|
|
76
|
-
import { registerDestructor } from '@ember/destroyable';
|
|
77
|
-
import { getInternalComponentManager, setInternalComponentManager } from '@glimmer/manager';
|
|
78
50
|
|
|
79
|
-
function
|
|
80
|
-
|
|
51
|
+
function makeEntry(tracked, value) {
|
|
52
|
+
class Entry {}
|
|
53
|
+
const desc = tracked(Entry.prototype, 'current', { configurable: true, enumerable: true, writable: true, initializer: null });
|
|
54
|
+
Object.defineProperty(Entry.prototype, 'current', desc);
|
|
55
|
+
const entry = new Entry();
|
|
56
|
+
entry.current = value;
|
|
57
|
+
entry.consumed = false;
|
|
58
|
+
return entry;
|
|
81
59
|
}
|
|
82
60
|
|
|
83
|
-
|
|
61
|
+
// Register value for the first time. No-op if already registered.
|
|
62
|
+
export function register(value, tracked) {
|
|
63
|
+
if (!isRef(value)) return;
|
|
64
|
+
if (!entries.has(value)) {
|
|
65
|
+
entries.set(value, makeEntry(tracked, value));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
84
68
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
69
|
+
// Re-point an existing entry from oldValue to newValue.
|
|
70
|
+
// Also registers newValue under the same entry so future current() calls
|
|
71
|
+
// on newValue resolve to whatever is current at that point.
|
|
72
|
+
//
|
|
73
|
+
// Clearing liveInstanceQueues[newValue] here is the key to correctness for
|
|
74
|
+
// syncState across user-flow create/destroy cycles. Any instances of newValue
|
|
75
|
+
// created and destroyed by normal navigation *before* this swap happened are
|
|
76
|
+
// stale. By resetting the queue at swap time, only instances created *after*
|
|
77
|
+
// this update() call (the replacement instances Glimmer is about to create)
|
|
78
|
+
// are eligible to receive state from the old instances being torn down.
|
|
79
|
+
export function update(oldValue, newValue) {
|
|
80
|
+
if (!isRef(oldValue)) return;
|
|
81
|
+
const entry = entries.get(oldValue);
|
|
82
|
+
if (entry) {
|
|
83
|
+
entry.current = newValue;
|
|
84
|
+
if (isRef(newValue)) {
|
|
85
|
+
entries.set(newValue, entry);
|
|
86
|
+
// Clear the queue for newValue so only instances created after this
|
|
87
|
+
// swap are paired with old instances being torn down.
|
|
88
|
+
liveInstanceQueues.delete(newValue);
|
|
90
89
|
}
|
|
91
|
-
|
|
90
|
+
// oldValue is being retired — it will never be the target of a future
|
|
91
|
+
// dequeueInstance() call, so any queued instances for it are now stale.
|
|
92
|
+
liveInstanceQueues.delete(oldValue);
|
|
93
|
+
}
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
export function current(value) {
|
|
97
|
+
if (!isRef(value)) return value;
|
|
98
|
+
const entry = entries.get(value);
|
|
99
|
+
if (!entry) return value;
|
|
100
|
+
entry.consumed = true;
|
|
101
|
+
return entry.current;
|
|
102
|
+
}
|
|
99
103
|
|
|
100
|
-
export
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
super(owner, args);
|
|
105
|
-
const capturedArgs = capturedArgsStack[capturedArgsStack.length - 1];
|
|
106
|
-
const named = {};
|
|
107
|
-
const positional = [];
|
|
108
|
-
for (const name of Object.keys(args)) {
|
|
109
|
-
// Forward the caller's original, updatable reference (e.g. a path like
|
|
110
|
-
// "this.value") instead of wrapping the reified value in a fresh
|
|
111
|
-
// read-only compute ref. A read-only ref fails Ember's "You can only
|
|
112
|
-
// pass a path to mut" check, breaking {{mut @arg}} in wrapped components.
|
|
113
|
-
named[name] = capturedArgs?.named[name] ?? createComputeRef(() => args[name]);
|
|
114
|
-
}
|
|
115
|
-
const CurriedComponent = 0;
|
|
116
|
-
// After an accepted update this module is not re-evaluated, so the static
|
|
117
|
-
// TargetComponent binding still points at the pre-update module. Instances
|
|
118
|
-
// created after the update must curry the latest accepted class instead.
|
|
119
|
-
const Target = import.meta.hot?.data.latestModule?.default ?? TargetComponent;
|
|
120
|
-
this.curried = curry(CurriedComponent, Target, owner, { positional, named});
|
|
121
|
-
if (import.meta.hot) {
|
|
122
|
-
const callback = (module) => {
|
|
123
|
-
this.curried = curry(CurriedComponent, module.default, owner, { positional, named});
|
|
124
|
-
};
|
|
125
|
-
hotCallbacks.add(callback);
|
|
126
|
-
registerDestructor(this, () => {
|
|
127
|
-
hotCallbacks.delete(callback);
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
104
|
+
export function used(value) {
|
|
105
|
+
if (!isRef(value)) return false;
|
|
106
|
+
return Boolean(entries.get(value)?.consumed);
|
|
107
|
+
}
|
|
131
108
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
109
|
+
// Returns true if value has been registered with the HMR runtime.
|
|
110
|
+
// Used by setup-hmr-manager.ts to skip non-HMR components when building
|
|
111
|
+
// the liveInstances queue so stale entries don't corrupt later HMR swaps.
|
|
112
|
+
export function isHmrClass(value) {
|
|
113
|
+
return isRef(value) && entries.has(value);
|
|
135
114
|
}
|
|
136
115
|
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const shadowManager = Object.create(defaultManager);
|
|
147
|
-
shadowManager.create = function (owner, definition, vmArgs) {
|
|
148
|
-
capturedArgsStack.push(vmArgs.capture());
|
|
149
|
-
try {
|
|
150
|
-
return defaultManager.create(owner, definition, vmArgs);
|
|
151
|
-
} finally {
|
|
152
|
-
capturedArgsStack.pop();
|
|
116
|
+
// Called from setup-hmr-manager.ts's create() hook for each new HMR-registered
|
|
117
|
+
// component instance. Appends to the per-class queue in DOM (creation) order.
|
|
118
|
+
export function enqueueInstance(klass, instance) {
|
|
119
|
+
if (!isRef(klass)) return;
|
|
120
|
+
const q = liveInstanceQueues.get(klass);
|
|
121
|
+
if (q) {
|
|
122
|
+
q.push(instance);
|
|
123
|
+
} else {
|
|
124
|
+
liveInstanceQueues.set(klass, [instance]);
|
|
153
125
|
}
|
|
154
|
-
};
|
|
155
|
-
// Glimmer's debug render tree normally adds one 'component' node per
|
|
156
|
-
// invoked component instance (see VM_GET_COMPONENT_SELF_OP). Returning no
|
|
157
|
-
// nodes here suppresses that node for HotComponent itself, so only the
|
|
158
|
-
// real, curried target component (invoked in HotComponent's own template)
|
|
159
|
-
// shows up in ember-inspector's component tree - this is the same hook
|
|
160
|
-
// Ember core uses internally to keep its own {{outlet}}/{{mount}} wrapper
|
|
161
|
-
// machinery out of (or relabeled in) that tree (see OutletComponentManager
|
|
162
|
-
// and MountManager in ember-source).
|
|
163
|
-
shadowManager.getDebugCustomRenderTree = function () {
|
|
164
|
-
return [];
|
|
165
|
-
};
|
|
166
|
-
setInternalComponentManager(shadowManager, HotComponent);
|
|
167
|
-
`;
|
|
168
|
-
const cachedYields = {};
|
|
169
|
-
function getYieldsFromFile(filename, content, noCache) {
|
|
170
|
-
if (cachedYields[filename] && !noCache) {
|
|
171
|
-
return cachedYields[filename];
|
|
172
|
-
}
|
|
173
|
-
// very basic, todo: make this use AST
|
|
174
|
-
const matches = content.matchAll(/to=['"\\]+(\w+)['"\\]+/g);
|
|
175
|
-
const yields = new Set([...matches].map((m) => m === null || m === void 0 ? void 0 : m[1]).filter((m) => !!m));
|
|
176
|
-
if (noCache) {
|
|
177
|
-
return {
|
|
178
|
-
yields,
|
|
179
|
-
modules: [],
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
cachedYields[filename] = {
|
|
183
|
-
yields,
|
|
184
|
-
modules: [],
|
|
185
|
-
};
|
|
186
|
-
return cachedYields[filename];
|
|
187
|
-
}
|
|
188
|
-
function difference(a, b) {
|
|
189
|
-
const diff = [];
|
|
190
|
-
for (const bElement of b) {
|
|
191
|
-
if (!a.has(bElement)) {
|
|
192
|
-
diff.push(bElement);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return diff;
|
|
196
126
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
127
|
+
|
|
128
|
+
// Called from setup-hmr-manager.ts's willDestroy hook. Returns and removes the
|
|
129
|
+
// oldest queued instance for klass (FIFO = DOM order), or null if none.
|
|
130
|
+
export function dequeueInstance(klass) {
|
|
131
|
+
if (!isRef(klass)) return null;
|
|
132
|
+
const q = liveInstanceQueues.get(klass);
|
|
133
|
+
if (!q || q.length === 0) return null;
|
|
134
|
+
const instance = q.shift();
|
|
135
|
+
if (q.length === 0) liveInstanceQueues.delete(klass);
|
|
136
|
+
return instance ?? null;
|
|
201
137
|
}
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
function
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
138
|
+
|
|
139
|
+
// Called from setup-hmr-manager.ts's willDestroy hook when no HMR swap is in
|
|
140
|
+
// progress (normal navigation destroy). Removes this specific instance from the
|
|
141
|
+
// queue so destroyed instances don't accumulate indefinitely when a timer
|
|
142
|
+
// mounts/unmounts the same component repeatedly between saves.
|
|
143
|
+
export function removeInstance(klass, instance) {
|
|
144
|
+
if (!isRef(klass)) return;
|
|
145
|
+
const q = liveInstanceQueues.get(klass);
|
|
146
|
+
if (!q) return;
|
|
147
|
+
const idx = q.indexOf(instance);
|
|
148
|
+
if (idx !== -1) q.splice(idx, 1);
|
|
149
|
+
if (q.length === 0) liveInstanceQueues.delete(klass);
|
|
212
150
|
}
|
|
213
|
-
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
code: false,
|
|
230
|
-
configFile: false,
|
|
231
|
-
babelrc: false,
|
|
232
|
-
plugins: [
|
|
233
|
-
['@babel/plugin-syntax-typescript', { isTSX: true }],
|
|
234
|
-
['@babel/plugin-proposal-decorators', { version: '2022-03' }],
|
|
235
|
-
],
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
catch (_a) {
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
if (!result) {
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
// Prefer the default-exported class specifically (that's always the
|
|
245
|
-
// component itself) over "the first class with a superclass in the file",
|
|
246
|
-
// since a file can declare other, unrelated classes above it (a local
|
|
247
|
-
// helper class, etc.) that would otherwise be matched instead.
|
|
248
|
-
let superName = null;
|
|
249
|
-
traverse(result, {
|
|
250
|
-
ExportDefaultDeclaration(path) {
|
|
251
|
-
const declaration = path.node.declaration;
|
|
252
|
-
let classNode = declaration;
|
|
253
|
-
if (declaration.type === 'Identifier' && declaration.name) {
|
|
254
|
-
const binding = path.scope.getBinding(declaration.name);
|
|
255
|
-
if ((binding === null || binding === void 0 ? void 0 : binding.path.isClassDeclaration()) ||
|
|
256
|
-
(binding === null || binding === void 0 ? void 0 : binding.path.isClassExpression())) {
|
|
257
|
-
classNode = binding.path.node;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
const superClass = classNode.superClass;
|
|
261
|
-
if ((superClass === null || superClass === void 0 ? void 0 : superClass.type) === 'Identifier' && superClass.name) {
|
|
262
|
-
superName = superClass.name;
|
|
151
|
+
|
|
152
|
+
// Expose helpers on the global so setup-hmr-manager.ts's synchronous
|
|
153
|
+
// initialize() can call them without a dynamic import.
|
|
154
|
+
globalThis.__ember_vite_hmr = { current, isHmrClass, enqueueInstance, dequeueInstance, removeInstance };
|
|
155
|
+
`;
|
|
156
|
+
// `enforce: 'pre'` makes this run before Embroider's resolver, which would
|
|
157
|
+
// otherwise try to resolve the virtual id as a package and fail.
|
|
158
|
+
// `apply: 'serve'` keeps it out of production builds entirely.
|
|
159
|
+
export function hmrRuntime() {
|
|
160
|
+
return {
|
|
161
|
+
name: 'ember-vite-hmr-runtime',
|
|
162
|
+
enforce: 'pre',
|
|
163
|
+
apply: 'serve',
|
|
164
|
+
resolveId(source) {
|
|
165
|
+
if (source === hmrRuntimeId) {
|
|
166
|
+
return resolvedRuntimeId;
|
|
263
167
|
}
|
|
264
168
|
},
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// (e.g. a non-default-exported component, or an export form the check
|
|
269
|
-
// above doesn't specifically handle).
|
|
270
|
-
traverse(result, {
|
|
271
|
-
'ClassDeclaration|ClassExpression'(path) {
|
|
272
|
-
if (superName) {
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
const superClass = path.node.superClass;
|
|
276
|
-
if ((superClass === null || superClass === void 0 ? void 0 : superClass.type) === 'Identifier' && superClass.name) {
|
|
277
|
-
superName = superClass.name;
|
|
278
|
-
}
|
|
279
|
-
},
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
if (!superName) {
|
|
283
|
-
return null;
|
|
284
|
-
}
|
|
285
|
-
let source = null;
|
|
286
|
-
traverse(result, {
|
|
287
|
-
ImportDeclaration(path) {
|
|
288
|
-
if (source) {
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
if (path.node.specifiers.some((s) => s.local.name === superName)) {
|
|
292
|
-
source = path.node.source.value;
|
|
169
|
+
load(id) {
|
|
170
|
+
if (id === resolvedRuntimeId) {
|
|
171
|
+
return runtimeSource;
|
|
293
172
|
}
|
|
294
173
|
},
|
|
295
|
-
}
|
|
296
|
-
return source;
|
|
297
|
-
}
|
|
298
|
-
// If `content` (already run through `server.transformRequest`) registers a
|
|
299
|
-
// template on itself - either inline, or via a colocated `.hbs` import -
|
|
300
|
-
// returns that template's own source. Returns null when this file has no
|
|
301
|
-
// template of its own to scan, meaning any named blocks it uses have to be
|
|
302
|
-
// found on an ancestor class instead (see resolveYieldSource).
|
|
303
|
-
function resolveOwnTemplateSource(server, base, filename, content) {
|
|
304
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
305
|
-
var _a;
|
|
306
|
-
// For a classic component with a separately resolved template (a colocated
|
|
307
|
-
// `.hbs` file next to a backing class, or a template-only component), the
|
|
308
|
-
// compiled backing module only *imports* its template (e.g. `import TEMPLATE
|
|
309
|
-
// from "./foo.hbs?import"`); it doesn't inline it. Any {{yield ... to="..."}}
|
|
310
|
-
// usage lives in that template file, so it has to be fetched and scanned
|
|
311
|
-
// separately, otherwise named blocks other than "default" are never detected
|
|
312
|
-
// and get silently dropped by the generated hot-reload wrapper.
|
|
313
|
-
const templateImportMatch = content.match(/\bfrom\s*['"]([^'"]+\.hbs(?:\?[^'"]*)?)['"]/);
|
|
314
|
-
if (templateImportMatch) {
|
|
315
|
-
const templateSpecifier = stripBase(templateImportMatch[1], base);
|
|
316
|
-
const templateRes = yield server.transformRequest(templateSpecifier);
|
|
317
|
-
return { filename: templateSpecifier, content: (_a = templateRes === null || templateRes === void 0 ? void 0 : templateRes.code) !== null && _a !== void 0 ? _a : '' };
|
|
318
|
-
}
|
|
319
|
-
if (content.includes('setComponentTemplate(')) {
|
|
320
|
-
return { filename, content };
|
|
321
|
-
}
|
|
322
|
-
return null;
|
|
323
|
-
});
|
|
174
|
+
};
|
|
324
175
|
}
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
// by resolveOwnTemplateSource). But a component with no template of its own
|
|
329
|
-
// renders whatever template is registered on its nearest ancestor class (see
|
|
330
|
-
// resolveOwnTemplateSource's doc comment) - previously this only ever
|
|
331
|
-
// scanned the child's own (template-less) file, silently dropping every
|
|
332
|
-
// named block other than "default" for any component subclassed without its
|
|
333
|
-
// own template. This walks the `extends` chain, resolving each ancestor's
|
|
334
|
-
// import the same way Vite already resolved it in the already-transformed
|
|
335
|
-
// source, until one with its own template is found.
|
|
336
|
-
function resolveYieldSource(server, base, filename, content) {
|
|
337
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
338
|
-
const own = yield resolveOwnTemplateSource(server, base, filename, content);
|
|
339
|
-
if (own) {
|
|
340
|
-
return own;
|
|
341
|
-
}
|
|
342
|
-
const seen = new Set([filename]);
|
|
343
|
-
let currentContent = content;
|
|
344
|
-
for (let i = 0; i < MAX_EXTENDS_DEPTH; i++) {
|
|
345
|
-
const superSource = findSuperclassImportSource(currentContent);
|
|
346
|
-
if (!superSource) {
|
|
347
|
-
break;
|
|
348
|
-
}
|
|
349
|
-
const superSpecifier = stripBase(superSource, base);
|
|
350
|
-
// Framework/addon base classes live in node_modules and are never
|
|
351
|
-
// hot-tracked by this plugin (see the node_modules skip in `transform`);
|
|
352
|
-
// stop there rather than walking into vendored internals.
|
|
353
|
-
if (seen.has(superSpecifier) || superSpecifier.includes('node_modules')) {
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
seen.add(superSpecifier);
|
|
357
|
-
let superContent;
|
|
358
|
-
try {
|
|
359
|
-
// Unlike the `.hbs` specifier resolveOwnTemplateSource requests
|
|
360
|
-
// (always something Vite itself already resolved for a file we
|
|
361
|
-
// successfully transformed), this specifier can be anything a
|
|
362
|
-
// superclass import resolved to, including forms
|
|
363
|
-
// `transformRequest` can't handle. Failing to resolve an ancestor's
|
|
364
|
-
// template is the same "no named blocks found" outcome as before
|
|
365
|
-
// this fix existed - it must never fail the whole component load.
|
|
366
|
-
const superRes = yield server.transformRequest(superSpecifier);
|
|
367
|
-
superContent = superRes === null || superRes === void 0 ? void 0 : superRes.code;
|
|
368
|
-
}
|
|
369
|
-
catch (_a) {
|
|
370
|
-
break;
|
|
371
|
-
}
|
|
372
|
-
if (!superContent) {
|
|
373
|
-
break;
|
|
374
|
-
}
|
|
375
|
-
const superOwn = yield resolveOwnTemplateSource(server, base, superSpecifier, superContent);
|
|
376
|
-
if (superOwn) {
|
|
377
|
-
return superOwn;
|
|
378
|
-
}
|
|
379
|
-
currentContent = superContent;
|
|
380
|
-
}
|
|
381
|
-
return { filename, content };
|
|
382
|
-
});
|
|
176
|
+
// Helper function to normalize paths consistently across platforms
|
|
177
|
+
function normalizePath(inputPath) {
|
|
178
|
+
return inputPath.replace(/\\/g, '/');
|
|
383
179
|
}
|
|
384
|
-
const virtualPrefix = '/ember-vite-hmr/virtual/component:';
|
|
385
|
-
// Embroider's resolver registry. It's the binding the app entry's HMR hook
|
|
386
|
-
// mutates, so we use it to locate that entry. Embroider keeps this as an
|
|
387
|
-
// internal literal (not a public export), so we mirror the string here.
|
|
388
|
-
const compatModulesSpecifier = '@embroider/virtual/compat-modules';
|
|
389
180
|
export function hmr(enableViteHmrForModes = ['development']) {
|
|
390
181
|
let base = '/';
|
|
182
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
391
183
|
let server;
|
|
392
|
-
|
|
184
|
+
const mainPlugin = {
|
|
393
185
|
name: 'hmr-plugin',
|
|
394
186
|
enforce: 'post',
|
|
395
187
|
config(config, env) {
|
|
396
188
|
var _a;
|
|
397
|
-
// The per-component hot wrapper (see getHotComponent) imports the first
|
|
398
|
-
// four of these, but it is generated/served on demand, so it is never
|
|
399
|
-
// part of the static module graph Vite's dependency scanner crawls on
|
|
400
|
-
// the first pass. `@ember/component/template-only` has the same
|
|
401
|
-
// problem for a different reason: it's injected by the template
|
|
402
|
-
// compiler into a template-only component's compiled output, which the
|
|
403
|
-
// scanner (source-only) never sees either. Without pre-declaring them,
|
|
404
|
-
// the browser requests them on boot, Vite discovers "new" deps and
|
|
405
|
-
// triggers a re-optimize + full page reload.
|
|
406
|
-
//
|
|
407
|
-
// We must use the Embroider-rewritten `ember-source/...` subpaths: the
|
|
408
|
-
// bare `@glimmer/reference` etc. specifiers the wrapper writes cannot be
|
|
409
|
-
// resolved by optimizeDeps.include. (`@glimmer/component` and
|
|
410
|
-
// `@glimmer/tracking` are omitted on purpose — normal app code already
|
|
411
|
-
// pulls them into the scan.)
|
|
412
189
|
if (!enableViteHmrForModes.includes(env.mode)) {
|
|
413
190
|
return;
|
|
414
191
|
}
|
|
415
192
|
// With `optimizeDeps.noDiscovery: true`, Vite never scans the app's own
|
|
416
|
-
// source, so nothing else pulls these
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
// There's nothing useful this hook can pre-declare in that mode, so
|
|
422
|
-
// skip it entirely rather than fight the user's own dep-optimization
|
|
423
|
-
// config.
|
|
193
|
+
// source, so nothing else pulls these glimmer subpaths into the optimizer.
|
|
194
|
+
// Forcing them into `include` then creates a second, separately pre-bundled
|
|
195
|
+
// copy of the glimmer VM alongside the unoptimized one the rest of the
|
|
196
|
+
// (unscanned) app actually uses, leading to "The global context for Glimmer
|
|
197
|
+
// VM was not set" (#554). Skip entirely in that mode.
|
|
424
198
|
if ((_a = config.optimizeDeps) === null || _a === void 0 ? void 0 : _a.noDiscovery) {
|
|
425
199
|
return;
|
|
426
200
|
}
|
|
427
201
|
return {
|
|
428
202
|
optimizeDeps: {
|
|
429
203
|
include: [
|
|
430
|
-
'ember-source/@glimmer/reference/index.js',
|
|
431
|
-
'ember-source/@glimmer/runtime/index.js',
|
|
432
|
-
'ember-source/@ember/destroyable/index.js',
|
|
433
|
-
'ember-source/@glimmer/manager/index.js',
|
|
434
204
|
'ember-source/@ember/component/template-only.js',
|
|
435
205
|
],
|
|
436
206
|
},
|
|
@@ -452,22 +222,6 @@ export function hmr(enableViteHmrForModes = ['development']) {
|
|
|
452
222
|
if (id.includes('@ember-vite-hmr/setup-ember-hmr.js')) {
|
|
453
223
|
return id;
|
|
454
224
|
}
|
|
455
|
-
if (importer === null || importer === void 0 ? void 0 : importer.startsWith(virtualPrefix)) {
|
|
456
|
-
const newImporter = path.join(process.cwd(), 'package.json');
|
|
457
|
-
importer = newImporter;
|
|
458
|
-
return this.resolve(id, importer, meta);
|
|
459
|
-
}
|
|
460
|
-
if (id.startsWith(virtualPrefix)) {
|
|
461
|
-
let [imp, specifier] = id
|
|
462
|
-
.split('?')[0]
|
|
463
|
-
.slice(virtualPrefix.length)
|
|
464
|
-
.split('::');
|
|
465
|
-
if (imp === null || imp === void 0 ? void 0 : imp.startsWith('.')) {
|
|
466
|
-
const r = yield this.resolve(imp, importer);
|
|
467
|
-
return id.replace(`${imp}::${specifier}`, `${r.id}::${specifier}`);
|
|
468
|
-
}
|
|
469
|
-
return id;
|
|
470
|
-
}
|
|
471
225
|
if (id === '/ember-vite-hmr/services/vite-hot-reload') {
|
|
472
226
|
return this.resolve('ember-vite-hmr/services/vite-hot-reload', path.resolve(process.cwd(), 'package.json'), meta);
|
|
473
227
|
}
|
|
@@ -478,32 +232,6 @@ export function hmr(enableViteHmrForModes = ['development']) {
|
|
|
478
232
|
if (id.includes('@ember-vite-hmr/setup-ember-hmr.js')) {
|
|
479
233
|
return yield readFile(path.resolve(__dirname, '..', 'setup-ember-hmr.js'), 'utf8');
|
|
480
234
|
}
|
|
481
|
-
if (id.startsWith(virtualPrefix)) {
|
|
482
|
-
if (!server) {
|
|
483
|
-
// During build, server is not available
|
|
484
|
-
return null;
|
|
485
|
-
}
|
|
486
|
-
let [imp, specifier] = id
|
|
487
|
-
.split('?')[0]
|
|
488
|
-
.slice(virtualPrefix.length, -'.gts'.length)
|
|
489
|
-
.split('::');
|
|
490
|
-
imp = imp.replace('embroider_virtual', '@embroider/virtual');
|
|
491
|
-
let filename = imp;
|
|
492
|
-
if (filename.includes('__vpc__')) {
|
|
493
|
-
filename = filename.split('__vpc__')[0];
|
|
494
|
-
}
|
|
495
|
-
const res = yield server.transformRequest(filename);
|
|
496
|
-
const content = res === null || res === void 0 ? void 0 : res.code;
|
|
497
|
-
const { filename: yieldSourceFilename, content: yieldSourceContent } = yield resolveYieldSource(server, base, filename, content !== null && content !== void 0 ? content : '');
|
|
498
|
-
const resId = yield this.resolve(yieldSourceFilename, path.resolve(process.cwd(), 'package.json'));
|
|
499
|
-
// Strip any query string so the cache key matches the plain file path that
|
|
500
|
-
// `hotUpdate` below receives when that template file is edited.
|
|
501
|
-
const cacheKey = resId.id.split('?')[0];
|
|
502
|
-
const cached = getYieldsFromFile(cacheKey, yieldSourceContent !== null && yieldSourceContent !== void 0 ? yieldSourceContent : '');
|
|
503
|
-
const yields = cached.yields;
|
|
504
|
-
cached.modules.push(id);
|
|
505
|
-
return getHotComponent(imp, specifier, [...yields].filter((y) => !!y));
|
|
506
|
-
}
|
|
507
235
|
});
|
|
508
236
|
},
|
|
509
237
|
transformIndexHtml(html) {
|
|
@@ -522,49 +250,69 @@ export function hmr(enableViteHmrForModes = ['development']) {
|
|
|
522
250
|
];
|
|
523
251
|
},
|
|
524
252
|
handleHotUpdate(ctx) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
253
|
+
var _a, _b;
|
|
254
|
+
const file = ctx.file.split('?')[0];
|
|
528
255
|
const otherModules = [];
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (
|
|
537
|
-
|
|
256
|
+
// ── .hbs pair-component wiring (unchanged) ───────────────────────────
|
|
257
|
+
if (file.endsWith('.hbs')) {
|
|
258
|
+
const pairedModule = ctx.modules.find((m) => [...m.importers].find((i) => i.id.startsWith('embroider_virtual') &&
|
|
259
|
+
i.id.endsWith('-embroider-pair-component')));
|
|
260
|
+
if (pairedModule) {
|
|
261
|
+
const pairComponent = [...pairedModule.importers].find((i) => i.id.startsWith('embroider_virtual') &&
|
|
262
|
+
i.id.endsWith('-embroider-pair-component'));
|
|
263
|
+
if (pairComponent) {
|
|
264
|
+
const componentModule = [...pairComponent.clientImportedModules].find((cim) => cim.id.split('?')[0].match(/\/component\.(js|ts|gjs|gts)/));
|
|
265
|
+
if (componentModule) {
|
|
266
|
+
otherModules.push(componentModule);
|
|
267
|
+
}
|
|
538
268
|
}
|
|
539
269
|
}
|
|
270
|
+
return [...ctx.modules, ...otherModules];
|
|
540
271
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
272
|
+
// ── subclass propagation ──────────────────────────────────────────────
|
|
273
|
+
// When a component file (e.g. block-base.gts) self-accepts, Vite stops
|
|
274
|
+
// propagation before any subclass module (e.g. block-child.ts extends
|
|
275
|
+
// BlockBase) can be notified. The subclass's importer (e.g. equipment.gts)
|
|
276
|
+
// therefore never fires its accept(block-child, cb) callback, so
|
|
277
|
+
// template__imports__.BlockChild keeps pointing at the old class with
|
|
278
|
+
// the old template.
|
|
279
|
+
//
|
|
280
|
+
// Fix: for every component file that changed, walk its direct importers
|
|
281
|
+
// in Vite's module graph. Any importer that is itself a component file
|
|
282
|
+
// under /components/ is a potential subclass — add it to the update set
|
|
283
|
+
// so Vite re-evaluates it too. That re-evaluation runs its own
|
|
284
|
+
// self-accept callback (update(OldChild, NewChild)) and causes the
|
|
285
|
+
// parent template's accept(block-child, cb) to fire, swapping in the new
|
|
286
|
+
// subclass and its newly-inherited template.
|
|
287
|
+
// Subclass propagation only applies to files under /components/ — route
|
|
288
|
+
// templates (.gts under /templates/) import components but are not
|
|
289
|
+
// subclasses of them. Including templates would force Vite to re-evaluate
|
|
290
|
+
// equipment.gts when resource-holder.gts changes, breaking the existing
|
|
291
|
+
// accept(dep, cb) wiring that already handles that case correctly.
|
|
292
|
+
const isComponentFile = (p) => !p.includes('node_modules') &&
|
|
293
|
+
!p.includes('/-components/') &&
|
|
294
|
+
p.includes('/components/') &&
|
|
295
|
+
(p.endsWith('.gjs') ||
|
|
296
|
+
p.endsWith('.gts') ||
|
|
297
|
+
p.endsWith('.ts') ||
|
|
298
|
+
p.endsWith('.js'));
|
|
299
|
+
if (isComponentFile(file)) {
|
|
300
|
+
for (const mod of ctx.modules) {
|
|
301
|
+
for (const importer of mod.importers) {
|
|
302
|
+
const importerId = normalizePath((_b = (_a = importer.id) === null || _a === void 0 ? void 0 : _a.split('?')[0]) !== null && _b !== void 0 ? _b : '');
|
|
303
|
+
if (isComponentFile(importerId) &&
|
|
304
|
+
!ctx.modules.some((m) => m.id === importer.id) &&
|
|
305
|
+
!otherModules.includes(importer)) {
|
|
306
|
+
otherModules.push(importer);
|
|
561
307
|
}
|
|
562
308
|
}
|
|
563
309
|
}
|
|
564
|
-
}
|
|
310
|
+
}
|
|
311
|
+
return [...ctx.modules, ...otherModules];
|
|
565
312
|
},
|
|
566
313
|
transform(source, id) {
|
|
567
314
|
return __awaiter(this, void 0, void 0, function* () {
|
|
315
|
+
var _a, _b;
|
|
568
316
|
if (process.env.EMBER_VITE_HMR_ENABLED !== 'true') {
|
|
569
317
|
return source;
|
|
570
318
|
}
|
|
@@ -593,9 +341,6 @@ export function hmr(enableViteHmrForModes = ['development']) {
|
|
|
593
341
|
});
|
|
594
342
|
}`;
|
|
595
343
|
}
|
|
596
|
-
if (resourcePath.includes('ember-vite-hmr/virtual/components')) {
|
|
597
|
-
return source;
|
|
598
|
-
}
|
|
599
344
|
if (resourcePath.includes('node_modules')) {
|
|
600
345
|
return source;
|
|
601
346
|
}
|
|
@@ -622,133 +367,90 @@ export function hmr(enableViteHmrForModes = ['development']) {
|
|
|
622
367
|
importStatements = cached.importStatements;
|
|
623
368
|
}
|
|
624
369
|
else {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
if (prop.type === 'ObjectProperty' &&
|
|
651
|
-
prop.key.type === 'Identifier') {
|
|
652
|
-
if (prop.key.name === 'importVar' &&
|
|
653
|
-
prop.value.type === 'StringLiteral') {
|
|
654
|
-
importVar = prop.value.value;
|
|
655
|
-
}
|
|
656
|
-
else if (prop.key.name === 'bindings' &&
|
|
657
|
-
prop.value.type === 'ArrayExpression') {
|
|
658
|
-
bindings = prop.value.elements
|
|
659
|
-
.filter((el) => (el === null || el === void 0 ? void 0 : el.type) ===
|
|
660
|
-
'StringLiteral')
|
|
661
|
-
.map((el) => el.value);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
370
|
+
// Fall back to extracting metadata from the __hmr_import_metadata__
|
|
371
|
+
// export the babel plugin emits into the source.
|
|
372
|
+
const metaMatch = source.match(/export const __hmr_import_metadata__ = (\{[\s\S]*?\});/);
|
|
373
|
+
if (metaMatch) {
|
|
374
|
+
try {
|
|
375
|
+
// eslint-disable-next-line no-new-func
|
|
376
|
+
const meta = new Function(`return ${metaMatch[1]}`)();
|
|
377
|
+
importVar = meta.importVar;
|
|
378
|
+
bindings = (_a = meta.bindings) !== null && _a !== void 0 ? _a : [];
|
|
379
|
+
}
|
|
380
|
+
catch (_c) {
|
|
381
|
+
// unparseable metadata — skip
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Recover importStatements by scanning the source for each binding
|
|
385
|
+
if (importVar && bindings.length > 0) {
|
|
386
|
+
for (const binding of bindings) {
|
|
387
|
+
const importMatch = source.match(new RegExp(`import\\s+(?:(${binding})|(\\{[^}]*\\b${binding}\\b[^}]*\\})|\\*\\s+as\\s+(${binding}))\\s+from\\s+['"]([^'"]+)['"]`));
|
|
388
|
+
if (importMatch) {
|
|
389
|
+
const src = importMatch[4];
|
|
390
|
+
let specifier = 'default';
|
|
391
|
+
if (importMatch[2]) {
|
|
392
|
+
// named import — find alias or name
|
|
393
|
+
const namedMatch = importMatch[2].match(new RegExp(`(\\w+)\\s+as\\s+${binding}|${binding}`));
|
|
394
|
+
specifier = (_b = namedMatch === null || namedMatch === void 0 ? void 0 : namedMatch[1]) !== null && _b !== void 0 ? _b : binding;
|
|
666
395
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
ImportDeclaration(path) {
|
|
673
|
-
const importSource = path.node.source.value;
|
|
674
|
-
for (const specifier of path.node.specifiers) {
|
|
675
|
-
const local = specifier.local.name;
|
|
676
|
-
if (bindings.includes(local)) {
|
|
677
|
-
let specifierName = 'default';
|
|
678
|
-
if (specifier.type === 'ImportDefaultSpecifier') {
|
|
679
|
-
specifierName = 'default';
|
|
680
|
-
}
|
|
681
|
-
else if (specifier.type === 'ImportSpecifier') {
|
|
682
|
-
// For named imports, use the imported name
|
|
683
|
-
// Handle both Identifier and StringLiteral types
|
|
684
|
-
const imported = specifier.imported;
|
|
685
|
-
specifierName =
|
|
686
|
-
imported.type === 'Identifier'
|
|
687
|
-
? imported.name
|
|
688
|
-
: imported.value;
|
|
689
|
-
}
|
|
690
|
-
else if (specifier.type === 'ImportNamespaceSpecifier') {
|
|
691
|
-
specifierName = '*';
|
|
692
|
-
}
|
|
693
|
-
importStatements.push({
|
|
694
|
-
local,
|
|
695
|
-
source: importSource,
|
|
696
|
-
specifier: specifierName,
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
},
|
|
701
|
-
});
|
|
396
|
+
else if (importMatch[3]) {
|
|
397
|
+
specifier = '*';
|
|
398
|
+
}
|
|
399
|
+
importStatements.push({ local: binding, source: src, specifier });
|
|
400
|
+
}
|
|
702
401
|
}
|
|
703
402
|
}
|
|
704
403
|
}
|
|
705
404
|
// Process metadata if we found importVar (even with empty bindings)
|
|
706
405
|
if (importVar) {
|
|
707
|
-
// Generate hot reload code for each import (only if we have bindings)
|
|
708
406
|
const hotReloadStatements = [];
|
|
709
407
|
for (const imp of importStatements) {
|
|
710
408
|
// Resolve the import to check if it's from node_modules
|
|
711
409
|
const resolved = yield this.resolve(imp.source, resourcePath, {});
|
|
712
410
|
if ((resolved === null || resolved === void 0 ? void 0 : resolved.id) &&
|
|
713
411
|
normalizePath(resolved.id).includes('node_modules')) {
|
|
714
|
-
// Skip node_modules imports
|
|
715
412
|
continue;
|
|
716
413
|
}
|
|
717
|
-
|
|
718
|
-
|
|
414
|
+
// Each imported binding is already a local variable in scope thanks
|
|
415
|
+
// to its import statement. Register it by value on first load so
|
|
416
|
+
// `current(binding)` (called by the template__imports__ getter) has
|
|
417
|
+
// an entry to read from. When the dep module reloads, update() sets
|
|
418
|
+
// entry.current = newVal — the getter re-reads it, invalidating only
|
|
419
|
+
// the scope that contains the getter call, not the parent scope.
|
|
420
|
+
// No assignment to template__imports__.X needed: the getter handles it.
|
|
719
421
|
hotReloadStatements.push(`
|
|
720
422
|
(async () => {
|
|
721
|
-
const
|
|
722
|
-
const {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
if (isComponent) {
|
|
733
|
-
const c = await import('${virtualPath}');
|
|
734
|
-
${importVar}.${imp.local} = c.default;
|
|
735
|
-
import.meta.hot.accept('${virtualPath}', (c) => {
|
|
736
|
-
${importVar}.${imp.local} = c['${imp.specifier}'];
|
|
737
|
-
});
|
|
738
|
-
import.meta.hot.accept('${imp.source}');
|
|
739
|
-
}
|
|
423
|
+
const { register: ember_vite_hmr_register, update: ember_vite_hmr_update, used: ember_vite_hmr_used } = await import(${JSON.stringify(hmrRuntimeId)});
|
|
424
|
+
const { tracked: ember_vite_hmr_tracked } = await import('@glimmer/tracking');
|
|
425
|
+
ember_vite_hmr_register(${imp.local}, ember_vite_hmr_tracked);
|
|
426
|
+
import.meta.hot.accept(${JSON.stringify(imp.source)}, (m) => {
|
|
427
|
+
if (m) {
|
|
428
|
+
const newVal = m[${JSON.stringify(imp.specifier === 'default' ? 'default' : imp.specifier)}];
|
|
429
|
+
ember_vite_hmr_update(${imp.local}, newVal);
|
|
430
|
+
} else if (!ember_vite_hmr_used(${imp.local})) {
|
|
431
|
+
import.meta.hot.invalidate('nothing rendered ${imp.local} through the HMR runtime');
|
|
432
|
+
}
|
|
433
|
+
});
|
|
740
434
|
})();`);
|
|
741
435
|
}
|
|
742
436
|
// Always remove the metadata export
|
|
743
437
|
source = source.replace(/export const __hmr_import_metadata__[^;]+;/, '');
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
438
|
+
// Do NOT add a self-accept in the importVar block.
|
|
439
|
+
//
|
|
440
|
+
// When module A imports module B, and B's template__imports__ class is in
|
|
441
|
+
// A's scope, A must be notified when B changes (so A's accept callback can
|
|
442
|
+
// swap in the new B class via template__imports__.B = newB). If B were to
|
|
443
|
+
// self-accept, Vite would stop propagation before A's callback fires — A's
|
|
444
|
+
// template__imports__ would never be updated, and Glimmer would never
|
|
445
|
+
// re-render. The same applies to route templates: they rely on Ember's
|
|
446
|
+
// canAcceptNew mechanism (added below), which self-accept would bypass.
|
|
447
|
+
//
|
|
448
|
+
// Components without importVar (no template imports) get a self-accept in
|
|
449
|
+
// the section below, after the supportedPaths check.
|
|
450
|
+
const hotReloadCode = hotReloadStatements.length > 0
|
|
451
|
+
? `\nif (import.meta.hot) {\n${hotReloadStatements.join('\n')}\n}`
|
|
452
|
+
: '';
|
|
453
|
+
source = source + hotReloadCode;
|
|
752
454
|
}
|
|
753
455
|
const supportedPaths = ['routers', 'controllers', 'routes', 'templates'];
|
|
754
456
|
const supportedFileNames = [
|
|
@@ -764,6 +466,56 @@ ${hotReloadStatements.join('\n')}
|
|
|
764
466
|
}
|
|
765
467
|
if (!supportedPaths.some((s) => resourcePath.includes(`/${s}/`)) &&
|
|
766
468
|
!supportedFileNames.some((s) => resourcePath.endsWith(s))) {
|
|
469
|
+
// Component files (including template-less .ts/.js ones that just extend
|
|
470
|
+
// another component) that have no cross-component imports need a
|
|
471
|
+
// self-accept boundary so edits to them don't propagate up to a full
|
|
472
|
+
// page reload. Restrict plain .ts/.js to paths under /components/ to
|
|
473
|
+
// avoid accidentally self-accepting services, utilities, adapters, etc.
|
|
474
|
+
const isComponentFile = resourcePath.endsWith('.gjs') ||
|
|
475
|
+
resourcePath.endsWith('.gts') ||
|
|
476
|
+
((resourcePath.endsWith('.ts') || resourcePath.endsWith('.js')) &&
|
|
477
|
+
resourcePath.includes('/components/'));
|
|
478
|
+
if (isComponentFile && !importVar) {
|
|
479
|
+
// Self-accept with a callback that updates the tracked-cell registry
|
|
480
|
+
// so Glimmer re-renders and swaps in the new class. This handles
|
|
481
|
+
// edits to the component's own class (new method, changed @tracked
|
|
482
|
+
// property, etc.) — without it, a bare accept() would silently
|
|
483
|
+
// discard the new class and leave the running instance on the old one.
|
|
484
|
+
//
|
|
485
|
+
// Only do this when there is no importVar (no template imports of
|
|
486
|
+
// other components). When importVar is present, the component's own
|
|
487
|
+
// importer already has an accept(dep, cb) that fires when this file
|
|
488
|
+
// changes — adding a self-accept here would stop Vite's propagation
|
|
489
|
+
// before that callback can fire, breaking the re-render. The same
|
|
490
|
+
// applies to re-export barrels: a self-accept on my-button.ts stops
|
|
491
|
+
// Vite walking up to index.ts's importer, silencing the accept
|
|
492
|
+
// callback that updates template__imports__.
|
|
493
|
+
//
|
|
494
|
+
// The babel plugin rewrites `export default <expr>` into
|
|
495
|
+
// `const __hmr_default__ = <expr>; export { __hmr_default__ as default }`
|
|
496
|
+
// giving us a stable local variable to register by value.
|
|
497
|
+
// hot.data.default carries the old value across self-accept
|
|
498
|
+
// re-evaluations so update() can find the existing entry.
|
|
499
|
+
return `${source}
|
|
500
|
+
if (import.meta.hot) {
|
|
501
|
+
(async () => {
|
|
502
|
+
const { register: ember_vite_hmr_register, update: ember_vite_hmr_update, used: ember_vite_hmr_used } = await import(${JSON.stringify(hmrRuntimeId)});
|
|
503
|
+
const { tracked: ember_vite_hmr_tracked } = await import('@glimmer/tracking');
|
|
504
|
+
const _hmr_prev = import.meta.hot.data.default;
|
|
505
|
+
import.meta.hot.data.default = __hmr_default__;
|
|
506
|
+
if (_hmr_prev) {
|
|
507
|
+
ember_vite_hmr_update(_hmr_prev, __hmr_default__);
|
|
508
|
+
}
|
|
509
|
+
ember_vite_hmr_register(__hmr_default__, ember_vite_hmr_tracked);
|
|
510
|
+
import.meta.hot.accept((m) => {
|
|
511
|
+
if (m && !ember_vite_hmr_used(import.meta.hot.data.default)) {
|
|
512
|
+
import.meta.hot.invalidate('nothing rendered this component through the HMR runtime');
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
})();
|
|
516
|
+
}
|
|
517
|
+
`;
|
|
518
|
+
}
|
|
767
519
|
return source;
|
|
768
520
|
}
|
|
769
521
|
if (supportedPaths.includes('templates') &&
|
|
@@ -785,4 +537,5 @@ ${hotReloadStatements.join('\n')}
|
|
|
785
537
|
});
|
|
786
538
|
},
|
|
787
539
|
};
|
|
540
|
+
return [hmrRuntime(), mainPlugin];
|
|
788
541
|
}
|