UNPKG

single-spa

Version:

The router for easy microfrontends

1,462 lines (1,247 loc) 71.7 kB
/* single-spa@6.0.3 - UMD ES5 - dev */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.singleSpa = {})); })(this, (function (exports) { 'use strict'; var singleSpa = /*#__PURE__*/Object.freeze({ __proto__: null, get start () { return start; }, get ensureJQuerySupport () { return ensureJQuerySupport; }, get setBootstrapMaxTime () { return setBootstrapMaxTime; }, get setMountMaxTime () { return setMountMaxTime; }, get setUnmountMaxTime () { return setUnmountMaxTime; }, get setUnloadMaxTime () { return setUnloadMaxTime; }, get registerApplication () { return registerApplication; }, get unregisterApplication () { return unregisterApplication; }, get getMountedApps () { return getMountedApps; }, get getAppStatus () { return getAppStatus; }, get unloadApplication () { return unloadApplication; }, get checkActivityFunctions () { return checkActivityFunctions; }, get getAppNames () { return getAppNames; }, get pathToActiveWhen () { return pathToActiveWhen; }, get navigateToUrl () { return navigateToUrl; }, get patchHistoryApi () { return patchHistoryApi; }, get triggerAppChange () { return triggerAppChange; }, get addErrorHandler () { return addErrorHandler; }, get removeErrorHandler () { return removeErrorHandler; }, get mountRootParcel () { return mountRootParcel; }, get NOT_LOADED () { return NOT_LOADED; }, get LOADING_SOURCE_CODE () { return LOADING_SOURCE_CODE; }, get NOT_BOOTSTRAPPED () { return NOT_BOOTSTRAPPED; }, get BOOTSTRAPPING () { return BOOTSTRAPPING; }, get NOT_MOUNTED () { return NOT_MOUNTED; }, get MOUNTING () { return MOUNTING; }, get UPDATING () { return UPDATING; }, get LOAD_ERROR () { return LOAD_ERROR; }, get MOUNTED () { return MOUNTED; }, get UNLOADING () { return UNLOADING; }, get UNMOUNTING () { return UNMOUNTING; }, get SKIP_BECAUSE_BROKEN () { return SKIP_BECAUSE_BROKEN; } }); function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function (obj) { return typeof obj; }; } else { _typeof = function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; var NativeCustomEvent = commonjsGlobal.CustomEvent; function useNative () { try { var p = new NativeCustomEvent('cat', { detail: { foo: 'bar' } }); return 'cat' === p.type && 'bar' === p.detail.foo; } catch (e) { } return false; } /** * Cross-browser `CustomEvent` constructor. * * https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent.CustomEvent * * @public */ var customEvent = useNative() ? NativeCustomEvent : // IE >= 9 'undefined' !== typeof document && 'function' === typeof document.createEvent ? function CustomEvent (type, params) { var e = document.createEvent('CustomEvent'); if (params) { e.initCustomEvent(type, params.bubbles, params.cancelable, params.detail); } else { e.initCustomEvent(type, false, false, void 0); } return e; } : // IE <= 8 function CustomEvent (type, params) { var e = document.createEventObject(); e.type = type; if (params) { e.bubbles = Boolean(params.bubbles); e.cancelable = Boolean(params.cancelable); e.detail = params.detail; } else { e.bubbles = false; e.cancelable = false; e.detail = void 0; } return e; }; var errorHandlers = []; function handleAppError(err, app, newStatus) { var transformedErr = transformErr(err, app, newStatus); if (errorHandlers.length) { errorHandlers.forEach(function (handler) { return handler(transformedErr); }); } else { setTimeout(function () { throw transformedErr; }); } } function addErrorHandler(handler) { if (typeof handler !== "function") { throw Error(formatErrorMessage(28, "a single-spa error handler must be a function")); } errorHandlers.push(handler); } function removeErrorHandler(handler) { if (typeof handler !== "function") { throw Error(formatErrorMessage(29, "a single-spa error handler must be a function")); } var removedSomething = false; errorHandlers = errorHandlers.filter(function (h) { var isHandler = h === handler; removedSomething = removedSomething || isHandler; return !isHandler; }); return removedSomething; } function formatErrorMessage(code, msg) { for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { args[_key - 2] = arguments[_key]; } return "single-spa minified message #".concat(code, ": ").concat(msg ? msg + " " : "", "See https://single-spa.js.org/error/?code=").concat(code).concat(args.length ? "&arg=".concat(args.join("&arg=")) : ""); } function transformErr(ogErr, appOrParcel, newStatus) { var errPrefix = "".concat(objectType(appOrParcel), " '").concat(toName(appOrParcel), "' died in status ").concat(appOrParcel.status, ": "); var result; if (ogErr instanceof Error) { try { ogErr.message = errPrefix + ogErr.message; } catch (err) { /* Some errors have read-only message properties, in which case there is nothing * that we can do. */ } result = ogErr; } else { console.warn(formatErrorMessage(30, "While ".concat(appOrParcel.status, ", '").concat(toName(appOrParcel), "' rejected its lifecycle function promise with a non-Error. This will cause stack traces to not be accurate."), appOrParcel.status, toName(appOrParcel))); try { result = Error(errPrefix + JSON.stringify(ogErr)); } catch (err) { // If it's not an Error and you can't stringify it, then what else can you even do to it? result = ogErr; } } result.appOrParcelName = toName(appOrParcel); // We set the status after transforming the error so that the error message // references the state the application was in before the status change. appOrParcel.status = newStatus; return result; } var NOT_LOADED = "NOT_LOADED"; var LOADING_SOURCE_CODE = "LOADING_SOURCE_CODE"; var NOT_BOOTSTRAPPED = "NOT_BOOTSTRAPPED"; var BOOTSTRAPPING = "BOOTSTRAPPING"; var NOT_MOUNTED = "NOT_MOUNTED"; var MOUNTING = "MOUNTING"; var MOUNTED = "MOUNTED"; var UPDATING = "UPDATING"; var UNMOUNTING = "UNMOUNTING"; var UNLOADING = "UNLOADING"; var LOAD_ERROR = "LOAD_ERROR"; var SKIP_BECAUSE_BROKEN = "SKIP_BECAUSE_BROKEN"; function isActive(app) { return app.status === MOUNTED; } function shouldBeActive(app) { try { return app.activeWhen(window.location); } catch (err) { handleAppError(err, app, SKIP_BECAUSE_BROKEN); return false; } } function toName(app) { return app.name; } function isParcel(appOrParcel) { return Boolean(appOrParcel.unmountThisParcel); } function objectType(appOrParcel) { return isParcel(appOrParcel) ? "parcel" : "application"; } // Object.assign() is not available in IE11. And the babel compiled output for object spread // syntax checks a bunch of Symbol stuff and is almost a kb. So this function is the smaller replacement. function assign() { for (var i = arguments.length - 1; i > 0; i--) { for (var key in arguments[i]) { if (key === "__proto__") { continue; } arguments[i - 1][key] = arguments[i][key]; } } return arguments[0]; } /* the array.prototype.find polyfill on npmjs.com is ~20kb (not worth it) * and lodash is ~200kb (not worth it) */ function find(arr, func) { for (var i = 0; i < arr.length; i++) { if (func(arr[i])) { return arr[i]; } } return null; } function validLifecycleFn(fn) { return fn && (typeof fn === "function" || isArrayOfFns(fn)); function isArrayOfFns(arr) { return Array.isArray(arr) && !find(arr, function (item) { return typeof item !== "function"; }); } } function flattenFnArray(appOrParcel, lifecycle) { var fns = appOrParcel[lifecycle] || []; fns = Array.isArray(fns) ? fns : [fns]; if (fns.length === 0) { fns = [function () { return Promise.resolve(); }]; } var type = objectType(appOrParcel); var name = toName(appOrParcel); return function (props) { return fns.reduce(function (resultPromise, fn, index) { return resultPromise.then(function () { var thisPromise = fn(props); return smellsLikeAPromise(thisPromise) ? thisPromise : Promise.reject(formatErrorMessage(15, "Within ".concat(type, " ").concat(name, ", the lifecycle function ").concat(lifecycle, " at array index ").concat(index, " did not return a promise"), type, name, lifecycle, index)); }); }, Promise.resolve()); }; } function smellsLikeAPromise(promise) { return promise && typeof promise.then === "function" && typeof promise.catch === "function"; } var profileEntries = []; function getProfilerData() { return profileEntries; } /** * * @type {'application' | 'parcel' | 'routing'} ProfileType * * @param {ProfileType} type * @param {String} name * @param {number} start * @param {number} end */ function addProfileEntry(type, name, kind, start, end, operationSucceeded) { profileEntries.push({ type: type, name: name, start: start, end: end, kind: kind, operationSucceeded: operationSucceeded }); } function toBootstrapPromise(appOrParcel, hardFail) { var startTime, profileEventType; return Promise.resolve().then(function () { if (appOrParcel.status !== NOT_BOOTSTRAPPED) { return appOrParcel; } { profileEventType = isParcel(appOrParcel) ? "parcel" : "application"; startTime = performance.now(); } appOrParcel.status = BOOTSTRAPPING; if (!appOrParcel.bootstrap) { // Default implementation of bootstrap return Promise.resolve().then(successfulBootstrap); } return reasonableTime(appOrParcel, "bootstrap").then(successfulBootstrap).catch(function (err) { { addProfileEntry(profileEventType, toName(appOrParcel), "bootstrap", startTime, performance.now(), false); } if (hardFail) { throw transformErr(err, appOrParcel, SKIP_BECAUSE_BROKEN); } else { handleAppError(err, appOrParcel, SKIP_BECAUSE_BROKEN); return appOrParcel; } }); }); function successfulBootstrap() { appOrParcel.status = NOT_MOUNTED; { addProfileEntry(profileEventType, toName(appOrParcel), "bootstrap", startTime, performance.now(), true); } return appOrParcel; } } function toUnmountPromise(appOrParcel, hardFail) { return Promise.resolve().then(function () { if (appOrParcel.status !== MOUNTED) { return appOrParcel; } var startTime, profileEventType; { startTime = performance.now(); profileEventType = isParcel(appOrParcel) ? "parcel" : "application"; } appOrParcel.status = UNMOUNTING; var unmountChildrenParcels = Object.keys(appOrParcel.parcels).map(function (parcelId) { return appOrParcel.parcels[parcelId].unmountThisParcel(); }); return Promise.all(unmountChildrenParcels).then(unmountAppOrParcel, function (parcelError) { // There is a parcel unmount error return unmountAppOrParcel().then(function () { // Unmounting the app/parcel succeeded, but unmounting its children parcels did not var parentError = Error(parcelError.message); if (hardFail) { throw transformErr(parentError, appOrParcel, SKIP_BECAUSE_BROKEN); } else { handleAppError(parentError, appOrParcel, SKIP_BECAUSE_BROKEN); } }); }).then(function () { return appOrParcel; }); function unmountAppOrParcel() { // We always try to unmount the appOrParcel, even if the children parcels failed to unmount. return reasonableTime(appOrParcel, "unmount").then(function () { // The appOrParcel needs to stay in a broken status if its children parcels fail to unmount { appOrParcel.status = NOT_MOUNTED; } { addProfileEntry(profileEventType, toName(appOrParcel), "unmount", startTime, performance.now(), true); } }, function (err) { { addProfileEntry(profileEventType, toName(appOrParcel), "unmount", startTime, performance.now(), false); } if (hardFail) { throw transformErr(err, appOrParcel, SKIP_BECAUSE_BROKEN); } else { handleAppError(err, appOrParcel, SKIP_BECAUSE_BROKEN); } }); } }); } var beforeFirstMountFired = false; var firstMountFired = false; function toMountPromise(appOrParcel, hardFail) { return Promise.resolve().then(function () { if (appOrParcel.status !== NOT_MOUNTED) { return appOrParcel; } var startTime, profileEventType; { profileEventType = isParcel(appOrParcel) ? "parcel" : "application"; startTime = performance.now(); } if (!beforeFirstMountFired) { window.dispatchEvent(new customEvent("single-spa:before-first-mount")); beforeFirstMountFired = true; } appOrParcel.status = MOUNTING; return reasonableTime(appOrParcel, "mount").then(function () { appOrParcel.status = MOUNTED; if (!firstMountFired) { window.dispatchEvent(new customEvent("single-spa:first-mount")); firstMountFired = true; } { addProfileEntry(profileEventType, toName(appOrParcel), "mount", startTime, performance.now(), true); } return appOrParcel; }).catch(function (err) { // If we fail to mount the appOrParcel, we should attempt to unmount it before putting in SKIP_BECAUSE_BROKEN // We temporarily put the appOrParcel into MOUNTED status so that toUnmountPromise actually attempts to unmount it // instead of just doing a no-op. appOrParcel.status = MOUNTED; return toUnmountPromise(appOrParcel, true).then(setSkipBecauseBroken, setSkipBecauseBroken); function setSkipBecauseBroken() { { addProfileEntry(profileEventType, toName(appOrParcel), "mount", startTime, performance.now(), false); } if (!hardFail) { handleAppError(err, appOrParcel, SKIP_BECAUSE_BROKEN); return appOrParcel; } else { throw transformErr(err, appOrParcel, SKIP_BECAUSE_BROKEN); } } }); }); } function toUpdatePromise(appOrParcel) { return Promise.resolve().then(function () { var startTime, profileEventType; { profileEventType = isParcel(appOrParcel) ? "parcel" : "application"; startTime = performance.now(); } if (appOrParcel.status !== MOUNTED) { throw Error(formatErrorMessage(32, "Cannot update parcel '".concat(toName(appOrParcel), "' because it is not mounted"), toName(appOrParcel))); } appOrParcel.status = UPDATING; return reasonableTime(appOrParcel, "update").then(function () { appOrParcel.status = MOUNTED; { addProfileEntry(profileEventType, toName(appOrParcel), "update", startTime, performance.now(), true); } return appOrParcel; }).catch(function (err) { { addProfileEntry(profileEventType, toName(appOrParcel), "update", startTime, performance.now(), false); } throw transformErr(err, appOrParcel, SKIP_BECAUSE_BROKEN); }); }); } var parcelCount = 0; var rootParcels = { parcels: {} }; // This is a public api, exported to users of single-spa function mountRootParcel() { return mountParcel.apply(rootParcels, arguments); } function mountParcel(config, customProps) { var owningAppOrParcel = this; // Validate inputs if (!config || _typeof(config) !== "object" && typeof config !== "function") { throw Error(formatErrorMessage(2, "Cannot mount parcel without a config object or config loading function")); } if (config.name && typeof config.name !== "string") { throw Error(formatErrorMessage(3, "Parcel name must be a string, if provided. Was given ".concat(_typeof(config.name)), _typeof(config.name))); } var id = parcelCount++; var name = config.name || "parcel-".concat(id); if (_typeof(customProps) !== "object") { throw Error(formatErrorMessage(4, "Parcel ".concat(name, " has invalid customProps -- must be an object but was given ").concat(_typeof(customProps)), name, _typeof(customProps))); } if (!customProps.domElement) { throw Error(formatErrorMessage(5, "Parcel ".concat(name, " cannot be mounted without a domElement provided as a prop"), name)); } var passedConfigLoadingFunction = typeof config === "function"; var configLoadingFunction = passedConfigLoadingFunction ? config : function () { return Promise.resolve(config); }; // Internal representation var parcel = { id: id, parcels: {}, status: passedConfigLoadingFunction ? LOADING_SOURCE_CODE : NOT_BOOTSTRAPPED, customProps: customProps, parentName: toName(owningAppOrParcel), unmountThisParcel: function unmountThisParcel() { return mountPromise.then(function () { if (parcel.status !== MOUNTED) { throw Error(formatErrorMessage(6, "Cannot unmount parcel '".concat(name, "' -- it is in a ").concat(parcel.status, " status"), name, parcel.status)); } return toUnmountPromise(parcel, true); }).then(function (value) { if (parcel.parentName) { delete owningAppOrParcel.parcels[parcel.id]; } return value; }).then(function (value) { resolveUnmount(value); return value; }).catch(function (err) { parcel.status = SKIP_BECAUSE_BROKEN; rejectUnmount(err); throw err; }); } }; // We return an external representation var externalRepresentation; // Add to owning app or parcel owningAppOrParcel.parcels[id] = parcel; var loadPromise = configLoadingFunction(); if (!loadPromise || typeof loadPromise.then !== "function") { throw Error(formatErrorMessage(7, "When mounting a parcel, the config loading function must return a promise that resolves with the parcel config")); } loadPromise = loadPromise.then(function (config) { if (!config) { throw Error(formatErrorMessage(8, "When mounting a parcel, the config loading function returned a promise that did not resolve with a parcel config")); } name = config.name || "parcel-".concat(id); if ( // ES Module objects don't have the object prototype Object.prototype.hasOwnProperty.call(config, "bootstrap") && !validLifecycleFn(config.bootstrap)) { throw Error(formatErrorMessage(9, "Parcel ".concat(name, " provided an invalid bootstrap function"), name)); } if (!validLifecycleFn(config.mount)) { throw Error(formatErrorMessage(10, "Parcel ".concat(name, " must have a valid mount function"), name)); } if (!validLifecycleFn(config.unmount)) { throw Error(formatErrorMessage(11, "Parcel ".concat(name, " must have a valid unmount function"), name)); } if (config.update && !validLifecycleFn(config.update)) { throw Error(formatErrorMessage(12, "Parcel ".concat(name, " provided an invalid update function"), name)); } var bootstrap = flattenFnArray(config, "bootstrap"); var mount = flattenFnArray(config, "mount"); var unmount = flattenFnArray(config, "unmount"); parcel.status = NOT_BOOTSTRAPPED; parcel.name = name; parcel.bootstrap = bootstrap; parcel.mount = mount; parcel.unmount = unmount; parcel.timeouts = ensureValidAppTimeouts(config.timeouts); if (config.update) { parcel.update = flattenFnArray(config, "update"); externalRepresentation.update = function (customProps) { parcel.customProps = customProps; return promiseWithoutReturnValue(toUpdatePromise(parcel)); }; } }); // Start bootstrapping and mounting // The .then() causes the work to be put on the event loop instead of happening immediately var bootstrapPromise = loadPromise.then(function () { return toBootstrapPromise(parcel, true); }); var mountPromise = bootstrapPromise.then(function () { return toMountPromise(parcel, true); }); var resolveUnmount, rejectUnmount; var unmountPromise = new Promise(function (resolve, reject) { resolveUnmount = resolve; rejectUnmount = reject; }); externalRepresentation = { mount: function mount() { return promiseWithoutReturnValue(Promise.resolve().then(function () { if (parcel.status !== NOT_MOUNTED) { throw Error(formatErrorMessage(13, "Cannot mount parcel '".concat(name, "' -- it is in a ").concat(parcel.status, " status"), name, parcel.status)); } // Add to owning app or parcel owningAppOrParcel.parcels[id] = parcel; return toMountPromise(parcel); })); }, unmount: function unmount() { return promiseWithoutReturnValue(parcel.unmountThisParcel()); }, getStatus: function getStatus() { return parcel.status; }, loadPromise: promiseWithoutReturnValue(loadPromise), bootstrapPromise: promiseWithoutReturnValue(bootstrapPromise), mountPromise: promiseWithoutReturnValue(mountPromise), unmountPromise: promiseWithoutReturnValue(unmountPromise) }; return externalRepresentation; } function promiseWithoutReturnValue(promise) { return promise.then(function () { return null; }); } function getProps(appOrParcel) { var name = toName(appOrParcel); var customProps = typeof appOrParcel.customProps === "function" ? appOrParcel.customProps(name, window.location) : appOrParcel.customProps; if (_typeof(customProps) !== "object" || customProps === null || Array.isArray(customProps)) { customProps = {}; console.warn(formatErrorMessage(40, "single-spa: ".concat(name, "'s customProps function must return an object. Received ").concat(customProps)), name, customProps); } var result = assign({}, customProps, { name: name, mountParcel: mountParcel.bind(appOrParcel), singleSpa: singleSpa }); if (isParcel(appOrParcel)) { result.unmountSelf = appOrParcel.unmountThisParcel; } return result; } var defaultWarningMillis = 1000; var globalTimeoutConfig = { bootstrap: { millis: 4000, dieOnTimeout: false, warningMillis: defaultWarningMillis }, mount: { millis: 3000, dieOnTimeout: false, warningMillis: defaultWarningMillis }, unmount: { millis: 3000, dieOnTimeout: false, warningMillis: defaultWarningMillis }, unload: { millis: 3000, dieOnTimeout: false, warningMillis: defaultWarningMillis }, update: { millis: 3000, dieOnTimeout: false, warningMillis: defaultWarningMillis } }; function setBootstrapMaxTime(time, dieOnTimeout, warningMillis) { if (typeof time !== "number" || time <= 0) { throw Error(formatErrorMessage(16, "bootstrap max time must be a positive integer number of milliseconds")); } globalTimeoutConfig.bootstrap = { millis: time, dieOnTimeout: dieOnTimeout, warningMillis: warningMillis || defaultWarningMillis }; } function setMountMaxTime(time, dieOnTimeout, warningMillis) { if (typeof time !== "number" || time <= 0) { throw Error(formatErrorMessage(17, "mount max time must be a positive integer number of milliseconds")); } globalTimeoutConfig.mount = { millis: time, dieOnTimeout: dieOnTimeout, warningMillis: warningMillis || defaultWarningMillis }; } function setUnmountMaxTime(time, dieOnTimeout, warningMillis) { if (typeof time !== "number" || time <= 0) { throw Error(formatErrorMessage(18, "unmount max time must be a positive integer number of milliseconds")); } globalTimeoutConfig.unmount = { millis: time, dieOnTimeout: dieOnTimeout, warningMillis: warningMillis || defaultWarningMillis }; } function setUnloadMaxTime(time, dieOnTimeout, warningMillis) { if (typeof time !== "number" || time <= 0) { throw Error(formatErrorMessage(19, "unload max time must be a positive integer number of milliseconds")); } globalTimeoutConfig.unload = { millis: time, dieOnTimeout: dieOnTimeout, warningMillis: warningMillis || defaultWarningMillis }; } function reasonableTime(appOrParcel, lifecycle) { var timeoutConfig = appOrParcel.timeouts[lifecycle]; var warningPeriod = timeoutConfig.warningMillis; var type = objectType(appOrParcel); return new Promise(function (resolve, reject) { var finished = false; var errored = false; appOrParcel[lifecycle](getProps(appOrParcel)).then(function (val) { finished = true; resolve(val); }).catch(function (val) { finished = true; reject(val); }); setTimeout(function () { return maybeTimingOut(1); }, warningPeriod); setTimeout(function () { return maybeTimingOut(true); }, timeoutConfig.millis); var errMsg = formatErrorMessage(31, "Lifecycle function ".concat(lifecycle, " for ").concat(type, " ").concat(toName(appOrParcel), " lifecycle did not resolve or reject for ").concat(timeoutConfig.millis, " ms."), lifecycle, type, toName(appOrParcel), timeoutConfig.millis); function maybeTimingOut(shouldError) { if (!finished) { if (shouldError === true) { errored = true; if (timeoutConfig.dieOnTimeout) { reject(Error(errMsg)); } else { console.error(errMsg); //don't resolve or reject, we're waiting this one out } } else if (!errored) { var numWarnings = shouldError; var numMillis = numWarnings * warningPeriod; console.warn(errMsg); if (numMillis + warningPeriod < timeoutConfig.millis) { setTimeout(function () { return maybeTimingOut(numWarnings + 1); }, warningPeriod); } } } } }); } function ensureValidAppTimeouts(timeouts) { var result = {}; for (var key in globalTimeoutConfig) { result[key] = assign({}, globalTimeoutConfig[key], timeouts && timeouts[key] || {}); } return result; } function toLoadPromise(appOrParcel) { return Promise.resolve().then(function () { if (appOrParcel.loadPromise) { return appOrParcel.loadPromise; } if (appOrParcel.status !== NOT_LOADED && appOrParcel.status !== LOAD_ERROR) { return appOrParcel; } var startTime; { startTime = performance.now(); } appOrParcel.status = LOADING_SOURCE_CODE; var appOpts, isUserErr; return appOrParcel.loadPromise = Promise.resolve().then(function () { var loadPromise = appOrParcel.loadApp(getProps(appOrParcel)); if (!smellsLikeAPromise(loadPromise)) { // The name of the app will be prepended to this error message inside of the handleAppError function isUserErr = true; throw Error(formatErrorMessage(33, "single-spa loading function did not return a promise. Check the second argument to registerApplication('".concat(toName(appOrParcel), "', loadingFunction, activityFunction)"), toName(appOrParcel))); } return loadPromise.then(function (val) { appOrParcel.loadErrorTime = null; appOpts = val; var validationErrMessage, validationErrCode; if (_typeof(appOpts) !== "object") { validationErrCode = 34; { validationErrMessage = "does not export anything"; } } if ( // ES Modules don't have the Object prototype Object.prototype.hasOwnProperty.call(appOpts, "bootstrap") && !validLifecycleFn(appOpts.bootstrap)) { validationErrCode = 35; { validationErrMessage = "does not export a valid bootstrap function or array of functions"; } } if (!validLifecycleFn(appOpts.mount)) { validationErrCode = 36; { validationErrMessage = "does not export a mount function or array of functions"; } } if (!validLifecycleFn(appOpts.unmount)) { validationErrCode = 37; { validationErrMessage = "does not export a unmount function or array of functions"; } } var type = objectType(appOpts); if (validationErrCode) { var appOptsStr; try { appOptsStr = JSON.stringify(appOpts); } catch (_unused) {} console.error(formatErrorMessage(validationErrCode, "The loading function for single-spa ".concat(type, " '").concat(toName(appOrParcel), "' resolved with the following, which does not have bootstrap, mount, and unmount functions"), type, toName(appOrParcel), appOptsStr), appOpts); handleAppError(validationErrMessage, appOrParcel, SKIP_BECAUSE_BROKEN); return appOrParcel; } if (appOpts.devtools && appOpts.devtools.overlays) { appOrParcel.devtools.overlays = assign({}, appOrParcel.devtools.overlays, appOpts.devtools.overlays); } appOrParcel.status = NOT_BOOTSTRAPPED; appOrParcel.bootstrap = flattenFnArray(appOpts, "bootstrap"); appOrParcel.mount = flattenFnArray(appOpts, "mount"); appOrParcel.unmount = flattenFnArray(appOpts, "unmount"); appOrParcel.unload = flattenFnArray(appOpts, "unload"); appOrParcel.timeouts = ensureValidAppTimeouts(appOpts.timeouts); delete appOrParcel.loadPromise; { addProfileEntry("application", toName(appOrParcel), "load", startTime, performance.now(), true); } return appOrParcel; }); }).catch(function (err) { delete appOrParcel.loadPromise; var newStatus; if (isUserErr) { newStatus = SKIP_BECAUSE_BROKEN; } else { newStatus = LOAD_ERROR; appOrParcel.loadErrorTime = new Date().getTime(); } handleAppError(err, appOrParcel, newStatus); { addProfileEntry("application", toName(appOrParcel), "load", startTime, performance.now(), false); } return appOrParcel; }); }); } var isInBrowser = typeof window !== "undefined"; /* We capture navigation event listeners so that we can make sure * that application navigation listeners are not called until * single-spa has ensured that the correct applications are * unmounted and mounted. */ var capturedEventListeners = { hashchange: [], popstate: [] }; var routingEventsListeningTo = ["hashchange", "popstate"]; function navigateToUrl(obj) { var url; if (typeof obj === "string") { url = obj; } else if (this && this.href) { url = this.href; } else if (obj && obj.currentTarget && obj.currentTarget.href && obj.preventDefault) { url = obj.currentTarget.href; obj.preventDefault(); } else { throw Error(formatErrorMessage(14, "singleSpaNavigate/navigateToUrl must be either called with a string url, with an <a> tag as its context, or with an event whose currentTarget is an <a> tag")); } var current = parseUri(window.location.href); var destination = parseUri(url); if (url.indexOf("#") === 0) { window.location.hash = destination.hash; } else if (current.host !== destination.host && destination.host) { { window.location.href = url; } } else if (destination.pathname === current.pathname && destination.search === current.search) { window.location.hash = destination.hash; } else { // different path, host, or query params window.history.pushState(null, null, url); } } function callCapturedEventListeners(eventArguments) { var _this = this; if (eventArguments) { var eventType = eventArguments[0].type; if (routingEventsListeningTo.indexOf(eventType) >= 0) { capturedEventListeners[eventType].forEach(function (listener) { try { // The error thrown by application event listener should not break single-spa down. // Just like https://github.com/single-spa/single-spa/blob/85f5042dff960e40936f3a5069d56fc9477fac04/src/navigation/reroute.js#L140-L146 did listener.apply(_this, eventArguments); } catch (e) { setTimeout(function () { throw e; }); } }); } } } var urlRerouteOnly; function urlReroute() { reroute([], arguments); } function patchedUpdateState(updateState, methodName) { return function () { var urlBefore = window.location.href; var result = updateState.apply(this, arguments); var urlAfter = window.location.href; if (!urlRerouteOnly || urlBefore !== urlAfter) { // fire an artificial popstate event so that // single-spa applications know about routing that // occurs in a different application window.dispatchEvent(createPopStateEvent(window.history.state, methodName)); } return result; }; } function createPopStateEvent(state, originalMethodName) { // https://github.com/single-spa/single-spa/issues/224 and https://github.com/single-spa/single-spa-angular/issues/49 // We need a popstate event even though the browser doesn't do one by default when you call replaceState, so that // all the applications can reroute. We explicitly identify this extraneous event by setting singleSpa=true and // singleSpaTrigger=<pushState|replaceState> on the event instance. var evt; try { evt = new PopStateEvent("popstate", { state: state }); } catch (err) { // IE 11 compatibility https://github.com/single-spa/single-spa/issues/299 // https://docs.microsoft.com/en-us/openspecs/ie_standards/ms-html5e/bd560f47-b349-4d2c-baa8-f1560fb489dd evt = document.createEvent("PopStateEvent"); evt.initPopStateEvent("popstate", false, false, state); } evt.singleSpa = true; evt.singleSpaTrigger = originalMethodName; return evt; } var originalReplaceState = null; var historyApiIsPatched = false; // We patch the history API so single-spa is notified of all calls to pushState/replaceState. // We patch addEventListener/removeEventListener so we can capture all popstate/hashchange event listeners, // and delay calling them until single-spa has finished mounting/unmounting applications function patchHistoryApi(opts) { if (historyApiIsPatched) { throw Error(formatErrorMessage(43, "single-spa: patchHistoryApi() was called after the history api was already patched.")); } // True by default, as a performance optimization that reduces // the number of extraneous popstate events urlRerouteOnly = opts && opts.hasOwnProperty("urlRerouteOnly") ? opts.urlRerouteOnly : true; historyApiIsPatched = true; originalReplaceState = window.history.replaceState; // We will trigger an app change for any routing events. window.addEventListener("hashchange", urlReroute); window.addEventListener("popstate", urlReroute); // Monkeypatch addEventListener so that we can ensure correct timing var originalAddEventListener = window.addEventListener; var originalRemoveEventListener = window.removeEventListener; window.addEventListener = function (eventName, fn) { if (typeof fn === "function") { if (routingEventsListeningTo.indexOf(eventName) >= 0 && !find(capturedEventListeners[eventName], function (listener) { return listener === fn; })) { capturedEventListeners[eventName].push(fn); return; } } return originalAddEventListener.apply(this, arguments); }; window.removeEventListener = function (eventName, listenerFn) { if (typeof listenerFn === "function") { if (routingEventsListeningTo.indexOf(eventName) >= 0) { capturedEventListeners[eventName] = capturedEventListeners[eventName].filter(function (fn) { return fn !== listenerFn; }); } } return originalRemoveEventListener.apply(this, arguments); }; window.history.pushState = patchedUpdateState(window.history.pushState, "pushState"); window.history.replaceState = patchedUpdateState(originalReplaceState, "replaceState"); } // Detect if single-spa has already been loaded on the page. // If so, warn because this can result in lots of problems, including // lots of extraneous popstate events and unexpected results for // apis like getAppNames(). if (isInBrowser) { if (window.singleSpaNavigate) { console.warn(formatErrorMessage(41, "single-spa has been loaded twice on the page. This can result in unexpected behavior.")); } else { /* For convenience in `onclick` attributes, we expose a global function for navigating to * whatever an <a> tag's href is. */ window.singleSpaNavigate = navigateToUrl; } } function parseUri(str) { var anchor = document.createElement("a"); anchor.href = str; return anchor; } var hasInitialized = false; function ensureJQuerySupport() { var jQuery = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : window.jQuery; if (!jQuery) { if (window.$ && window.$.fn && window.$.fn.jquery) { jQuery = window.$; } } if (jQuery && !hasInitialized) { var originalJQueryOn = jQuery.fn.on; var originalJQueryOff = jQuery.fn.off; jQuery.fn.on = function (eventString, fn) { return captureRoutingEvents.call(this, originalJQueryOn, window.addEventListener, eventString, fn, arguments); }; jQuery.fn.off = function (eventString, fn) { return captureRoutingEvents.call(this, originalJQueryOff, window.removeEventListener, eventString, fn, arguments); }; hasInitialized = true; } } function captureRoutingEvents(originalJQueryFunction, nativeFunctionToCall, eventString, fn, originalArgs) { if (typeof eventString !== "string") { return originalJQueryFunction.apply(this, originalArgs); } var eventNames = eventString.split(/\s+/); eventNames.forEach(function (eventName) { if (routingEventsListeningTo.indexOf(eventName) >= 0) { nativeFunctionToCall(eventName, fn); eventString = eventString.replace(eventName, ""); } }); if (eventString.trim() === "") { return this; } else { return originalJQueryFunction.apply(this, originalArgs); } } var appsToUnload = {}; function toUnloadPromise(appOrParcel) { return Promise.resolve().then(function () { var unloadInfo = appsToUnload[toName(appOrParcel)]; if (!unloadInfo) { /* No one has called unloadApplication for this app, */ return appOrParcel; } if (appOrParcel.status === NOT_LOADED) { /* This app is already unloaded. We just need to clean up * anything that still thinks we need to unload the app. */ finishUnloadingApp(appOrParcel, unloadInfo); return appOrParcel; } if (appOrParcel.status === UNLOADING) { /* Both unloadApplication and reroute want to unload this app. * It only needs to be done once, though. */ return unloadInfo.promise.then(function () { return appOrParcel; }); } if (appOrParcel.status !== NOT_MOUNTED && appOrParcel.status !== LOAD_ERROR) { /* The app cannot be unloaded until it is unmounted. */ return appOrParcel; } var startTime; { startTime = performance.now(); } var unloadPromise = appOrParcel.status === LOAD_ERROR ? Promise.resolve() : reasonableTime(appOrParcel, "unload"); appOrParcel.status = UNLOADING; return unloadPromise.then(function () { { addProfileEntry("application", toName(appOrParcel), "unload", startTime, performance.now(), true); } finishUnloadingApp(appOrParcel, unloadInfo); return appOrParcel; }).catch(function (err) { { addProfileEntry("application", toName(appOrParcel), "unload", startTime, performance.now(), false); } errorUnloadingApp(appOrParcel, unloadInfo, err); return appOrParcel; }); }); } function finishUnloadingApp(app, unloadInfo) { delete appsToUnload[toName(app)]; // Unloaded apps don't have lifecycles delete app.bootstrap; delete app.mount; delete app.unmount; delete app.unload; app.status = NOT_LOADED; /* resolve the promise of whoever called unloadApplication. * This should be done after all other cleanup/bookkeeping */ unloadInfo.resolve(); } function errorUnloadingApp(app, unloadInfo, err) { delete appsToUnload[toName(app)]; // Unloaded apps don't have lifecycles delete app.bootstrap; delete app.mount; delete app.unmount; delete app.unload; handleAppError(err, app, SKIP_BECAUSE_BROKEN); unloadInfo.reject(err); } function addAppToUnload(app, promiseGetter, resolve, reject) { appsToUnload[toName(app)] = { app: app, resolve: resolve, reject: reject }; Object.defineProperty(appsToUnload[toName(app)], "promise", { get: promiseGetter }); } function getAppUnloadInfo(appName) { return appsToUnload[appName]; } var apps = []; function getAppChanges() { var appsToUnload = [], appsToUnmount = [], appsToLoad = [], appsToMount = []; // We re-attempt to download applications in LOAD_ERROR after a timeout of 200 milliseconds var currentTime = new Date().getTime(); apps.forEach(function (app) { var appShouldBeActive = app.status !== SKIP_BECAUSE_BROKEN && shouldBeActive(app); switch (app.status) { case LOAD_ERROR: if (appShouldBeActive && currentTime - app.loadErrorTime >= 200) { appsToLoad.push(app); } break; case NOT_LOADED: case LOADING_SOURCE_CODE: if (appShouldBeActive) { appsToLoad.push(app); } break; case NOT_BOOTSTRAPPED: case NOT_MOUNTED: if (!appShouldBeActive && getAppUnloadInfo(toName(app))) { appsToUnload.push(app); } else if (appShouldBeActive) { appsToMount.push(app); } break; case MOUNTED: if (!appShouldBeActive) { appsToUnmount.push(app); } break; // all other statuses are ignored } }); return { appsToUnload: appsToUnload, appsToUnmount: appsToUnmount, appsToLoad: appsToLoad, appsToMount: appsToMount }; } function getMountedApps() { return apps.filter(isActive).map(toName); } function getAppNames() { return apps.map(toName); } // used in devtools, not (currently) exposed as a single-spa API function getRawAppData() { return [].concat(apps); } function getAppStatus(appName) { var app = find(apps, function (app) { return toName(app) === appName; }); return app ? app.status : null; } var startWarningInitialized = false; function registerApplication(appNameOrConfig, appOrLoadApp, activeWhen, customProps) { var registration = sanitizeArguments(appNameOrConfig, appOrLoadApp, activeWhen, customProps); if (!isStarted() && !startWarningInitialized) { startWarningInitialized = true; setTimeout(function () { if (!isStarted()) { console.warn(formatErrorMessage(1, "singleSpa.start() has not been called, 5000ms after single-spa was loaded. Before start() is called, apps can be declared and loaded, but not bootstrapped or mounted.")); } }, 5000); } if (getAppNames().indexOf(registration.name) !== -1) throw Error(formatErrorMessage(21, "There is already an app registered with name ".concat(registration.name), registration.name)); apps.push(assign({ loadErrorTime: null, status: NOT_LOADED, parcels: {}, devtools: { overlays: { options: {}, selectors: [] } } }, registration)); if (isInBrowser) { ensureJQuerySupport(); reroute(); } } function checkActivityFunctions() { var location = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : window.location; return apps.filter(function (app) { return app.activeWhen(location); }).map(toName); } function unregisterApplication(appName) { if (apps.filter(function (app) { return toName(app) === appName; }).length === 0) { throw Error(formatErrorMessage(25, "Cannot unregister application '".concat(appName, "' because no such application has been registered"), appName)); } var unloadPromise = isInBrowser ? // See https://github.com/single-spa/single-spa/issues/871 for why waitForUnmount is false unloadApplication(appName, { waitForUnmount: false }) : Promise.resolve(); return unloadPromise.then(function () { var appIndex = apps.map(toName).indexOf(appName); apps.splice(appIndex, 1); }); } function unloadApplication(appName) { var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { waitForUnmount: false }; if (typeof appName !== "string") { throw Error(formatErrorMessage(26, "unloadApplication requires a string 'appName'")); } var app = find(apps, function (App) { return toName(App) === appName; }); if (!app) { throw Error(formatErrorMessage(27, "Could not unload application '".concat(appName, "' because no such application has been registered"), appName)); } var appUnloadInfo = getAppUnloadInfo(toName(app)); if (opts && opts.waitForUnmount) { // We need to wait for unmount before unloading the app if (appUnloadInfo) { // Someone else is already waiting for this, too return appUnloadInfo.promise; } else { // We're the first ones wanting the app to be resolved. var promise = new Promise(function (resolve, reject) { addAppToUnload(app, function () { return promise; }, resolve, reject); }); return promise; } } else { /* We should unmount the app, unload it, and remount it immediately. */ var resultPromise; if (appUnloadInfo) { // Someone else is already waiting for this app to unload resultPromise = appUnloadInfo.promise; immediatelyUnloadApp(app, appUnloadInfo.resolve, appUnloadInfo.reject); } else { // We're the first ones wanting the app to be resolved. resultPromise = new Promise(function (resolve, reject) { addAppToUnload(app, function () { return resultPromise; }, resolve, reject); immediatelyUnloadApp(app, resolve, reject); }); } return resultPromise; } } function immediatelyUnloadApp(app, resolve, reject) { Promise.resolve().then(function () { // Before unmounting the application, we first must wait for it to finish mounting // Otherwise, the test for issue 871 in unregister-application.spec.js fails because // the application isn't really unmounted. if (find(checkActivityFunctions(), function (activeApp) { return activeApp === toName(app); })) { return triggerAppChange(); } }).then(function () { return toUnmountPromise(app).then(toUnloadPromise).then(function () { resolve(); setTimeout(function () {