Comparing version 0.0.7 to 0.0.8
@@ -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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
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
149891
17
2315
1
32
40
5