Repository: berialjs/berial Branch: master Commit: 12770c698e06 Files: 29 Total size: 30.2 KB Directory structure: gitextract_21d_bcf2/ ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── example/ │ ├── .gitignore │ ├── App.js │ ├── README.md │ ├── index.css │ ├── index.html │ ├── index.js │ ├── package.json │ └── webpack.config.js ├── package.json ├── plugin/ │ ├── bridge-event.ts │ └── package.json ├── rollup.config.js ├── src/ │ ├── entity.ts │ ├── html-loader.ts │ ├── index.ts │ ├── mixin.ts │ ├── sandbox.ts │ ├── types.ts │ └── util.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ dist coverage node_modules ================================================ FILE: .eslintrc ================================================ { "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 2019, "sourceType": "module", "ecmaFeatures": { "jsx": true } }, "extends": [ "prettier/@typescript-eslint", "plugin:prettier/recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended" ], "plugins": ["@typescript-eslint"], "globals": { "__DEV__": true }, "env": { "browser": true }, "settings": { "react": { "pragma": "React", "version": "detect" } }, "rules": { "@typescript-eslint/no-use-before-define": 0, "@typescript-eslint/no-var-requires": 0, "@typescript-eslint/no-unused-vars": 0, "@typescript-eslint/ban-ts-comment": 1, "@typescript-eslint/camelcase": 0, "@typescript-eslint/interface-name-prefix": 0, "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/no-empty-function": 0, "@typescript-eslint/explicit-function-return-type": 2, "@typescript-eslint/explicit-module-boundary-types": 0, "@typescript-eslint/no-non-null-assertion": 0, "no-shadow": 1, "prefer-const": 0, "jsx-quotes": 0, "prefer-rest-params":0, "react/prop-types": 0, "react/no-deprecated": 1, "react/display-name": 1 } } ================================================ FILE: .gitattributes ================================================ # Use Unix line endings in all text files. * text=auto eol=lf ================================================ FILE: .github/workflows/ci.yml ================================================ name: ci on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - run: npm i - run: npm run check ================================================ FILE: .gitignore ================================================ # dependencies node_modules/ npm-debug.log* yarn-error.log yarn.lock lerna-debug.log **/**/node_modules/ **/**/**/node_modules/ # production /dist # misc .DS_Store .cache coverage # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw* # Lock files package-lock.json yarn.lock ================================================ FILE: .prettierignore ================================================ dist/ ================================================ FILE: .prettierrc ================================================ tabWidth: 2 useTabs: false semi: false arrowParens: always singleQuote: true printWidth: 80 trailingComma: none ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 yisar h-a-n-a Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

berial logo

Berial

:imp: Simple micro-front-end framework.

Build Status npm-v npm-d

### Why Berial Berial is a new approach to a popular idea: build a javascript framework for front-end microservices. There are any wonderful features of it, such as Asynchronous rendering pipeline, Web components (shadow DOM + scoped css), JavaScript sandbox (Proxy). Note: diffence form fre, Berial will pay attention to business value. ### Use ```html ``` ### License MIT ©yisar ©h-a-n-a ================================================ FILE: example/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env # next.js build output .next .idea/ server/config.js server/hcy.js server/san.js dist/ ================================================ FILE: example/App.js ================================================ import { h, useEffect } from 'fre' import { register } from '../dist/berial.esm' function App() { const changeRoute = (pathname) => { history.pushState({}, '', pathname) } return
} export default App ================================================ FILE: example/README.md ================================================ # Example ``` yarn install yarn start ``` ================================================ FILE: example/index.css ================================================ .header { height: 60px; display: flex; align-items: center; padding: 0 24px; } .header button { padding:10px 30px; cursor: pointer; background: #000; color: #fff; border: none; margin: 10px; } ================================================ FILE: example/index.html ================================================
================================================ FILE: example/index.js ================================================ import { h, render } from 'fre' import './index.css' import { register } from '../dist/berial.esm' import App from './App' if (!window.IS_BERIAL_SANDBOX) { render(, document.getElementById('app')) } register([ { name: 'child-fre', url: 'https://berial-child-fre.vercel.app', path: ({ pathname }) => pathname !== '/react' && pathname !== '/vue', allowList:['document'] }, { name: 'child-react', url: 'https://berial-child-react.vercel.app', path: ({ pathname }) => pathname === '/react' }, { name: 'child-vue', url: 'https://berial-child-vue.vercel.app', path: ({ pathname }) => pathname === '/vue' } ]) ================================================ FILE: example/package.json ================================================ { "scripts": { "start": "cross-env NODE_ENV=development webpack-dev-server --mode production" }, "dependencies": { "fre": "^2.0.0-alpha.1" }, "devDependencies": { "@babel/core": "^7.6.4", "@babel/plugin-transform-react-jsx": "^7.3.0", "@babel/preset-env": "^7.6.3", "babel-loader": "^8.0.6", "concurrently": "^5.3.0", "cross-env": "^6.0.3", "css-loader": "^3.2.0", "html-webpack-plugin": "^3.2.0", "mini-css-extract-plugin": "^0.8.0", "style-loader": "^1.0.1", "webpack": "^4.41.1", "webpack-cli": "^3.3.9", "webpack-dev-server": "^3.8.2" } } ================================================ FILE: example/webpack.config.js ================================================ const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = { entry: './index.js', output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js', library: 'one-app', libraryTarget: 'umd', umdNamedDefine: true, publicPath: process.env.NODE_ENV === 'production' ? 'https://berial.vercel.app' : 'http://localhost:3000' }, module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { plugins: [ [ '@babel/plugin-transform-react-jsx', { pragma: 'h' } ] ] } } }, { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] } ] }, optimization: { splitChunks: false }, plugins: [ new HtmlWebpackPlugin({ template: './index.html' }), new MiniCssExtractPlugin({ filename: '[name].css' }) ], devServer: { headers: { 'Access-Control-Allow-Origin': '*' }, contentBase: path.join(__dirname, 'dist'), compress: true, port: 3000, historyApiFallback: true, hot: true, open: true } } ================================================ FILE: package.json ================================================ { "name": "berial", "version": "0.1.6", "description": "micro frontend", "main": "dist/berial.js", "module": "dist/berial.esm.js", "types": "dist/types/index.d.ts", "scripts": { "build": "rollup -c", "dev": "rollup -c --watch", "check": "run-p fmt-check lint", "fix": "run-s \"lint -- --fix\"", "fmt": "run-s \"fmt-check -- --write\"", "fmt-check": "prettier --check **/*.{json,ts}", "lint": "eslint **/*.ts", "type": "tsc --project tsconfig.json --skipLibCheck --noEmit" }, "repository": { "type": "git", "url": "git+https://github.com/berialjs/berial.git" }, "keywords": [], "author": "", "license": "MIT", "bugs": { "url": "https://github.com/berialjs/berial/issues" }, "homepage": "https://github.com/berialjs/berial#readme", "devDependencies": { "cross-env": "^7.0.2", "eslint": "^7.5.0", "eslint-config-prettier": "^6.11.0", "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-react": "^7.20.5", "http-server": "^0.12.3", "husky": "^4.2.5", "npm-run-all": "^4.1.5", "prettier": "^2.0.5", "rollup": "^2.23.0", "rollup-plugin-dts": "^1.4.11", "rollup-plugin-typescript2": "^0.27.1", "serve": "^11.3.2", "tslib": "^2.0.0", "typescript": "^3.9.7", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0" }, "husky": { "hooks": { "pre-commit": "npm run fmt && npm run fix;" } } } ================================================ FILE: plugin/bridge-event.ts ================================================ import { mixin } from 'berial' export function bridgeEvent(): void { mixin({ boostrap }) } async function boostrap(app): Promise { const shadowRoot = app.host const define = Object.defineProperty const fromNode = shadowRoot, toNode = shadowRoot.host BRIDGE_EVENT_NAMES.map((eventName) => { fromNode.addEventListener(eventName, (fromEvent) => { fromEvent.stopPropagation() const Event = fromEvent.constructor const toEvent = new Event(eventName, { ...fromEvent, bubbles: true, cancelable: true, composed: true }) const { path = [], target = path[0], srcElement = path[0], toElement = path[0], preventDefault } = fromEvent as any define(toEvent, 'path', { get: () => path }) define(toEvent, 'target', { get: () => target }) define(toEvent, 'srcElement', { get: () => srcElement }) define(toEvent, 'toElement', { get: () => toElement }) define(toEvent, 'preventDefault', { value: () => { preventDefault.call(fromEvent) return preventDefault.call(toEvent) } }) toNode.dispatchEvent(toEvent) }) }) } const BRIDGE_EVENT_NAMES = [ 'abort', 'animationcancel', 'animationend', 'animationiteration', 'auxclick', 'blur', 'change', 'click', 'close', 'contextmenu', 'doubleclick', 'error', 'focus', 'gotpointercapture', 'input', 'keydown', 'keypress', 'keyup', 'load', 'loadend', 'loadstart', 'lostpointercapture', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'pointercancel', 'pointerdown', 'pointerenter', 'pointerleave', 'pointermove', 'pointerout', 'pointerover', 'pointerup', 'reset', 'resize', 'scroll', 'select', 'selectionchange', 'selectstart', 'submit', 'touchcancel', 'touchmove', 'touchstart', 'transitioncancel', 'transitionend', 'drag', 'dragend', 'dragenter', 'dragexit', 'dragleave', 'dragover', 'dragstart', 'drop', 'focusout' ] ================================================ FILE: plugin/package.json ================================================ { "name": "compat", "version": "0.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "tsc index.ts" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "berial": "^0.0.4" } } ================================================ FILE: rollup.config.js ================================================ import typescript from 'rollup-plugin-typescript2' export default { input: 'src/index.ts', output: [ { file: 'dist/berial.d.ts', format: 'esm', exports: 'named' }, { file: 'dist/berial.esm.js', format: 'esm', sourcemap: true }, { file: 'dist/berial.js', format: 'umd', sourcemap: true, name: 'berial' } ], plugins: [ typescript({ tsconfig: 'tsconfig.json', removeComments: true, useTsconfigDeclarationDir: true, }), ] } ================================================ FILE: src/entity.ts ================================================ import type { App } from './types' import { mapMixin } from './mixin' import { importHtml } from './html-loader' export enum Status { NOT_LOADED = 'NOT_LOADED', LOADING = 'LOADING', NOT_BOOTSTRAPPED = 'NOT_BOOTSTRAPPED', BOOTSTRAPPING = 'BOOTSTRAPPING', NOT_MOUNTED = 'NOT_MOUNTED', MOUNTING = 'MOUNTING', MOUNTED = 'MOUNTED', UPDATING = 'UPDATING', UPDATED = 'UPDATED', UNMOUNTING = 'UNMOUNTING' } let apps: App[] = [] export function register(appArray: any[]): void { appArray.forEach((app: any) => (app.status = Status.NOT_LOADED)) apps = appArray hack() reroute() } function reroute(): void { const { loads, mounts, unmounts } = getAppChanges() perform() async function perform(): Promise { unmounts.map(runUnmount) loads.map(async (app) => { app = await runLoad(app) app = await runBootstrap(app) return runMount(app) }) mounts.map(async (app) => { app = await runBootstrap(app) return runMount(app) }) } } function getAppChanges(): { unmounts: App[] loads: App[] mounts: App[] } { const unmounts: App[] = [] const loads: App[] = [] const mounts: App[] = [] apps.forEach((app: any) => { const isActive = typeof app.path === 'function' ? app.path(window.location) : window.location.pathname === app.path switch (app.status) { case Status.NOT_LOADED: case Status.LOADING: isActive && loads.push(app) break case Status.NOT_BOOTSTRAPPED: case Status.BOOTSTRAPPING: case Status.NOT_MOUNTED: isActive && mounts.push(app) break case Status.MOUNTED: !isActive && unmounts.push(app) break } }) return { unmounts, loads, mounts } } function compose( fns: ((app: App) => Promise)[] ): (app: App) => Promise { fns = Array.isArray(fns) ? fns : [fns] return (app: App): Promise => fns.reduce((p, fn) => p.then(() => fn(app)), Promise.resolve()) } async function runLoad(app: App): Promise { if (app.loaded) return app.loaded app.loaded = Promise.resolve().then(async () => { app.status = Status.LOADING let mixinLife = mapMixin() app.host = await loadShadowDOM(app) const { dom, lifecycles } = await importHtml(app) app.host?.appendChild(dom) app.status = Status.NOT_BOOTSTRAPPED 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 return app }) return app.loaded } function loadShadowDOM(app: App): Promise { return new Promise((resolve, reject) => { class Berial extends HTMLElement { static get tag(): string { return app.name } constructor() { super() resolve(this.attachShadow({ mode: 'open' })) } } const hasDef = window.customElements.get(app.name) if (!hasDef) { customElements.define(app.name, Berial) } }) } async function runUnmount(app: App): Promise { if (app.status != Status.MOUNTED) { return app } app.status = Status.UNMOUNTING await app.unmount(app) app.status = Status.NOT_MOUNTED return app } async function runBootstrap(app: App): Promise { if (app.status !== Status.NOT_BOOTSTRAPPED) { return app } app.status = Status.BOOTSTRAPPING await app.bootstrap(app) app.status = Status.NOT_MOUNTED return app } async function runMount(app: App): Promise { if (app.status !== Status.NOT_MOUNTED) { return app } app.status = Status.MOUNTING await app.mount(app) app.status = Status.MOUNTED return app } function hack(): void { 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 = { hashchange: [], popstate: [] } as any function hackEventListener(func: any): any { return function (name: any, fn: any): any { if (name === 'hashchange' || name === 'popstate') { if (!captured[name].some((l: any) => l == fn)) { captured[name].push(fn) return } else { captured[name] = captured[name].filter((l: any) => l !== fn) return } } return func.apply(this, arguments as any) } } function hackHistory(fn: any): () => void { return function (): void { const before = window.location.href fn.apply(window.history, arguments) const after = window.location.href if (before !== after) { new PopStateEvent('popstate') reroute() } } } ================================================ FILE: src/html-loader.ts ================================================ import { request } from './util' import { runScript } from './sandbox' const ALL_SCRIPT_REGEX = /)<[^<]*)*<\/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 = /]*>[\s\S]*?<\/style>/gi const STYLE_TYPE_REGEX = /\s+rel=('|")?stylesheet\1.*/ const STYLE_HREF_REGEX = /.*\shref=('|")?([^>'"\s]+)/ const STYLE_IGNORE_REGEX = //i const HTML_COMMENT_REGEX = //g const SCRIPT_IGNORE_REGEX = //i export function getInlineCode(match: any): string { const start = match.indexOf('>') + 1 const end = match.lastIndexOf('<') return match.substring(start, end) } function hasProtocol(url: string): any { return ( url.startsWith('//') || url.startsWith('http://') || url.startsWith('https://') ) } function getEntirePath(path: string, baseURI: string): string { return new URL(path, baseURI).toString() } export const genLinkReplaceSymbol = (linkHref: any): string => `` export const genScriptReplaceSymbol = (scriptSrc: any): string => `` export const inlineScriptReplaceSymbol = `` export const genIgnoreAssetReplaceSymbol = (url: any): string => `` 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] } } export async function importHtml(app: any): Promise { 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 } scripts = await Promise.all( scripts.map((s: string) => hasProtocol(s) ? request(s) : s.endsWith('.js') || s.endsWith('.jsx') ? request(window.origin + s) : s ) ) styles = styles.map((s: string) => hasProtocol(s) || s.endsWith('.css') ? `` : `