UNPKG

preboot

Version:

Record server view events and play back to Angular client view

894 lines (881 loc) 34.2 kB
import { InjectionToken, APP_BOOTSTRAP_LISTENER, Optional, Inject, PLATFORM_ID, ApplicationRef, NgModule } from '@angular/core'; import { isPlatformServer, isPlatformBrowser, DOCUMENT } from '@angular/common'; import { filter, take } from 'rxjs/operators'; /** * Attempt to generate key from node position in the DOM */ function getNodeKeyForPreboot(nodeContext) { const ancestors = []; const root = nodeContext.root; const node = nodeContext.node; let temp = node; // walk up the tree from the target node up to the root while (temp && temp !== root.serverNode && temp !== root.clientNode) { ancestors.push(temp); temp = temp.parentNode; } // note: if temp doesn't exist here it means root node wasn't found if (temp) { ancestors.push(temp); } // now go backwards starting from the root, appending the appName to unique // identify the node later.. const name = node.nodeName || 'unknown'; let key = name; const len = ancestors.length; for (let i = len - 1; i >= 0; i--) { temp = ancestors[i]; if (temp.childNodes && i > 0) { for (let j = 0; j < temp.childNodes.length; j++) { if (temp.childNodes[j] === ancestors[i - 1]) { key += '_s' + (j + 1); break; } } } } return key; } /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ const PREBOOT_NONCE = new InjectionToken('PrebootNonce'); /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ function _window() { return { prebootData: window['prebootData'], getComputedStyle: window.getComputedStyle, document: document }; } class EventReplayer { constructor() { this.clientNodeCache = {}; this.replayStarted = false; } /** * Window setting and getter to facilitate testing of window * in non-browser environments */ setWindow(win) { this.win = win; } /** * Window setting and getter to facilitate testing of window * in non-browser environments */ getWindow() { if (!this.win) { this.win = _window(); } return this.win; } /** * Replay all events for all apps. this can only be run once. * if called multiple times, will only do something once */ replayAll() { if (this.replayStarted) { return; } else { this.replayStarted = true; } // loop through each of the preboot apps const prebootData = this.getWindow().prebootData || {}; const apps = prebootData.apps || []; apps.forEach(appData => this.replayForApp(appData)); // once all events have been replayed and buffers switched, then we cleanup preboot this.cleanup(prebootData); } /** * Replay all events for one app (most of the time there is just one app) * @param appData */ replayForApp(appData) { appData = (appData || {}); // try catch around events b/c even if error occurs, we still move forward try { const events = appData.events || []; // replay all the events from the server view onto the client view events.forEach(event => this.replayEvent(appData, event)); } catch (ex) { console.error(ex); } // if we are buffering, switch the buffers this.switchBuffer(appData); } /** * Replay one particular event * @param appData * @param prebootEvent */ replayEvent(appData, prebootEvent) { appData = (appData || {}); prebootEvent = (prebootEvent || {}); const event = prebootEvent.event; const serverNode = prebootEvent.node || {}; const nodeKey = prebootEvent.nodeKey; const clientNode = this.findClientNode({ root: appData.root, node: serverNode, nodeKey: nodeKey }); // if client node can't be found, log a warning if (!clientNode) { console.warn(`Trying to dispatch event ${event.type} to node ${nodeKey} but could not find client node. Server node is: ${serverNode}`); return; } // now dispatch events and whatnot to the client node clientNode.checked = serverNode.checked; clientNode.selected = serverNode.selected; clientNode.value = serverNode.value; clientNode.dispatchEvent(event); } /** * Switch the buffer for one particular app (i.e. display the client * view and destroy the server view) * @param appData */ switchBuffer(appData) { appData = (appData || {}); const root = (appData.root || {}); const serverView = root.serverNode; const clientView = root.clientNode; // if no client view or the server view is the body or client // and server view are the same, then don't do anything and return if (!clientView || !serverView || serverView === clientView || serverView.nodeName === 'BODY') { return; } // do a try-catch just in case something messed up try { // get the server view display mode const gcs = this.getWindow().getComputedStyle; const display = gcs(serverView).getPropertyValue('display') || 'block'; // first remove the server view serverView.remove ? serverView.remove() : (serverView.style.display = 'none'); // now add the client view clientView.style.display = display; } catch (ex) { console.error(ex); } } /** * Finally, set focus, remove all the event listeners and remove * any freeze screen that may be there * @param prebootData */ cleanup(prebootData) { prebootData = prebootData || {}; const listeners = prebootData.listeners || []; // set focus on the active node AFTER a small delay to ensure buffer // switched const activeNode = prebootData.activeNode; if (activeNode != null) { setTimeout(() => this.setFocus(activeNode), 1); } // remove all event listeners for (const listener of listeners) { listener.node.removeEventListener(listener.eventName, listener.handler); } // remove the freeze overlay if it exists const doc = this.getWindow().document; const prebootOverlay = doc.getElementById('prebootOverlay'); if (prebootOverlay) { prebootOverlay.remove ? prebootOverlay.remove() : prebootOverlay.parentNode !== null ? prebootOverlay.parentNode.removeChild(prebootOverlay) : prebootOverlay.style.display = 'none'; } // clear out the data stored for each app prebootData.apps = []; this.clientNodeCache = {}; // send event to document that signals preboot complete // constructor is not supported by older browsers ( i.e. IE9-11 ) // in these browsers, the type of CustomEvent will be "object" if (typeof CustomEvent === 'function') { const completeEvent = new CustomEvent('PrebootComplete'); doc.dispatchEvent(completeEvent); } else { console.warn(`Could not dispatch PrebootComplete event. You can fix this by including a polyfill for CustomEvent.`); } } setFocus(activeNode) { // only do something if there is an active node if (!activeNode || !activeNode.node || !activeNode.nodeKey) { return; } // find the client node in the new client view const clientNode = this.findClientNode(activeNode); if (clientNode) { // set focus on the client node clientNode.focus(); // set selection if a modern browser (i.e. IE9+, etc.) const selection = activeNode.selection; if (clientNode.setSelectionRange && selection) { try { clientNode .setSelectionRange(selection.start, selection.end, selection.direction); } catch (ex) { } } } } /** * Given a node from the server rendered view, find the equivalent * node in the client rendered view. We do this by the following approach: * 1. take the name of the server node tag (ex. div or h1 or input) * 2. add either id (ex. div#myid) or class names (ex. div.class1.class2) * 3. use that value as a selector to get all the matching client nodes * 4. loop through all client nodes found and for each generate a key value * 5. compare the client key to the server key; once there is a match, * we have our client node * * NOTE: this only works when the client view is almost exactly the same as * the server view. we will need an improvement here in the future to account * for situations where the client view is different in structure from the * server view */ findClientNode(serverNodeContext) { serverNodeContext = (serverNodeContext || {}); const serverNode = serverNodeContext.node; const root = serverNodeContext.root; // if no server or client root, don't do anything if (!root || !root.serverNode || !root.clientNode) { return null; } // we use the string of the node to compare to the client node & as key in // cache const serverNodeKey = serverNodeContext.nodeKey || getNodeKeyForPreboot(serverNodeContext); // if client node already in cache, return it if (this.clientNodeCache[serverNodeKey]) { return this.clientNodeCache[serverNodeKey]; } // get the selector for client nodes const className = (serverNode.className || '').replace('ng-binding', '').trim(); let selector = serverNode.tagName; if (serverNode.id) { selector += `#${serverNode.id}`; } else if (className) { selector += `.${className.replace(/ /g, '.')}`; } // select all possible client nodes and look through them to try and find a // match const rootClientNode = root.clientNode; let clientNodes = rootClientNode.querySelectorAll(selector); // if nothing found, then just try the tag name as a final option if (!clientNodes.length) { console.log(`nothing found for ${selector} so using ${serverNode.tagName}`); clientNodes = rootClientNode.querySelectorAll(serverNode.tagName); } const length = clientNodes.length; for (let i = 0; i < length; i++) { const clientNode = clientNodes.item(i); // get the key for the client node const clientNodeKey = getNodeKeyForPreboot({ root: root, node: clientNode }); // if the client node key is exact match for the server node key, then we // found the client node if (clientNodeKey === serverNodeKey) { this.clientNodeCache[serverNodeKey] = clientNode; return clientNode; } } // if we get here and there is one clientNode, use it as a fallback if (clientNodes.length === 1) { this.clientNodeCache[serverNodeKey] = clientNodes[0]; return clientNodes[0]; } // if we get here it means we couldn't find the client node so give the user // a warning console.warn(`No matching client node found for ${serverNodeKey}. You can fix this by assigning this element a unique id attribute.`); return null; } } /** * Called right away to initialize preboot * * @param opts All the preboot options * @param win */ function initAll(opts, win) { const theWindow = (win || window); // Add the preboot options to the preboot data and then add the data to // the window so it can be used later by the client. // Only set new options if they're not already set - we may have multiple app roots // and each of them invokes the init function separately. const data = (theWindow.prebootData = { opts: opts, apps: [], listeners: [] }); return () => start(data, theWindow); } /** * Start up preboot by going through each app and assigning the appropriate * handlers. Normally this wouldn't be called directly, but we have set it up so * that it can for older versions of Universal. * * @param prebootData Global preboot data object that contains options and will * have events * @param win Optional param to pass in mock window for testing purposes */ function start(prebootData, win) { const theWindow = (win || window); const _document = (theWindow.document || {}); // Remove the current script from the DOM so that child indexes match // between the client & the server. The script is already running so it // doesn't affect it. const currentScript = _document.currentScript || // Support: IE 9-11 only // IE doesn't support document.currentScript. Since the script is invoked // synchronously, though, the current running script is just the last one // currently in the document. [].slice.call(_document.getElementsByTagName('script'), -1)[0]; if (!currentScript) { console.error('Preboot initialization failed, no currentScript has been detected.'); return; } let serverNode = currentScript.parentNode; if (!serverNode) { console.error('Preboot initialization failed, the script is detached'); return; } serverNode.removeChild(currentScript); const opts = prebootData.opts || {}; let eventSelectors = opts.eventSelectors || []; // get the root info const appRoot = prebootData.opts ? getAppRoot(_document, prebootData.opts, serverNode) : null; // we track all events for each app in the prebootData object which is on // the global scope; each `start` invocation adds data for one app only. const appData = { root: appRoot, events: [] }; if (prebootData.apps) { prebootData.apps.push(appData); } eventSelectors = eventSelectors.map(eventSelector => { if (!eventSelector.hasOwnProperty('replay')) { eventSelector.replay = true; } return eventSelector; }); // loop through all the eventSelectors and create event handlers eventSelectors.forEach(eventSelector => handleEvents(_document, prebootData, appData, eventSelector)); } /** * Create an overlay div and add it to the DOM so it can be used * if a freeze event occurs * * @param _document The global document object (passed in for testing purposes) * @returns Element The overlay node is returned */ function createOverlay(_document) { let overlay = _document.createElement('div'); overlay.setAttribute('id', 'prebootOverlay'); overlay.setAttribute('style', 'display:none;position:absolute;left:0;' + 'top:0;width:100%;height:100%;z-index:999999;background:black;opacity:.3'); _document.documentElement.appendChild(overlay); return overlay; } /** * Get references to the current app root node based on input options. Users can * initialize preboot either by specifying appRoot which is just one or more * selectors for apps. This section option is useful for people that are doing their own * buffering (i.e. they have their own client and server view) * * @param _document The global document object used to attach the overlay * @param opts Options passed in by the user to init() * @param serverNode The server node serving as application root * @returns ServerClientRoot An array of root info for the current app */ function getAppRoot(_document, opts, serverNode) { const root = { serverNode }; // if we are doing buffering, we need to create the buffer for the client // else the client root is the same as the server root.clientNode = opts.buffer ? createBuffer(root) : root.serverNode; // create an overlay if not disabled ,that can be used later if a freeze event occurs if (!opts.disableOverlay) { root.overlay = createOverlay(_document); } return root; } /** * Under given server root, for given selector, record events * * @param _document * @param prebootData * @param appData * @param eventSelector */ function handleEvents(_document, prebootData, appData, eventSelector) { const serverRoot = appData.root.serverNode; // don't do anything if no server root if (!serverRoot) { return; } // Attach delegated event listeners for each event selector. // We need to use delegated events as only the top level server node // exists at this point. eventSelector.events.forEach((eventName) => { // get the appropriate handler and add it as an event listener const handler = createListenHandler(_document, prebootData, eventSelector, appData); // attach the handler in the capture phase so that it fires even if // one of the handlers below calls stopPropagation() serverRoot.addEventListener(eventName, handler, true); // need to keep track of listeners so we can do node.removeEventListener() // when preboot done if (prebootData.listeners) { prebootData.listeners.push({ node: serverRoot, eventName, handler }); } }); } /** * Create handler for events that we will record */ function createListenHandler(_document, prebootData, eventSelector, appData) { const CARET_EVENTS = ['keyup', 'keydown', 'focusin', 'mouseup', 'mousedown']; const CARET_NODES = ['INPUT', 'TEXTAREA']; // Support: IE 9-11 only // IE uses a prefixed `matches` version const matches = _document.documentElement.matches || _document.documentElement.msMatchesSelector; const opts = prebootData.opts; return function (event) { const node = event.target; // a delegated handlers on document is used so we need to check if // event target matches a desired selector if (!matches.call(node, eventSelector.selector)) { return; } const root = appData.root; const eventName = event.type; // if no node or no event name, just return if (!node || !eventName) { return; } // if key codes set for eventSelector, then don't do anything if event // doesn't include key const keyCodes = eventSelector.keyCodes; if (keyCodes && keyCodes.length) { const matchingKeyCodes = keyCodes.filter(keyCode => event.which === keyCode); // if there are not matches (i.e. key entered NOT one of the key codes) // then don't do anything if (!matchingKeyCodes.length) { return; } } // if for a given set of events we are preventing default, do that if (eventSelector.preventDefault) { event.preventDefault(); } // if an action handler passed in, use that if (eventSelector.action) { eventSelector.action(node, event); } // get the node key for a given node const nodeKey = getNodeKeyForPreboot({ root: root, node: node }); // record active node if (CARET_EVENTS.indexOf(eventName) >= 0) { // if it's an caret node, get the selection for the active node const isCaretNode = CARET_NODES.indexOf(node.tagName ? node.tagName : '') >= 0; prebootData.activeNode = { root: root, node: node, nodeKey: nodeKey, selection: isCaretNode ? getSelection(node) : undefined }; } else if (eventName !== 'change' && eventName !== 'focusout') { prebootData.activeNode = undefined; } // if overlay is not disabled and we are freezing the UI if (opts && !opts.disableOverlay && eventSelector.freeze) { const overlay = root.overlay; // show the overlay overlay.style.display = 'block'; // hide the overlay after 10 seconds just in case preboot.complete() never // called setTimeout(() => { overlay.style.display = 'none'; }, 10000); } // we will record events for later replay unless explicitly marked as // doNotReplay if (eventSelector.replay) { appData.events.push({ node, nodeKey, event, name: eventName }); } }; } /** * Get the selection data that is later used to set the cursor after client view * is active */ function getSelection(node) { node = node || {}; const nodeValue = node.value || ''; const selection = { start: nodeValue.length, end: nodeValue.length, direction: 'forward' }; // if browser support selectionStart on node (Chrome, FireFox, IE9+) try { if (node.selectionStart || node.selectionStart === 0) { selection.start = node.selectionStart; selection.end = node.selectionEnd ? node.selectionEnd : 0; selection.direction = node.selectionDirection ? node.selectionDirection : 'none'; } } catch (ex) { } return selection; } /** * Create buffer for a given node * * @param root All the data related to a particular app * @returns Returns the root client node. */ function createBuffer(root) { const serverNode = root.serverNode; // if no rootServerNode OR the selector is on the entire html doc or the body // OR no parentNode, don't buffer if (!serverNode || !serverNode.parentNode || serverNode === document.documentElement || serverNode === document.body) { return serverNode; } // create shallow clone of server root const rootClientNode = serverNode.cloneNode(false); // we want the client to write to a hidden div until the time for switching // the buffers rootClientNode.style.display = 'none'; // insert the client node before the server and return it serverNode.parentNode.insertBefore(rootClientNode, serverNode); // mark server node as not to be touched by AngularJS - needed for ngUpgrade serverNode.setAttribute('ng-non-bindable', ''); // return the rootClientNode return rootClientNode; } const eventRecorder = { start, createOverlay, getAppRoot, handleEvents, createListenHandler, getSelection, createBuffer }; const initFunctionName = 'prebootInitFn'; // exporting default options in case developer wants to use these + custom on // top const defaultOptions = { buffer: true, replay: true, disableOverlay: false, // these are the default events are are listening for an transferring from // server view to client view eventSelectors: [ // for recording changes in form elements { selector: 'input,textarea', events: ['keypress', 'keyup', 'keydown', 'input', 'change'] }, { selector: 'select,option', events: ['change'] }, // when user hits return button in an input box { selector: 'input', events: ['keyup'], preventDefault: true, keyCodes: [13], freeze: true }, // when user submit form (press enter, click on button/input[type="submit"]) { selector: 'form', events: ['submit'], preventDefault: true, freeze: true }, // for tracking focus (no need to replay) { selector: 'input,textarea', events: ['focusin', 'focusout', 'mousedown', 'mouseup'], replay: false }, // user clicks on a button { selector: 'button', events: ['click'], preventDefault: true, freeze: true } ] }; /** * Get the event recorder code based on all functions in event.recorder.ts * and the getNodeKeyForPreboot function. */ function getEventRecorderCode() { const eventRecorderFunctions = []; for (const funcName in eventRecorder) { if (eventRecorder.hasOwnProperty(funcName)) { const fn = eventRecorder[funcName].toString(); const fnCleaned = fn.replace('common_1.', ''); eventRecorderFunctions.push(fnCleaned); } } // this is common function used to get the node key eventRecorderFunctions.push(getNodeKeyForPreboot.toString()); // add new line characters for readability return '\n\n' + eventRecorderFunctions.join('\n\n') + '\n\n'; } /** * Used by the server side version of preboot. The main purpose is to get the * inline code that can be inserted into the server view. * Returns the definitions of the prebootInit function called in code returned by * getInlineInvocation for each server node separately. * * @param customOptions PrebootRecordOptions that override the defaults * @returns Generated inline preboot code with just functions definitions * to be used separately */ function getInlineDefinition(customOptions) { const opts = assign({}, defaultOptions, customOptions); // safety check to make sure options passed in are valid validateOptions(opts); const scriptCode = getEventRecorderCode(); const optsStr = stringifyWithFunctions(opts); // wrap inline preboot code with a self executing function in order to create scope const initAllStr = initAll.toString(); return `var ${initFunctionName} = (function() { ${scriptCode} return (${initAllStr.replace('common_1.', '')})(${optsStr}); })();`; } /** * Used by the server side version of preboot. The main purpose is to get the * inline code that can be inserted into the server view. * Invokes the prebootInit function defined in getInlineDefinition with proper * parameters. Each appRoot should get a separate inlined code from a separate * call to getInlineInvocation but only one inlined code from getInlineDefinition. * * @returns Generated inline preboot code with just invocations of functions from * getInlineDefinition */ function getInlineInvocation() { return `${initFunctionName}();`; } /** * Throw an error if issues with any options * @param opts */ function validateOptions(opts) { if (!opts.appRoot || !opts.appRoot.length) { throw new Error('The appRoot is missing from preboot options. ' + 'This is needed to find the root of your application. ' + 'Set this value in the preboot options to be a selector for the root element of your app.'); } } /** * Object.assign() is not fully supporting in TypeScript, so * this is just a simple implementation of it * * @param target The target object * @param optionSets Any number of addition objects that are added on top of the * target * @returns A new object that contains all the merged values */ function assign(target, ...optionSets) { if (target === undefined || target === null) { throw new TypeError('Cannot convert undefined or null to object'); } const output = Object(target); for (let index = 0; index < optionSets.length; index++) { const source = optionSets[index]; if (source !== undefined && source !== null) { for (const nextKey in source) { if (source.hasOwnProperty && source.hasOwnProperty(nextKey)) { output[nextKey] = source[nextKey]; } } } } return output; } /** * Stringify an object and include functions. This is needed since we are * letting users pass in options that include custom functions for things like * the freeze handler or action when an event occurs * * @param obj This is the object you want to stringify that includes some * functions * @returns The stringified version of an object */ function stringifyWithFunctions(obj) { const FUNC_START = 'START_FUNCTION_HERE'; const FUNC_STOP = 'STOP_FUNCTION_HERE'; // first stringify except mark off functions with markers let str = JSON.stringify(obj, function (_key, value) { // if the value is a function, we want to wrap it with markers if (!!(value && value.constructor && value.call && value.apply)) { return FUNC_START + value.toString() + FUNC_STOP; } else { return value; } }); // now we use the markers to replace function strings with actual functions let startFuncIdx = str.indexOf(FUNC_START); let stopFuncIdx; let fn; while (startFuncIdx >= 0) { stopFuncIdx = str.indexOf(FUNC_STOP); // pull string out fn = str.substring(startFuncIdx + FUNC_START.length, stopFuncIdx); fn = fn.replace(/\\n/g, '\n'); str = str.substring(0, startFuncIdx - 1) + fn + str.substring(stopFuncIdx + FUNC_STOP.length + 1); startFuncIdx = str.indexOf(FUNC_START); } return str; } /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ const PREBOOT_SCRIPT_CLASS = 'preboot-inline-script'; const PREBOOT_OPTIONS = new InjectionToken('PrebootOptions'); function createScriptFromCode(doc, nonce, inlineCode) { const script = doc.createElement('script'); if (nonce) { script.nonce = nonce; } script.className = PREBOOT_SCRIPT_CLASS; script.textContent = inlineCode; return script; } function PREBOOT_FACTORY(doc, prebootOpts, nonce, platformId, appRef, eventReplayer) { return () => { validateOptions(prebootOpts); if (isPlatformServer(platformId)) { const inlineCodeDefinition = getInlineDefinition(prebootOpts); const scriptWithDefinition = createScriptFromCode(doc, nonce, inlineCodeDefinition); const inlineCodeInvocation = getInlineInvocation(); const existingScripts = doc.getElementsByClassName(PREBOOT_SCRIPT_CLASS); // Check to see if preboot scripts are already inlined before adding them // to the DOM. If they are, update the nonce to be current. if (existingScripts.length === 0) { const baseList = []; const appRootSelectors = baseList.concat(prebootOpts.appRoot); doc.head.appendChild(scriptWithDefinition); appRootSelectors .map(selector => ({ selector, appRootElem: doc.querySelector(selector) })) .forEach(({ selector, appRootElem }) => { if (!appRootElem) { console.log(`No server node found for selector: ${selector}`); return; } const scriptWithInvocation = createScriptFromCode(doc, nonce, inlineCodeInvocation); appRootElem.insertBefore(scriptWithInvocation, appRootElem.firstChild); }); } else if (existingScripts.length > 0 && nonce) { existingScripts[0].nonce = nonce; } } if (isPlatformBrowser(platformId)) { const replay = prebootOpts.replay != null ? prebootOpts.replay : true; if (replay) { appRef.isStable .pipe(filter(stable => stable), take(1)).subscribe(() => { eventReplayer.replayAll(); }); } } }; } const PREBOOT_PROVIDER = { provide: APP_BOOTSTRAP_LISTENER, useFactory: PREBOOT_FACTORY, deps: [ DOCUMENT, PREBOOT_OPTIONS, [new Optional(), new Inject(PREBOOT_NONCE)], PLATFORM_ID, ApplicationRef, EventReplayer, ], multi: true }; /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ class PrebootModule { static withConfig(opts) { return { ngModule: PrebootModule, providers: [{ provide: PREBOOT_OPTIONS, useValue: opts }] }; } } PrebootModule.decorators = [ { type: NgModule, args: [{ providers: [EventReplayer, PREBOOT_PROVIDER] },] } ]; /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * Generated bundle index. Do not edit. */ export { EventReplayer, PREBOOT_NONCE, PrebootModule, _window, assign, createBuffer, createListenHandler, createOverlay, defaultOptions, getAppRoot, getEventRecorderCode, getInlineDefinition, getInlineInvocation, getNodeKeyForPreboot, getSelection, handleEvents, initAll, initFunctionName, start, stringifyWithFunctions, validateOptions, PREBOOT_OPTIONS as ɵa, PREBOOT_FACTORY as ɵb, PREBOOT_PROVIDER as ɵc }; //# sourceMappingURL=preboot.js.map