-
Notifications
You must be signed in to change notification settings - Fork 382
/
hot-swaps.ts
219 lines (192 loc) · 8.77 KB
/
hot-swaps.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { isFalse, isNull, isUndefined } from '@lwc/shared';
import { VM, scheduleRehydration, forceRehydration } from './vm';
import { isComponentConstructor } from './def';
import { LightningElementConstructor } from './base-lightning-element';
import { Template } from './template';
import { markComponentAsDirty } from './component';
import { isTemplateRegistered } from './secure-template';
import { StylesheetFactory, TemplateStylesheetFactories, unrenderStylesheet } from './stylesheet';
import { assertNotProd, flattenStylesheets } from './utils';
import { WeakMultiMap } from './weak-multimap';
let swappedTemplateMap: WeakMap<Template, Template> = /*@__PURE__@*/ new WeakMap();
let swappedComponentMap: WeakMap<LightningElementConstructor, LightningElementConstructor> =
/*@__PURE__@*/ new WeakMap();
let swappedStyleMap: WeakMap<StylesheetFactory, StylesheetFactory> = /*@__PURE__@*/ new WeakMap();
// The important thing here is the weak values – VMs are transient (one per component instance) and should be GC'ed,
// so we don't want to create strong references to them.
// The weak keys are kind of useless, because Templates, LightningElementConstructors, and StylesheetFactories are
// never GC'ed. But maybe they will be someday, so we may as well use weak keys too.
// The "pure" annotations are so that Rollup knows for sure it can remove these from prod mode
let activeTemplates: WeakMultiMap<Template, VM> = /*@__PURE__@*/ new WeakMultiMap();
let activeComponents: WeakMultiMap<LightningElementConstructor, VM> =
/*@__PURE__@*/ new WeakMultiMap();
let activeStyles: WeakMultiMap<StylesheetFactory, VM> = /*@__PURE__@*/ new WeakMultiMap();
// Only used in LWC's Karma tests
if (process.env.NODE_ENV === 'test-karma-lwc') {
// Used to reset the global state between test runs
(window as any).__lwcResetHotSwaps = () => {
swappedTemplateMap = new WeakMap();
swappedComponentMap = new WeakMap();
swappedStyleMap = new WeakMap();
activeTemplates = new WeakMultiMap();
activeComponents = new WeakMultiMap();
activeStyles = new WeakMultiMap();
};
}
function rehydrateHotTemplate(tpl: Template): boolean {
const list = activeTemplates.get(tpl);
for (const vm of list) {
if (isFalse(vm.isDirty)) {
// forcing the vm to rehydrate in the micro-task:
markComponentAsDirty(vm);
scheduleRehydration(vm);
}
}
// Resetting the Set since these VMs are no longer related to this template, instead
// they will get re-associated once these instances are rehydrated.
activeTemplates.delete(tpl);
return true;
}
function rehydrateHotStyle(style: StylesheetFactory): boolean {
const activeVMs = activeStyles.get(style);
unrenderStylesheet(style);
for (const vm of activeVMs) {
// if a style definition is swapped, we must reset
// vm's template content in the next micro-task:
forceRehydration(vm);
}
// Resetting the Set since these VMs are no longer related to this style, instead
// they will get re-associated once these instances are rehydrated.
activeStyles.delete(style);
return true;
}
function rehydrateHotComponent(Ctor: LightningElementConstructor): boolean {
const list = activeComponents.get(Ctor);
let canRefreshAllInstances = true;
for (const vm of list) {
const { owner } = vm;
if (!isNull(owner)) {
// if a component class definition is swapped, we must reset
// owner's template content in the next micro-task:
forceRehydration(owner);
} else {
// the hot swapping for components only work for instances of components
// created from a template, root elements can't be swapped because we
// don't have a way to force the creation of the element with the same state
// of the current element.
// Instead, we can report the problem to the caller so it can take action,
// for example: reload the entire page.
canRefreshAllInstances = false;
}
}
// resetting the Set since these VMs are no longer related to this constructor, instead
// they will get re-associated once these instances are rehydrated.
activeComponents.delete(Ctor);
return canRefreshAllInstances;
}
export function getTemplateOrSwappedTemplate(tpl: Template): Template {
assertNotProd(); // this method should never leak to prod
// TODO [#4154]: shows stale content when swapping content back and forth multiple times
const visited: Set<Template> = new Set();
while (swappedTemplateMap.has(tpl) && !visited.has(tpl)) {
visited.add(tpl);
tpl = swappedTemplateMap.get(tpl)!;
}
return tpl;
}
export function getComponentOrSwappedComponent(
Ctor: LightningElementConstructor
): LightningElementConstructor {
assertNotProd(); // this method should never leak to prod
// TODO [#4154]: shows stale content when swapping content back and forth multiple times
const visited: Set<LightningElementConstructor> = new Set();
while (swappedComponentMap.has(Ctor) && !visited.has(Ctor)) {
visited.add(Ctor);
Ctor = swappedComponentMap.get(Ctor)!;
}
return Ctor;
}
export function getStyleOrSwappedStyle(style: StylesheetFactory): StylesheetFactory {
assertNotProd(); // this method should never leak to prod
// TODO [#4154]: shows stale content when swapping content back and forth multiple times
const visited: Set<StylesheetFactory> = new Set();
while (swappedStyleMap.has(style) && !visited.has(style)) {
visited.add(style);
style = swappedStyleMap.get(style)!;
}
return style;
}
function addActiveStylesheets(stylesheets: TemplateStylesheetFactories | undefined | null, vm: VM) {
if (isUndefined(stylesheets) || isNull(stylesheets)) {
// Ignore non-existent stylesheets
return;
}
for (const stylesheet of flattenStylesheets(stylesheets)) {
// this is necessary because we don't hold the list of styles
// in the vm, we only hold the selected (already swapped template)
// but the styles attached to the template might not be the actual
// active ones, but the swapped versions of those.
const swappedStylesheet = getStyleOrSwappedStyle(stylesheet);
// this will allow us to keep track of the stylesheet that are
// being used by a hot component
activeStyles.add(swappedStylesheet, vm);
}
}
export function setActiveVM(vm: VM) {
assertNotProd(); // this method should never leak to prod
// tracking active component
const Ctor = vm.def.ctor;
// this will allow us to keep track of the hot components
activeComponents.add(Ctor, vm);
// tracking active template
const template = vm.cmpTemplate;
if (!isNull(template)) {
// this will allow us to keep track of the templates that are
// being used by a hot component
activeTemplates.add(template, vm);
// Tracking active styles from the template or the VM. `template.stylesheets` are implicitly associated
// (e.g. `foo.css` associated with `foo.html`), whereas `vm.stylesheets` are from `static stylesheets`.
addActiveStylesheets(template.stylesheets, vm);
addActiveStylesheets(vm.stylesheets, vm);
}
}
export function swapTemplate(oldTpl: Template, newTpl: Template): boolean {
if (process.env.NODE_ENV !== 'production') {
if (isTemplateRegistered(oldTpl) && isTemplateRegistered(newTpl)) {
swappedTemplateMap.set(oldTpl, newTpl);
return rehydrateHotTemplate(oldTpl);
} else {
throw new TypeError(`Invalid Template`);
}
}
return false;
}
export function swapComponent(
oldComponent: LightningElementConstructor,
newComponent: LightningElementConstructor
): boolean {
if (process.env.NODE_ENV !== 'production') {
if (isComponentConstructor(oldComponent) && isComponentConstructor(newComponent)) {
swappedComponentMap.set(oldComponent, newComponent);
return rehydrateHotComponent(oldComponent);
} else {
throw new TypeError(`Invalid Component`);
}
}
return false;
}
export function swapStyle(oldStyle: StylesheetFactory, newStyle: StylesheetFactory): boolean {
if (process.env.NODE_ENV !== 'production') {
// TODO [#1887]: once the support for registering styles is implemented
// we can add the validation of both styles around this block.
swappedStyleMap.set(oldStyle, newStyle);
return rehydrateHotStyle(oldStyle);
}
return false;
}