diff --git a/babel/index.js b/babel/index.js index 82d042d7..8f7455dc 100644 --- a/babel/index.js +++ b/babel/index.js @@ -67,4 +67,4 @@ module.exports = { assumptions, }, }, -}; +}; \ No newline at end of file diff --git a/babel/plugins/RegisterModuleInjectBuildId.js b/babel/plugins/RegisterModuleInjectBuildId.js index f15dfe79..e1ee0bc2 100644 --- a/babel/plugins/RegisterModuleInjectBuildId.js +++ b/babel/plugins/RegisterModuleInjectBuildId.js @@ -27,4 +27,4 @@ module.exports = { }, }, }, -}; +}; \ No newline at end of file diff --git a/platform/src/index.js b/platform/src/index.js index ff9156fd..a85459c6 100644 --- a/platform/src/index.js +++ b/platform/src/index.js @@ -2,7 +2,6 @@ import "regenerator-runtime/runtime"; import Module from "./component/Module"; import { SET_LANGUAGE, REFRESH, SET_SHARED, CLEAR_SHARED } from "./constants"; -import registerModule from "./register"; function setLanguage(language) { return { @@ -42,10 +41,9 @@ const actions = { refresh, }; -export { Module, actions, registerModule }; +export { Module, actions }; export default { Module, actions, - registerModule, }; diff --git a/platform/src/kernel/index.js b/platform/src/kernel/index.js index 2d47c1e8..6eae4762 100644 --- a/platform/src/kernel/index.js +++ b/platform/src/kernel/index.js @@ -3,7 +3,6 @@ import "regenerator-runtime/runtime"; /* istanbul ignore file */ import Module from "../component/Module"; import * as constants from "../constants"; -import registerModule from "../register"; import createDynamicMiddleware from "./middleware/dynamic"; import createLoaderMiddleware from "./middleware/loader"; @@ -24,7 +23,6 @@ export { modulesReducer, createDynamicMiddleware, createSagaMiddleware, - registerModule, getStore, setStore, manualCleanup, @@ -40,7 +38,6 @@ export default { modulesReducer, createDynamicMiddleware, createSagaMiddleware, - registerModule, getStore, setStore, manualCleanup, diff --git a/platform/src/kernel/registry/assets.js b/platform/src/kernel/registry/assets.js index 2011c7ec..ecd30e1a 100644 --- a/platform/src/kernel/registry/assets.js +++ b/platform/src/kernel/registry/assets.js @@ -4,58 +4,6 @@ import { warning } from "../../utils"; const CLIENT_TIMEOUT = 30 * 1000; -export class SequentialProgramEvaluator { - static queue = []; - static compiling = false; - - static compile(name, data) { - return new Promise((resolve) => { - this.queue.push({ - data, - name, - resolve, - }); - this.tick(); - }); - } - - static tick() { - /* istanbul ignore next */ - if (this.compiling) { - return; - } - const item = this.queue.shift(); - if (!item) { - this.compiling = false; - return; - } - this.compiling = true; - const sandbox = { - __SANDBOX_SCOPE__: {}, - }; - try { - top.__SANDBOX_SCOPE__ = sandbox.__SANDBOX_SCOPE__; - - new Function("", item.data)({}); - } catch (error) { - if (!(item.data.startsWith("!") || item.data.startsWith("/*"))) { - warning(`asset for module ${item.name} is not a module`); - } else { - warning(`module ${item.name} failed to adapt`); - } - sandbox.__SANDBOX_SCOPE__.component = () => { - throw error; - }; - } finally { - delete top.__SANDBOX_SCOPE__; - } - item.resolve(sandbox.__SANDBOX_SCOPE__); - this.compiling = false; - this.tick(); - return; - } -} - /* istanbul ignore next */ async function clientCache(name) { try { @@ -185,9 +133,135 @@ async function downloadProgram(name, program, controller) { if (!program) { return {}; } - const data = await downloadAsset(program.url, controller); - const content = await data.text(); - return SequentialProgramEvaluator.compile(name, content); + + console.log('Downloading program', program); + + const pre = top.document.createElement('script'); + pre.async = false; + pre.defer = false; + pre.innerHTML = ` + + function callback(data) { + + console.log('lastuiJsonp.push called', data); + + //const volatile = []; + + //for (const item in data[1]) { + //console.log(item); + //} + + //top.lastuiJsonp.push(data); + + data[2](); + + } + +self.name = "registration-${name}"; +self.lastuiJsonp = []; +self.lastuiJsonp.push = callback.bind(null) + +self.onerror = function(_message, _file, _line, _col, error) { + console.log(self.name, "caught uncaught error", error); + self.__SANDBOX_SCOPE__ = { + component() { + throw error; + }, + }; + return false; +}`; + + for (const dll in top) { + if (dll.startsWith('rocker_so')) { + pre.innerHTML += ` +self.${dll} = top.${dll};`; + } + } + + const script = top.document.createElement('script'); + + // INFO in dev mode the module script tries to connect to webpack's websocket server and overlay, this should not happen + script.src = program.url; + script.async = false; + script.defer = false; + + const post = top.document.createElement('script'); + post.async = false; + post.defer = false; + post.innerHTML = ` +console.log("Done", self); +self.__SANDBOX_SCOPE__ = {}; + `; + + const iframe = top.document.createElement('iframe'); + + // INFO yields "An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing." + //iframe.sandbox = 'allow-same-origin allow-scripts'; + + const trap = {}; + + try { + + iframe.src = "about:blank"; + + //iframe.srcdoc = bootstrap.outerHTML + script.outerHTML; + + top.document.head.appendChild(iframe); + + console.log('Waiting for program', program); + + //.bind(iframe.contentWindow); // INFO browser possibly recycles frame window references + + // INFO not ideal because its non blocking e.g. POST script executes before SCRIPT script + const registration = await new Promise(function(resolve, _reject) { + let wasSet = false; + + //const ref = iframe.contentWindow; + + console.log('defining property for program', program, 'in scope', iframe.contentWindow); + + // INFO for some reason this property is always defined on a firstly inserted iframe + // maybe hoisting? + Object.defineProperty(iframe.contentWindow, "__SANDBOX_SCOPE__", { + set(value) { + console.log('frame called set on __SANDBOX_SCOPE__ for program', program, value, 'wasSet', wasSet); + //if (wasSet) { + //return false; + //} + //wasSet = true; + //delete iframe.contentWindow.__SANDBOX_SCOPE__; + //resolve(value); + return true; + }, + get() { + return trap; + } + }); + + // TODO need a serial execution here + iframe.contentDocument.head.appendChild(pre); + iframe.contentDocument.head.appendChild(script); + iframe.contentDocument.head.appendChild(post); + }); + + console.log('Checking the contents of __SANDBOX_SCOPE__ for', program, 'is', registration); + + Object.assign(trap, registration); + + console.log('Done for program', program); + + // INFO now need to move implicitely injected styles from frame into main frame + } catch (error) { + console.log('frame error', error); + } finally { + top.document.head.removeChild(iframe); + } + + console.log('Downloaded program', program); + + console.log('Result is', trap) + + return trap; } export { downloadAsset, downloadProgram }; diff --git a/platform/src/register.js b/platform/src/register.js deleted file mode 100644 index 12b6ee6e..00000000 --- a/platform/src/register.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from "react"; - -import { warning } from "./utils"; - -/* istanbul ignore next */ -function isGenerator(val) { - return /\[object Generator|GeneratorFunction\]/.test(Object.prototype.toString.call(val)); -} - -/* istanbul ignore next */ -function isFunction(val) { - return /\[object Function|AsyncFunction\]/.test(Object.prototype.toString.call(val)); -} - -export default function (scope) { - if (!scope) { - return; - } - if (scope.constructor !== Object) { - throw new Error(`registerModule accepts only plain object, was called with ${typeof scope}`); - } - if (scope.BUILD_ID) { - if (typeof scope.BUILD_ID !== "string") { - warning(`implicit attribute "BUILD_ID" provided in registerModule is not string`); - } else { - top.__SANDBOX_SCOPE__.BUILD_ID = scope.BUILD_ID; - } - } - if (scope.component) { - if (!(isFunction(scope.component) || scope.component instanceof React.Component)) { - warning(`attribute "component" provided in registerModule is not function or React.Component`); - } else { - top.__SANDBOX_SCOPE__.component = scope.component; - } - } - if (scope.fallback) { - if (!(isFunction(scope.fallback) || scope.fallback instanceof React.Component)) { - warning(`attribute "fallback" provided in registerModule is not function or React.Component`); - } else { - top.__SANDBOX_SCOPE__.fallback = scope.fallback; - } - } - if (scope.reducers) { - if (scope.reducers.constructor !== Object) { - warning(`attribute "reducers" provided in registerModule is not plain object`); - } else { - top.__SANDBOX_SCOPE__.reducers = scope.reducers; - } - } - if (scope.middleware) { - if (!isFunction(scope.middleware) || isGenerator(scope.middleware)) { - warning(`attribute "middleware" provided in registerModule is not function or async function`); - } else { - top.__SANDBOX_SCOPE__.middleware = scope.middleware; - } - } - if (scope.saga) { - if (!isGenerator(scope.saga)) { - warning(`attribute "saga" provided in registerModule is not generator function or async generator function`); - } else { - top.__SANDBOX_SCOPE__.saga = scope.saga; - } - } - if (scope.props) { - if (scope.props.constructor !== Object) { - warning(`attribute "props" provided in registerModule is not plain object`); - } else { - top.__SANDBOX_SCOPE__.props = scope.props; - } - } -} diff --git a/webpack/config/module/development.js b/webpack/config/module/development.js index ce37c88a..85aec665 100644 --- a/webpack/config/module/development.js +++ b/webpack/config/module/development.js @@ -26,6 +26,7 @@ module.exports = merge(require("../../internal/base.js"), require("../../interna "react-dom$": "react-dom/profiling", "scheduler/tracing": "scheduler/tracing-profiling", "@lastui/rocker/platform/kernel": "@lastui/rocker/platform", + "@lastui/rocker/register": path.resolve(__dirname, "..", "..", "loaders", "ModuleRegistration", "runtime.js"), }, }, output: { diff --git a/webpack/config/module/production.js b/webpack/config/module/production.js index 241fb4a5..7b0332c9 100644 --- a/webpack/config/module/production.js +++ b/webpack/config/module/production.js @@ -17,6 +17,7 @@ module.exports = merge(require("../../internal/base.js"), require("../../interna resolve: { alias: { "@lastui/rocker/platform/kernel": "@lastui/rocker/platform", + "@lastui/rocker/register": path.resolve(__dirname, "..", "..", "loaders", "ModuleRegistration", "runtime.js"), }, }, output: { diff --git a/webpack/internal/development.js b/webpack/internal/development.js index 75208e14..325f6d88 100644 --- a/webpack/internal/development.js +++ b/webpack/internal/development.js @@ -23,7 +23,7 @@ module.exports = { errorDetails: true, errorStack: true, }, - devtool: "eval-cheap-module-source-map", + devtool: "cheap-module-source-map", watch: true, devServer: { hot: false, @@ -43,6 +43,7 @@ module.exports = { host: "0.0.0.0", port: settings.DEV_SERVER_PORT, client: { + reconnect: 2, overlay: { errors: true, runtimeErrors: true, diff --git a/webpack/loaders/ModuleRegistration/runtime.js b/webpack/loaders/ModuleRegistration/runtime.js new file mode 100644 index 00000000..8d8f4270 --- /dev/null +++ b/webpack/loaders/ModuleRegistration/runtime.js @@ -0,0 +1,73 @@ +function isGenerator(val) { + return /\[object Generator|GeneratorFunction\]/.test(Object.prototype.toString.call(val)); +} + +function isFunction(val) { + return /\[object Function|AsyncFunction\]/.test(Object.prototype.toString.call(val)); +} + +module.exports = function (scope) { + const result = {} + + if (!scope) { + return; + } + + const objectConstructor = Object.toString(); + + if (scope.constructor.toString() !== objectConstructor) { + throw new Error(`registerModule accepts only plain object, was called with ${typeof scope}`); + } + + if (scope.BUILD_ID) { + if (typeof scope.BUILD_ID !== "string") { + console.error(`implicit attribute "BUILD_ID" provided in registerModule is not string`); + } else { + result.BUILD_ID = scope.BUILD_ID; + } + } + if (scope.component) { + if (!isFunction(scope.component)) { + console.error(`attribute "component" provided in registerModule is not function`); + } else { + result.component = scope.component; + } + } + if (scope.fallback) { + if (!isFunction(scope.fallback)) { + console.error(`attribute "fallback" provided in registerModule is not function`); + } else { + result.fallback = scope.fallback; + } + } + if (scope.reducers) { + if (scope.reducers.constructor.toString() !== objectConstructor) { + console.error(`attribute "reducers" provided in registerModule is not plain object`); + } else { + result.reducers = scope.reducers; + } + } + if (scope.middleware) { + if (!isFunction(scope.middleware) || isGenerator(scope.middleware)) { + console.error(`attribute "middleware" provided in registerModule is not function or async function`); + } else { + result.middleware = scope.middleware; + } + } + if (scope.saga) { + if (!isGenerator(scope.saga)) { + console.error(`attribute "saga" provided in registerModule is not generator function or async generator function`); + } else { + result.saga = scope.saga; + } + } + if (scope.props) { + if (scope.props.constructor.toString() !== objectConstructor) { + console.error(`attribute "props" provided in registerModule is not plain object`); + } else { + result.props = scope.props; + } + } + + self.__SANDBOX_SCOPE__ = result; +}