Comparing version 0.0.7 to 0.0.9
@@ -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
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
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
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
179111
3065
32
9
5