-
Notifications
You must be signed in to change notification settings - Fork 798
/
Copy pathbootstrap-lazy.ts
291 lines (263 loc) · 11.1 KB
/
bootstrap-lazy.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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
import { BUILD } from '@app-data';
import { getHostRef, plt, registerHost, supportsShadow, win } from '@platform';
import { addHostEventListeners } from '@runtime';
import { CMP_FLAGS, queryNonceMetaTagContent } from '@utils';
import type * as d from '../declarations';
import { connectedCallback } from './connected-callback';
import { disconnectedCallback } from './disconnected-callback';
import {
patchChildSlotNodes,
patchCloneNode,
patchPseudoShadowDom,
patchSlotAppendChild,
patchTextContent,
} from './dom-extras';
import { hmrStart } from './hmr-component';
import { createTime, installDevTools } from './profile';
import { proxyComponent } from './proxy-component';
import { HYDRATED_CSS, PLATFORM_FLAGS, PROXY_FLAGS, SLOT_FB_CSS } from './runtime-constants';
import { hydrateScopedToShadow } from './styles';
import { appDidLoad } from './update-component';
export { setNonce } from '@platform';
export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d.CustomElementsDefineOptions = {}) => {
if (BUILD.profile && performance.mark) {
performance.mark('st:app:start');
}
installDevTools();
if (!win.document) {
console.warn('Stencil: No document found. Skipping bootstrapping lazy components.');
return;
}
const endBootstrap = createTime('bootstrapLazy');
const cmpTags: string[] = [];
const exclude = options.exclude || [];
const customElements = win.customElements;
const head = win.document.head;
const metaCharset = /*@__PURE__*/ head.querySelector('meta[charset]');
const dataStyles = /*@__PURE__*/ win.document.createElement('style');
const deferredConnectedCallbacks: { connectedCallback: () => void }[] = [];
let appLoadFallback: any;
let isBootstrapping = true;
Object.assign(plt, options);
plt.$resourcesUrl$ = new URL(options.resourcesUrl || './', win.document.baseURI).href;
if (BUILD.asyncQueue) {
if (options.syncQueue) {
plt.$flags$ |= PLATFORM_FLAGS.queueSync;
}
}
if (BUILD.hydrateClientSide) {
// If the app is already hydrated there is not point to disable the
// async queue. This will improve the first input delay
plt.$flags$ |= PLATFORM_FLAGS.appLoaded;
}
if (BUILD.hydrateClientSide && BUILD.shadowDom) {
hydrateScopedToShadow();
}
let hasSlotRelocation = false;
lazyBundles.map((lazyBundle) => {
lazyBundle[1].map((compactMeta) => {
const cmpMeta: d.ComponentRuntimeMeta = {
$flags$: compactMeta[0],
$tagName$: compactMeta[1],
$members$: compactMeta[2],
$listeners$: compactMeta[3],
};
// Check if we are using slots outside the shadow DOM in this component.
// We'll use this information later to add styles for `slot-fb` elements
if (cmpMeta.$flags$ & CMP_FLAGS.hasSlotRelocation) {
hasSlotRelocation = true;
}
if (BUILD.member) {
cmpMeta.$members$ = compactMeta[2];
}
if (BUILD.hostListener) {
cmpMeta.$listeners$ = compactMeta[3];
}
if (BUILD.reflect) {
cmpMeta.$attrsToReflect$ = [];
}
if (BUILD.watchCallback) {
cmpMeta.$watchers$ = compactMeta[4] ?? {};
}
if (BUILD.shadowDom && !supportsShadow && cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) {
// TODO(STENCIL-854): Remove code related to legacy shadowDomShim field
cmpMeta.$flags$ |= CMP_FLAGS.needsShadowDomShim;
}
const tagName =
BUILD.transformTagName && options.transformTagName
? options.transformTagName(cmpMeta.$tagName$)
: cmpMeta.$tagName$;
const HostElement = class extends HTMLElement {
['s-p']: Promise<void>[];
['s-rc']: (() => void)[];
hasRegisteredEventListeners = false;
// StencilLazyHost
constructor(self: HTMLElement) {
// @ts-ignore
super(self);
self = this;
registerHost(self, cmpMeta);
if (BUILD.shadowDom && cmpMeta.$flags$ & CMP_FLAGS.shadowDomEncapsulation) {
// this component is using shadow dom
// and this browser supports shadow dom
// add the read-only property "shadowRoot" to the host element
// adding the shadow root build conditionals to minimize runtime
if (supportsShadow) {
if (!self.shadowRoot) {
// we don't want to call `attachShadow` if there's already a shadow root
// attached to the component
if (BUILD.shadowDelegatesFocus) {
self.attachShadow({
mode: 'open',
delegatesFocus: !!(cmpMeta.$flags$ & CMP_FLAGS.shadowDelegatesFocus),
});
} else {
self.attachShadow({ mode: 'open' });
}
} else {
// we want to check to make sure that the mode for the shadow
// root already attached to the element (i.e. created via DSD)
// is set to 'open' since that's the only mode we support
if (self.shadowRoot.mode !== 'open') {
throw new Error(
`Unable to re-use existing shadow root for ${cmpMeta.$tagName$}! Mode is set to ${self.shadowRoot.mode} but Stencil only supports open shadow roots.`,
);
}
}
} else if (!BUILD.hydrateServerSide && !('shadowRoot' in self)) {
(self as any).shadowRoot = self;
}
}
}
connectedCallback() {
const hostRef = getHostRef(this);
/**
* The `connectedCallback` lifecycle event can potentially be fired multiple times
* if the element is removed from the DOM and re-inserted. This is not a common use case,
* but it can happen in some scenarios. To prevent registering the same event listeners
* multiple times, we will only register them once.
*/
if (!this.hasRegisteredEventListeners) {
this.hasRegisteredEventListeners = true;
addHostEventListeners(this, hostRef, cmpMeta.$listeners$, false);
}
if (appLoadFallback) {
clearTimeout(appLoadFallback);
appLoadFallback = null;
}
if (isBootstrapping) {
// connectedCallback will be processed once all components have been registered
deferredConnectedCallbacks.push(this);
} else {
plt.jmp(() => connectedCallback(this));
}
}
disconnectedCallback() {
plt.jmp(() => disconnectedCallback(this));
/**
* Clear up references within the `$vnode$` object to the DOM
* node that was removed. This is necessary to ensure that these
* references used as keys in the `hostRef` object can be properly
* garbage collected.
*
* Also remove the reference from `deferredConnectedCallbacks` array
* otherwise removed instances won't get garbage collected.
*/
plt.raf(() => {
const hostRef = getHostRef(this);
const i = deferredConnectedCallbacks.findIndex((host) => host === this);
if (i > -1) {
deferredConnectedCallbacks.splice(i, 1);
}
if (hostRef?.$vnode$?.$elm$ instanceof Node && !hostRef.$vnode$.$elm$.isConnected) {
delete hostRef.$vnode$.$elm$;
}
});
}
componentOnReady() {
return getHostRef(this).$onReadyPromise$;
}
};
// TODO(STENCIL-914): this check and `else` block can go away and be replaced by just the `scoped` check
if (BUILD.experimentalSlotFixes) {
// This check is intentionally not combined with the surrounding `experimentalSlotFixes` check
// since, moving forward, we only want to patch the pseudo shadow DOM when the component is scoped
if (BUILD.scoped && cmpMeta.$flags$ & CMP_FLAGS.scopedCssEncapsulation) {
patchPseudoShadowDom(HostElement.prototype);
}
} else {
if (BUILD.slotChildNodesFix) {
patchChildSlotNodes(HostElement.prototype);
}
if (BUILD.cloneNodeFix) {
patchCloneNode(HostElement.prototype);
}
if (BUILD.appendChildSlotFix) {
patchSlotAppendChild(HostElement.prototype);
}
if (BUILD.scopedSlotTextContentFix && cmpMeta.$flags$ & CMP_FLAGS.scopedCssEncapsulation) {
patchTextContent(HostElement.prototype);
}
}
// if the component is formAssociated we need to set that on the host
// element so that it will be ready for `attachInternals` to be called on
// it later on
if (BUILD.formAssociated && cmpMeta.$flags$ & CMP_FLAGS.formAssociated) {
(HostElement as any).formAssociated = true;
}
if (BUILD.hotModuleReplacement) {
// if we're in an HMR dev build then we need to set up the callback
// which will carry out the work of actually replacing the module for
// this particular component
((HostElement as any).prototype as d.HostElement)['s-hmr'] = function (hmrVersionId: string) {
hmrStart(this, cmpMeta, hmrVersionId);
};
}
cmpMeta.$lazyBundleId$ = lazyBundle[0];
if (!exclude.includes(tagName) && !customElements.get(tagName)) {
cmpTags.push(tagName);
customElements.define(
tagName,
proxyComponent(HostElement as any, cmpMeta, PROXY_FLAGS.isElementConstructor) as any,
);
}
});
});
// Only bother generating CSS if we have components
// TODO(STENCIL-1118): Add test cases for CSS content based on conditionals
if (cmpTags.length > 0) {
// Add styles for `slot-fb` elements if any of our components are using slots outside the Shadow DOM
if (hasSlotRelocation) {
dataStyles.textContent += SLOT_FB_CSS;
}
// Add hydration styles
if (BUILD.invisiblePrehydration && (BUILD.hydratedClass || BUILD.hydratedAttribute)) {
dataStyles.textContent += cmpTags.sort() + HYDRATED_CSS;
}
// If we have styles, add them to the DOM
if (dataStyles.innerHTML.length) {
dataStyles.setAttribute('data-styles', '');
// Apply CSP nonce to the style tag if it exists
const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(win.document);
if (nonce != null) {
dataStyles.setAttribute('nonce', nonce);
}
// Insert the styles into the document head
// NOTE: this _needs_ to happen last so we can ensure the nonce (and other attributes) are applied
head.insertBefore(dataStyles, metaCharset ? metaCharset.nextSibling : head.firstChild);
}
}
// Process deferred connectedCallbacks now all components have been registered
isBootstrapping = false;
if (deferredConnectedCallbacks.length) {
deferredConnectedCallbacks.map((host) => host.connectedCallback());
} else {
if (BUILD.profile) {
plt.jmp(() => (appLoadFallback = setTimeout(appDidLoad, 30, 'timeout')));
} else {
plt.jmp(() => (appLoadFallback = setTimeout(appDidLoad, 30)));
}
}
// Fallback appLoad event
endBootstrap();
};