@micro-zoe/micro-app
Version:
A lightweight, efficient and powerful micro front-end framework
1,492 lines (1,485 loc) • 385 kB
JavaScript
const version = '1.0.0-rc.24';
// do not use isUndefined
const isBrowser = typeof window !== 'undefined';
// do not use isUndefined
const globalThis = (typeof global !== 'undefined')
? global
: ((typeof window !== 'undefined')
? window
: ((typeof self !== 'undefined') ? self : Function('return this')()));
const noopFalse = () => false;
// Array.isArray
const isArray = Array.isArray;
// Object.assign
const assign = Object.assign;
// Object prototype methods
const rawDefineProperty = Object.defineProperty;
const rawDefineProperties = Object.defineProperties;
const rawToString = Object.prototype.toString;
const rawHasOwnProperty = Object.prototype.hasOwnProperty;
const toTypeString = (value) => rawToString.call(value);
// is Undefined
function isUndefined(target) {
return target === undefined;
}
// is Null
function isNull(target) {
return target === null;
}
// is String
function isString(target) {
return typeof target === 'string';
}
// is Boolean
function isBoolean(target) {
return typeof target === 'boolean';
}
// is Number
function isNumber(target) {
return typeof target === 'number';
}
// is function
function isFunction(target) {
return typeof target === 'function';
}
// is PlainObject
function isPlainObject(target) {
return toTypeString(target) === '[object Object]';
}
// is Object
function isObject(target) {
return !isNull(target) && typeof target === 'object';
}
// is Promise
function isPromise(target) {
return toTypeString(target) === '[object Promise]';
}
// is bind function
function isBoundFunction(target) {
var _a;
return isFunction(target) && ((_a = target.name) === null || _a === void 0 ? void 0 : _a.indexOf('bound ')) === 0 && !target.hasOwnProperty('prototype');
}
// is constructor function
function isConstructor(target) {
var _a;
if (isFunction(target)) {
const targetStr = target.toString();
return (((_a = target.prototype) === null || _a === void 0 ? void 0 : _a.constructor) === target &&
Object.getOwnPropertyNames(target.prototype).length > 1) ||
/^function\s+[A-Z]/.test(targetStr) ||
/^class\s+/.test(targetStr);
}
return false;
}
// is ShadowRoot
function isShadowRoot(target) {
return typeof ShadowRoot !== 'undefined' && target instanceof ShadowRoot;
}
function isURL(target) {
var _a;
return target instanceof URL || !!((_a = target) === null || _a === void 0 ? void 0 : _a.href);
}
// iframe element not instanceof base app Element, use tagName instead
function isElement(target) {
var _a;
return target instanceof Element || isString((_a = target) === null || _a === void 0 ? void 0 : _a.tagName);
}
// iframe node not instanceof base app Node, use nodeType instead
function isNode(target) {
var _a;
return target instanceof Node || isNumber((_a = target) === null || _a === void 0 ? void 0 : _a.nodeType);
}
function isAnchorElement(target) {
return toTypeString(target) === '[object HTMLAnchorElement]';
}
function isAudioElement(target) {
return toTypeString(target) === '[object HTMLAudioElement]';
}
function isVideoElement(target) {
return toTypeString(target) === '[object HTMLVideoElement]';
}
function isLinkElement(target) {
return toTypeString(target) === '[object HTMLLinkElement]';
}
function isBodyElement(target) {
return toTypeString(target) === '[object HTMLBodyElement]';
}
function isStyleElement(target) {
return toTypeString(target) === '[object HTMLStyleElement]';
}
function isScriptElement(target) {
return toTypeString(target) === '[object HTMLScriptElement]';
}
function isIFrameElement(target) {
return toTypeString(target) === '[object HTMLIFrameElement]';
}
function isDivElement(target) {
return toTypeString(target) === '[object HTMLDivElement]';
}
function isImageElement(target) {
return toTypeString(target) === '[object HTMLImageElement]';
}
function isBaseElement(target) {
return toTypeString(target) === '[object HTMLBaseElement]';
}
function isDocumentFragment(target) {
return toTypeString(target) === '[object DocumentFragment]';
}
function isDocumentShadowRoot(target) {
return toTypeString(target) === '[object ShadowRoot]';
}
function isMicroAppBody(target) {
return isElement(target) && target.tagName.toUpperCase() === 'MICRO-APP-BODY';
}
function isMicroAppHead(target) {
return isElement(target) && target.tagName.toUpperCase() === 'MICRO-APP-HEAD';
}
function isWebComponentElement(target) {
let result = toTypeString(target) === '[object HTMLElement]';
if (result) {
const tagName = target.tagName.toUpperCase();
result = result && !tagName.startsWith('MICRO-APP');
}
return result;
}
// is ProxyDocument
function isProxyDocument(target) {
return toTypeString(target) === '[object ProxyDocument]';
}
function isTargetExtension(path, suffix) {
try {
return createURL(path).pathname.split('.').pop() === suffix;
}
catch (_a) {
return false;
}
}
function includes(target, searchElement, fromIndex) {
if (target == null) {
throw new TypeError('includes target is null or undefined');
}
const O = Object(target);
const len = parseInt(O.length, 10) || 0;
if (len === 0)
return false;
// @ts-ignore
fromIndex = parseInt(fromIndex, 10) || 0;
let i = Math.max(fromIndex >= 0 ? fromIndex : len + fromIndex, 0);
while (i < len) {
// NaN !== NaN
if (searchElement === O[i] || (searchElement !== searchElement && O[i] !== O[i])) {
return true;
}
i++;
}
return false;
}
/**
* format error log
* @param msg message
* @param appName app name, default is null
*/
function logError(msg, appName = null, ...rest) {
const appNameTip = appName && isString(appName) ? ` app ${appName}:` : '';
if (isString(msg)) {
console.error(`[micro-app]${appNameTip} ${msg}`, ...rest);
}
else {
console.error(`[micro-app]${appNameTip}`, msg, ...rest);
}
}
/**
* format warn log
* @param msg message
* @param appName app name, default is null
*/
function logWarn(msg, appName = null, ...rest) {
const appNameTip = appName && isString(appName) ? ` app ${appName}:` : '';
if (isString(msg)) {
console.warn(`[micro-app]${appNameTip} ${msg}`, ...rest);
}
else {
console.warn(`[micro-app]${appNameTip}`, msg, ...rest);
}
}
/**
* async execution
* @param fn callback
* @param args params
*/
function defer(fn, ...args) {
Promise.resolve().then(fn.bind(null, ...args));
}
/**
* async execution with macro task
* @param fn callback
* @param args params
*/
function macro(fn, delay = 0, ...args) {
setTimeout(fn.bind(null, ...args), delay);
}
/**
* create URL as MicroLocation
*/
const createURL = (function () {
class Location extends URL {
}
return (path, base) => {
return (base ? new Location('' + path, base) : new Location('' + path));
};
})();
/**
* Add address protocol
* @param url address
*/
function addProtocol(url) {
return url.startsWith('//') ? `${globalThis.location.protocol}${url}` : url;
}
/**
* format URL address
* note the scenes:
* 1. micro-app -> attributeChangedCallback
* 2. preFetch
*/
function formatAppURL(url, appName = null) {
if (!isString(url) || !url)
return '';
try {
const { origin, pathname, search } = createURL(addProtocol(url), (window.rawWindow || window).location.href);
/**
* keep the original url unchanged, such as .html .node .php .net .etc, search, except hash
* BUG FIX: Never using '/' to complete url, refer to https://github.com/jd-opensource/micro-app/issues/1147
*/
const fullPath = `${origin}${pathname}${search}`;
return /^https?:\/\//.test(fullPath) ? fullPath : '';
}
catch (e) {
logError(e, appName);
return '';
}
}
/**
* format name
* note the scenes:
* 1. micro-app -> attributeChangedCallback
* 2. event_center -> EventCenterForMicroApp -> constructor
* 3. event_center -> EventCenterForBaseApp -> all methods
* 4. preFetch
* 5. plugins
* 6. router api (push, replace)
*/
function formatAppName(name) {
if (!isString(name) || !name)
return '';
return name.replace(/(^\d+)|([^\w\d-_])/gi, '');
}
/**
* Get valid address, such as
* 1. https://domain/xx/xx.html to https://domain/xx/
* 2. https://domain/xx to https://domain/xx/
* @param url app.url
*/
function getEffectivePath(url) {
const { origin, pathname } = createURL(url);
if (/\.(\w+)$/.test(pathname)) {
const pathArr = `${origin}${pathname}`.split('/');
pathArr.pop();
return pathArr.join('/') + '/';
}
return `${origin}${pathname}/`.replace(/\/\/$/, '/');
}
/**
* Complete address
* @param path address
* @param baseURI base url(app.url)
*/
function CompletionPath(path, baseURI) {
if (!path ||
/^((((ht|f)tps?)|file):)?\/\//.test(path) ||
/^(data|blob):/.test(path))
return path;
return createURL(path, getEffectivePath(addProtocol(baseURI))).toString();
}
/**
* Get the folder where the link resource is located,
* which is used to complete the relative address in the css
* @param linkPath full link address
*/
function getLinkFileDir(linkPath) {
const pathArr = linkPath.split('/');
pathArr.pop();
return addProtocol(pathArr.join('/') + '/');
}
/**
* promise stream
* @param promiseList promise list
* @param successCb success callback
* @param errorCb failed callback
* @param finallyCb finally callback
*/
function promiseStream(promiseList, successCb, errorCb, finallyCb) {
let finishedNum = 0;
function isFinished() {
if (++finishedNum === promiseList.length && finallyCb)
finallyCb();
}
promiseList.forEach((p, i) => {
if (isPromise(p)) {
p.then((res) => {
successCb({ data: res, index: i });
isFinished();
}).catch((err) => {
errorCb({ error: err, index: i });
isFinished();
});
}
else {
successCb({ data: p, index: i });
isFinished();
}
});
}
// Check whether the browser supports module script
function isSupportModuleScript() {
const s = document.createElement('script');
return 'noModule' in s;
}
// Create a random symbol string
function createNonceSrc() {
return 'inline-' + Math.random().toString(36).substr(2, 15);
}
// Array deduplication
function unique(array) {
return array.filter(function (item) {
return item in this ? false : (this[item] = true);
}, Object.create(null));
}
// requestIdleCallback polyfill
const requestIdleCallback = globalThis.requestIdleCallback ||
function (fn) {
const lastTime = Date.now();
return setTimeout(function () {
fn({
didTimeout: false,
timeRemaining() {
return Math.max(0, 50 - (Date.now() - lastTime));
},
});
}, 1);
};
/**
* Wrap requestIdleCallback with promise
* Exec callback when browser idle
*/
function promiseRequestIdle(callback) {
return new Promise((resolve) => {
requestIdleCallback(() => {
callback(resolve);
});
});
}
/**
* Record the currently running app.name
*/
let currentAppName = null;
function setCurrentAppName(appName) {
currentAppName = appName;
}
// get the currently running app.name
function getCurrentAppName() {
return currentAppName;
}
function throttleDeferForSetAppName(appName) {
if (currentAppName !== appName && !getPreventSetState()) {
setCurrentAppName(appName);
defer(() => {
setCurrentAppName(null);
});
}
}
// only for iframe document.body(head).querySelector(querySelectorAll)
let iframeCurrentAppName = null;
function setIframeCurrentAppName(appName) {
iframeCurrentAppName = appName;
}
function getIframeCurrentAppName() {
return iframeCurrentAppName;
}
function throttleDeferForIframeAppName(appName) {
if (iframeCurrentAppName !== appName && !getPreventSetState()) {
setIframeCurrentAppName(appName);
defer(() => {
setIframeCurrentAppName(null);
});
}
}
// prevent set app name
let preventSetState = false;
function getPreventSetState() {
return preventSetState;
}
/**
* prevent set appName
* usage:
* removeDomScope(true)
* -----> element scope point to base app <-----
* removeDomScope(false)
*/
function removeDomScope(force) {
if (force !== false) {
setCurrentAppName(null);
setIframeCurrentAppName(null);
if (force && !preventSetState) {
preventSetState = true;
defer(() => {
preventSetState = false;
});
}
}
else {
preventSetState = false;
}
}
/**
* Create pure elements
*/
function pureCreateElement(tagName, options) {
const element = (window.rawDocument || document).createElement(tagName, options);
if (element.__MICRO_APP_NAME__)
delete element.__MICRO_APP_NAME__;
element.__PURE_ELEMENT__ = true;
return element;
}
// is invalid key of querySelector
function isInvalidQuerySelectorKey(key) {
return !key || /(^\d)|([^\w\d-_\u4e00-\u9fa5])/gi.test(key);
}
// unique element
function isUniqueElement(key) {
return (/^body$/i.test(key) ||
/^head$/i.test(key) ||
/^html$/i.test(key) ||
/^title$/i.test(key) ||
/^:root$/i.test(key));
}
/**
* get micro-app element
* @param target app container
*/
function getRootContainer(target) {
return (isShadowRoot(target) ? target.host : target);
}
/**
* trim start & end
*/
function trim(str) {
return str ? str.replace(/^\s+|\s+$/g, '') : '';
}
function isFireFox() {
return navigator.userAgent.indexOf('Firefox') > -1;
}
/**
* Transforms a queryString into object.
* @param search - search string to parse
* @returns a query object
*/
function parseQuery(search) {
const result = {};
const queryList = search.split('&');
// we will not decode the key/value to ensure that the values are consistent when update URL
for (const queryItem of queryList) {
const eqPos = queryItem.indexOf('=');
const key = eqPos < 0 ? queryItem : queryItem.slice(0, eqPos);
const value = eqPos < 0 ? null : queryItem.slice(eqPos + 1);
if (key in result) {
let currentValue = result[key];
if (!isArray(currentValue)) {
currentValue = result[key] = [currentValue];
}
currentValue.push(value);
}
else {
result[key] = value;
}
}
return result;
}
/**
* Transforms an object to query string
* @param queryObject - query object to stringify
* @returns query string without the leading `?`
*/
function stringifyQuery(queryObject) {
let result = '';
for (const key in queryObject) {
const value = queryObject[key];
if (isNull(value)) {
result += (result.length ? '&' : '') + key;
}
else {
const valueList = isArray(value) ? value : [value];
valueList.forEach(value => {
if (!isUndefined(value)) {
result += (result.length ? '&' : '') + key;
if (!isNull(value))
result += '=' + value;
}
});
}
}
return result;
}
/**
* Register or unregister callback/guard with Set
*/
function useSetRecord() {
const handlers = new Set();
function add(handler) {
handlers.add(handler);
return () => {
if (handlers.has(handler))
return handlers.delete(handler);
return false;
};
}
return {
add,
list: () => handlers,
};
}
/**
* record data with Map
*/
function useMapRecord() {
const data = new Map();
function add(key, value) {
data.set(key, value);
return () => {
if (data.has(key))
return data.delete(key);
return false;
};
}
return {
add,
get: (key) => data.get(key),
delete: (key) => {
if (data.has(key))
return data.delete(key);
return false;
}
};
}
function getAttributes(element) {
const attr = element.attributes;
const attrMap = new Map();
for (let i = 0; i < attr.length; i++) {
attrMap.set(attr[i].name, attr[i].value);
}
return attrMap;
}
/**
* if fiberTasks exist, wrap callback with promiseRequestIdle
* if not, execute callback
* @param fiberTasks fiber task list
* @param callback action callback
*/
function injectFiberTask(fiberTasks, callback) {
if (fiberTasks) {
fiberTasks.push(() => promiseRequestIdle((resolve) => {
callback();
resolve();
}));
}
else {
callback();
}
}
/**
* serial exec fiber task of link, style, script
* @param tasks task array or null
*/
function serialExecFiberTasks(tasks) {
return (tasks === null || tasks === void 0 ? void 0 : tasks.reduce((pre, next) => pre.then(next), Promise.resolve())) || null;
}
/**
* inline script start with inline-xxx
* @param address source address
*/
function isInlineScript(address) {
return address.startsWith('inline-');
}
/**
* call function with try catch
* @param fn target function
* @param appName app.name
* @param args arguments
*/
function execMicroAppGlobalHook(fn, appName, hookName, ...args) {
try {
isFunction(fn) && fn(...args);
}
catch (e) {
logError(`An error occurred in app ${appName} window.${hookName} \n`, null, e);
}
}
/**
* remove all childNode from target node
* @param $dom target node
*/
function clearDOM($dom) {
while ($dom === null || $dom === void 0 ? void 0 : $dom.firstChild) {
$dom.removeChild($dom.firstChild);
}
}
function instanceOf(instance, constructor) {
if (instance === null || instance === undefined) {
return false;
}
else if (!isFunction(constructor)) {
throw new TypeError("Right-hand side of 'instanceof' is not callable");
}
else if (typeof instance === 'number' || typeof instance === 'string' || typeof instance === 'boolean') {
// 检查 obj 是否是基本类型的包装器实例
return false;
}
let proto = Object.getPrototypeOf(instance);
while (proto) {
if (proto === constructor.prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
/**
* Format event name
* In with sandbox, child event and lifeCycles bind to microAppElement, there are two events with same name - mounted unmount, it should be handled specifically to prevent conflicts
* Issue: https://github.com/jd-opensource/micro-app/issues/1161
* @param type event name
* @param appName app name
*/
const formatEventList = ['mounted', 'unmount'];
function formatEventType(type, appName) {
return formatEventList.includes(type) ? `${type}-${appName}` : type;
}
/**
* Is the object empty
* target maybe number, string, array ...
*/
function isEmptyObject(target) {
return isPlainObject(target) ? !Object.keys(target).length : true;
}
function formatEventInfo(event, element) {
Object.defineProperties(event, {
currentTarget: {
get() {
return element;
}
},
target: {
get() {
return element;
}
},
});
}
/**
* dispatch lifeCycles event to base app
* created, beforemount, mounted, unmount, error
* @param element container
* @param appName app.name
* @param lifecycleName lifeCycle name
* @param error param from error hook
*/
function dispatchLifecyclesEvent(element, appName, lifecycleName, error) {
var _a;
if (!element) {
return logWarn(`element does not exist in lifecycle ${lifecycleName}`, appName);
}
element = getRootContainer(element);
// clear dom scope before dispatch lifeCycles event to base app, especially mounted & unmount
removeDomScope();
const detail = assign({
name: appName,
container: element,
}, error && {
error
});
const event = new CustomEvent(lifecycleName, {
detail,
});
formatEventInfo(event, element);
// global hooks
if (isFunction((_a = microApp.options.lifeCycles) === null || _a === void 0 ? void 0 : _a[lifecycleName])) {
microApp.options.lifeCycles[lifecycleName](event, appName);
}
element.dispatchEvent(event);
}
/**
* Dispatch custom event to micro app
* @param app app
* @param eventName event name ['mounted', 'unmount', 'appstate-change', 'statechange']
* @param detail event detail
*/
function dispatchCustomEventToMicroApp(app, eventName, detail = {}) {
var _a;
const event = new CustomEvent(formatEventType(eventName, app.name), {
detail,
});
(_a = app.sandBox) === null || _a === void 0 ? void 0 : _a.microAppWindow.dispatchEvent(event);
}
/**
* fetch source of html, js, css
* @param url source path
* @param appName app name
* @param config fetch options
*/
function fetchSource(url, appName = null, options = {}) {
/**
* When child navigate to new async page, click event will scope dom to child and then fetch new source
* this may cause error when fetch rewrite by baseApp
* e.g.
* baseApp: <script crossorigin src="https://sgm-static.jd.com/sgm-2.8.0.js" name="SGMH5" sid="6f88a6e4ba4b4ae5acef2ec22c075085" appKey="jdb-adminb2b-pc"></script>
*/
removeDomScope();
if (isFunction(microApp.options.fetch)) {
return microApp.options.fetch(url, options, appName);
}
// Don’t use globalEnv.rawWindow.fetch, will cause sgm-2.8.0.js throw error in nest app
return window.fetch(url, options).then((res) => {
return res.text();
});
}
class HTMLLoader {
static getInstance() {
if (!this.instance) {
this.instance = new HTMLLoader();
}
return this.instance;
}
/**
* run logic of load and format html
* @param successCb success callback
* @param errorCb error callback, type: (err: Error, meetFetchErr: boolean) => void
*/
run(app, successCb) {
const appName = app.name;
const htmlUrl = app.ssrUrl || app.url;
const isJsResource = isTargetExtension(htmlUrl, 'js');
const htmlPromise = isJsResource
? Promise.resolve(`<micro-app-head><script src='${htmlUrl}'></script></micro-app-head><micro-app-body></micro-app-body>`)
: fetchSource(htmlUrl, appName, { cache: 'no-cache' });
htmlPromise.then((htmlStr) => {
if (!htmlStr) {
const msg = 'html is empty, please check in detail';
app.onerror(new Error(msg));
return logError(msg, appName);
}
htmlStr = this.formatHTML(htmlUrl, htmlStr, appName);
successCb(htmlStr, app);
}).catch((e) => {
logError(`Failed to fetch data from ${app.url}, micro-app stop rendering`, appName, e);
app.onLoadError(e);
});
}
formatHTML(htmlUrl, htmlStr, appName) {
return this.processHtml(htmlUrl, htmlStr, appName, microApp.options.plugins)
.replace(/<head[^>]*>[\s\S]*?<\/head>/i, (match) => {
return match
.replace(/<head/i, '<micro-app-head')
.replace(/<\/head>/i, '</micro-app-head>');
})
.replace(/<body[^>]*>[\s\S]*?<\/body>/i, (match) => {
return match
.replace(/<body/i, '<micro-app-body')
.replace(/<\/body>/i, '</micro-app-body>');
});
}
processHtml(url, code, appName, plugins) {
var _a;
if (!plugins)
return code;
const mergedPlugins = [];
plugins.global && mergedPlugins.push(...plugins.global);
((_a = plugins.modules) === null || _a === void 0 ? void 0 : _a[appName]) && mergedPlugins.push(...plugins.modules[appName]);
if (mergedPlugins.length > 0) {
return mergedPlugins.reduce((preCode, plugin) => {
if (isPlainObject(plugin) && isFunction(plugin.processHtml)) {
return plugin.processHtml(preCode, url);
}
return preCode;
}, code);
}
return code;
}
}
// common reg
const rootSelectorREG = /(^|\s+)(html|:root)(?=[\s>~[.#:]+|$)/;
const bodySelectorREG = /(^|\s+)((html[\s>~]+body)|body)(?=[\s>~[.#:]+|$)/;
function parseError(msg, linkPath) {
msg = linkPath ? `${linkPath} ${msg}` : msg;
const err = new Error(msg);
err.reason = msg;
if (linkPath) {
err.filename = linkPath;
}
throw err;
}
/**
* Reference https://github.com/reworkcss/css
* CSSParser mainly deals with 3 scenes: styleRule, @, and comment
* And scopecss deals with 2 scenes: selector & url
* And can also disable scopecss with inline comments
*/
class CSSParser {
constructor() {
this.cssText = ''; // css content
this.prefix = ''; // prefix as micro-app[name=xxx]
this.baseURI = ''; // domain name
this.linkPath = ''; // link resource address, if it is the style converted from link, it will have linkPath
this.result = ''; // parsed cssText
this.scopecssDisable = false; // use block comments /* scopecss-disable */ to disable scopecss in your file, and use /* scopecss-enable */ to enable scopecss
this.scopecssDisableSelectors = []; // disable or enable scopecss for specific selectors
this.scopecssDisableNextLine = false; // use block comments /* scopecss-disable-next-line */ to disable scopecss on a specific line
// https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule
this.mediaRule = this.createMatcherForRuleWithChildRule(/^@media *([^{]+)/, '@media');
// https://developer.mozilla.org/en-US/docs/Web/API/CSSSupportsRule
this.supportsRule = this.createMatcherForRuleWithChildRule(/^@supports *([^{]+)/, '@supports');
this.documentRule = this.createMatcherForRuleWithChildRule(/^@([-\w]+)?document *([^{]+)/, '@document');
this.hostRule = this.createMatcherForRuleWithChildRule(/^@host\s*/, '@host');
// :global is CSS Modules rule, it will be converted to normal syntax
// private globalRule = this.createMatcherForRuleWithChildRule(/^:global([^{]*)/, ':global')
// https://developer.mozilla.org/en-US/docs/Web/API/CSSImportRule
this.importRule = this.createMatcherForNoneBraceAtRule('import');
// Removed in most browsers
this.charsetRule = this.createMatcherForNoneBraceAtRule('charset');
// https://developer.mozilla.org/en-US/docs/Web/API/CSSNamespaceRule
this.namespaceRule = this.createMatcherForNoneBraceAtRule('namespace');
// https://developer.mozilla.org/en-US/docs/Web/CSS/@container
this.containerRule = this.createMatcherForRuleWithChildRule(/^@container *([^{]+)/, '@container');
}
exec(cssText, prefix, baseURI, linkPath) {
this.cssText = cssText;
this.prefix = prefix;
this.baseURI = baseURI;
this.linkPath = linkPath || '';
this.matchRules();
return isFireFox() ? decodeURIComponent(this.result) : this.result;
}
reset() {
this.cssText = this.prefix = this.baseURI = this.linkPath = this.result = '';
this.scopecssDisable = this.scopecssDisableNextLine = false;
this.scopecssDisableSelectors = [];
}
// core action for match rules
matchRules() {
this.matchLeadingSpaces();
this.matchComments();
while (this.cssText.length &&
this.cssText.charAt(0) !== '}' &&
(this.matchAtRule() || this.matchStyleRule())) {
this.matchComments();
}
}
// https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleRule
matchStyleRule() {
const selectors = this.formatSelector(true);
// reset scopecssDisableNextLine
this.scopecssDisableNextLine = false;
if (!selectors)
return this.printError('selector missing', this.linkPath);
this.recordResult(selectors);
this.matchComments();
this.styleDeclarations();
this.matchLeadingSpaces();
return true;
}
formatSelector(skip) {
const m = this.commonMatch(/^[^{]+/, skip);
if (!m)
return false;
/**
* NOTE:
* 1. :is(h1, h2, h3):has(+ h2, + h3, + h4) {}
* should be ==> micro-app[name=xxx] :is(h1, h2, h3):has(+ h2, + h3, + h4) {}
* 2. :dir(ltr) {}
* should be ==> micro-app[name=xxx] :dir(ltr) {}
* 3. body :not(div, .fancy) {}
* should be ==> micro-app[name=xxx] micro-app-body :not(div, .fancy) {}
* 4. .a, .b, li:nth-child(3)
* should be ==> micro-app[name=xxx] .a, micro-app[name=xxx] .b, micro-app[name=xxx] li:nth-child(3)
* 5. :is(.a, .b, .c) a {}
* should be ==> micro-app[name=xxx] :is(.a, .b, .c) a {}
* 6. :where(.a, .b, .c) a {}
* should be ==> micro-app[name=xxx] :where(.a, .b, .c) a {}
*/
const attributeValues = {};
const matchRes = m[0].replace(/\[([^\]=]+)(?:=([^\]]+))?\]/g, (match, p1, p2) => {
const mock = `__mock_${p1}Value__`;
attributeValues[mock] = p2;
return match.replace(p2, mock);
});
return matchRes.replace(/(^|,[\n\s]*)([^,]+)/g, (_, separator, selector) => {
selector = trim(selector);
selector = selector.replace(/\[[^\]=]+(?:=([^\]]+))?\]/g, (match, p1) => {
if (attributeValues[p1]) {
return match.replace(p1, attributeValues[p1]);
}
return match;
});
if (selector && !(this.scopecssDisableNextLine ||
(this.scopecssDisable && (!this.scopecssDisableSelectors.length ||
this.scopecssDisableSelectors.includes(selector))) ||
rootSelectorREG.test(selector))) {
if (bodySelectorREG.test(selector)) {
selector = selector.replace(bodySelectorREG, this.prefix + ' micro-app-body');
}
else {
selector = this.prefix + ' ' + selector;
}
}
return separator + selector;
});
}
// https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration
styleDeclarations() {
if (!this.matchOpenBrace())
return this.printError("Declaration missing '{'", this.linkPath);
this.matchAllDeclarations();
if (!this.matchCloseBrace())
return this.printError("Declaration missing '}'", this.linkPath);
return true;
}
matchAllDeclarations(nesting = 0) {
let cssValue = this.commonMatch(/^(?:url\(["']?(?:[^)"'}]+)["']?\)|[^{}/])*/, true)[0];
if (cssValue) {
if (!this.scopecssDisableNextLine &&
(!this.scopecssDisable || this.scopecssDisableSelectors.length)) {
cssValue = cssValue.replace(/url\((["']?)(.*?)\1\)/gm, (all, _, $1) => {
if (/^((data|blob):|#|%23)/.test($1) || /^(https?:)?\/\//.test($1)) {
return all;
}
// ./a/b.png ../a/b.png a/b.png
if (/^((\.\.?\/)|[^/])/.test($1) && this.linkPath) {
this.baseURI = getLinkFileDir(this.linkPath);
}
return `url("${CompletionPath($1, this.baseURI)}")`;
});
}
this.recordResult(cssValue);
}
// reset scopecssDisableNextLine
this.scopecssDisableNextLine = false;
if (!this.cssText.length)
return;
// extract comments in declarations
if (this.cssText.charAt(0) === '/') {
if (this.cssText.charAt(1) === '*') {
this.matchComments();
}
else {
this.commonMatch(/\/+/);
}
}
else if (this.cssText.charAt(0) === '{') {
this.matchOpenBrace();
nesting++;
}
else if (this.cssText.charAt(0) === '}') {
if (nesting < 1)
return;
this.matchCloseBrace();
nesting--;
}
return this.matchAllDeclarations(nesting);
}
matchAtRule() {
if (this.cssText[0] !== '@')
return false;
// reset scopecssDisableNextLine
this.scopecssDisableNextLine = false;
return this.keyframesRule() ||
this.mediaRule() ||
this.customMediaRule() ||
this.supportsRule() ||
this.importRule() ||
this.charsetRule() ||
this.namespaceRule() ||
this.containerRule() ||
this.documentRule() ||
this.pageRule() ||
this.hostRule() ||
this.fontFaceRule() ||
this.layerRule();
}
// :global is CSS Modules rule, it will be converted to normal syntax
// private matchGlobalRule (): boolean | void {
// if (this.cssText[0] !== ':') return false
// // reset scopecssDisableNextLine
// this.scopecssDisableNextLine = false
// return this.globalRule()
// }
// https://developer.mozilla.org/en-US/docs/Web/API/CSSKeyframesRule
keyframesRule() {
if (!this.commonMatch(/^@([-\w]+)?keyframes\s*/))
return false;
if (!this.commonMatch(/^[^{]+/))
return this.printError('@keyframes missing name', this.linkPath);
this.matchComments();
if (!this.matchOpenBrace())
return this.printError("@keyframes missing '{'", this.linkPath);
this.matchComments();
while (this.keyframeRule()) {
this.matchComments();
}
if (!this.matchCloseBrace())
return this.printError("@keyframes missing '}'", this.linkPath);
this.matchLeadingSpaces();
return true;
}
keyframeRule() {
let r;
const valList = [];
while (r = this.commonMatch(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/)) {
valList.push(r[1]);
this.commonMatch(/^,\s*/);
}
if (!valList.length)
return false;
this.styleDeclarations();
this.matchLeadingSpaces();
return true;
}
// https://github.com/postcss/postcss-custom-media
customMediaRule() {
if (!this.commonMatch(/^@custom-media\s+(--[^\s]+)\s*([^{;]+);/))
return false;
this.matchLeadingSpaces();
return true;
}
// https://developer.mozilla.org/en-US/docs/Web/API/CSSPageRule
pageRule() {
if (!this.commonMatch(/^@page */))
return false;
this.formatSelector(false);
// reset scopecssDisableNextLine
this.scopecssDisableNextLine = false;
return this.commonHandlerForAtRuleWithSelfRule('page');
}
// https://developer.mozilla.org/en-US/docs/Web/API/CSSFontFaceRule
fontFaceRule() {
if (!this.commonMatch(/^@font-face\s*/))
return false;
return this.commonHandlerForAtRuleWithSelfRule('font-face');
}
// https://developer.mozilla.org/en-US/docs/Web/CSS/@layer
layerRule() {
if (!this.commonMatch(/^@layer\s*([^{;]+)/))
return false;
if (!this.matchOpenBrace())
return !!this.commonMatch(/^[;]+/);
this.matchComments();
this.matchRules();
if (!this.matchCloseBrace())
return this.printError('@layer missing \'}\'', this.linkPath);
this.matchLeadingSpaces();
return true;
}
// common matcher for @media, @supports, @document, @host, :global, @container
createMatcherForRuleWithChildRule(reg, name) {
return () => {
if (!this.commonMatch(reg))
return false;
if (!this.matchOpenBrace())
return this.printError(`${name} missing '{'`, this.linkPath);
this.matchComments();
this.matchRules();
if (!this.matchCloseBrace())
return this.printError(`${name} missing '}'`, this.linkPath);
this.matchLeadingSpaces();
return true;
};
}
// common matcher for @import, @charset, @namespace
createMatcherForNoneBraceAtRule(name) {
const reg = new RegExp('^@' + name + '\\s*([^;]+);');
return () => {
if (!this.commonMatch(reg))
return false;
this.matchLeadingSpaces();
return true;
};
}
// common handler for @font-face, @page
commonHandlerForAtRuleWithSelfRule(name) {
if (!this.matchOpenBrace())
return this.printError(`@${name} missing '{'`, this.linkPath);
this.matchAllDeclarations();
if (!this.matchCloseBrace())
return this.printError(`@${name} missing '}'`, this.linkPath);
this.matchLeadingSpaces();
return true;
}
// match and slice comments
matchComments() {
while (this.matchComment())
;
}
// css comment
matchComment() {
if (this.cssText.charAt(0) !== '/' || this.cssText.charAt(1) !== '*')
return false;
// reset scopecssDisableNextLine
this.scopecssDisableNextLine = false;
let i = 2;
while (this.cssText.charAt(i) !== '' && (this.cssText.charAt(i) !== '*' || this.cssText.charAt(i + 1) !== '/'))
++i;
i += 2;
if (this.cssText.charAt(i - 1) === '') {
return this.printError('End of comment missing', this.linkPath);
}
// get comment content
let commentText = this.cssText.slice(2, i - 2);
this.recordResult(`/*${commentText}*/`);
commentText = trim(commentText.replace(/^\s*!/, ''));
// set ignore config
if (commentText === 'scopecss-disable-next-line') {
this.scopecssDisableNextLine = true;
}
else if (/^scopecss-disable/.test(commentText)) {
if (commentText === 'scopecss-disable') {
this.scopecssDisable = true;
}
else {
this.scopecssDisable = true;
const ignoreRules = commentText.replace('scopecss-disable', '').split(',');
ignoreRules.forEach((rule) => {
this.scopecssDisableSelectors.push(trim(rule));
});
}
}
else if (commentText === 'scopecss-enable') {
this.scopecssDisable = false;
this.scopecssDisableSelectors = [];
}
this.cssText = this.cssText.slice(i);
this.matchLeadingSpaces();
return true;
}
commonMatch(reg, skip = false) {
const matchArray = reg.exec(this.cssText);
if (!matchArray)
return;
const matchStr = matchArray[0];
this.cssText = this.cssText.slice(matchStr.length);
if (!skip)
this.recordResult(matchStr);
return matchArray;
}
matchOpenBrace() {
return this.commonMatch(/^{\s*/);
}
matchCloseBrace() {
return this.commonMatch(/^}\s*/);
}
// match and slice the leading spaces
matchLeadingSpaces() {
this.commonMatch(/^\s*/);
}
// splice string
recordResult(strFragment) {
// Firefox performance degradation when string contain special characters, see https://github.com/jd-opensource/micro-app/issues/256
if (isFireFox()) {
this.result += encodeURIComponent(strFragment);
}
else {
this.result += strFragment;
}
}
printError(msg, linkPath) {
if (this.cssText.length) {
parseError(msg, linkPath);
}
}
}
/**
* common method of bind CSS
*/
function commonAction(styleElement, appName, prefix, baseURI, linkPath) {
if (!styleElement.__MICRO_APP_HAS_SCOPED__) {
styleElement.__MICRO_APP_HAS_SCOPED__ = true;
let result = null;
try {
result = parser.exec(styleElement.textContent, prefix, baseURI, linkPath);
parser.reset();
}
catch (e) {
parser.reset();
logError('An error occurred while parsing CSS:\n', appName, e);
}
if (result)
styleElement.textContent = result;
}
}
let parser;
/**
* scopedCSS
* @param styleElement target style element
* @param appName app name
*/
function scopedCSS(styleElement, app, linkPath) {
if (app.scopecss) {
const prefix = createPrefix(app.name);
if (!parser)
parser = new CSSParser();
const escapeRegExp = (regStr) => regStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
if (styleElement.textContent) {
commonAction(styleElement, app.name, prefix, app.url, linkPath);
const observer = new MutationObserver(() => {
const escapedPrefix = escapeRegExp(prefix);
const isPrefixed = styleElement.textContent && new RegExp(escapedPrefix).test(styleElement.textContent);
observer.disconnect();
if (!isPrefixed) {
styleElement.__MICRO_APP_HAS_SCOPED__ = false;
scopedCSS(styleElement, app, linkPath);
}
});
observer.observe(styleElement, { childList: true, characterData: true });
}
else {
const observer = new MutationObserver(function () {
observer.disconnect();
// styled-component will be ignore
if (styleElement.textContent && !styleElement.hasAttribute('data-styled')) {
commonAction(styleElement, app.name, prefix, app.url, linkPath);
}
});
observer.observe(styleElement, { childList: true });
}
}
return styleElement;
}
function createPrefix(appName, reg = false) {
const regCharacter = reg ? '\\' : '';
return `${microApp.tagName}${regCharacter}[name=${appName}${regCharacter}]`;
}
function eventHandler(event, element) {
Object.defineProperties(event, {
currentTarget: {
get() {
return element;
}
},
srcElement: {
get() {
return element;
}
},
target: {
get() {
return element;
}
},
});
}
function dispatchOnLoadEvent(element) {
const event = new CustomEvent('load');
eventHandler(event, element);
if (isFunction(element.onload)) {
element.onload(event);
}
else {
element.dispatchEvent(event);
}
}
function dispatchOnErrorEvent(element) {
const event = new CustomEvent('error');
eventHandler(event, element);
if (isFunction(element.onerror)) {
element.onerror(event);
}
else {
element.dispatchEvent(event);
}
}
/**
* SourceCenter is a resource management center
* All html, js, css will be recorded and processed here
* NOTE:
* 1. All resources are global and shared between apps
* 2. Pay attention to the case of html with parameters
* 3. The resource is first processed by the plugin
*/
function createSourceCenter() {
const linkList = new Map();
const scriptList = new Map();
function createSourceHandler(targetList) {
return {
setInfo(address, info) {
targetList.set(address, info);
},
getInfo(address) {
var _a;
return (_a = targetList.get(address)) !== null && _a !== void 0 ? _a : null;
},
hasInfo(address) {
return targetList.has(address);
},
deleteInfo(address) {
return targetList.delete(address);
}
};
}
return {
link: createSourceHandler(linkList),
script: Object.assign(Object.assign({}, createSourceHandler(scriptList)), { deleteInlineInfo(addressList) {
addressList.forEach((address) => {
if (isInlineScript(address)) {
scriptList.delete(address);
}
});
} }),
};
}
var sourceCenter = createSourceCenter();
/**
*
* @param appName app.name
* @param linkInfo linkInfo of current address
*/
function getExistParseCode(appName, prefix, linkInfo) {
const appSpace = linkInfo.appSpace;
for (const item in appSpace) {
if (item !== appName) {
const appSpaceData = appSpace[item];
if (appSpaceData.parsedCode) {
return appSpaceData.parsedCode.replace(new RegExp(createPrefix(item, true), 'g'), prefix);
}
}
}
}
// transfer the attributes on the link to convertStyle
function setConvertStyleAttr(convertStyle, attrs) {
attrs.forEach((value, key) => {
if (key === 'rel')
return;
if (key === 'href')
key = 'data-origin-href';
globalEnv.rawSetAttribute.call(convertStyle, key, value);
});
}
/**
* Extract link elements
* @param link link element
* @param parent parent element of link
* @param app app
* @param microAppHead micro-app-head element
* @param isDynamic dynamic insert
*/
function extractLinkFromHtml(link, parent, app, isDynamic = false) {
const rel = link.getAttribute('rel');
let href = link.getAttribute('href');
let replaceComment = null;
if (rel === 'stylesheet' && href) {
href = CompletionPath(href, app.url);
let linkInfo = sourceCenter.link.getInfo(href);
const appSpaceData = {
attrs: getAttributes(link),
};
if (!linkInfo) {
linkInfo = {
code: '',
appSpace: {
[app.name]: appSpaceData,
}
};
}
else {
linkInfo.appSpace[app.name] = linkInfo.appSpace[app.name] || appSpaceData;
}
sourceCenter.link.setInfo(href, linkInfo);
if (!isDynamic) {
app.source.links.add(href);
replaceComment = document.createComment(`link element with href=${href} move to micro-app-head as style element`);
linkInfo.appSpace[app.name].placeholder = replaceComment;
}
else {
return { address: href, linkInfo };
}
}
else if (rel && ['prefetch', 'preload', 'prerender', 'modulepreload', 'icon'].includes(rel)) {
// preload prefetch prerender ....
if (isDynamic) {
replaceComment = document.createComment(`link element with rel=${rel}${href ? ' & href=' + href : ''} removed by micro-app`);
}
else {
parent === null || parent === void 0 ? void 0 : parent.removeChild(link);
}
}
else if (href) {
// dns-prefetch preconnect modulepreload search ....
globalEnv.rawSetAttribute.call(link, 'href', CompletionPath(href, app.url));
}
if (isDynamic) {
return { replaceComment };
}
else if (replaceComment) {
return parent === null || parent === void 0 ? void 0 : parent.replaceChild(replaceComment, link);
}
}
/**
* Get link remote resources
* @param wrapElement htmlDom
* @param app app
* @param microAppHead micro-app-head
*/
function fetchLinksFromHtml(wrapElement, app, microAppHead, fiberStyleResult) {
const styleList = Array.from(app.source.links);
const fetchLinkPromise = styleList.map((address) => {
const linkInfo = sourceCenter.link.getInfo(address);
return linkInfo.code ? linkInfo.code : fetchSource(address, app.name);
});
const fiberLinkTasks = fiberStyleResult ? [] : null;
promiseStream(fetchLinkPromise, (res) => {
injectFiberTask(fiberLinkTasks, () => fetchLinkSuccess(styleList[res.index], res.data, microAppHead, app));
}, (err) => {
logError(err, app.name);
}, () => {
/**
* 1. If fiberStyleResult exist, fiberLinkTasks must exist
* 2. Download link source while processing style
* 3. Process style first, and then process link
*/
if (fiberStyleResult) {
fiberStyleResult.then(() => {
fiberLinkTasks.push(() => Promise.resolve(app.onLoad({ html: wrapElement })));
serialExecFiberTasks(fiberLinkTasks);
});
}
else {
app.onLoad({ html: wrapElement });
}
});
}
/**
* Fetch link succeeded, replace placeholder with style tag
* NOTE:
* 1. Only exec when init, no longer exec when remount
* 2. Only handler html link element, not dynamic link or style
* 3. The same prefix can reuse parsedCode
* 4. Async exec with requestIdleCallback in prefetch or fiber
* 5. appSpace[app.name].placeholder/attrs must exist
* @param address resource address
* @param code link source code
* @param microAppHead micro-app-head
* @param app app instance
*/
function fetchLinkSuccess(address, code, microAppHead, app) {
/**
* linkInfo must exist, but linkInfo.code not
* so we set code to linkInfo.code
*/
const linkInfo = sourceCenter.link.getInfo(address);
linkInfo.code = code;
const appSpaceData = linkInfo.appSpace[app.n