Comparing version
@@ -37,3 +37,3 @@ (function (global, factory) { | ||
plugins.add(plugin); | ||
plugin(args); | ||
plugin(...args); | ||
} | ||
@@ -62,98 +62,100 @@ } | ||
function run(code, options = {}) { | ||
function request(url, option) { | ||
return fetch(url, Object.assign({ mode: 'cors' }, option)).then((res) => res.text()); | ||
} | ||
function runScript(code, allow = {}) { | ||
try { | ||
if (checkSyntax(code)) { | ||
const handler = { | ||
get(obj, prop) { | ||
return Reflect.has(obj, prop) ? obj[prop] : null; | ||
}, | ||
set(obj, prop, value) { | ||
Reflect.set(obj, prop, value); | ||
return true; | ||
}, | ||
has(obj, prop) { | ||
return obj && Reflect.has(obj, prop); | ||
const handler = { | ||
get(obj, prop) { | ||
return Reflect.has(obj, prop) ? obj[prop] : null; | ||
}, | ||
set(obj, prop, value) { | ||
Reflect.set(obj, prop, value); | ||
return true; | ||
}, | ||
has(obj, prop) { | ||
return obj && Reflect.has(obj, prop); | ||
} | ||
}; | ||
const captureHandler = { | ||
get(obj, prop) { | ||
return Reflect.get(obj, prop); | ||
}, | ||
set() { | ||
return true; | ||
}, | ||
has() { | ||
return true; | ||
} | ||
}; | ||
const allowList = Object.assign({ IS_BERIAL_SANDBOX: true, __proto__: null, console, | ||
String, | ||
Number, | ||
Array, | ||
Symbol, | ||
Math, | ||
Object, | ||
Promise, | ||
RegExp, | ||
JSON, | ||
Date, | ||
Function, | ||
parseInt, | ||
document, | ||
location, | ||
performance, | ||
MessageChannel, | ||
SVGElement, | ||
HTMLElement, | ||
HTMLIFrameElement, | ||
history, | ||
Map, | ||
Set, | ||
WeakMap, | ||
WeakSet, | ||
Error, | ||
localStorage, | ||
decodeURI, | ||
encodeURI, | ||
decodeURIComponent, | ||
encodeURIComponent, fetch: fetch.bind(window), setTimeout: setTimeout.bind(window), clearTimeout: clearTimeout.bind(window), setInterval: setInterval.bind(window), clearInterval: clearInterval.bind(window), requestAnimationFrame: requestAnimationFrame.bind(window), cancelAnimationFrame: cancelAnimationFrame.bind(window), addEventListener: addEventListener.bind(window), removeEventListener: removeEventListener.bind(window), eval: function (code) { | ||
return runScript('return ' + code, {}); | ||
}, alert: function () { | ||
alert('Sandboxed alert:' + arguments[0]); | ||
}, innerHeight, | ||
innerWidth, | ||
outerHeight, | ||
outerWidth, | ||
pageXOffset, | ||
pageYOffset, | ||
screen, | ||
screenLeft, | ||
screenTop, | ||
screenX, | ||
screenY, | ||
scrollBy, | ||
scrollTo, | ||
scrollX, | ||
scrollY }, allow); | ||
if (!Object.isFrozen(String.prototype)) { | ||
for (const k in allowList) { | ||
const fn = allowList[k]; | ||
if (typeof fn === 'object' && fn.prototype) { | ||
Object.freeze(fn.prototype); | ||
} | ||
}; | ||
const captureHandler = { | ||
get(obj, prop) { | ||
return Reflect.get(obj, prop); | ||
}, | ||
set() { | ||
return true; | ||
}, | ||
has() { | ||
return true; | ||
if (typeof fn === 'function') { | ||
Object.freeze(fn); | ||
} | ||
}; | ||
const allowList = Object.assign({ IS_BERIAL_SANDBOX: true, __proto__: null, console, | ||
String, | ||
Number, | ||
Array, | ||
Symbol, | ||
Math, | ||
Object, | ||
Promise, | ||
RegExp, | ||
JSON, | ||
Date, | ||
Function, | ||
parseInt, | ||
document, | ||
navigator, | ||
location, | ||
performance, | ||
MessageChannel, | ||
SVGElement, | ||
HTMLElement, | ||
HTMLIFrameElement, | ||
history, | ||
Map, | ||
Set, | ||
WeakMap, | ||
WeakSet, | ||
Error, | ||
localStorage, | ||
decodeURI, | ||
encodeURI, | ||
decodeURIComponent, | ||
encodeURIComponent, fetch: fetch.bind(window), setTimeout: setTimeout.bind(window), clearTimeout: clearTimeout.bind(window), setInterval: setInterval.bind(window), clearInterval: clearInterval.bind(window), requestAnimationFrame: requestAnimationFrame.bind(window), cancelAnimationFrame: cancelAnimationFrame.bind(window), addEventListener: addEventListener.bind(window), removeEventListener: removeEventListener.bind(window), eval: function (code) { | ||
return run('return ' + code, null); | ||
}, alert: function () { | ||
alert('Sandboxed alert:' + arguments[0]); | ||
}, innerHeight, | ||
innerWidth, | ||
outerHeight, | ||
outerWidth, | ||
pageXOffset, | ||
pageYOffset, | ||
screen, | ||
screenLeft, | ||
screenTop, | ||
screenX, | ||
screenY, | ||
scrollBy, | ||
scrollTo, | ||
scrollX, | ||
scrollY }, (options.allowList || {})); | ||
if (!Object.isFrozen(String.prototype)) { | ||
for (const k in allowList) { | ||
const fn = allowList[k]; | ||
if (typeof fn === 'object' && fn.prototype) { | ||
Object.freeze(fn.prototype); | ||
} | ||
if (typeof fn === 'function') { | ||
Object.freeze(fn); | ||
} | ||
} | ||
} | ||
const proxy = new Proxy(allowList, handler); | ||
const capture = new Proxy({ | ||
__proto__: null, | ||
proxy, | ||
globalThis: new Proxy(allowList, handler), | ||
window: new Proxy(allowList, handler), | ||
self: new Proxy(allowList, handler) | ||
}, captureHandler); | ||
return Function('proxy', 'capture', `with(capture) { | ||
} | ||
const proxy = new Proxy(allowList, handler); | ||
const capture = new Proxy({ | ||
__proto__: null, | ||
proxy, | ||
globalThis: new Proxy(allowList, handler), | ||
window: new Proxy(allowList, handler), | ||
self: new Proxy(allowList, handler) | ||
}, captureHandler); | ||
return Function('proxy', 'capture', `with(capture) { | ||
with(proxy) { | ||
@@ -167,3 +169,2 @@ return (function(){ | ||
}`)(proxy, capture); | ||
} | ||
} | ||
@@ -174,161 +175,159 @@ catch (e) { | ||
} | ||
function checkSyntax(code) { | ||
Function(code); | ||
if (/\bimport\s*(?:[(]|\/[*]|\/\/|<!--|-->)/.test(code)) { | ||
throw new Error('Dynamic imports are blocked'); | ||
} | ||
return true; | ||
} | ||
function error(trigger, msg) { | ||
if (typeof trigger === 'string') | ||
msg = trigger; | ||
if (!trigger) | ||
return; | ||
throw new Error(`[Berial: Error]: ${msg}`); | ||
const ALL_SCRIPT_REGEX = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi; | ||
const SCRIPT_TAG_REGEX = /<(script)\s+((?!type=('|')text\/ng-template\3).)*?>.*?<\/\1>/is; | ||
const SCRIPT_SRC_REGEX = /.*\ssrc=('|")?([^>'"\s]+)/; | ||
const SCRIPT_ENTRY_REGEX = /.*\sentry\s*.*/; | ||
const LINK_TAG_REGEX = /<(link)\s+.*?>/gi; | ||
const LINK_IGNORE_REGEX = /.*ignore\s*.*/; | ||
const LINK_PRELOAD_OR_PREFETCH_REGEX = /\srel=('|")?(preload|prefetch)\1/; | ||
const LINK_HREF_REGEX = /.*\shref=('|")?([^>'"\s]+)/; | ||
const STYLE_TAG_REGEX = /<style[^>]*>[\s\S]*?<\/style>/gi; | ||
const STYLE_TYPE_REGEX = /\s+rel=('|")?stylesheet\1.*/; | ||
const STYLE_HREF_REGEX = /.*\shref=('|")?([^>'"\s]+)/; | ||
const STYLE_IGNORE_REGEX = /<style(\s+|\s+.+\s+)ignore(\s*|\s+.*)>/i; | ||
const HTML_COMMENT_REGEX = /<!--([\s\S]*?)-->/g; | ||
const SCRIPT_IGNORE_REGEX = /<script(\s+|\s+.+\s+)ignore(\s*|\s+.*)>/i; | ||
function getInlineCode(match) { | ||
const start = match.indexOf('>') + 1; | ||
const end = match.lastIndexOf('<'); | ||
return match.substring(start, end); | ||
} | ||
function request(url, option) { | ||
console.log(url); | ||
if (!window.fetch) { | ||
error("It looks like that your browser doesn't support fetch. Polyfill is needed before you use it."); | ||
} | ||
return fetch(url, Object.assign({ mode: 'cors' }, option)).then((res) => res.text()); | ||
function hasProtocol(url) { | ||
return (url.startsWith('//') || | ||
url.startsWith('http://') || | ||
url.startsWith('https://')); | ||
} | ||
function lifecycleCheck(lifecycle) { | ||
const keys = ['bootstrap', 'mount', 'unmount']; | ||
keys.forEach((key) => { | ||
if (!(key in lifecycle)) { | ||
error(`It looks like that you didn't export the lifecycle hook [${key}], which would cause a mistake.`); | ||
function getEntirePath(path, baseURI) { | ||
return new URL(path, baseURI).toString(); | ||
} | ||
const genLinkReplaceSymbol = (linkHref) => `<!-- link ${linkHref} replaced by import-html-entry -->`; | ||
const genScriptReplaceSymbol = (scriptSrc) => `<!-- script ${scriptSrc} replaced by import-html-entry -->`; | ||
const inlineScriptReplaceSymbol = `<!-- inline scripts replaced by import-html-entry -->`; | ||
const genIgnoreAssetReplaceSymbol = (url) => `<!-- ignore asset ${url || 'file'} replaced by import-html-entry -->`; | ||
function parse(tpl, baseURI) { | ||
let scripts = []; | ||
const styles = []; | ||
let entry = null; | ||
const template = tpl | ||
.replace(HTML_COMMENT_REGEX, '') | ||
.replace(LINK_TAG_REGEX, (match) => { | ||
const styleType = !!match.match(STYLE_TYPE_REGEX); | ||
console.log(styleType); | ||
if (styleType) { | ||
const styleHref = match.match(STYLE_HREF_REGEX); | ||
const styleIgnore = match.match(LINK_IGNORE_REGEX); | ||
if (styleHref) { | ||
const href = styleHref && styleHref[2]; | ||
let newHref = href; | ||
if (href && !hasProtocol(href)) { | ||
newHref = getEntirePath(href, baseURI); | ||
} | ||
if (styleIgnore) { | ||
return genIgnoreAssetReplaceSymbol(newHref); | ||
} | ||
styles.push(newHref); | ||
return genLinkReplaceSymbol(newHref); | ||
} | ||
} | ||
const preloadOrPrefetchType = !!match.match(LINK_PRELOAD_OR_PREFETCH_REGEX); | ||
if (preloadOrPrefetchType) { | ||
const linkHref = match.match(LINK_HREF_REGEX); | ||
if (linkHref) { | ||
const href = linkHref[2]; | ||
if (href && !hasProtocol(href)) { | ||
const newHref = getEntirePath(href, baseURI); | ||
return match.replace(href, newHref); | ||
} | ||
} | ||
} | ||
return match; | ||
}) | ||
.replace(STYLE_TAG_REGEX, (match) => { | ||
if (STYLE_IGNORE_REGEX.test(match)) { | ||
return genIgnoreAssetReplaceSymbol('style file'); | ||
} | ||
return match; | ||
}) | ||
.replace(ALL_SCRIPT_REGEX, (match) => { | ||
const scriptIgnore = match.match(SCRIPT_IGNORE_REGEX); | ||
if (SCRIPT_TAG_REGEX.test(match) && match.match(SCRIPT_SRC_REGEX)) { | ||
const matchedScriptEntry = match.match(SCRIPT_ENTRY_REGEX); | ||
const matchedScriptSrcMatch = match.match(SCRIPT_SRC_REGEX); | ||
let matchedScriptSrc = matchedScriptSrcMatch && matchedScriptSrcMatch[2]; | ||
if (entry && matchedScriptEntry) { | ||
throw new SyntaxError('You should not set multiply entry script!'); | ||
} | ||
else { | ||
if (matchedScriptSrc && !hasProtocol(matchedScriptSrc)) { | ||
matchedScriptSrc = getEntirePath(matchedScriptSrc, baseURI); | ||
} | ||
entry = entry || (matchedScriptEntry && matchedScriptSrc); | ||
} | ||
if (scriptIgnore) { | ||
return genIgnoreAssetReplaceSymbol(matchedScriptSrc || 'js file'); | ||
} | ||
if (matchedScriptSrc) { | ||
scripts.push(matchedScriptSrc); | ||
return genScriptReplaceSymbol(matchedScriptSrc); | ||
} | ||
return match; | ||
} | ||
else { | ||
if (scriptIgnore) { | ||
return genIgnoreAssetReplaceSymbol('js file'); | ||
} | ||
const code = getInlineCode(match); | ||
const isPureCommentBlock = code | ||
.split(/[\r\n]+/) | ||
.every((line) => !line.trim() || line.trim().startsWith('//')); | ||
if (!isPureCommentBlock) { | ||
scripts.push(match); | ||
} | ||
return inlineScriptReplaceSymbol; | ||
} | ||
}); | ||
} | ||
const MATCH_ANY_OR_NO_PROPERTY = /["'=\w\s\/]*/; | ||
const SCRIPT_URL_RE = new RegExp('<\\s*script' + | ||
MATCH_ANY_OR_NO_PROPERTY.source + | ||
'(?:src="(.+?)")' + | ||
MATCH_ANY_OR_NO_PROPERTY.source + | ||
'(?:\\/>|>[\\s]*<\\s*\\/script>)?', 'g'); | ||
const SCRIPT_CONTENT_RE = new RegExp('<\\s*script' + | ||
MATCH_ANY_OR_NO_PROPERTY.source + | ||
'>([\\w\\W]+?)<\\s*\\/script>', 'g'); | ||
const MATCH_NONE_QUOTE_MARK = /[^"]/; | ||
const CSS_URL_RE = new RegExp('<\\s*link[^>]*' + | ||
'href="(' + | ||
MATCH_NONE_QUOTE_MARK.source + | ||
'+.css' + | ||
MATCH_NONE_QUOTE_MARK.source + | ||
'*)"' + | ||
MATCH_ANY_OR_NO_PROPERTY.source + | ||
'>(?:\\s*<\\s*\\/link>)?', 'g'); | ||
const STYLE_RE = /<\s*style\s*>([^<]*)<\s*\/style>/g; | ||
const BODY_CONTENT_RE = /<\s*body[^>]*>([\w\W]*)<\s*\/body>/; | ||
const SCRIPT_ANY_RE = /<\s*script[^>]*>[\s\S]*?(<\s*\/script[^>]*>)/g; | ||
const TEST_URL = /^(?:https?):\/\/[-a-zA-Z0-9.]+/; | ||
const REPLACED_BY_BERIAL = 'Script replaced by Berial.'; | ||
function importHtml(app) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const template = yield request(app.url); | ||
const styleNodes = yield loadCSS(template); | ||
const bodyNode = loadBody(template); | ||
const lifecycle = yield loadScript(template, app.name); | ||
return { lifecycle, styleNodes, bodyNode }; | ||
}); | ||
} | ||
function loadScript(template, name) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const { scriptURLs, scripts } = parseScript(template); | ||
const fetchedScripts = yield Promise.all(scriptURLs.map((url) => request(url))); | ||
const scriptsToLoad = fetchedScripts.concat(scripts); | ||
let bootstrap = []; | ||
let unmount = []; | ||
let mount = []; | ||
scriptsToLoad.forEach((script) => { | ||
const lifecycles = run(script, {})[name]; | ||
bootstrap = [...bootstrap, lifecycles.bootstrap]; | ||
mount = [...mount, lifecycles.mount]; | ||
unmount = [...unmount, lifecycles.unmount]; | ||
}); | ||
return { bootstrap, unmount, mount }; | ||
}); | ||
} | ||
function parseScript(template) { | ||
const scriptURLs = []; | ||
const scripts = []; | ||
SCRIPT_URL_RE.lastIndex = SCRIPT_CONTENT_RE.lastIndex = 0; | ||
let match; | ||
while ((match = SCRIPT_URL_RE.exec(template))) { | ||
let captured = match[1].trim(); | ||
if (!captured) | ||
continue; | ||
if (!TEST_URL.test(captured)) { | ||
captured = window.location.origin + captured; | ||
} | ||
scriptURLs.push(captured); | ||
} | ||
while ((match = SCRIPT_CONTENT_RE.exec(template))) { | ||
const captured = match[1].trim(); | ||
if (!captured) | ||
continue; | ||
scripts.push(captured); | ||
} | ||
scripts = scripts.filter((s) => !!s); | ||
return { | ||
scriptURLs, | ||
scripts | ||
template, | ||
scripts, | ||
styles, | ||
entry: entry || scripts[scripts.length - 1] | ||
}; | ||
} | ||
function loadCSS(template) { | ||
function importHtml(app) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const { cssURLs, styles } = parseCSS(template); | ||
const fetchedStyles = yield Promise.all(cssURLs.map((url) => request(url))); | ||
return toStyleNodes(fetchedStyles.concat(styles)); | ||
function toStyleNodes(styles) { | ||
return styles.map((style) => { | ||
const styleNode = document.createElement('style'); | ||
styleNode.appendChild(document.createTextNode(style)); | ||
return styleNode; | ||
}); | ||
let template = '', scripts, styles; | ||
if (app.scripts) { | ||
scripts = app.scripts || []; | ||
styles = app.styles || []; | ||
} | ||
else { | ||
const tpl = yield request(app.url); | ||
let res = parse(tpl, ''); | ||
scripts = res.scripts; | ||
styles = res.styles; | ||
template = res.template; | ||
} | ||
scripts = yield Promise.all(scripts.map((s) => hasProtocol(s) | ||
? request(s) | ||
: s.endsWith('.js') | ||
? request(window.origin + s) | ||
: s)); | ||
styles = styles.map((s) => hasProtocol(s) || s.endsWith('.css') ? `<link rel="stylesheet" href="${s}" ></link>` : `<style>${s}<style>`); | ||
template = template; | ||
let lifecycles = null; | ||
scripts.forEach((script) => __awaiter(this, void 0, void 0, function* () { | ||
lifecycles = runScript(script, app.allowList)[app.name]; | ||
})); | ||
const dom = document.createDocumentFragment(); | ||
const body = document.createElement('template'); | ||
let out = ''; | ||
styles.forEach((s) => (out += s)); | ||
out += template; | ||
body.innerHTML = out; | ||
dom.appendChild(body.content.cloneNode(true)); | ||
return { dom, lifecycles }; | ||
}); | ||
} | ||
function parseCSS(template) { | ||
const cssURLs = []; | ||
const styles = []; | ||
CSS_URL_RE.lastIndex = STYLE_RE.lastIndex = 0; | ||
let match; | ||
while ((match = CSS_URL_RE.exec(template))) { | ||
let captured = match[1].trim(); | ||
if (!captured) | ||
continue; | ||
if (!TEST_URL.test(captured)) { | ||
captured = window.location.origin + captured; | ||
} | ||
cssURLs.push(captured); | ||
} | ||
while ((match = STYLE_RE.exec(template))) { | ||
const captured = match[1].trim(); | ||
if (!captured) | ||
continue; | ||
styles.push(captured); | ||
} | ||
return { | ||
cssURLs, | ||
styles | ||
}; | ||
} | ||
function loadBody(template) { | ||
var _a, _b; | ||
let bodyContent = (_b = (_a = template.match(BODY_CONTENT_RE)) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : ''; | ||
bodyContent = bodyContent.replace(SCRIPT_ANY_RE, scriptReplacer); | ||
const body = document.createElement('template'); | ||
body.innerHTML = bodyContent; | ||
return body; | ||
function scriptReplacer(substring) { | ||
const matchedURL = SCRIPT_URL_RE.exec(substring); | ||
if (matchedURL) { | ||
return `<!-- ${REPLACED_BY_BERIAL} Original script url: ${matchedURL[1]} -->`; | ||
} | ||
return `<!-- ${REPLACED_BY_BERIAL} Original script: inline script -->`; | ||
} | ||
} | ||
@@ -348,14 +347,7 @@ var Status; | ||
})(Status || (Status = {})); | ||
let started = false; | ||
const apps = new Set(); | ||
function register(name, url, match) { | ||
apps.add({ | ||
name, | ||
url, | ||
match, | ||
status: Status.NOT_LOADED | ||
}); | ||
} | ||
function start() { | ||
started = true; | ||
let apps = []; | ||
function register(appArray) { | ||
appArray.forEach((app) => (app.status = Status.NOT_LOADED)); | ||
apps = appArray; | ||
hack(); | ||
reroute(); | ||
@@ -365,8 +357,3 @@ } | ||
const { loads, mounts, unmounts } = getAppChanges(); | ||
return started ? perform() : init(); | ||
function init() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
yield Promise.all(loads.map(runLoad)); | ||
}); | ||
} | ||
perform(); | ||
function perform() { | ||
@@ -392,3 +379,3 @@ return __awaiter(this, void 0, void 0, function* () { | ||
apps.forEach((app) => { | ||
const isActive = app.match(window.location); | ||
const isActive = app.path(window.location); | ||
switch (app.status) { | ||
@@ -406,2 +393,3 @@ case Status.NOT_LOADED: | ||
!isActive && unmounts.push(app); | ||
break; | ||
} | ||
@@ -423,12 +411,9 @@ }); | ||
let mixinLife = mapMixin(); | ||
app.host = (yield loadShadowDOM(app)); | ||
const { lifecycle: selfLife, bodyNode, styleNodes } = yield importHtml(app); | ||
lifecycleCheck(selfLife); | ||
(_a = app.host.shadowRoot) === null || _a === void 0 ? void 0 : _a.appendChild(bodyNode.content.cloneNode(true)); | ||
for (const k of styleNodes) | ||
app.host.shadowRoot.insertBefore(k, app.host.shadowRoot.firstChild); | ||
app.host = yield loadShadowDOM(app); | ||
const { dom, lifecycles } = yield importHtml(app); | ||
(_a = app.host) === null || _a === void 0 ? void 0 : _a.appendChild(dom); | ||
app.status = Status.NOT_BOOTSTRAPPED; | ||
app.bootstrap = compose(mixinLife.bootstrap.concat(selfLife.bootstrap)); | ||
app.mount = compose(mixinLife.mount.concat(selfLife.mount)); | ||
app.unmount = compose(mixinLife.unmount.concat(selfLife.unmount)); | ||
app.bootstrap = compose(mixinLife.bootstrap.concat(lifecycles.bootstrap)); | ||
app.mount = compose(mixinLife.mount.concat(lifecycles.mount)); | ||
app.unmount = compose(mixinLife.unmount.concat(lifecycles.unmount)); | ||
delete app.loaded; | ||
@@ -441,21 +426,16 @@ return app; | ||
function loadShadowDOM(app) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return new Promise((resolve) => { | ||
class Berial extends HTMLElement { | ||
static get tag() { | ||
return app.name; | ||
} | ||
connectedCallback() { | ||
resolve(this); | ||
} | ||
constructor() { | ||
super(); | ||
this.attachShadow({ mode: 'open' }); | ||
} | ||
return new Promise((resolve, reject) => { | ||
class Berial extends HTMLElement { | ||
static get tag() { | ||
return app.name; | ||
} | ||
const hasDef = window.customElements.get(app.name); | ||
if (!hasDef) { | ||
customElements.define(app.name, Berial); | ||
constructor() { | ||
super(); | ||
resolve(this.attachShadow({ mode: 'open' })); | ||
} | ||
}); | ||
} | ||
const hasDef = window.customElements.get(app.name); | ||
if (!hasDef) { | ||
customElements.define(app.name, Berial); | ||
} | ||
}); | ||
@@ -496,2 +476,10 @@ } | ||
} | ||
function hack() { | ||
window.addEventListener = hackEventListener(window.addEventListener); | ||
window.removeEventListener = hackEventListener(window.removeEventListener); | ||
window.history.pushState = hackHistory(window.history.pushState); | ||
window.history.replaceState = hackHistory(window.history.replaceState); | ||
window.addEventListener('hashchange', reroute); | ||
window.addEventListener('popstate', reroute); | ||
} | ||
const captured = { | ||
@@ -501,50 +489,33 @@ hashchange: [], | ||
}; | ||
window.addEventListener('hashchange', reroute); | ||
window.addEventListener('popstate', reroute); | ||
const oldAEL = window.addEventListener; | ||
const oldREL = window.removeEventListener; | ||
window.addEventListener = function (name, fn) { | ||
if ((name === 'hashchange' || name === 'popstate') && | ||
!captured[name].some((l) => l == fn)) { | ||
captured[name].push(fn); | ||
return; | ||
} | ||
return oldAEL.apply(this, arguments); | ||
}; | ||
window.removeEventListener = function (name, fn) { | ||
if (name === 'hashchange' || name === 'popstate') { | ||
captured[name] = captured[name].filter((l) => l !== fn); | ||
return; | ||
} | ||
return oldREL.apply(this, arguments); | ||
}; | ||
function polyfillHistory(fn) { | ||
function hackEventListener(func) { | ||
return function (name, fn) { | ||
if (name === 'hashchange' || name === 'popstate') { | ||
if (!captured[name].some((l) => l == fn)) { | ||
captured[name].push(fn); | ||
return; | ||
} | ||
else { | ||
captured[name] = captured[name].filter((l) => l !== fn); | ||
return; | ||
} | ||
} | ||
return func.apply(this, arguments); | ||
}; | ||
} | ||
function hackHistory(fn) { | ||
return function () { | ||
const before = window.location.href; | ||
fn.apply(this, arguments); | ||
fn.apply(window.history, arguments); | ||
const after = window.location.href; | ||
if (before !== after) { | ||
reroute(new PopStateEvent('popstate')); | ||
new PopStateEvent('popstate'); | ||
reroute(); | ||
} | ||
}; | ||
} | ||
window.history.pushState = polyfillHistory(window.history.pushState); | ||
window.history.replaceState = polyfillHistory(window.history.replaceState); | ||
const Berial = { | ||
start, | ||
register, | ||
importHtml, | ||
run, | ||
mixin, | ||
use | ||
}; | ||
exports.Berial = Berial; | ||
exports.default = Berial; | ||
exports.importHtml = importHtml; | ||
exports.mixin = mixin; | ||
exports.register = register; | ||
exports.run = run; | ||
exports.start = start; | ||
exports.runScript = runScript; | ||
exports.use = use; | ||
@@ -551,0 +522,0 @@ |
{ | ||
"name": "berial", | ||
"version": "0.0.7", | ||
"version": "0.0.8", | ||
"description": "micro frontend", | ||
@@ -10,4 +10,4 @@ "main": "dist/berial.js", | ||
"build": "rollup -c", | ||
"dev": "rollup -c --watch", | ||
"check": "run-p fmt-check lint", | ||
"dev": "rollup -c --watch", | ||
"fix": "run-s \"lint -- --fix\"", | ||
@@ -17,5 +17,6 @@ "fmt": "run-s \"fmt-check -- --write\"", | ||
"lint": "eslint **/*.ts", | ||
"serve": "serve ./demo/", | ||
"type": "tsc --project tsconfig.json --skipLibCheck --noEmit", | ||
"test": "karma start karma.conf.js" | ||
"demo:init": "cd example/parent && npm run install:all && cd ../..", | ||
"demo:serve": "npm run build && cd example/parent && npm start && cd ../..", | ||
"demo:build": "npm run build && cd example/parent && npm run build:all" | ||
}, | ||
@@ -34,10 +35,3 @@ "repository": { | ||
"devDependencies": { | ||
"@rollup/plugin-replace": "^2.3.3", | ||
"@types/chai": "^4.2.12", | ||
"@types/mocha": "^8.0.3", | ||
"@types/node": "^10.12.24", | ||
"@types/sinon": "^9.0.5", | ||
"@typescript-eslint/eslint-plugin": "^3.7.0", | ||
"@typescript-eslint/parser": "^3.7.0", | ||
"chai": "^4.2.0", | ||
"cross-env": "^7.0.2", | ||
"eslint": "^7.5.0", | ||
@@ -47,24 +41,20 @@ "eslint-config-prettier": "^6.11.0", | ||
"eslint-plugin-react": "^7.20.5", | ||
"http-server": "^0.12.3", | ||
"husky": "^4.2.5", | ||
"karma": "^5.1.1", | ||
"karma-chrome-launcher": "^3.1.0", | ||
"karma-mocha": "^2.0.1", | ||
"karma-spec-reporter": "0.0.32", | ||
"karma-typescript": "^5.1.0", | ||
"mocha": "^8.1.1", | ||
"npm-run-all": "^4.1.5", | ||
"prettier": "^2.0.5", | ||
"puppeteer": "^5.2.1", | ||
"rollup": "^2.23.0", | ||
"rollup-plugin-dts": "^1.4.11", | ||
"rollup-plugin-typescript2": "^0.27.1", | ||
"serve": "^11.3.2", | ||
"sinon": "^9.0.3", | ||
"tslib": "^2.0.0", | ||
"typescript": "^3.9.7" | ||
"typescript": "^3.9.7", | ||
"@typescript-eslint/eslint-plugin": "^3.7.0", | ||
"@typescript-eslint/parser": "^3.7.0" | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "run-p fmt fix;" | ||
"pre-commit": "npm run fmt && npm run fix;" | ||
} | ||
} | ||
} |
@@ -8,3 +8,3 @@ import { mixin } from 'berial' | ||
async function boostrap(app): Promise<void> { | ||
const shadowRoot = app.host.shadowRoot | ||
const shadowRoot = app.host | ||
const define = Object.defineProperty | ||
@@ -17,3 +17,2 @@ const fromNode = shadowRoot, | ||
const Event = fromEvent.constructor | ||
// @ts-ignore | ||
const toEvent = new Event(eventName, { | ||
@@ -20,0 +19,0 @@ ...fromEvent, |
@@ -10,16 +10,10 @@ <p align="center"><img src="https://avatars0.githubusercontent.com/u/68577605?s=200&v=4" alt="berial logo" width="150"></p> | ||
### Feature | ||
### Why Berial | ||
- lifecycle loop | ||
Berial is a new approach to a popular idea: build a javascript framework for front-end microservices. | ||
- shadow dom | ||
There are any wonderful features of it, such as Asynchronous rendering pipeline, Web components (shadow DOM + scoped css), JavaScript sandbox (Proxy). | ||
- scoped css | ||
Note: diffence form fre, Berial will pay attention to business value. | ||
- proxy sandbox | ||
- html loader | ||
- mixins | ||
### Use | ||
@@ -29,38 +23,19 @@ | ||
<one-app></one-app> | ||
<two-app></two-app> | ||
``` | ||
```js | ||
import { register, start } from 'berial' | ||
register( | ||
'one-app', | ||
'http://localhost:3000/one.html', | ||
(location) => location.hash === '#/app1' | ||
) | ||
register( | ||
'two-app', | ||
'http://localhost:3000/two.html', | ||
(location) => location.hash === '#/app2' | ||
) | ||
start() | ||
<script type="module"> | ||
import { register } from 'berial' | ||
register([{ | ||
name: 'one-app' | ||
url: '1.html' | ||
allowList: {} // 沙箱白名单 | ||
},{ | ||
name: 'two-app' | ||
scripts: ['2.js'] // 可选 | ||
}]) | ||
</script> | ||
``` | ||
### mixins | ||
```js | ||
import { mixin } from 'berial' | ||
mixin({ | ||
bootstrap: () => {}, | ||
mount: () => {}, | ||
unmount: () => {} | ||
}) | ||
``` | ||
mixins will apply all apps | ||
### License | ||
MIT ©yisar ©h-a-n-a |
import typescript from 'rollup-plugin-typescript2' | ||
import replace from '@rollup/plugin-replace' | ||
@@ -7,9 +6,9 @@ export default { | ||
output: [ | ||
// { | ||
// file: 'dist/es/berial.esm.js', | ||
// format: 'esm', | ||
// sourcemap: true, | ||
// name: 'berial' | ||
// }, | ||
{ file: 'dist/berial.d.ts', format: 'esm', exports: 'named' }, | ||
{ | ||
file: 'dist/berial.esm.js', | ||
format: 'esm', | ||
sourcemap: true | ||
}, | ||
{ | ||
file: 'dist/berial.js', | ||
@@ -22,11 +21,8 @@ format: 'umd', | ||
plugins: [ | ||
replace({ | ||
__DEV__: process.env.NODE_ENV !== 'production' | ||
}), | ||
typescript({ | ||
tsconfig: 'tsconfig.json', | ||
removeComments: true, | ||
useTsconfigDeclarationDir: true | ||
}) | ||
useTsconfigDeclarationDir: true, | ||
}), | ||
] | ||
} |
@@ -1,164 +0,192 @@ | ||
import type { App, PromiseFn, Lifecycles, ProxyType } from './types' | ||
import { run } from './sandbox' | ||
import { request } from './util' | ||
import { runScript } from './sandbox' | ||
const MATCH_ANY_OR_NO_PROPERTY = /["'=\w\s\/]*/ | ||
const SCRIPT_URL_RE = new RegExp( | ||
'<\\s*script' + | ||
MATCH_ANY_OR_NO_PROPERTY.source + | ||
'(?:src="(.+?)")' + | ||
MATCH_ANY_OR_NO_PROPERTY.source + | ||
'(?:\\/>|>[\\s]*<\\s*\\/script>)?', | ||
'g' | ||
) | ||
const SCRIPT_CONTENT_RE = new RegExp( | ||
'<\\s*script' + | ||
MATCH_ANY_OR_NO_PROPERTY.source + | ||
'>([\\w\\W]+?)<\\s*\\/script>', | ||
'g' | ||
) | ||
const MATCH_NONE_QUOTE_MARK = /[^"]/ | ||
const CSS_URL_RE = new RegExp( | ||
'<\\s*link[^>]*' + | ||
'href="(' + | ||
MATCH_NONE_QUOTE_MARK.source + | ||
'+.css' + | ||
MATCH_NONE_QUOTE_MARK.source + | ||
'*)"' + | ||
MATCH_ANY_OR_NO_PROPERTY.source + | ||
'>(?:\\s*<\\s*\\/link>)?', | ||
'g' | ||
) | ||
const STYLE_RE = /<\s*style\s*>([^<]*)<\s*\/style>/g | ||
const BODY_CONTENT_RE = /<\s*body[^>]*>([\w\W]*)<\s*\/body>/ | ||
const SCRIPT_ANY_RE = /<\s*script[^>]*>[\s\S]*?(<\s*\/script[^>]*>)/g | ||
const TEST_URL = /^(?:https?):\/\/[-a-zA-Z0-9.]+/ | ||
const ALL_SCRIPT_REGEX = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi | ||
const SCRIPT_TAG_REGEX = /<(script)\s+((?!type=('|')text\/ng-template\3).)*?>.*?<\/\1>/is | ||
const SCRIPT_SRC_REGEX = /.*\ssrc=('|")?([^>'"\s]+)/ | ||
const SCRIPT_ENTRY_REGEX = /.*\sentry\s*.*/ | ||
const LINK_TAG_REGEX = /<(link)\s+.*?>/gi | ||
const LINK_IGNORE_REGEX = /.*ignore\s*.*/ | ||
const LINK_PRELOAD_OR_PREFETCH_REGEX = /\srel=('|")?(preload|prefetch)\1/ | ||
const LINK_HREF_REGEX = /.*\shref=('|")?([^>'"\s]+)/ | ||
const STYLE_TAG_REGEX = /<style[^>]*>[\s\S]*?<\/style>/gi | ||
const STYLE_TYPE_REGEX = /\s+rel=('|")?stylesheet\1.*/ | ||
const STYLE_HREF_REGEX = /.*\shref=('|")?([^>'"\s]+)/ | ||
const STYLE_IGNORE_REGEX = /<style(\s+|\s+.+\s+)ignore(\s*|\s+.*)>/i | ||
const HTML_COMMENT_REGEX = /<!--([\s\S]*?)-->/g | ||
const SCRIPT_IGNORE_REGEX = /<script(\s+|\s+.+\s+)ignore(\s*|\s+.*)>/i | ||
const REPLACED_BY_BERIAL = 'Script replaced by Berial.' | ||
export async function importHtml( | ||
app: App | ||
): Promise<{ | ||
lifecycle: Lifecycles | ||
styleNodes: HTMLStyleElement[] | ||
bodyNode: HTMLTemplateElement | ||
}> { | ||
const template = await request(app.url as string) | ||
const styleNodes = await loadCSS(template) | ||
const bodyNode = loadBody(template) | ||
const lifecycle = await loadScript(template, app.name) | ||
return { lifecycle, styleNodes, bodyNode } | ||
export function getInlineCode(match: any): string { | ||
const start = match.indexOf('>') + 1 | ||
const end = match.lastIndexOf('<') | ||
return match.substring(start, end) | ||
} | ||
export async function loadScript( | ||
template: string, | ||
name: string | ||
): Promise<Lifecycles> { | ||
const { scriptURLs, scripts } = parseScript(template) | ||
const fetchedScripts = await Promise.all( | ||
scriptURLs.map((url) => request(url)) | ||
function hasProtocol(url: string): any { | ||
return ( | ||
url.startsWith('//') || | ||
url.startsWith('http://') || | ||
url.startsWith('https://') | ||
) | ||
const scriptsToLoad = fetchedScripts.concat(scripts) | ||
let bootstrap: PromiseFn[] = [] | ||
let unmount: PromiseFn[] = [] | ||
let mount: PromiseFn[] = [] | ||
scriptsToLoad.forEach((script) => { | ||
const lifecycles = run(script, {})[name] | ||
bootstrap = [...bootstrap, lifecycles.bootstrap] | ||
mount = [...mount, lifecycles.mount] | ||
unmount = [...unmount, lifecycles.unmount] | ||
}) | ||
return { bootstrap, unmount, mount } | ||
} | ||
function parseScript( | ||
template: string | ||
): { | ||
scriptURLs: string[] | ||
scripts: string[] | ||
} { | ||
const scriptURLs: string[] = [] | ||
const scripts: string[] = [] | ||
SCRIPT_URL_RE.lastIndex = SCRIPT_CONTENT_RE.lastIndex = 0 | ||
let match | ||
while ((match = SCRIPT_URL_RE.exec(template))) { | ||
let captured = match[1].trim() | ||
if (!captured) continue | ||
if (!TEST_URL.test(captured)) { | ||
captured = window.location.origin + captured | ||
} | ||
scriptURLs.push(captured) | ||
} | ||
while ((match = SCRIPT_CONTENT_RE.exec(template))) { | ||
const captured = match[1].trim() | ||
if (!captured) continue | ||
scripts.push(captured) | ||
} | ||
return { | ||
scriptURLs, | ||
scripts | ||
} | ||
function getEntirePath(path: string, baseURI: string): string { | ||
return new URL(path, baseURI).toString() | ||
} | ||
async function loadCSS(template: string): Promise<HTMLStyleElement[]> { | ||
const { cssURLs, styles } = parseCSS(template) | ||
const fetchedStyles = await Promise.all(cssURLs.map((url) => request(url))) | ||
return toStyleNodes(fetchedStyles.concat(styles)) | ||
export const genLinkReplaceSymbol = (linkHref: any): string => | ||
`<!-- link ${linkHref} replaced by import-html-entry -->` | ||
export const genScriptReplaceSymbol = (scriptSrc: any): string => | ||
`<!-- script ${scriptSrc} replaced by import-html-entry -->` | ||
export const inlineScriptReplaceSymbol = `<!-- inline scripts replaced by import-html-entry -->` | ||
export const genIgnoreAssetReplaceSymbol = (url: any): string => | ||
`<!-- ignore asset ${url || 'file'} replaced by import-html-entry -->` | ||
function toStyleNodes(styles: string[]): HTMLStyleElement[] { | ||
return styles.map((style) => { | ||
const styleNode = document.createElement('style') | ||
styleNode.appendChild(document.createTextNode(style)) | ||
return styleNode | ||
export function parse(tpl: string, baseURI: string): any { | ||
let scripts: string[] = [] | ||
const styles: string[] = [] | ||
let entry: any = null | ||
const template = tpl | ||
.replace(HTML_COMMENT_REGEX, '') | ||
.replace(LINK_TAG_REGEX, (match) => { | ||
const styleType = !!match.match(STYLE_TYPE_REGEX) | ||
console.log(styleType) | ||
if (styleType) { | ||
const styleHref = match.match(STYLE_HREF_REGEX) | ||
const styleIgnore = match.match(LINK_IGNORE_REGEX) | ||
if (styleHref) { | ||
const href = styleHref && styleHref[2] | ||
let newHref = href | ||
if (href && !hasProtocol(href)) { | ||
newHref = getEntirePath(href, baseURI) | ||
} | ||
if (styleIgnore) { | ||
return genIgnoreAssetReplaceSymbol(newHref) | ||
} | ||
styles.push(newHref) | ||
return genLinkReplaceSymbol(newHref) | ||
} | ||
} | ||
const preloadOrPrefetchType = !!match.match( | ||
LINK_PRELOAD_OR_PREFETCH_REGEX | ||
) | ||
if (preloadOrPrefetchType) { | ||
const linkHref = match.match(LINK_HREF_REGEX) | ||
if (linkHref) { | ||
const href = linkHref[2] | ||
if (href && !hasProtocol(href)) { | ||
const newHref = getEntirePath(href, baseURI) | ||
return match.replace(href, newHref) | ||
} | ||
} | ||
} | ||
return match | ||
}) | ||
.replace(STYLE_TAG_REGEX, (match) => { | ||
if (STYLE_IGNORE_REGEX.test(match)) { | ||
return genIgnoreAssetReplaceSymbol('style file') | ||
} | ||
return match | ||
}) | ||
.replace(ALL_SCRIPT_REGEX, (match) => { | ||
const scriptIgnore = match.match(SCRIPT_IGNORE_REGEX) | ||
if (SCRIPT_TAG_REGEX.test(match) && match.match(SCRIPT_SRC_REGEX)) { | ||
const matchedScriptEntry = match.match(SCRIPT_ENTRY_REGEX) | ||
const matchedScriptSrcMatch = match.match(SCRIPT_SRC_REGEX) | ||
let matchedScriptSrc = matchedScriptSrcMatch && matchedScriptSrcMatch[2] | ||
if (entry && matchedScriptEntry) { | ||
throw new SyntaxError('You should not set multiply entry script!') | ||
} else { | ||
if (matchedScriptSrc && !hasProtocol(matchedScriptSrc)) { | ||
matchedScriptSrc = getEntirePath(matchedScriptSrc, baseURI) | ||
} | ||
entry = entry || (matchedScriptEntry && matchedScriptSrc) | ||
} | ||
if (scriptIgnore) { | ||
return genIgnoreAssetReplaceSymbol(matchedScriptSrc || 'js file') | ||
} | ||
if (matchedScriptSrc) { | ||
scripts.push(matchedScriptSrc) | ||
return genScriptReplaceSymbol(matchedScriptSrc) | ||
} | ||
return match | ||
} else { | ||
if (scriptIgnore) { | ||
return genIgnoreAssetReplaceSymbol('js file') | ||
} | ||
const code = getInlineCode(match) | ||
const isPureCommentBlock = code | ||
.split(/[\r\n]+/) | ||
.every((line) => !line.trim() || line.trim().startsWith('//')) | ||
if (!isPureCommentBlock) { | ||
scripts.push(match) | ||
} | ||
return inlineScriptReplaceSymbol | ||
} | ||
}) | ||
scripts = scripts.filter((s: string) => !!s) | ||
return { | ||
template, | ||
scripts, | ||
styles, | ||
entry: entry || scripts[scripts.length - 1] | ||
} | ||
} | ||
function parseCSS( | ||
template: string | ||
): { | ||
cssURLs: string[] | ||
styles: string[] | ||
} { | ||
const cssURLs: string[] = [] | ||
const styles: string[] = [] | ||
CSS_URL_RE.lastIndex = STYLE_RE.lastIndex = 0 | ||
let match | ||
while ((match = CSS_URL_RE.exec(template))) { | ||
let captured = match[1].trim() | ||
if (!captured) continue | ||
if (!TEST_URL.test(captured)) { | ||
captured = window.location.origin + captured | ||
} | ||
cssURLs.push(captured) | ||
} | ||
while ((match = STYLE_RE.exec(template))) { | ||
const captured = match[1].trim() | ||
if (!captured) continue | ||
styles.push(captured) | ||
} | ||
return { | ||
cssURLs, | ||
export async function importHtml(app: any): Promise<any> { | ||
let template = '', | ||
scripts, | ||
styles | ||
if (app.scripts) { | ||
scripts = app.scripts || [] | ||
styles = app.styles || [] | ||
} else { | ||
const tpl = await request(app.url as string) | ||
let res = parse(tpl, '') | ||
scripts = res.scripts | ||
styles = res.styles | ||
template = res.template | ||
} | ||
} | ||
function loadBody(template: string): HTMLTemplateElement { | ||
let bodyContent = template.match(BODY_CONTENT_RE)?.[1] ?? '' | ||
bodyContent = bodyContent.replace(SCRIPT_ANY_RE, scriptReplacer) | ||
scripts = await Promise.all( | ||
scripts.map((s: string) => | ||
hasProtocol(s) | ||
? request(s) | ||
: s.endsWith('.js') | ||
? request(window.origin + s) | ||
: s | ||
) | ||
) | ||
styles = styles.map((s: string) => | ||
hasProtocol(s) || s.endsWith('.css') ? `<link rel="stylesheet" href="${s}" ></link>` : `<style>${s}<style>` | ||
) | ||
template = template | ||
let lifecycles = null | ||
scripts.forEach(async (script: any) => { | ||
lifecycles = runScript(script, app.allowList)[app.name] | ||
}) | ||
const dom = document.createDocumentFragment() | ||
const body = document.createElement('template') | ||
body.innerHTML = bodyContent | ||
return body | ||
function scriptReplacer(substring: string): string { | ||
const matchedURL = SCRIPT_URL_RE.exec(substring) | ||
if (matchedURL) { | ||
return `<!-- ${REPLACED_BY_BERIAL} Original script url: ${matchedURL[1]} -->` | ||
} | ||
return `<!-- ${REPLACED_BY_BERIAL} Original script: inline script -->` | ||
} | ||
let out = '' | ||
styles.forEach((s: string) => (out += s)) | ||
out += template | ||
body.innerHTML = out | ||
dom.appendChild(body.content.cloneNode(true)) | ||
return { dom, lifecycles } | ||
} |
@@ -1,17 +0,6 @@ | ||
import { start, register } from './app' | ||
import { register } from './entity' | ||
import { mixin, use } from './mixin' | ||
import { importHtml } from './html-loader' | ||
import { run } from './sandbox' | ||
import { runScript } from './sandbox' | ||
export const Berial = { | ||
start, | ||
register, | ||
importHtml, | ||
run, | ||
mixin, | ||
use | ||
} | ||
export default Berial | ||
export { start, register, importHtml, run, mixin, use } | ||
export { register, importHtml, runScript, mixin, use } |
@@ -1,14 +0,14 @@ | ||
import type { Lifecycles } from './types' | ||
import type { Lifecycles, Mixin, Plugin } from './types' | ||
const mixins: any = new Set() | ||
const plugins: any = new Set() | ||
const mixins: Set<Mixin> = new Set() | ||
const plugins: Set<Plugin> = new Set() | ||
export function use(plugin: (args: any) => any, ...args: any): void { | ||
export function use(plugin: Plugin, ...args: any[]): void { | ||
if (!plugins.has(plugin)) { | ||
plugins.add(plugin) | ||
plugin(args) | ||
plugin(...args) | ||
} | ||
} | ||
export function mixin(mix: any): void { | ||
export function mixin(mix: Mixin): void { | ||
if (!mixins.has(mix)) { | ||
@@ -20,3 +20,3 @@ mixins.add(mix) | ||
export function mapMixin(): Lifecycles { | ||
const out: any = { | ||
const out: Lifecycles = { | ||
load: [], | ||
@@ -27,4 +27,4 @@ bootstrap: [], | ||
} | ||
mixins.forEach((item: any) => { | ||
item.load && out.load.push(item.load) | ||
mixins.forEach((item: Mixin) => { | ||
item.load && out.load!.push(item.load) | ||
item.bootstrap && out.bootstrap.push(item.bootstrap) | ||
@@ -31,0 +31,0 @@ item.mount && out.mount.push(item.mount) |
@@ -1,125 +0,123 @@ | ||
export function run(code: string, options: any = {}): any { | ||
export function runScript(code: string, allow: any = {}): any { | ||
try { | ||
if (checkSyntax(code)) { | ||
const handler = { | ||
get(obj: any, prop: string): any { | ||
return Reflect.has(obj, prop) ? obj[prop] : null | ||
}, | ||
set(obj: any, prop: string, value: any): boolean { | ||
Reflect.set(obj, prop, value) | ||
return true | ||
}, | ||
has(obj: any, prop: string): boolean { | ||
return obj && Reflect.has(obj, prop) | ||
} | ||
const handler = { | ||
get(obj: any, prop: string): any { | ||
return Reflect.has(obj, prop) ? obj[prop] : null | ||
}, | ||
set(obj: any, prop: string, value: any): boolean { | ||
Reflect.set(obj, prop, value) | ||
return true | ||
}, | ||
has(obj: any, prop: string): boolean { | ||
return obj && Reflect.has(obj, prop) | ||
} | ||
const captureHandler = { | ||
get(obj: any, prop: string): any { | ||
return Reflect.get(obj, prop) | ||
}, | ||
set(): boolean { | ||
return true | ||
}, | ||
has(): boolean { | ||
return true | ||
} | ||
} | ||
const captureHandler = { | ||
get(obj: any, prop: string): any { | ||
return Reflect.get(obj, prop) | ||
}, | ||
set(): boolean { | ||
return true | ||
}, | ||
has(): boolean { | ||
return true | ||
} | ||
} | ||
const allowList = { | ||
IS_BERIAL_SANDBOX: true, | ||
__proto__: null, | ||
console, | ||
String, | ||
Number, | ||
Array, | ||
Symbol, | ||
Math, | ||
Object, | ||
Promise, | ||
RegExp, | ||
JSON, | ||
Date, | ||
Function, | ||
parseInt, | ||
document, | ||
navigator, | ||
location, | ||
performance, | ||
MessageChannel, | ||
SVGElement, | ||
HTMLElement, | ||
HTMLIFrameElement, | ||
history, | ||
Map, | ||
Set, | ||
WeakMap, | ||
WeakSet, | ||
Error, | ||
localStorage, | ||
decodeURI, | ||
encodeURI, | ||
decodeURIComponent, | ||
encodeURIComponent, | ||
fetch: fetch.bind(window), | ||
setTimeout: setTimeout.bind(window), | ||
clearTimeout: clearTimeout.bind(window), | ||
setInterval: setInterval.bind(window), | ||
clearInterval: clearInterval.bind(window), | ||
requestAnimationFrame: requestAnimationFrame.bind(window), | ||
cancelAnimationFrame: cancelAnimationFrame.bind(window), | ||
addEventListener: addEventListener.bind(window), | ||
removeEventListener: removeEventListener.bind(window), | ||
// eslint-disable-next-line no-shadow | ||
eval: function (code: string): any { | ||
return run('return ' + code, null) | ||
}, | ||
alert: function (): void { | ||
alert('Sandboxed alert:' + arguments[0]) | ||
}, | ||
// position related properties | ||
innerHeight, | ||
innerWidth, | ||
outerHeight, | ||
outerWidth, | ||
pageXOffset, | ||
pageYOffset, | ||
screen, | ||
screenLeft, | ||
screenTop, | ||
screenX, | ||
screenY, | ||
scrollBy, | ||
scrollTo, | ||
scrollX, | ||
scrollY, | ||
// custom allow list | ||
...(options.allowList || {}) | ||
} | ||
const allowList = { | ||
IS_BERIAL_SANDBOX: true, | ||
__proto__: null, | ||
console, | ||
String, | ||
Number, | ||
Array, | ||
Symbol, | ||
Math, | ||
Object, | ||
Promise, | ||
RegExp, | ||
JSON, | ||
Date, | ||
Function, | ||
parseInt, | ||
document, | ||
location, | ||
performance, | ||
MessageChannel, | ||
SVGElement, | ||
HTMLElement, | ||
HTMLIFrameElement, | ||
history, | ||
Map, | ||
Set, | ||
WeakMap, | ||
WeakSet, | ||
Error, | ||
localStorage, | ||
decodeURI, | ||
encodeURI, | ||
decodeURIComponent, | ||
encodeURIComponent, | ||
fetch: fetch.bind(window), | ||
setTimeout: setTimeout.bind(window), | ||
clearTimeout: clearTimeout.bind(window), | ||
setInterval: setInterval.bind(window), | ||
clearInterval: clearInterval.bind(window), | ||
requestAnimationFrame: requestAnimationFrame.bind(window), | ||
cancelAnimationFrame: cancelAnimationFrame.bind(window), | ||
addEventListener: addEventListener.bind(window), | ||
removeEventListener: removeEventListener.bind(window), | ||
// eslint-disable-next-line no-shadow | ||
eval: function (code: string): any { | ||
return runScript('return ' + code, {}) | ||
}, | ||
alert: function (): void { | ||
alert('Sandboxed alert:' + arguments[0]) | ||
}, | ||
// position related properties | ||
innerHeight, | ||
innerWidth, | ||
outerHeight, | ||
outerWidth, | ||
pageXOffset, | ||
pageYOffset, | ||
screen, | ||
screenLeft, | ||
screenTop, | ||
screenX, | ||
screenY, | ||
scrollBy, | ||
scrollTo, | ||
scrollX, | ||
scrollY, | ||
// custom allow list | ||
...allow | ||
} | ||
if (!Object.isFrozen(String.prototype)) { | ||
for (const k in allowList) { | ||
const fn = allowList[k] | ||
if (typeof fn === 'object' && fn.prototype) { | ||
Object.freeze(fn.prototype) | ||
} | ||
if (typeof fn === 'function') { | ||
Object.freeze(fn) | ||
} | ||
if (!Object.isFrozen(String.prototype)) { | ||
for (const k in allowList) { | ||
const fn = allowList[k] | ||
if (typeof fn === 'object' && fn.prototype) { | ||
Object.freeze(fn.prototype) | ||
} | ||
if (typeof fn === 'function') { | ||
Object.freeze(fn) | ||
} | ||
} | ||
const proxy = new Proxy(allowList, handler) | ||
const capture = new Proxy( | ||
{ | ||
__proto__: null, | ||
proxy, | ||
globalThis: new Proxy(allowList, handler), | ||
window: new Proxy(allowList, handler), | ||
self: new Proxy(allowList, handler) | ||
}, | ||
captureHandler | ||
) | ||
return Function( | ||
'proxy', | ||
'capture', | ||
`with(capture) { | ||
} | ||
const proxy = new Proxy(allowList, handler) | ||
const capture = new Proxy( | ||
{ | ||
__proto__: null, | ||
proxy, | ||
globalThis: new Proxy(allowList, handler), | ||
window: new Proxy(allowList, handler), | ||
self: new Proxy(allowList, handler) | ||
}, | ||
captureHandler | ||
) | ||
return Function( | ||
'proxy', | ||
'capture', | ||
`with(capture) { | ||
with(proxy) { | ||
@@ -133,4 +131,3 @@ return (function(){ | ||
}` | ||
)(proxy, capture) | ||
} | ||
)(proxy, capture) | ||
} catch (e) { | ||
@@ -140,8 +137,1 @@ throw e | ||
} | ||
function checkSyntax(code: string): boolean { | ||
Function(code) | ||
if (/\bimport\s*(?:[(]|\/[*]|\/\/|<!--|-->)/.test(code)) { | ||
throw new Error('Dynamic imports are blocked') | ||
} | ||
return true | ||
} |
@@ -1,2 +0,2 @@ | ||
import type { Status } from './app' | ||
import type { Status } from './entity' | ||
@@ -12,7 +12,17 @@ export type Lifecycles = ToArray<Lifecycle> | ||
export type Mixin = { | ||
load?: PromiseFn | ||
mount?: PromiseFn | ||
unmount?: PromiseFn | ||
bootstrap?: PromiseFn | ||
} | ||
export type Plugin = (...args: any[]) => any | ||
export type App = { | ||
name: string | ||
node: HTMLElement | ||
url: ((props: App['props']) => Lifecycle) | string | ||
match: (location: Location) => boolean | ||
host: HTMLElement | ||
host: DocumentFragment | ||
props: Record<string, unknown> | ||
@@ -19,0 +29,0 @@ status: Status |
@@ -20,9 +20,2 @@ import type { Lifecycle, Lifecycles } from './types' | ||
export function request(url: string, option?: RequestInit): Promise<string> { | ||
console.log(url) | ||
if (!window.fetch) { | ||
error( | ||
"It looks like that your browser doesn't support fetch. Polyfill is needed before you use it." | ||
) | ||
} | ||
return fetch(url, { | ||
@@ -34,15 +27,4 @@ mode: 'cors', | ||
export function lifecycleCheck(lifecycle: Lifecycle | Lifecycles): void { | ||
const keys = ['bootstrap', 'mount', 'unmount'] | ||
keys.forEach((key) => { | ||
if (!(key in lifecycle)) { | ||
error( | ||
`It looks like that you didn't export the lifecycle hook [${key}], which would cause a mistake.` | ||
) | ||
} | ||
}) | ||
} | ||
export function reverse<T>(arr: T[]): T[] { | ||
return Array.from(arr).reverse() | ||
} |
@@ -5,6 +5,4 @@ { | ||
"sourceMap": true, | ||
"declaration": true, | ||
"declarationDir": "dist/types/", | ||
"noImplicitAny": true, | ||
"removeComments":true, | ||
"removeComments": true, | ||
"target": "es6", | ||
@@ -16,6 +14,10 @@ "module": "es6", | ||
"downlevelIteration": true, | ||
"noImplicitThis": false, | ||
"jsx": "react", | ||
"lib": ["es6", "dom"], | ||
"baseUrl": ".", | ||
"outDir": "./dist" | ||
"outDir": "./dist", | ||
"paths": { | ||
"@/*": ["./src/*"] | ||
} | ||
}, | ||
@@ -22,0 +24,0 @@ "include": ["src/**/*"], |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
149891
51.46%17
-39.29%2315
23.07%2
-71.43%32
-41.82%40
-38.46%2
100%