Comparing version
@@ -37,3 +37,3 @@ (function (global, factory) { | ||
plugins.add(plugin); | ||
plugin(args); | ||
plugin(...args); | ||
} | ||
@@ -62,2 +62,3 @@ } | ||
let queue = []; | ||
function run(code, options = {}) { | ||
@@ -121,3 +122,3 @@ try { | ||
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); | ||
return run('return ' + code, {}); | ||
}, alert: function () { | ||
@@ -174,4 +175,28 @@ alert('Sandboxed alert:' + arguments[0]); | ||
} | ||
function observeDoucument(host) { | ||
new MutationObserver((mutations) => { | ||
mutations.forEach((m) => __awaiter(this, void 0, void 0, function* () { | ||
switch (m.type) { | ||
case 'childList': | ||
if (m.target !== host) { | ||
for (let i = 0; i < m.addedNodes.length; i++) { | ||
const node = m.addedNodes[i]; | ||
if (node instanceof HTMLScriptElement) { | ||
const src = node.getAttribute('src') || ''; | ||
queue.push(src); | ||
} | ||
else { | ||
host.appendChild(node); | ||
} | ||
} | ||
} | ||
break; | ||
} | ||
})); | ||
}).observe(document, { childList: true, subtree: true }); | ||
} | ||
function getcurrentQueue() { | ||
return queue; | ||
} | ||
function checkSyntax(code) { | ||
Function(code); | ||
if (/\bimport\s*(?:[(]|\/[*]|\/\/|<!--|-->)/.test(code)) { | ||
@@ -205,2 +230,5 @@ throw new Error('Dynamic imports are blocked'); | ||
} | ||
function reverse(arr) { | ||
return Array.from(arr).reverse(); | ||
} | ||
@@ -216,2 +244,3 @@ const MATCH_ANY_OR_NO_PROPERTY = /["'=\w\s\/]*/; | ||
'>([\\w\\W]+?)<\\s*\\/script>', 'g'); | ||
const SCRIPT_URL_OR_CONTENT_RE = new RegExp('(?:' + SCRIPT_URL_RE.source + ')|(?:' + SCRIPT_CONTENT_RE.source + ')', 'g'); | ||
const MATCH_NONE_QUOTE_MARK = /[^"]/; | ||
@@ -227,2 +256,3 @@ const CSS_URL_RE = new RegExp('<\\s*link[^>]*' + | ||
const STYLE_RE = /<\s*style\s*>([^<]*)<\s*\/style>/g; | ||
const CSS_URL_OR_STYLE_RE = new RegExp('(?:' + CSS_URL_RE.source + ')|(?:' + STYLE_RE.source + ')', 'g'); | ||
const BODY_CONTENT_RE = /<\s*body[^>]*>([\w\W]*)<\s*\/body>/; | ||
@@ -234,23 +264,46 @@ const SCRIPT_ANY_RE = /<\s*script[^>]*>[\s\S]*?(<\s*\/script[^>]*>)/g; | ||
return __awaiter(this, void 0, void 0, function* () { | ||
observeDoucument(app.host); | ||
const template = yield request(app.url); | ||
const styleNodes = yield loadCSS(template); | ||
const bodyNode = loadBody(template); | ||
const lifecycle = yield loadScript(template, app.name); | ||
const lifecycle = yield loadScript(template, app); | ||
return { lifecycle, styleNodes, bodyNode }; | ||
}); | ||
} | ||
function loadScript(template, name) { | ||
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]; | ||
}); | ||
function process(queue) { | ||
Promise.all(queue.map((v) => { | ||
if (TEST_URL.test(v)) | ||
return request(v); | ||
return v; | ||
})).then((q1) => { | ||
queue.length = 0; | ||
q1.forEach(getLyfecycles); | ||
const q2 = getcurrentQueue(); | ||
if (q2.length > 0) | ||
process(q2); | ||
}); | ||
} | ||
process(parseScript(template)); | ||
function getLyfecycles(script) { | ||
let lifecycles = run(script, {})[name]; | ||
if (lifecycles) { | ||
bootstrap = | ||
typeof lifecycles.bootstrap === 'function' | ||
? [...bootstrap, lifecycles.bootstrap] | ||
: bootstrap; | ||
mount = | ||
typeof lifecycles.mount === 'function' | ||
? [...mount, lifecycles.mount] | ||
: mount; | ||
unmount = | ||
typeof lifecycles.unmount === 'function' | ||
? [...unmount, lifecycles.unmount] | ||
: unmount; | ||
} | ||
} | ||
return { bootstrap, unmount, mount }; | ||
@@ -260,33 +313,30 @@ }); | ||
function parseScript(template) { | ||
const scriptURLs = []; | ||
const scripts = []; | ||
SCRIPT_URL_RE.lastIndex = SCRIPT_CONTENT_RE.lastIndex = 0; | ||
const scriptList = []; | ||
SCRIPT_URL_OR_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; | ||
while ((match = SCRIPT_URL_OR_CONTENT_RE.exec(template))) { | ||
let captured; | ||
if (match[1]) { | ||
captured = match[1].trim(); | ||
if (!TEST_URL.test(captured)) { | ||
captured = window.location.origin + captured; | ||
} | ||
} | ||
scriptURLs.push(captured); | ||
else if (match[2]) { | ||
captured = match[2].trim(); | ||
} | ||
captured && scriptList.push(captured); | ||
} | ||
while ((match = SCRIPT_CONTENT_RE.exec(template))) { | ||
const captured = match[1].trim(); | ||
if (!captured) | ||
continue; | ||
scripts.push(captured); | ||
} | ||
return { | ||
scriptURLs, | ||
scripts | ||
}; | ||
return scriptList; | ||
} | ||
function loadCSS(template) { | ||
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 styles = yield Promise.all(parseCSS(template).map((v) => { | ||
if (TEST_URL.test(v)) | ||
return request(v); | ||
return v; | ||
})); | ||
return toStyleNodes(styles); | ||
function toStyleNodes(s) { | ||
return s.map((style) => { | ||
const styleNode = document.createElement('style'); | ||
@@ -300,25 +350,19 @@ styleNode.appendChild(document.createTextNode(style)); | ||
function parseCSS(template) { | ||
const cssURLs = []; | ||
const styles = []; | ||
CSS_URL_RE.lastIndex = STYLE_RE.lastIndex = 0; | ||
const cssList = []; | ||
CSS_URL_OR_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; | ||
while ((match = CSS_URL_OR_STYLE_RE.exec(template))) { | ||
let captured; | ||
if (match[1]) { | ||
captured = match[1].trim(); | ||
if (!TEST_URL.test(captured)) { | ||
captured = window.location.origin + captured; | ||
} | ||
} | ||
cssURLs.push(captured); | ||
else if (match[2]) { | ||
captured = match[2].trim(); | ||
} | ||
captured && cssList.push(captured); | ||
} | ||
while ((match = STYLE_RE.exec(template))) { | ||
const captured = match[1].trim(); | ||
if (!captured) | ||
continue; | ||
styles.push(captured); | ||
} | ||
return { | ||
cssURLs, | ||
styles | ||
}; | ||
return cssList; | ||
} | ||
@@ -425,8 +469,8 @@ function loadBody(template) { | ||
let mixinLife = mapMixin(); | ||
app.host = (yield loadShadowDOM(app)); | ||
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); | ||
(_a = app.host) === null || _a === void 0 ? void 0 : _a.appendChild(bodyNode.content.cloneNode(true)); | ||
for (const k of reverse(styleNodes)) | ||
app.host.insertBefore(k, app.host.firstChild); | ||
app.status = Status.NOT_BOOTSTRAPPED; | ||
@@ -443,21 +487,16 @@ app.bootstrap = compose(mixinLife.bootstrap.concat(selfLife.bootstrap)); | ||
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); | ||
} | ||
}); | ||
@@ -524,6 +563,7 @@ } | ||
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(); | ||
} | ||
@@ -535,13 +575,2 @@ }; | ||
const Berial = { | ||
start, | ||
register, | ||
importHtml, | ||
run, | ||
mixin, | ||
use | ||
}; | ||
exports.Berial = Berial; | ||
exports.default = Berial; | ||
exports.importHtml = importHtml; | ||
@@ -548,0 +577,0 @@ exports.mixin = mixin; |
@@ -5,3 +5,3 @@ import { h, render,useEffect } from 'fre' | ||
useEffect(()=>{ | ||
document.title = '111' | ||
document.title = 'child-fre' | ||
}) | ||
@@ -24,3 +24,3 @@ return <div> | ||
console.log('fre mount') | ||
render(<App />, host.shadowRoot.getElementById('root')) | ||
render(<App />, host.getElementById('root')) | ||
} | ||
@@ -30,4 +30,4 @@ | ||
console.log('fre unmout') | ||
const root = host.shadowRoot.getElementById('root') | ||
const root = host.getElementById('root') | ||
root.innerHTML = '' | ||
} |
{ | ||
"scripts": { | ||
"build": "cross-env NODE_ENV=production webpack --mode production", | ||
"build:test": "cross-env NODE_ENV=test webpack --mode production", | ||
"start": "cross-env NODE_ENV=development webpack-dev-server --mode production" | ||
@@ -5,0 +6,0 @@ }, |
@@ -15,3 +15,3 @@ const path = require('path') | ||
process.env.NODE_ENV === 'production' | ||
? 'https://s-sh-16-clicli.oss.dogecdn.com/' | ||
? 'https://berial-child-fre.vercel.app' | ||
: 'http://localhost:3001' | ||
@@ -63,5 +63,5 @@ }, | ||
hot: true, | ||
inline: false, | ||
inline: false | ||
// lazy: true, | ||
} | ||
} |
@@ -16,3 +16,3 @@ import React from 'react' | ||
ReactDOM.render(<App />, host.shadowRoot.getElementById('root')) | ||
ReactDOM.render(<App />, host.getElementById('root')) | ||
} | ||
@@ -23,4 +23,4 @@ | ||
const root = host.shadowRoot.getElementById('root') | ||
const root = host.getElementById('root') | ||
ReactDOM.unmountComponentAtNode(root) | ||
} |
@@ -8,2 +8,3 @@ { | ||
"build": "cross-env NODE_ENV=production webpack --mode production", | ||
"build:test": "cross-env NODE_ENV=test webpack --mode production", | ||
"start": "cross-env NODE_ENV=development webpack-dev-server --mode production" | ||
@@ -10,0 +11,0 @@ }, |
@@ -15,3 +15,3 @@ const path = require('path') | ||
process.env.NODE_ENV === 'production' | ||
? 'https://s-sh-16-clicli.oss.dogecdn.com/' | ||
? 'https://berial-child-react.vercel.app' | ||
: 'http://localhost:3002' | ||
@@ -18,0 +18,0 @@ }, |
@@ -27,6 +27,3 @@ import Vue from 'vue' | ||
const appNode = host.shadowRoot | ||
.getElementById('root') | ||
.appendChild(document.createElement('div')) | ||
const appNode = host.getElementById('root').appendChild(document.createElement('div')) | ||
mountEl = new Vue({ | ||
@@ -43,4 +40,4 @@ el: appNode, | ||
mountEl.$destroy() | ||
const root = host.shadowRoot.getElementById('root') | ||
const root = host.getElementById('root') | ||
root.innerHTML = '' | ||
} |
@@ -8,2 +8,3 @@ { | ||
"build": "cross-env NODE_ENV=production webpack --mode production", | ||
"build:test": "cross-env NODE_ENV=test webpack --mode production", | ||
"start": "cross-env NODE_ENV=development webpack-dev-server --mode production" | ||
@@ -10,0 +11,0 @@ }, |
@@ -16,3 +16,3 @@ const path = require('path') | ||
process.env.NODE_ENV === 'production' | ||
? 'https://s-sh-16-clicli.oss.dogecdn.com/' | ||
? 'https://berial-child-vue.vercel.app' | ||
: 'http://localhost:3003' | ||
@@ -19,0 +19,0 @@ }, |
@@ -7,2 +7,4 @@ import { start, register } from '../../dist/berial' | ||
const isProduction = process.env.NODE_ENV === 'production' | ||
render(<App />, document.getElementById('app')) | ||
@@ -12,3 +14,5 @@ | ||
'child-fre', | ||
'http://localhost:3001', | ||
isProduction | ||
? 'https://berial-child-fre.vercel.app' | ||
: 'http://localhost:3001', | ||
(location) => location.pathname === '/' | ||
@@ -19,3 +23,5 @@ ) | ||
'child-react', | ||
'http://localhost:3002', | ||
isProduction | ||
? 'https://berial-child-react.vercel.app' | ||
: 'http://localhost:3002', | ||
(location) => /^\/react/.test(location.pathname) | ||
@@ -26,6 +32,8 @@ ) | ||
'child-vue', | ||
'http://localhost:3003', | ||
isProduction | ||
? 'https://berial-child-vue.vercel.app' | ||
: 'http://localhost:3003', | ||
(location) => /^\/vue/.test(location.pathname) | ||
) | ||
start() | ||
start() |
{ | ||
"scripts": { | ||
"build": "cross-env NODE_ENV=production webpack --mode production", | ||
"build:berial": "cd ../../ && npm run build && cd example/parent", | ||
"start": "npm run build:berial && concurrently \"npm run start:parent\" \"npm run start:child-fre\" \"npm run start:child-react\" \"npm run start:child-vue\"", | ||
"build:test": "cross-env NODE_ENV=test webpack --mode production", | ||
"build:all": "concurrently \"npm run build\" \"npm run build:child-fre\" \"npm run build:child-react\" \"npm run build:child-vue\"", | ||
"start": "concurrently \"npm run start:parent\" \"npm run start:child-fre\" \"npm run start:child-react\" \"npm run start:child-vue\"", | ||
"start:parent": "cross-env NODE_ENV=development webpack-dev-server --mode production", | ||
@@ -13,3 +14,6 @@ "start:child-fre": "cd ../child-fre && npm start", | ||
"install:child-react": "cd ../child-react && npm i", | ||
"install:child-vue": "cd ../child-vue && npm i" | ||
"install:child-vue": "cd ../child-vue && npm i", | ||
"build:child-fre": "cd ../child-fre && npm run build", | ||
"build:child-react": "cd ../child-react && npm run build", | ||
"build:child-vue": "cd ../child-vue && npm run build" | ||
}, | ||
@@ -16,0 +20,0 @@ "dependencies": { |
@@ -15,3 +15,3 @@ const path = require('path') | ||
process.env.NODE_ENV === 'production' | ||
? 'https://s-sh-16-clicli.oss.dogecdn.com/' | ||
? 'https://berial.vercel.app' | ||
: 'http://localhost:3000' | ||
@@ -18,0 +18,0 @@ }, |
@@ -9,3 +9,3 @@ /* eslint-disable */ | ||
frameworks: ['mocha', 'karma-typescript'], | ||
files: ['src/**/*.ts', 'test/**/*.ts'], | ||
files: ['src/**/*.ts', 'test/unit/**/*.ts'], | ||
preprocessors: { | ||
@@ -12,0 +12,0 @@ 'src/**/*.ts': 'karma-typescript', |
{ | ||
"name": "berial", | ||
"version": "0.0.7", | ||
"version": "0.0.9", | ||
"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,8 @@ "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", | ||
"test": "karma start karma.conf.js", | ||
"e2e": "cross-env NODE_ENV=test mocha --timeout 600000 test/e2e/**/*.spec.js" | ||
}, | ||
@@ -42,2 +45,3 @@ "repository": { | ||
"chai": "^4.2.0", | ||
"cross-env": "^7.0.2", | ||
"eslint": "^7.5.0", | ||
@@ -47,2 +51,4 @@ "eslint-config-prettier": "^6.11.0", | ||
"eslint-plugin-react": "^7.20.5", | ||
"execa": "^4.0.3", | ||
"http-server": "^0.12.3", | ||
"husky": "^4.2.5", | ||
@@ -59,2 +65,3 @@ "karma": "^5.1.1", | ||
"rollup": "^2.23.0", | ||
"rollup-plugin-dts": "^1.4.11", | ||
"rollup-plugin-typescript2": "^0.27.1", | ||
@@ -68,5 +75,5 @@ "serve": "^11.3.2", | ||
"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 | ||
@@ -11,0 +11,0 @@ const fromNode = shadowRoot, |
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, | ||
}), | ||
] | ||
} |
import type { App } from './types' | ||
import { mapMixin } from './mixin' | ||
import { importHtml } from './html-loader' | ||
import { lifecycleCheck } from './util' | ||
import { lifecycleCheck, reverse } from './util' | ||
export enum Status { | ||
@@ -27,3 +27,3 @@ NOT_LOADED = 'NOT_LOADED', | ||
status: Status.NOT_LOADED | ||
} as App) | ||
}) | ||
} | ||
@@ -102,8 +102,8 @@ | ||
let mixinLife = mapMixin() | ||
app.host = (await loadShadowDOM(app)) as any | ||
app.host = await loadShadowDOM(app) | ||
const { lifecycle: selfLife, bodyNode, styleNodes } = await importHtml(app) | ||
lifecycleCheck(selfLife) | ||
app.host.shadowRoot?.appendChild(bodyNode.content.cloneNode(true)) | ||
for (const k of styleNodes) | ||
app.host.shadowRoot!.insertBefore(k, app.host.shadowRoot!.firstChild) | ||
app.host?.appendChild(bodyNode.content.cloneNode(true)) | ||
for (const k of reverse(styleNodes)) | ||
app.host!.insertBefore(k, app.host!.firstChild) | ||
app.status = Status.NOT_BOOTSTRAPPED | ||
@@ -119,4 +119,4 @@ app.bootstrap = compose(mixinLife.bootstrap.concat(selfLife.bootstrap)) | ||
async function loadShadowDOM(app: App): Promise<HTMLElement> { | ||
return new Promise<HTMLElement>((resolve) => { | ||
function loadShadowDOM(app: App): Promise<DocumentFragment> { | ||
return new Promise((resolve, reject) => { | ||
class Berial extends HTMLElement { | ||
@@ -126,8 +126,5 @@ static get tag(): string { | ||
} | ||
connectedCallback(): void { | ||
resolve(this) | ||
} | ||
constructor() { | ||
super() | ||
this.attachShadow({ mode: 'open' }) | ||
resolve(this.attachShadow({ mode: 'open' })) | ||
} | ||
@@ -204,8 +201,7 @@ } | ||
const before = window.location.href | ||
// @ts-ignore | ||
fn.apply(this, arguments) | ||
fn.apply(window.history, arguments) | ||
const after = window.location.href | ||
if (before !== after) { | ||
// @ts-ignore | ||
reroute(new PopStateEvent('popstate')) | ||
new PopStateEvent('popstate') | ||
reroute() | ||
} | ||
@@ -212,0 +208,0 @@ } |
@@ -1,4 +0,3 @@ | ||
import type { App, PromiseFn, Lifecycles, ProxyType } from './types' | ||
import { run } from './sandbox' | ||
import type { App, PromiseFn, Lifecycles } from './types' | ||
import { run, observeDoucument, getcurrentQueue } from './sandbox' | ||
import { request } from './util' | ||
@@ -21,2 +20,6 @@ | ||
) | ||
const SCRIPT_URL_OR_CONTENT_RE = new RegExp( | ||
'(?:' + SCRIPT_URL_RE.source + ')|(?:' + SCRIPT_CONTENT_RE.source + ')', | ||
'g' | ||
) | ||
const MATCH_NONE_QUOTE_MARK = /[^"]/ | ||
@@ -35,2 +38,6 @@ const CSS_URL_RE = new RegExp( | ||
const STYLE_RE = /<\s*style\s*>([^<]*)<\s*\/style>/g | ||
const CSS_URL_OR_STYLE_RE = new RegExp( | ||
'(?:' + CSS_URL_RE.source + ')|(?:' + STYLE_RE.source + ')', | ||
'g' | ||
) | ||
const BODY_CONTENT_RE = /<\s*body[^>]*>([\w\W]*)<\s*\/body>/ | ||
@@ -49,6 +56,7 @@ const SCRIPT_ANY_RE = /<\s*script[^>]*>[\s\S]*?(<\s*\/script[^>]*>)/g | ||
}> { | ||
observeDoucument(app.host) | ||
const template = await request(app.url as string) | ||
const styleNodes = await loadCSS(template) | ||
const bodyNode = loadBody(template) | ||
const lifecycle = await loadScript(template, app.name) | ||
const lifecycle = await loadScript(template, app) | ||
return { lifecycle, styleNodes, bodyNode } | ||
@@ -59,59 +67,73 @@ } | ||
template: string, | ||
name: string | ||
{ name }: any | ||
): Promise<Lifecycles> { | ||
const { scriptURLs, scripts } = parseScript(template) | ||
const fetchedScripts = await Promise.all( | ||
scriptURLs.map((url) => request(url)) | ||
) | ||
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] | ||
}) | ||
function process(queue: any): void { | ||
Promise.all( | ||
queue.map((v: string) => { | ||
if (TEST_URL.test(v)) return request(v) | ||
return v | ||
}) | ||
).then((q1: any) => { | ||
queue.length = 0 | ||
q1.forEach(getLyfecycles) | ||
const q2 = getcurrentQueue() | ||
if (q2.length > 0) process(q2) | ||
}) | ||
} | ||
process(parseScript(template)) | ||
function getLyfecycles(script: string): void { | ||
let lifecycles = run(script, {})[name] | ||
if (lifecycles) { | ||
bootstrap = | ||
typeof lifecycles.bootstrap === 'function' | ||
? [...bootstrap, lifecycles.bootstrap] | ||
: bootstrap | ||
mount = | ||
typeof lifecycles.mount === 'function' | ||
? [...mount, lifecycles.mount] | ||
: mount | ||
unmount = | ||
typeof lifecycles.unmount === 'function' | ||
? [...unmount, lifecycles.unmount] | ||
: 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 | ||
function parseScript(template: string): string[] { | ||
const scriptList = [] | ||
SCRIPT_URL_OR_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 | ||
while ((match = SCRIPT_URL_OR_CONTENT_RE.exec(template))) { | ||
let captured | ||
if (match[1]) { | ||
captured = match[1].trim() | ||
if (!TEST_URL.test(captured)) { | ||
captured = window.location.origin + captured | ||
} | ||
} else if (match[2]) { | ||
captured = match[2].trim() | ||
} | ||
scriptURLs.push(captured) | ||
captured && scriptList.push(captured) | ||
} | ||
while ((match = SCRIPT_CONTENT_RE.exec(template))) { | ||
const captured = match[1].trim() | ||
if (!captured) continue | ||
scripts.push(captured) | ||
} | ||
return { | ||
scriptURLs, | ||
scripts | ||
} | ||
return scriptList | ||
} | ||
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)) | ||
const styles = await Promise.all( | ||
parseCSS(template).map((v: string) => { | ||
if (TEST_URL.test(v)) return request(v) | ||
return v | ||
}) | ||
) | ||
return toStyleNodes(styles) | ||
function toStyleNodes(styles: string[]): HTMLStyleElement[] { | ||
return styles.map((style) => { | ||
function toStyleNodes(s: string[]): HTMLStyleElement[] { | ||
return s.map((style) => { | ||
const styleNode = document.createElement('style') | ||
@@ -124,29 +146,19 @@ styleNode.appendChild(document.createTextNode(style)) | ||
function parseCSS( | ||
template: string | ||
): { | ||
cssURLs: string[] | ||
styles: string[] | ||
} { | ||
const cssURLs: string[] = [] | ||
const styles: string[] = [] | ||
CSS_URL_RE.lastIndex = STYLE_RE.lastIndex = 0 | ||
function parseCSS(template: string): string[] { | ||
const cssList: string[] = [] | ||
CSS_URL_OR_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 | ||
while ((match = CSS_URL_OR_STYLE_RE.exec(template))) { | ||
let captured | ||
if (match[1]) { | ||
captured = match[1].trim() | ||
if (!TEST_URL.test(captured)) { | ||
captured = window.location.origin + captured | ||
} | ||
} else if (match[2]) { | ||
captured = match[2].trim() | ||
} | ||
cssURLs.push(captured) | ||
captured && cssList.push(captured) | ||
} | ||
while ((match = STYLE_RE.exec(template))) { | ||
const captured = match[1].trim() | ||
if (!captured) continue | ||
styles.push(captured) | ||
} | ||
return { | ||
cssURLs, | ||
styles | ||
} | ||
return cssList | ||
} | ||
@@ -153,0 +165,0 @@ |
@@ -6,13 +6,2 @@ import { start, register } from './app' | ||
export const Berial = { | ||
start, | ||
register, | ||
importHtml, | ||
run, | ||
mixin, | ||
use | ||
} | ||
export default Berial | ||
export { start, register, importHtml, run, 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) |
@@ -0,1 +1,3 @@ | ||
let queue: string[] = [] | ||
export function run(code: string, options: any = {}): any { | ||
@@ -74,3 +76,3 @@ try { | ||
eval: function (code: string): any { | ||
return run('return ' + code, null) | ||
return run('return ' + code, {}) | ||
}, | ||
@@ -140,4 +142,31 @@ alert: function (): void { | ||
} | ||
export function observeDoucument(host: any): void { | ||
new MutationObserver((mutations) => { | ||
mutations.forEach(async (m: any) => { | ||
switch (m.type) { | ||
case 'childList': | ||
if (m.target !== host) { | ||
for (let i = 0; i < m.addedNodes.length; i++) { | ||
const node = m.addedNodes[i] | ||
if (node instanceof HTMLScriptElement) { | ||
const src = node.getAttribute('src') || '' | ||
queue.push(src) | ||
} else { | ||
host.appendChild(node) | ||
} | ||
} | ||
} | ||
break | ||
default: | ||
} | ||
}) | ||
}).observe(document, { childList: true, subtree: true }) | ||
} | ||
export function getcurrentQueue(): any { | ||
return queue | ||
} | ||
function checkSyntax(code: string): boolean { | ||
Function(code) | ||
if (/\bimport\s*(?:[(]|\/[*]|\/\/|<!--|-->)/.test(code)) { | ||
@@ -144,0 +173,0 @@ throw new Error('Dynamic imports are blocked') |
@@ -12,2 +12,11 @@ import type { Status } from './app' | ||
export type Mixin = { | ||
load?: PromiseFn | ||
mount?: PromiseFn | ||
unmount?: PromiseFn | ||
bootstrap?: PromiseFn | ||
} | ||
export type Plugin = (...args: any[]) => any | ||
export type App = { | ||
@@ -17,3 +26,3 @@ name: string | ||
match: (location: Location) => boolean | ||
host: HTMLElement | ||
host: DocumentFragment | ||
props: Record<string, unknown> | ||
@@ -20,0 +29,0 @@ status: Status |
@@ -5,6 +5,4 @@ { | ||
"sourceMap": true, | ||
"declaration": true, | ||
"declarationDir": "dist/types/", | ||
"noImplicitAny": true, | ||
"removeComments":true, | ||
"removeComments": true, | ||
"target": "es6", | ||
@@ -19,3 +17,6 @@ "module": "es6", | ||
"baseUrl": ".", | ||
"outDir": "./dist" | ||
"outDir": "./dist", | ||
"paths": { | ||
"@/*": ["./src/*"] | ||
} | ||
}, | ||
@@ -22,0 +23,0 @@ "include": ["src/**/*"], |
{ | ||
"extends": "./tsconfig.json", | ||
"include": ["src/**/*", "test/**/*"], | ||
"include": ["src/**/*", "test/unit/**/*"], | ||
"exclude": ["node_modules"] | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
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
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
179111
80.98%3065
62.95%32
14.29%10
42.86%2
100%