ember-vite-hmr 2.3.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/dist/instance-initializers/setup-hmr-manager.js +106 -22
- 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 +297 -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 +261 -0
- package/dist/lib/babel-plugin.d.ts +3 -31
- package/dist/lib/babel-plugin.js +80 -410
- package/dist/lib/hmr.d.ts +3 -1
- package/dist/lib/hmr.js +355 -413
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -28,11 +28,42 @@ export default class MyComponent extends Component {
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
This hook is called after automatic state synchronization but before the old instance is destroyed, allowing you to:
|
|
31
|
+
|
|
31
32
|
- Perform custom state migrations
|
|
32
33
|
- Clean up resources
|
|
33
34
|
- Log HMR events
|
|
34
35
|
- Handle complex state transitions
|
|
35
36
|
|
|
37
|
+
### Private class fields
|
|
38
|
+
|
|
39
|
+
Services are hot-reloaded through a generated proxy that forwards property
|
|
40
|
+
access to the current delegate instance. Native `#private` class fields and
|
|
41
|
+
methods only work when `this` is the exact instance that declared them, so
|
|
42
|
+
this plugin rewrites every native `#private` member declared in your app's
|
|
43
|
+
own source (on the service itself and any ancestor class it extends, as
|
|
44
|
+
long as that ancestor is also part of your app) to a plain, uniquely-named
|
|
45
|
+
property. An undecorated field is also made non-enumerable (via
|
|
46
|
+
`Object.defineProperty`), so it's hidden from `for...in`, `Object.keys`,
|
|
47
|
+
`JSON.stringify`, and spread — the same as a native `#private` member. A
|
|
48
|
+
private method already was non-enumerable, since class method syntax is
|
|
49
|
+
non-enumerable by default regardless of this rewrite. A
|
|
50
|
+
`@tracked #field` is the one exception: the decorator turns it into an
|
|
51
|
+
accessor on the prototype rather than an instance data property, so it's
|
|
52
|
+
left exactly as the plain rewrite produces it, enumerable or not according
|
|
53
|
+
to whatever `@tracked` itself does with an ordinary public field. In every
|
|
54
|
+
case this only emulates privacy: the member is reachable and writable from
|
|
55
|
+
outside via its generated name (e.g. through `Object.getOwnPropertyNames`,
|
|
56
|
+
`Reflect.ownKeys`, or devtools) — unlike a real native `#private` field. It's
|
|
57
|
+
obscured, not encapsulated, so don't rely on it as a serialization or
|
|
58
|
+
security boundary.
|
|
59
|
+
|
|
60
|
+
The one case this can't cover: a base class living in `node_modules` (never
|
|
61
|
+
processed by this plugin) that declares its own native `#private` fields.
|
|
62
|
+
A service extending such a class would still need `this` to be the real
|
|
63
|
+
instance to read those fields, so proxying that service could break. Ember's
|
|
64
|
+
own `Service`/`EmberObject`/`CoreObject` chain declares no native `#private`
|
|
65
|
+
fields, so ordinary Ember apps are unaffected.
|
|
66
|
+
|
|
36
67
|
## Installation
|
|
37
68
|
|
|
38
69
|
```
|
|
@@ -3,6 +3,13 @@ import Service from '@ember/service';
|
|
|
3
3
|
import Component from '@glimmer/component';
|
|
4
4
|
import { getInternalComponentManager } from '@glimmer/manager';
|
|
5
5
|
|
|
6
|
+
// Accessor for the HMR runtime helpers exposed on the global by
|
|
7
|
+
// virtual:ember-vite-hmr-runtime. Only set in dev/serve mode (guarded by
|
|
8
|
+
// `if (!import.meta.hot)` at the call site).
|
|
9
|
+
|
|
10
|
+
function hmrRuntime() {
|
|
11
|
+
return globalThis.__ember_vite_hmr;
|
|
12
|
+
}
|
|
6
13
|
function findPropertyDescriptor(component, key) {
|
|
7
14
|
let proto = component;
|
|
8
15
|
while (proto) {
|
|
@@ -11,6 +18,33 @@ function findPropertyDescriptor(component, key) {
|
|
|
11
18
|
proto = Object.getPrototypeOf(proto);
|
|
12
19
|
}
|
|
13
20
|
}
|
|
21
|
+
|
|
22
|
+
// The private property key `ember-resources`' `wrapForPlainUsage()` uses on
|
|
23
|
+
// the plain object it wraps in a Proxy for `resource()` (which reactiveweb's
|
|
24
|
+
// `trackedTask` uses internally) - see
|
|
25
|
+
// https://github.com/NullVoxPopuli/ember-resources/blob/main/ember-resources/src/plain/index.ts.
|
|
26
|
+
// Carrying such a proxy's *reference* over onto a new HMR-swapped instance
|
|
27
|
+
// (instead of leaving the new instance's own, correctly-parented resource in
|
|
28
|
+
// place) is unsafe: the proxy's underlying helper cache is lazily created on
|
|
29
|
+
// first property read and permanently bound, at that point, to whichever
|
|
30
|
+
// component instance was passed as `context` when `resource()` was called -
|
|
31
|
+
// for a carried-over proxy that's always the *old*, already-destroyed
|
|
32
|
+
// instance, so the first read after the swap throws "Attempted to associate
|
|
33
|
+
// a destroyable child with an object that is already destroying or
|
|
34
|
+
// destroyed" - see issue #563.
|
|
35
|
+
//
|
|
36
|
+
// `in` is used (rather than reading a property, or `Object.keys`/
|
|
37
|
+
// `Object.prototype.toString.call`) because it's the one reflective
|
|
38
|
+
// operation `wrapForPlainUsage`'s Proxy doesn't trap (no `has` handler), so
|
|
39
|
+
// it forwards to a plain `[[HasProperty]]` on the underlying target without
|
|
40
|
+
// ever invoking the lazy getter - it can't accidentally trigger the same
|
|
41
|
+
// crash it's checking for. If `ember-resources` ever renames this key, this
|
|
42
|
+
// check simply stops matching and behavior falls back to today's (already
|
|
43
|
+
// broken, pre-existing) state - it does not introduce a new failure mode.
|
|
44
|
+
const RESOURCE_INTERMEDIATE_VALUE_KEY = '__Intermediate_Value__';
|
|
45
|
+
function isUnsafeToCarryOver(value) {
|
|
46
|
+
return (typeof value === 'object' || typeof value === 'function') && value !== null && RESOURCE_INTERMEDIATE_VALUE_KEY in value;
|
|
47
|
+
}
|
|
14
48
|
function getState(component, skip) {
|
|
15
49
|
const state = {};
|
|
16
50
|
if (!component) {
|
|
@@ -30,35 +64,41 @@ function getState(component, skip) {
|
|
|
30
64
|
if (typeof value === 'function') {
|
|
31
65
|
continue;
|
|
32
66
|
}
|
|
67
|
+
|
|
68
|
+
// Skip resource()-backed proxies - see isUnsafeToCarryOver above.
|
|
69
|
+
if (isUnsafeToCarryOver(value)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
33
72
|
if (entry) {
|
|
34
|
-
|
|
35
|
-
|
|
73
|
+
// Note: don't probe `entry.value` any further here (e.g. via
|
|
74
|
+
// `Object.prototype.toString.call`) to decide whether it's a plain
|
|
75
|
+
// function - the `typeof value === 'function'` check above already
|
|
76
|
+
// covers that, and a stricter check on `entry.value` (which may be a
|
|
77
|
+
// resource proxy, e.g. reactiveweb's `trackedTask`) can trigger that
|
|
78
|
+
// proxy's `get` trap on first touch, lazily invoking a helper against
|
|
79
|
+
// this component instance - see issue #557.
|
|
80
|
+
if (entry.writable) {
|
|
81
|
+
state[key] = value;
|
|
36
82
|
}
|
|
37
83
|
if (entry.set) {
|
|
38
|
-
state[key] =
|
|
84
|
+
state[key] = value;
|
|
39
85
|
}
|
|
40
86
|
}
|
|
41
87
|
}
|
|
42
88
|
return state;
|
|
43
89
|
}
|
|
44
|
-
function syncState(instance) {
|
|
45
|
-
const args = instance.args;
|
|
46
|
-
if (args.__hot__) {
|
|
47
|
-
const oldInstance = args.__hot__.oldInstance;
|
|
48
|
-
const state = instance.__get_hot_state__?.() || args.__hot__.getState?.();
|
|
49
|
-
for (const k in state) {
|
|
50
|
-
if (instance[k] instanceof Service) {
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
instance[k] = state[k];
|
|
54
|
-
}
|
|
55
90
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
91
|
+
// Apply a stashed old-instance state to a freshly created new instance.
|
|
92
|
+
// Also calls the _hmrAccepted lifecycle hook if defined on the new instance.
|
|
93
|
+
function applySyncState(instance, state, oldInstance) {
|
|
94
|
+
for (const k in state) {
|
|
95
|
+
if (instance[k] instanceof Service) {
|
|
96
|
+
continue;
|
|
59
97
|
}
|
|
60
|
-
|
|
61
|
-
|
|
98
|
+
instance[k] = state[k];
|
|
99
|
+
}
|
|
100
|
+
if (typeof instance._hmrAccepted === 'function') {
|
|
101
|
+
instance._hmrAccepted(oldInstance);
|
|
62
102
|
}
|
|
63
103
|
}
|
|
64
104
|
function initialize() {
|
|
@@ -69,9 +109,53 @@ function initialize() {
|
|
|
69
109
|
const proto = Object.getPrototypeOf(ComponentManager);
|
|
70
110
|
const create = proto.create;
|
|
71
111
|
proto.create = function (...args) {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
const bucket = create.call(this, ...args);
|
|
113
|
+
const component = bucket.component;
|
|
114
|
+
|
|
115
|
+
// Enqueue this instance in the runtime's per-class FIFO queue. The runtime
|
|
116
|
+
// only enqueues when isHmrClass(klass) is true (registered HMR class), and
|
|
117
|
+
// clears the queue at update() time so user-flow stale entries are never
|
|
118
|
+
// present when willDestroy fires during an HMR swap.
|
|
119
|
+
const klass = component.constructor;
|
|
120
|
+
const rt = hmrRuntime();
|
|
121
|
+
if (rt?.isHmrClass(klass)) {
|
|
122
|
+
rt.enqueueInstance(klass, component);
|
|
123
|
+
}
|
|
124
|
+
return bucket;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Patch willDestroy on the Component prototype to detect HMR swaps and
|
|
128
|
+
// transfer state from the old instance to the freshly-created new one.
|
|
129
|
+
//
|
|
130
|
+
// Why willDestroy instead of registerDestructor: Glimmer creates the NEW
|
|
131
|
+
// component instance BEFORE calling willDestroy on the OLD one (willDestroy
|
|
132
|
+
// is scheduled in Ember's "actions" runloop queue, which runs after
|
|
133
|
+
// rendering). This ordering is exactly what we need: by willDestroy time,
|
|
134
|
+
// the new instance is already live in liveInstances.
|
|
135
|
+
const origWillDestroy = Component.prototype.willDestroy;
|
|
136
|
+
Component.prototype.willDestroy = function () {
|
|
137
|
+
const rt = hmrRuntime();
|
|
138
|
+
if (rt) {
|
|
139
|
+
const klass = this.constructor;
|
|
140
|
+
const newClass = rt.current(klass);
|
|
141
|
+
if (newClass !== klass) {
|
|
142
|
+
// The class was swapped via HMR — dequeue the next new instance for
|
|
143
|
+
// the replacement class (FIFO matches DOM/creation order) and
|
|
144
|
+
// schedule a state transfer onto it.
|
|
145
|
+
const newInstance = rt.dequeueInstance(newClass);
|
|
146
|
+
if (newInstance) {
|
|
147
|
+
const state = getState(this, ['args']);
|
|
148
|
+
const oldInstance = this;
|
|
149
|
+
setTimeout(() => applySyncState(newInstance, state, oldInstance));
|
|
150
|
+
}
|
|
151
|
+
} else if (rt.isHmrClass(klass)) {
|
|
152
|
+
// Normal destroy (navigation, timer, conditional) — remove this
|
|
153
|
+
// instance from the live queue so it doesn't accumulate as a stale
|
|
154
|
+
// entry that update() would otherwise have to clear at the next save.
|
|
155
|
+
rt.removeInstance(klass, this);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return origWillDestroy?.call(this);
|
|
75
159
|
};
|
|
76
160
|
const setupController = Route.prototype.setupController;
|
|
77
161
|
const StateCache = {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup-hmr-manager.js","sources":["../../src/instance-initializers/setup-hmr-manager.ts"],"sourcesContent":["import Route from '@ember/routing/route';\nimport Service from '@ember/service';\nimport Component from '@glimmer/component';\nimport { getInternalComponentManager } from '@glimmer/manager';\n\ninterface HotComponent extends Component<{ __hot__?: unknown }> {\n __get_hot_state__?: () => Record<string, unknown>;\n _hmrAccepted?: (oldInstance?: HotComponent) => void;\n [key: string]: unknown;\n}\n\ntype Mutable<T> = {\n -readonly [P in keyof T]: unknown;\n};\n\nfunction findPropertyDescriptor(\n component: HotComponent | Record<string, unknown>,\n key: string,\n) {\n let proto = component;\n while (proto) {\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (desc) return desc;\n proto = Object.getPrototypeOf(proto);\n }\n}\n\nfunction getState(component: HotComponent, skip: string[]) {\n const state: Record<string, unknown> = {};\n if (!component) {\n return state;\n }\n for (const key in component) {\n if (skip.includes(key)) continue;\n const entry = findPropertyDescriptor(component, key);\n const value = component[key as keyof Component];\n \n // Skip Service instances - they should not be synced\n if (value instanceof Service) {\n continue;\n }\n \n // Skip Function properties - they should not be synced\n if (typeof value === 'function') {\n continue;\n }\n \n if (entry) {\n if (\n entry.writable &&\n !Object.prototype.toString.call(entry.value).includes('Function')\n ) {\n state[key] = component[key as keyof Component];\n }\n if (entry.set) {\n state[key] = component[key as keyof Component];\n }\n }\n }\n return state;\n}\n\nfunction syncState(instance: Mutable<HotComponent>) {\n const args = instance.args as { __hot__?: { getState?: () => Record<string, unknown>; oldInstance?: HotComponent } };\n if (args.__hot__) {\n const oldInstance = args.__hot__.oldInstance;\n const state =\n (instance.__get_hot_state__ as (() => Record<string, unknown>) | undefined)?.() || args.__hot__.getState?.();\n for (const k in state) {\n if (instance[k as keyof HotComponent] instanceof Service) {\n continue;\n }\n instance[k as keyof HotComponent] = state[k];\n }\n \n // Call _hmrAccepted hook if defined\n if (typeof instance._hmrAccepted === 'function') {\n instance._hmrAccepted(oldInstance);\n }\n \n args.__hot__.getState = () => getState(instance as HotComponent, ['args']);\n args.__hot__.oldInstance = instance as HotComponent;\n }\n}\n\nexport function initialize() {\n // HMR-only: in a production build `import.meta.hot` is undefined, so this\n // (and the manager/route monkey-patches below) is dead-code-eliminated.\n if (!import.meta.hot) return;\n\n const ComponentManager = getInternalComponentManager(Component);\n const proto = Object.getPrototypeOf(ComponentManager);\n const create = proto.create;\n proto.create = function (...args: unknown[]) {\n const instance = create.call(this, ...args);\n setTimeout(() => syncState(instance.component));\n return instance;\n };\n\n const setupController = Route.prototype.setupController;\n\n const StateCache: Record<string, unknown> = {};\n\n Route.prototype.setupController = function (...args: Parameters<typeof setupController>) {\n const controller = args[0] as unknown as Record<string, unknown>;\n const r = setupController.call(this, ...args);\n const fullRouteName = this.fullRouteName;\n const state = StateCache[fullRouteName] as { router?: Record<string, unknown>; controller?: Record<string, unknown> } || {};\n const skip = ['_qpDelegate', 'target', 'queryParams'];\n const routerState = getState(state.router as HotComponent, skip);\n for (const k in routerState) {\n (this as unknown as Record<string, unknown>)[k] = routerState[k];\n }\n // Only re-apply controller state onto a *new* instance (an HMR swap). On\n // ordinary navigation it is the same singleton, and re-applying a\n // non-`@tracked` query param onto itself trips Ember's mandatory setter.\n if (state.controller && state.controller !== controller) {\n const controllerState = getState(state.controller as HotComponent, skip);\n for (const k in controllerState) {\n (controller as Record<string, unknown>)[k] = controllerState[k];\n }\n }\n StateCache[fullRouteName] = {\n route: this,\n controller,\n };\n return r;\n };\n}\n\nexport default {\n initialize,\n};\n"],"names":["findPropertyDescriptor","component","key","proto","desc","Object","getOwnPropertyDescriptor","getPrototypeOf","getState","skip","state","includes","entry","value","Service","writable","prototype","toString","call","set","syncState","instance","args","__hot__","oldInstance","__get_hot_state__","k","_hmrAccepted","initialize","import","meta","hot","ComponentManager","getInternalComponentManager","Component","create","setTimeout","setupController","Route","StateCache","controller","r","fullRouteName","routerState","router","controllerState","route"],"mappings":";;;;;AAeA,SAASA,sBAAsBA,CAC7BC,SAAiD,EACjDC,GAAW,EACX;EACA,IAAIC,KAAK,GAAGF,SAAS;AACrB,EAAA,OAAOE,KAAK,EAAE;IACZ,MAAMC,IAAI,GAAGC,MAAM,CAACC,wBAAwB,CAACH,KAAK,EAAED,GAAG,CAAC;IACxD,IAAIE,IAAI,EAAE,OAAOA,IAAI;AACrBD,IAAAA,KAAK,GAAGE,MAAM,CAACE,cAAc,CAACJ,KAAK,CAAC;AACtC,EAAA;AACF;AAEA,SAASK,QAAQA,CAACP,SAAuB,EAAEQ,IAAc,EAAE;EACzD,MAAMC,KAA8B,GAAG,EAAE;EACzC,IAAI,CAACT,SAAS,EAAE;AACd,IAAA,OAAOS,KAAK;AACd,EAAA;AACA,EAAA,KAAK,MAAMR,GAAG,IAAID,SAAS,EAAE;AAC3B,IAAA,IAAIQ,IAAI,CAACE,QAAQ,CAACT,GAAG,CAAC,EAAE;AACxB,IAAA,MAAMU,KAAK,GAAGZ,sBAAsB,CAACC,SAAS,EAAEC,GAAG,CAAC;AACpD,IAAA,MAAMW,KAAK,GAAGZ,SAAS,CAACC,GAAG,CAAoB;;AAE/C;IACA,IAAIW,KAAK,YAAYC,OAAO,EAAE;AAC5B,MAAA;AACF,IAAA;;AAEA;AACA,IAAA,IAAI,OAAOD,KAAK,KAAK,UAAU,EAAE;AAC/B,MAAA;AACF,IAAA;AAEA,IAAA,IAAID,KAAK,EAAE;MACT,IACEA,KAAK,CAACG,QAAQ,IACd,CAACV,MAAM,CAACW,SAAS,CAACC,QAAQ,CAACC,IAAI,CAACN,KAAK,CAACC,KAAK,CAAC,CAACF,QAAQ,CAAC,UAAU,CAAC,EACjE;AACAD,QAAAA,KAAK,CAACR,GAAG,CAAC,GAAGD,SAAS,CAACC,GAAG,CAAoB;AAChD,MAAA;MACA,IAAIU,KAAK,CAACO,GAAG,EAAE;AACbT,QAAAA,KAAK,CAACR,GAAG,CAAC,GAAGD,SAAS,CAACC,GAAG,CAAoB;AAChD,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA,OAAOQ,KAAK;AACd;AAEA,SAASU,SAASA,CAACC,QAA+B,EAAE;AAClD,EAAA,MAAMC,IAAI,GAAGD,QAAQ,CAACC,IAA8F;EACpH,IAAIA,IAAI,CAACC,OAAO,EAAE;AAChB,IAAA,MAAMC,WAAW,GAAGF,IAAI,CAACC,OAAO,CAACC,WAAW;AAC5C,IAAA,MAAMd,KAAK,GACRW,QAAQ,CAACI,iBAAiB,IAAoD,IAAIH,IAAI,CAACC,OAAO,CAACf,QAAQ,IAAI;AAC9G,IAAA,KAAK,MAAMkB,CAAC,IAAIhB,KAAK,EAAE;AACrB,MAAA,IAAIW,QAAQ,CAACK,CAAC,CAAuB,YAAYZ,OAAO,EAAE;AACxD,QAAA;AACF,MAAA;AACAO,MAAAA,QAAQ,CAACK,CAAC,CAAuB,GAAGhB,KAAK,CAACgB,CAAC,CAAC;AAC9C,IAAA;;AAEA;AACA,IAAA,IAAI,OAAOL,QAAQ,CAACM,YAAY,KAAK,UAAU,EAAE;AAC/CN,MAAAA,QAAQ,CAACM,YAAY,CAACH,WAAW,CAAC;AACpC,IAAA;AAEAF,IAAAA,IAAI,CAACC,OAAO,CAACf,QAAQ,GAAG,MAAMA,QAAQ,CAACa,QAAQ,EAAkB,CAAC,MAAM,CAAC,CAAC;AAC1EC,IAAAA,IAAI,CAACC,OAAO,CAACC,WAAW,GAAGH,QAAwB;AACrD,EAAA;AACF;AAEO,SAASO,UAAUA,GAAG;AAC3B;AACA;AACA,EAAA,IAAI,CAACC,MAAM,CAACC,IAAI,CAACC,GAAG,EAAE;AAEtB,EAAA,MAAMC,gBAAgB,GAAGC,2BAA2B,CAACC,SAAS,CAAC;AAC/D,EAAA,MAAM/B,KAAK,GAAGE,MAAM,CAACE,cAAc,CAACyB,gBAAgB,CAAC;AACrD,EAAA,MAAMG,MAAM,GAAGhC,KAAK,CAACgC,MAAM;AAC3BhC,EAAAA,KAAK,CAACgC,MAAM,GAAG,UAAU,GAAGb,IAAe,EAAE;IAC3C,MAAMD,QAAQ,GAAGc,MAAM,CAACjB,IAAI,CAAC,IAAI,EAAE,GAAGI,IAAI,CAAC;IAC3Cc,UAAU,CAAC,MAAMhB,SAAS,CAACC,QAAQ,CAACpB,SAAS,CAAC,CAAC;AAC/C,IAAA,OAAOoB,QAAQ;EACjB,CAAC;AAED,EAAA,MAAMgB,eAAe,GAAGC,KAAK,CAACtB,SAAS,CAACqB,eAAe;EAEvD,MAAME,UAAmC,GAAG,EAAE;EAE9CD,KAAK,CAACtB,SAAS,CAACqB,eAAe,GAAG,UAAU,GAAGf,IAAwC,EAAE;AACvF,IAAA,MAAMkB,UAAU,GAAGlB,IAAI,CAAC,CAAC,CAAuC;IAChE,MAAMmB,CAAC,GAAGJ,eAAe,CAACnB,IAAI,CAAC,IAAI,EAAE,GAAGI,IAAI,CAAC;AAC7C,IAAA,MAAMoB,aAAa,GAAG,IAAI,CAACA,aAAa;IACxC,MAAMhC,KAAK,GAAG6B,UAAU,CAACG,aAAa,CAAC,IAAkF,EAAE;IAC3H,MAAMjC,IAAI,GAAG,CAAC,aAAa,EAAE,QAAQ,EAAE,aAAa,CAAC;IACrD,MAAMkC,WAAW,GAAGnC,QAAQ,CAACE,KAAK,CAACkC,MAAM,EAAkBnC,IAAI,CAAC;AAChE,IAAA,KAAK,MAAMiB,CAAC,IAAIiB,WAAW,EAAE;AAC1B,MAAA,IAAI,CAAwCjB,CAAC,CAAC,GAAGiB,WAAW,CAACjB,CAAC,CAAC;AAClE,IAAA;AACA;AACA;AACA;IACA,IAAIhB,KAAK,CAAC8B,UAAU,IAAI9B,KAAK,CAAC8B,UAAU,KAAKA,UAAU,EAAE;MACvD,MAAMK,eAAe,GAAGrC,QAAQ,CAACE,KAAK,CAAC8B,UAAU,EAAkB/B,IAAI,CAAC;AACxE,MAAA,KAAK,MAAMiB,CAAC,IAAImB,eAAe,EAAE;AAC9BL,QAAAA,UAAU,CAA6Bd,CAAC,CAAC,GAAGmB,eAAe,CAACnB,CAAC,CAAC;AACjE,MAAA;AACF,IAAA;IACAa,UAAU,CAACG,aAAa,CAAC,GAAG;AAC1BI,MAAAA,KAAK,EAAE,IAAI;AACXN,MAAAA;KACD;AACD,IAAA,OAAOC,CAAC;EACV,CAAC;AACH;AAEA,sBAAe;AACbb,EAAAA;AACF,CAAC;;;;"}
|
|
1
|
+
{"version":3,"file":"setup-hmr-manager.js","sources":["../../src/instance-initializers/setup-hmr-manager.ts"],"sourcesContent":["import Route from '@ember/routing/route';\nimport Service from '@ember/service';\nimport Component from '@glimmer/component';\nimport { getInternalComponentManager } from '@glimmer/manager';\n\ninterface HotComponent extends Component {\n __get_hot_state__?: () => Record<string, unknown>;\n _hmrAccepted?: (oldInstance?: HotComponent) => void;\n [key: string]: unknown;\n}\n\ntype Mutable<T> = {\n -readonly [P in keyof T]: unknown;\n};\n\n// Accessor for the HMR runtime helpers exposed on the global by\n// virtual:ember-vite-hmr-runtime. Only set in dev/serve mode (guarded by\n// `if (!import.meta.hot)` at the call site).\ninterface HmrRuntime {\n current: (value: unknown) => unknown;\n isHmrClass: (value: unknown) => boolean;\n enqueueInstance: (klass: unknown, instance: HotComponent) => void;\n dequeueInstance: (klass: unknown) => HotComponent | null;\n removeInstance: (klass: unknown, instance: HotComponent) => void;\n}\n\nfunction hmrRuntime(): HmrRuntime | undefined {\n return (globalThis as unknown as { __ember_vite_hmr?: HmrRuntime }).__ember_vite_hmr;\n}\n\nfunction findPropertyDescriptor(\n component: HotComponent | Record<string, unknown>,\n key: string,\n) {\n let proto = component;\n while (proto) {\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (desc) return desc;\n proto = Object.getPrototypeOf(proto);\n }\n}\n\n// The private property key `ember-resources`' `wrapForPlainUsage()` uses on\n// the plain object it wraps in a Proxy for `resource()` (which reactiveweb's\n// `trackedTask` uses internally) - see\n// https://github.com/NullVoxPopuli/ember-resources/blob/main/ember-resources/src/plain/index.ts.\n// Carrying such a proxy's *reference* over onto a new HMR-swapped instance\n// (instead of leaving the new instance's own, correctly-parented resource in\n// place) is unsafe: the proxy's underlying helper cache is lazily created on\n// first property read and permanently bound, at that point, to whichever\n// component instance was passed as `context` when `resource()` was called -\n// for a carried-over proxy that's always the *old*, already-destroyed\n// instance, so the first read after the swap throws \"Attempted to associate\n// a destroyable child with an object that is already destroying or\n// destroyed\" - see issue #563.\n//\n// `in` is used (rather than reading a property, or `Object.keys`/\n// `Object.prototype.toString.call`) because it's the one reflective\n// operation `wrapForPlainUsage`'s Proxy doesn't trap (no `has` handler), so\n// it forwards to a plain `[[HasProperty]]` on the underlying target without\n// ever invoking the lazy getter - it can't accidentally trigger the same\n// crash it's checking for. If `ember-resources` ever renames this key, this\n// check simply stops matching and behavior falls back to today's (already\n// broken, pre-existing) state - it does not introduce a new failure mode.\nconst RESOURCE_INTERMEDIATE_VALUE_KEY = '__Intermediate_Value__';\n\nfunction isUnsafeToCarryOver(value: unknown): boolean {\n return (\n (typeof value === 'object' || typeof value === 'function') &&\n value !== null &&\n RESOURCE_INTERMEDIATE_VALUE_KEY in value\n );\n}\n\nfunction getState(component: HotComponent, skip: string[]) {\n const state: Record<string, unknown> = {};\n if (!component) {\n return state;\n }\n for (const key in component) {\n if (skip.includes(key)) continue;\n const entry = findPropertyDescriptor(component, key);\n const value = component[key as keyof Component];\n \n // Skip Service instances - they should not be synced\n if (value instanceof Service) {\n continue;\n }\n \n // Skip Function properties - they should not be synced\n if (typeof value === 'function') {\n continue;\n }\n\n // Skip resource()-backed proxies - see isUnsafeToCarryOver above.\n if (isUnsafeToCarryOver(value)) {\n continue;\n }\n\n if (entry) {\n // Note: don't probe `entry.value` any further here (e.g. via\n // `Object.prototype.toString.call`) to decide whether it's a plain\n // function - the `typeof value === 'function'` check above already\n // covers that, and a stricter check on `entry.value` (which may be a\n // resource proxy, e.g. reactiveweb's `trackedTask`) can trigger that\n // proxy's `get` trap on first touch, lazily invoking a helper against\n // this component instance - see issue #557.\n if (entry.writable) {\n state[key] = value;\n }\n if (entry.set) {\n state[key] = value;\n }\n }\n }\n return state;\n}\n\n// Apply a stashed old-instance state to a freshly created new instance.\n// Also calls the _hmrAccepted lifecycle hook if defined on the new instance.\nfunction applySyncState(\n instance: Mutable<HotComponent>,\n state: Record<string, unknown>,\n oldInstance: HotComponent | undefined,\n) {\n for (const k in state) {\n if (instance[k as keyof HotComponent] instanceof Service) {\n continue;\n }\n instance[k as keyof HotComponent] = state[k];\n }\n if (typeof instance._hmrAccepted === 'function') {\n instance._hmrAccepted(oldInstance);\n }\n}\n\nexport function initialize() {\n // HMR-only: in a production build `import.meta.hot` is undefined, so this\n // (and the manager/route monkey-patches below) is dead-code-eliminated.\n if (!import.meta.hot) return;\n\n const ComponentManager = getInternalComponentManager(Component);\n const proto = Object.getPrototypeOf(ComponentManager);\n const create = proto.create;\n proto.create = function (...args: unknown[]) {\n const bucket = create.call(this, ...args);\n const component = bucket.component as HotComponent;\n\n // Enqueue this instance in the runtime's per-class FIFO queue. The runtime\n // only enqueues when isHmrClass(klass) is true (registered HMR class), and\n // clears the queue at update() time so user-flow stale entries are never\n // present when willDestroy fires during an HMR swap.\n const klass = component.constructor as object;\n const rt = hmrRuntime();\n if (rt?.isHmrClass(klass)) {\n rt.enqueueInstance(klass, component);\n }\n\n return bucket;\n };\n\n // Patch willDestroy on the Component prototype to detect HMR swaps and\n // transfer state from the old instance to the freshly-created new one.\n //\n // Why willDestroy instead of registerDestructor: Glimmer creates the NEW\n // component instance BEFORE calling willDestroy on the OLD one (willDestroy\n // is scheduled in Ember's \"actions\" runloop queue, which runs after\n // rendering). This ordering is exactly what we need: by willDestroy time,\n // the new instance is already live in liveInstances.\n const origWillDestroy = Component.prototype.willDestroy;\n Component.prototype.willDestroy = function (this: HotComponent) {\n const rt = hmrRuntime();\n if (rt) {\n const klass = this.constructor as object;\n const newClass = rt.current(klass);\n if (newClass !== klass) {\n // The class was swapped via HMR — dequeue the next new instance for\n // the replacement class (FIFO matches DOM/creation order) and\n // schedule a state transfer onto it.\n const newInstance = rt.dequeueInstance(newClass);\n if (newInstance) {\n const state = getState(this, ['args']);\n const oldInstance = this;\n setTimeout(() => applySyncState(newInstance as Mutable<HotComponent>, state, oldInstance));\n }\n } else if (rt.isHmrClass(klass)) {\n // Normal destroy (navigation, timer, conditional) — remove this\n // instance from the live queue so it doesn't accumulate as a stale\n // entry that update() would otherwise have to clear at the next save.\n rt.removeInstance(klass, this);\n }\n }\n return origWillDestroy?.call(this);\n };\n\n const setupController = Route.prototype.setupController;\n\n const StateCache: Record<string, unknown> = {};\n\n Route.prototype.setupController = function (...args: Parameters<typeof setupController>) {\n const controller = args[0] as unknown as Record<string, unknown>;\n const r = setupController.call(this, ...args);\n const fullRouteName = this.fullRouteName;\n const state = StateCache[fullRouteName] as { router?: Record<string, unknown>; controller?: Record<string, unknown> } || {};\n const skip = ['_qpDelegate', 'target', 'queryParams'];\n const routerState = getState(state.router as HotComponent, skip);\n for (const k in routerState) {\n (this as unknown as Record<string, unknown>)[k] = routerState[k];\n }\n // Only re-apply controller state onto a *new* instance (an HMR swap). On\n // ordinary navigation it is the same singleton, and re-applying a\n // non-`@tracked` query param onto itself trips Ember's mandatory setter.\n if (state.controller && state.controller !== controller) {\n const controllerState = getState(state.controller as HotComponent, skip);\n for (const k in controllerState) {\n (controller as Record<string, unknown>)[k] = controllerState[k];\n }\n }\n StateCache[fullRouteName] = {\n route: this,\n controller,\n };\n return r;\n };\n}\n\nexport default {\n initialize,\n};\n"],"names":["hmrRuntime","globalThis","__ember_vite_hmr","findPropertyDescriptor","component","key","proto","desc","Object","getOwnPropertyDescriptor","getPrototypeOf","RESOURCE_INTERMEDIATE_VALUE_KEY","isUnsafeToCarryOver","value","getState","skip","state","includes","entry","Service","writable","set","applySyncState","instance","oldInstance","k","_hmrAccepted","initialize","import","meta","hot","ComponentManager","getInternalComponentManager","Component","create","args","bucket","call","klass","constructor","rt","isHmrClass","enqueueInstance","origWillDestroy","prototype","willDestroy","newClass","current","newInstance","dequeueInstance","setTimeout","removeInstance","setupController","Route","StateCache","controller","r","fullRouteName","routerState","router","controllerState","route"],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type * as BabelTypesNamespace from '@babel/types';
|
|
2
|
+
import type * as Babel from '@babel/core';
|
|
3
|
+
export declare function renamePrivateClassMembers(babel: typeof Babel, classPath: Babel.NodePath<BabelTypesNamespace.ClassDeclaration | BabelTypesNamespace.ClassExpression>, programPath: Babel.NodePath<BabelTypesNamespace.Program>): void;
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
function hasPrivateMembers(classBody) {
|
|
2
|
+
return classBody.body.some((member) => member.type === 'ClassPrivateProperty' ||
|
|
3
|
+
member.type === 'ClassPrivateMethod');
|
|
4
|
+
}
|
|
5
|
+
// The generated HMR proxy (see service-proxy.ts's `ctorBody`) reads and
|
|
6
|
+
// writes service state through a Proxy whose `get`/`set` traps forward to a
|
|
7
|
+
// separately constructed delegate instance. Native `#private` fields can
|
|
8
|
+
// only ever be accessed with `this` bound to the exact object that declared
|
|
9
|
+
// them, and a method called as `proxy.method()` runs with `this` set to the
|
|
10
|
+
// proxy, not the delegate -- so a `#private` field read from ANY function
|
|
11
|
+
// reached through the proxy (a prototype method or a plain own-property
|
|
12
|
+
// function assigned in the constructor) would throw "Cannot read private
|
|
13
|
+
// member ... from an object whose class did not declare it" once called
|
|
14
|
+
// through it. An earlier version of this proxy special-cased prototype
|
|
15
|
+
// methods by `.bind()`-ing them to the delegate, but that's exactly
|
|
16
|
+
// backwards for an own-property function value: `bind()` returns a fresh
|
|
17
|
+
// function object, silently dropping any metadata identity-sensitive code
|
|
18
|
+
// associates with the original one via a WeakMap (e.g.
|
|
19
|
+
// `modifier()`/`helper()`, `setComponentManager` -- see #560/#561).
|
|
20
|
+
//
|
|
21
|
+
// Rewriting `#foo` to a plain, uniquely-named, non-computed property
|
|
22
|
+
// sidesteps the whole problem instead: an ordinary property reads and
|
|
23
|
+
// writes correctly through the proxy's existing traps no matter what `this`
|
|
24
|
+
// is at the call site, so no per-property binding is needed at all. This
|
|
25
|
+
// runs generically on *every* class the app's own source defines (see the
|
|
26
|
+
// `Class` visitor in ../../babel-plugin.ts), not just service classes, so it
|
|
27
|
+
// also covers `#private` fields on an ancestor class the service extends,
|
|
28
|
+
// as long as that ancestor is part of the app's own source (and therefore
|
|
29
|
+
// passes through this same babel pass). It does NOT help with a `#private`
|
|
30
|
+
// field declared on an ancestor class that lives in `node_modules` -- that
|
|
31
|
+
// file is never passed through this transform, so its fields stay fully
|
|
32
|
+
// native, and a prototype method inherited from it would still crash if
|
|
33
|
+
// called through the proxy. Ember's own `Service`/`EmberObject`/`CoreObject`
|
|
34
|
+
// chain declares no native `#private` fields, so this doesn't affect normal
|
|
35
|
+
// Ember apps; a third-party base class that does would need to stop using
|
|
36
|
+
// native `#private` fields, or avoid extending it directly with an
|
|
37
|
+
// HMR-proxied service.
|
|
38
|
+
//
|
|
39
|
+
// A Symbol-keyed property (an earlier version of this rewrite) would keep
|
|
40
|
+
// the member non-enumerable, closer to real privacy, but Ember's `@tracked`
|
|
41
|
+
// rejects it outright: `@glimmer/tracking`'s decorator entry point
|
|
42
|
+
// distinguishes "I was invoked as a native decorator" from "I was invoked as
|
|
43
|
+
// `tracked({...})`" by checking `typeof key === 'string'` (see
|
|
44
|
+
// `isElementDescriptor` in `@ember/-internals`), so a `@tracked #count = 0`
|
|
45
|
+
// field renamed to a Symbol key silently falls through to the wrong branch
|
|
46
|
+
// and never tracks at all (verified empirically: reads back `undefined`,
|
|
47
|
+
// writes throw "Cannot assign to read only property"). A plain string key
|
|
48
|
+
// satisfies that check and lets `@tracked`/`decorator-transforms` treat the
|
|
49
|
+
// member exactly like an ordinary public tracked field.
|
|
50
|
+
//
|
|
51
|
+
// An *undecorated* private property still gets a second pass: after the
|
|
52
|
+
// class body is rewritten, `hideRenamedProperties` below appends
|
|
53
|
+
// `Object.defineProperty(this, <name>, { ..., enumerable: false })` calls to
|
|
54
|
+
// the constructor for every renamed instance property that has no
|
|
55
|
+
// decorator, so it stays invisible to `for...in`, `Object.keys`,
|
|
56
|
+
// `JSON.stringify` and spread -- closer to real privacy than the plain
|
|
57
|
+
// enumerable field this rewrite produced before. It's still just a string
|
|
58
|
+
// key reachable via `Object.getOwnPropertyNames`/`Reflect.ownKeys` or the
|
|
59
|
+
// generated name, not real encapsulation. A *decorated* private property
|
|
60
|
+
// (e.g. `@tracked #count`) is left exactly as the field-rewrite above
|
|
61
|
+
// produces it -- `decorator-transforms` turns it into an accessor pair on
|
|
62
|
+
// the prototype, not an instance data property, so there's no own property
|
|
63
|
+
// for `Object.defineProperty` to hide in the first place, and forcing one
|
|
64
|
+
// would fight the decorator's own storage instead of complementing it.
|
|
65
|
+
//
|
|
66
|
+
// This second pass only runs against *instance* properties: a static
|
|
67
|
+
// private field lives on the class itself, never reaches the HMR proxy's
|
|
68
|
+
// per-instance `for...in` sync loop, and isn't part of the proxy/delegate
|
|
69
|
+
// problem this file exists to solve, so it's left as the plain enumerable
|
|
70
|
+
// rewrite the first pass already produces.
|
|
71
|
+
//
|
|
72
|
+
// The one thing a generated name can't be checked against is a private
|
|
73
|
+
// member of the *same name* declared by an ancestor/subclass living in a
|
|
74
|
+
// different file (each file's uid generation only sees its own bindings) --
|
|
75
|
+
// deliberately not solved here, since the `hmrPriv<Name>` prefix makes a
|
|
76
|
+
// same-name collision across unrelated classes exceedingly unlikely in
|
|
77
|
+
// practice, and the alternative (whole-program cross-file analysis) is out
|
|
78
|
+
// of proportion to that risk.
|
|
79
|
+
export function renamePrivateClassMembers(babel, classPath, programPath) {
|
|
80
|
+
var _a;
|
|
81
|
+
const t = babel.types;
|
|
82
|
+
if (!hasPrivateMembers(classPath.node.body)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// `renamedNames` tracks which private member names get rewritten so the
|
|
86
|
+
// reference-rewriting traversal below only touches references to *this*
|
|
87
|
+
// class's own renamed members (see the `classPath.traverse` call further
|
|
88
|
+
// down).
|
|
89
|
+
const renamedNames = new Set();
|
|
90
|
+
for (const member of classPath.node.body.body) {
|
|
91
|
+
if (member.type === 'ClassPrivateProperty' ||
|
|
92
|
+
member.type === 'ClassPrivateMethod') {
|
|
93
|
+
renamedNames.add(member.key.id.name);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Existing (non-computed, identifier-keyed) member names in this class's
|
|
97
|
+
// own body, so a generated replacement name can never collide with an
|
|
98
|
+
// unrelated member already declared here.
|
|
99
|
+
const usedNames = new Set();
|
|
100
|
+
for (const member of classPath.node.body.body) {
|
|
101
|
+
const key = member.key;
|
|
102
|
+
const computed = member.computed;
|
|
103
|
+
if (key && !computed && t.isIdentifier(key)) {
|
|
104
|
+
usedNames.add(key.name);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const newNames = new Map();
|
|
108
|
+
const getNewName = (name) => {
|
|
109
|
+
let known = newNames.get(name);
|
|
110
|
+
if (known) {
|
|
111
|
+
return known;
|
|
112
|
+
}
|
|
113
|
+
const base = `hmrPriv${name.charAt(0).toUpperCase()}${name.slice(1)}`;
|
|
114
|
+
let candidate;
|
|
115
|
+
do {
|
|
116
|
+
candidate = programPath.scope.generateUidIdentifier(base).name;
|
|
117
|
+
} while (usedNames.has(candidate));
|
|
118
|
+
usedNames.add(candidate);
|
|
119
|
+
newNames.set(name, candidate);
|
|
120
|
+
return candidate;
|
|
121
|
+
};
|
|
122
|
+
// Names of renamed instance properties that should be hidden from
|
|
123
|
+
// enumeration afterward (see `hideRenamedProperties` below) -- only
|
|
124
|
+
// undecorated, non-static private *properties* qualify; see the comment
|
|
125
|
+
// above this function for why decorated and static ones are excluded.
|
|
126
|
+
const namesToHide = [];
|
|
127
|
+
// Rename the class's own private declarations directly off the body
|
|
128
|
+
// array -- not via `classPath.traverse` -- so a nested class's own
|
|
129
|
+
// (unrelated, possibly same-named) private declarations are never
|
|
130
|
+
// touched: they simply aren't in this array.
|
|
131
|
+
for (const memberPath of classPath.get('body').get('body')) {
|
|
132
|
+
if (memberPath.isClassPrivateProperty()) {
|
|
133
|
+
const node = memberPath.node;
|
|
134
|
+
const newName = getNewName(node.key.id.name);
|
|
135
|
+
memberPath.replaceWith(t.classProperty(t.identifier(newName), node.value, node.typeAnnotation, node.decorators, false, node.static));
|
|
136
|
+
if (!node.static && !((_a = node.decorators) === null || _a === void 0 ? void 0 : _a.length)) {
|
|
137
|
+
namesToHide.push(newName);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (memberPath.isClassPrivateMethod()) {
|
|
141
|
+
const node = memberPath.node;
|
|
142
|
+
const replacement = t.classMethod(node.kind, t.identifier(getNewName(node.key.id.name)), node.params, node.body, false, node.static, node.generator, node.async);
|
|
143
|
+
// `t.classMethod`'s builder has no `decorators` parameter -- has to be
|
|
144
|
+
// assigned after the fact or a decorated private method (e.g. `@action
|
|
145
|
+
// #foo() {}`) would silently lose its decorator in the rewrite.
|
|
146
|
+
replacement.decorators = node.decorators;
|
|
147
|
+
memberPath.replaceWith(replacement);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// A private name reference found inside a nested class only actually
|
|
151
|
+
// belongs to `classPath`'s own renamed declaration if no closer-enclosing
|
|
152
|
+
// class *redeclares* the same name -- private names resolve to the
|
|
153
|
+
// nearest enclosing class that declares them, same as any other lexical
|
|
154
|
+
// scoping, so a nested class legitimately shadowing `#secret` with its
|
|
155
|
+
// own `#secret` must keep referencing its own (native, untouched) field.
|
|
156
|
+
const declaresOwnPrivate = (cls, name) => cls.body.body.some((member) => (member.type === 'ClassPrivateProperty' ||
|
|
157
|
+
member.type === 'ClassPrivateMethod') &&
|
|
158
|
+
member.key.id.name === name);
|
|
159
|
+
const referenceBelongsToOuterClass = (path, name) => {
|
|
160
|
+
let enclosing = path.findParent((p) => p.isClass());
|
|
161
|
+
while (enclosing) {
|
|
162
|
+
if (enclosing.node === classPath.node) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (declaresOwnPrivate(enclosing.node, name)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
enclosing = enclosing.findParent((p) => p.isClass());
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
};
|
|
172
|
+
// Rewrite *references* to the just-renamed names anywhere within the
|
|
173
|
+
// class, including inside nested classes/closures. Private names are
|
|
174
|
+
// lexically scoped to their enclosing class body, not to the file, so
|
|
175
|
+
// code legally references an outer class's private field from inside a
|
|
176
|
+
// nested class via a captured `this` (e.g. a factory method that builds
|
|
177
|
+
// and returns a class closing over `self.#secret`). Blanket-skipping
|
|
178
|
+
// traversal into nested classes -- as an earlier version of this
|
|
179
|
+
// function did -- left such references as native `#foo` PrivateNames
|
|
180
|
+
// while the outer declaration was renamed to a Symbol, producing invalid
|
|
181
|
+
// output ("Private field must be declared in an enclosing class").
|
|
182
|
+
classPath.traverse({
|
|
183
|
+
'MemberExpression|OptionalMemberExpression'(path) {
|
|
184
|
+
const property = path.node.property;
|
|
185
|
+
if (t.isPrivateName(property) &&
|
|
186
|
+
renamedNames.has(property.id.name) &&
|
|
187
|
+
referenceBelongsToOuterClass(path, property.id.name)) {
|
|
188
|
+
path.node.property = t.identifier(getNewName(property.id.name));
|
|
189
|
+
path.node.computed = false;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
BinaryExpression(path) {
|
|
193
|
+
const left = path.node.left;
|
|
194
|
+
if (path.node.operator === 'in' &&
|
|
195
|
+
t.isPrivateName(left) &&
|
|
196
|
+
renamedNames.has(left.id.name) &&
|
|
197
|
+
referenceBelongsToOuterClass(path, left.id.name)) {
|
|
198
|
+
path.node.left = t.stringLiteral(getNewName(left.id.name));
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
if (namesToHide.length > 0) {
|
|
203
|
+
hideRenamedProperties(babel, classPath, namesToHide);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Inserts `Object.defineProperty(this, <name>, { value: this.<name>,
|
|
207
|
+
// writable: true, configurable: true, enumerable: false })` into the class's
|
|
208
|
+
// constructor for every name in `names`, so the renamed properties in
|
|
209
|
+
// `names` stop showing up in `for...in`, `Object.keys`, `JSON.stringify`,
|
|
210
|
+
// and spread -- the same visibility a plain class-field rewrite (which is
|
|
211
|
+
// always enumerable; field syntax has no way to declare otherwise) can't
|
|
212
|
+
// provide on its own.
|
|
213
|
+
//
|
|
214
|
+
// This inserts rather than replacing the field declaration in place so the
|
|
215
|
+
// field keeps initializing at its original position in declaration order --
|
|
216
|
+
// a later field's initializer that legally reads an earlier private field
|
|
217
|
+
// (`#foo = 1; bar = this.#foo + 1;`) still sees the real value, since fields
|
|
218
|
+
// finish initializing (in declaration order, interleaved with each other)
|
|
219
|
+
// before any explicit constructor statement -- including ones this function
|
|
220
|
+
// adds -- ever runs, regardless of where in the constructor body they're
|
|
221
|
+
// inserted.
|
|
222
|
+
//
|
|
223
|
+
// The statements go right after `super(...)` (derived class) or at the very
|
|
224
|
+
// top of the constructor (base class) rather than at the end: all fields
|
|
225
|
+
// are already initialized by that point, and a later `this.<name> = ...`
|
|
226
|
+
// assignment in the constructor only updates the property's `value` -- a
|
|
227
|
+
// plain assignment doesn't reapply `defineProperty`'s `enumerable: false`,
|
|
228
|
+
// so inserting early still reflects any later mutation correctly. Inserting
|
|
229
|
+
// early also means the hide calls still run even if the constructor's own
|
|
230
|
+
// logic contains an early `return` (e.g. a guard clause) after this point --
|
|
231
|
+
// appending at the end would make them dead code on that path.
|
|
232
|
+
//
|
|
233
|
+
// Reads the current value back off `this` (a plain `MemberExpression`)
|
|
234
|
+
// rather than a captured property descriptor: when this class is a subclass
|
|
235
|
+
// of a proxied HMR service, `this` after `super()` is the Proxy from
|
|
236
|
+
// `service-proxy.ts`'s generated constructor, and a plain read goes through
|
|
237
|
+
// its `get` trap, which already forwards correctly to the delegate the
|
|
238
|
+
// property actually lives on.
|
|
239
|
+
function hideRenamedProperties(babel, classPath, names) {
|
|
240
|
+
const t = babel.types;
|
|
241
|
+
const hideStatements = names.map((name) => t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('Object'), t.identifier('defineProperty')), [
|
|
242
|
+
t.thisExpression(),
|
|
243
|
+
t.stringLiteral(name),
|
|
244
|
+
t.objectExpression([
|
|
245
|
+
t.objectProperty(t.identifier('value'), t.memberExpression(t.thisExpression(), t.identifier(name))),
|
|
246
|
+
t.objectProperty(t.identifier('writable'), t.booleanLiteral(true)),
|
|
247
|
+
t.objectProperty(t.identifier('configurable'), t.booleanLiteral(true)),
|
|
248
|
+
t.objectProperty(t.identifier('enumerable'), t.booleanLiteral(false)),
|
|
249
|
+
]),
|
|
250
|
+
])));
|
|
251
|
+
const bodyPath = classPath.get('body');
|
|
252
|
+
const existingCtor = bodyPath
|
|
253
|
+
.get('body')
|
|
254
|
+
.find((member) => member.isClassMethod({ kind: 'constructor' }));
|
|
255
|
+
if (existingCtor) {
|
|
256
|
+
const ctorBody = existingCtor.get('body');
|
|
257
|
+
if (!classPath.node.superClass) {
|
|
258
|
+
ctorBody.unshiftContainer('body', hideStatements);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// Only a top-level `super(...)` statement is safe to insert after --
|
|
262
|
+
// one buried inside a conditional (`if (x) { super(a); } else {
|
|
263
|
+
// super(b); }`) isn't a single point in the body to anchor on. Fall
|
|
264
|
+
// back to appending at the end in that case: still not immune to an
|
|
265
|
+
// early `return`, but at least not a `ReferenceError` from reading
|
|
266
|
+
// `this` before `super()` has (unconditionally) run.
|
|
267
|
+
const superCallStatement = ctorBody
|
|
268
|
+
.get('body')
|
|
269
|
+
.find((statement) => {
|
|
270
|
+
if (!statement.isExpressionStatement())
|
|
271
|
+
return false;
|
|
272
|
+
const expr = statement.get('expression');
|
|
273
|
+
if (Array.isArray(expr) || !expr.isCallExpression())
|
|
274
|
+
return false;
|
|
275
|
+
const callee = expr.get('callee');
|
|
276
|
+
return !Array.isArray(callee) && callee.isSuper();
|
|
277
|
+
});
|
|
278
|
+
if (superCallStatement) {
|
|
279
|
+
superCallStatement.insertAfter(hideStatements);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
ctorBody.pushContainer('body', hideStatements);
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// No constructor declared: synthesize one. A derived class's implicit
|
|
287
|
+
// default constructor forwards every argument to `super` before running
|
|
288
|
+
// anything else, so match that exactly rather than dropping constructor
|
|
289
|
+
// arguments the real default constructor would have passed through.
|
|
290
|
+
const ctorStatements = classPath.node.superClass
|
|
291
|
+
? [
|
|
292
|
+
t.expressionStatement(t.callExpression(t.super(), [t.spreadElement(t.identifier('args'))])),
|
|
293
|
+
...hideStatements,
|
|
294
|
+
]
|
|
295
|
+
: hideStatements;
|
|
296
|
+
bodyPath.unshiftContainer('body', t.classMethod('constructor', t.identifier('constructor'), [t.restElement(t.identifier('args'))], t.blockStatement(ctorStatements)));
|
|
297
|
+
}
|