diff --git a/client/client.js b/client/client.js index c45758ab..18c993fa 100644 --- a/client/client.js +++ b/client/client.js @@ -101,4 +101,8 @@ client.processLifecycleQueues = async function processLifecycleQueues() { router._changed = false } +if (module.hot) { + client.klasses = {} +} + export default client diff --git a/client/index.js b/client/index.js index 0f9d22da..60660942 100644 --- a/client/index.js +++ b/client/index.js @@ -14,7 +14,6 @@ import rerender from './rerender' import router from './router' import settings from './settings' import state from './state' -import windowEvent from './windowEvent' import worker from './worker' context.page = page diff --git a/client/lazy.js b/client/lazy.js new file mode 100644 index 00000000..bebb6bc2 --- /dev/null +++ b/client/lazy.js @@ -0,0 +1,19 @@ +import LazyComponent from "../shared/lazyComponent" + +let next = null +const queue = [] + +async function preload() { + let importer = queue.pop() + await importer() + requestIdleCallback(preload) +} + +export default function lazy(_hash, importer) { + queue.push(importer) + cancelIdleCallback(next) + preload() + return class extends LazyComponent { + importer = importer + } +} \ No newline at end of file diff --git a/client/runtime.js b/client/runtime.js index 9c61d5d1..8942468e 100644 --- a/client/runtime.js +++ b/client/runtime.js @@ -4,11 +4,13 @@ import invoke from './invoke' import context from './context' import windowEvent from './windowEvent' import client from './client' +import lazy from './lazy' const $runtime = { element, fragment, invoke, + lazy } if (module.hot) { @@ -49,6 +51,7 @@ if (module.hot) { } } } + client.klasses[declaration.klass.hash] = declaration.klass declaration.klass.__hashes = declaration.hashes } windowEvent('environment') diff --git a/loaders/debug.js b/loaders/debug.js new file mode 100644 index 00000000..74ab76cb --- /dev/null +++ b/loaders/debug.js @@ -0,0 +1,6 @@ +module.exports = function (source, map) { + if (this.resourcePath.includes('ServerFunctions.njs')) { + require('fs').writeFileSync('debug.njs', source) + } + this.callback(null, source, map) +} \ No newline at end of file diff --git a/server/lazy.js b/server/lazy.js new file mode 100644 index 00000000..49d6510d --- /dev/null +++ b/server/lazy.js @@ -0,0 +1,18 @@ +import LazyComponent from "../shared/lazyComponent" + +const queue = {} + +export default function lazy(hash, importer) { + queue[hash] = importer + return class extends LazyComponent { + importer = importer + } +} + +export async function load(hash) { + const fileHash = module.hot ? hash.split('___')[0] : hash.slice(0, 8) + if (!queue[fileHash]) return + const importer = queue[fileHash] + delete queue[fileHash] + return importer() +} \ No newline at end of file diff --git a/server/registry.js b/server/registry.js index 8ec00228..9d19db0c 100644 --- a/server/registry.js +++ b/server/registry.js @@ -3,6 +3,7 @@ export default registry import reqres from "./reqres" import { generateContext } from "./context" import Nullstack from '.' +import { load } from "./lazy" export function register(klass, functionName) { if (functionName) { @@ -27,13 +28,14 @@ function bindStaticProps(klass) { if (!klass[propName]) { klass[propName] = klass[prop] } - function _invoke(...args) { + async function _invoke(...args) { if (underscored) { return klass[propName].call(klass, ...args) } const params = args[0] || {} const { request, response } = reqres const subcontext = generateContext({ request, response, ...params }) + await load(klass.hash) return klass[propName].call(klass, subcontext) } if (module.hot) { diff --git a/server/runtime.js b/server/runtime.js index d527fd69..b0004da8 100644 --- a/server/runtime.js +++ b/server/runtime.js @@ -1,12 +1,14 @@ import element from '../shared/element' import fragment from '../shared/fragment' import { register } from './registry' +import lazy from './lazy' import Nullstack from './index' const $runtime = { element, fragment, - register + register, + lazy } if (module.hot) { diff --git a/server/server.js b/server/server.js index 2f0e35e9..15da16b4 100644 --- a/server/server.js +++ b/server/server.js @@ -17,6 +17,7 @@ import reqres from './reqres' import generateRobots from './robots' import template from './template' import { generateServiceWorker } from './worker' +import { load } from './lazy' const server = express() @@ -124,6 +125,7 @@ server.start = function () { const { hash, methodName } = request.params const [invokerHash, boundHash] = hash.split('-') const key = `${invokerHash}.${methodName}` + await load(boundHash || invokerHash) const invokerKlass = registry[invokerHash] let boundKlass = invokerKlass if (boundHash) { @@ -159,6 +161,7 @@ server.start = function () { const [invokerHash, boundHash] = hash.split('-') const key = `${invokerHash}.${methodName}` let invokerKlass; + await load(boundHash || invokerHash) async function reply() { let boundKlass = invokerKlass if (boundHash) { @@ -186,10 +189,10 @@ server.start = function () { } async function delay() { invokerKlass = registry[invokerHash] - if (invokerKlass.__hashes[methodName] !== version) { + if (invokerKlass && invokerKlass.__hashes[methodName] !== version) { setTimeout(() => { delay() - }, 200) + }, 0) } else { reply() } diff --git a/shared/generateTree.js b/shared/generateTree.js index efb59128..2fe2bd56 100644 --- a/shared/generateTree.js +++ b/shared/generateTree.js @@ -27,7 +27,14 @@ async function generateBranch(siblings, node, depth, scope) { if (isClass(node)) { const key = generateKey(scope, node, depth) - const instance = scope.instances[key] || new node.type(scope) + let instance = scope.instances[key] + if (!instance) { + if (module.hot && node.type.hash) { + instance = new scope.klasses[node.type.hash](scope) + } else { + instance = new node.type(scope) + } + } instance.persistent = !!node.attributes.persistent instance.key = key instance._attributes = node.attributes diff --git a/shared/lazyComponent.js b/shared/lazyComponent.js new file mode 100644 index 00000000..e0a95621 --- /dev/null +++ b/shared/lazyComponent.js @@ -0,0 +1,25 @@ +import element from './element' + +class LazyComponent { + + constructor(scope) { + this.server = scope.context.server + } + + async initiate() { + this.component = (await this.importer()).default + } + + render() { + if (!this.component) return false + const { key, ...attributes } = this._attributes + return element(this.component, attributes) + } + + toJSON() { + return null + } + +} + +export default LazyComponent \ No newline at end of file diff --git a/tests/debug.njs b/tests/debug.njs new file mode 100644 index 00000000..87ec2e1f --- /dev/null +++ b/tests/debug.njs @@ -0,0 +1,246 @@ +import $runtime from "nullstack/runtime"; // server function works +import Nullstack from "nullstack"; +import { readFileSync } from "fs"; +const decodedString = "! * ' ( ) ; : @ & = + $ , / ? % # [ ]"; +class ServerFunctions extends Nullstack { + static hash = "src__ServerFunctions___ServerFunctions"; + count = 0; + year = null; + statement = ""; + response = ""; + clientOnly = ""; + static async getCountAsOne() { + return 1; + } + async setCountToOne() { + this.count = await this.getCountAsOne(); + } + static async getCount({ to }) { + return to; + } + async setCountToTwo() { + this.count = await this.getCount({ + to: 2 + }); + } + static async getDate({ input }) { + return input; + } + async setDate() { + const input = new Date("1992-10-16"); + const output = await this.getDate({ + input + }); + this.year = output.getFullYear(); + } + static async useNodeFileSystem() { + const text = readFileSync("src/ServerFunctions.njs", "utf-8"); + return text.split(`\n`)[0].trim(); + } + static async useFetchInNode() { + const response = await fetch("http://localhost:6969/robots.txt"); + const text = await response.text(); + return text.split(`\n`)[0].trim(); + } + static async getDoublePlusOne({ number }) { + return number * 2 + 1; + } + static async getEncodedString({ string }) { + return string === decodedString; + } + static async _privateFunction() { + return true; + } + static async getPrivateFunction({ request }) { + return this._privateFunction(); + } + static async getRequestUrl({ request }) { + return request.originalUrl.startsWith("/"); + } + async initiate() { + this.statement = await this.useNodeFileSystem(); + this.response = await this.useFetchInNode(); + this.doublePlusOneServer = await ServerFunctions.getDoublePlusOne({ + number: 34 + }); + this.originalUrl = await ServerFunctions.getRequestUrl(); + } + async hydrate() { + this.underlineRemovedFromClient = !ServerFunctions._privateFunction; + this.underlineStayOnServer = await ServerFunctions.getPrivateFunction(); + this.doublePlusOneClient = await ServerFunctions.getDoublePlusOne({ + number: 34 + }); + this.acceptsSpecialCharacters = await this.getEncodedString({ + string: decodedString + }); + this.hydratedOriginalUrl = await ServerFunctions.getRequestUrl(); + } + render() { + return /*#__PURE__*/ $runtime.element("div", { + "data-hydrated": this.hydrated, + __source: { + fileName: "C:\\Repositories\\nullstack\\nullstack\\tests\\src\\ServerFunctions.njs", + lineNumber: 90, + columnNumber: 7 + }, + __self: this + }, /*#__PURE__*/ $runtime.element("button", { + class: "set-count-to-one", + onclick: this.setCountToOne, + source: this, + __source: { + fileName: "C:\\Repositories\\nullstack\\nullstack\\tests\\src\\ServerFunctions.njs", + lineNumber: 91, + columnNumber: 9 + }, + __self: this + }, "1"), /*#__PURE__*/ $runtime.element("button", { + class: "set-count-to-two", + onclick: this.setCountToTwo, + source: this, + __source: { + fileName: "C:\\Repositories\\nullstack\\nullstack\\tests\\src\\ServerFunctions.njs", + lineNumber: 94, + columnNumber: 9 + }, + __self: this + }, "2"), /*#__PURE__*/ $runtime.element("button", { + class: "set-date", + onclick: this.setDate, + source: this, + __source: { + fileName: "C:\\Repositories\\nullstack\\nullstack\\tests\\src\\ServerFunctions.njs", + lineNumber: 97, + columnNumber: 9 + }, + __self: this + }, "1992"), /*#__PURE__*/ $runtime.element("div", { + "data-count": this.count, + __source: { + fileName: "C:\\Repositories\\nullstack\\nullstack\\tests\\src\\ServerFunctions.njs", + lineNumber: 100, + columnNumber: 9 + }, + __self: this + }), /*#__PURE__*/ $runtime.element("div", { + "data-year": this.year, + __source: { + fileName: "C:\\Repositories\\nullstack\\nullstack\\tests\\src\\ServerFunctions.njs", + lineNumber: 101, + columnNumber: 9 + }, + __self: this + }), /*#__PURE__*/ $runtime.element("div", { + "data-statement": this.statement, + __source: { + fileName: "C:\\Repositories\\nullstack\\nullstack\\tests\\src\\ServerFunctions.njs", + lineNumber: 102, + columnNumber: 9 + }, + __self: this + }), /*#__PURE__*/ $runtime.element("div", { + "data-response": this.response, + __source: { + fileName: "C:\\Repositories\\nullstack\\nullstack\\tests\\src\\ServerFunctions.njs", + lineNumber: 103, + columnNumber: 9 + }, + __self: this + }), /*#__PURE__*/ $runtime.element("div", { + "data-double-plus-one-server": this.doublePlusOneServer === 69, + __source: { + fileName: "C:\\Repositories\\nullstack\\nullstack\\tests\\src\\ServerFunctions.njs", + lineNumber: 104, + columnNumber: 9 + }, + __self: this + }), /*#__PURE__*/ $runtime.element("div", { + "data-double-plus-one-client": this.doublePlusOneClient === 69, + __source: { + fileName: "C:\\Repositories\\nullstack\\nullstack\\tests\\src\\ServerFunctions.njs", + lineNumber: 105, + columnNumber: 9 + }, + __self: this + }), /*#__PURE__*/ $runtime.element("div", { + "data-accepts-special-characters": this.acceptsSpecialCharacters, + __source: { + fileName: "C:\\Repositories\\nullstack\\nullstack\\tests\\src\\ServerFunctions.njs", + lineNumber: 106, + columnNumber: 9 + }, + __self: this + }), /*#__PURE__*/ $runtime.element("div", { + "data-underline-removed-from-client": this.underlineRemovedFromClient, + __source: { + fileName: "C:\\Repositories\\nullstack\\nullstack\\tests\\src\\ServerFunctions.njs", + lineNumber: 107, + columnNumber: 9 + }, + __self: this + }), /*#__PURE__*/ $runtime.element("div", { + "data-underline-stay-on-server": this.underlineStayOnServer, + __source: { + fileName: "C:\\Repositories\\nullstack\\nullstack\\tests\\src\\ServerFunctions.njs", + lineNumber: 108, + columnNumber: 9 + }, + __self: this + }), /*#__PURE__*/ $runtime.element("div", { + "data-hydrated-original-url": this.hydratedOriginalUrl, + __source: { + fileName: "C:\\Repositories\\nullstack\\nullstack\\tests\\src\\ServerFunctions.njs", + lineNumber: 109, + columnNumber: 9 + }, + __self: this + }), /*#__PURE__*/ $runtime.element("div", { + "data-original-url": this.originalUrl, + __source: { + fileName: "C:\\Repositories\\nullstack\\nullstack\\tests\\src\\ServerFunctions.njs", + lineNumber: 110, + columnNumber: 9 + }, + __self: this + })); + } +} +export default ServerFunctions; +$runtime.accept(module, "src\\ServerFunctions.njs", [ + "nullstack/runtime", + "nullstack", + "fs" +], [ + { + klass: ServerFunctions, + initiate: [ + "useNodeFileSystem", + "useFetchInNode", + "getDoublePlusOne", + "getRequestUrl" + ], + hashes: { + getCount: "64ef23a80103627bea3edaeb8f533934", + useFetchInNode: "3b93e3a8e5dc19945360e9523e0ded32", + getCountAsOne: "771a1b57771dd29a59b9c155a8d8db56", + getPrivateFunction: "f0f780a14ce2ed58903e2b33ccf0c955", + getDate: "4f8e7ea6c02ae89b19432d3e3a73da8e", + getDoublePlusOne: "1dd6a327579482f27a02eaf5a4bf8ac0", + useNodeFileSystem: "243b666d0b221adf5809bf54d21829d5", + getEncodedString: "88a0ffa79f8ec5a5d9aa928d61f79e68", + _privateFunction: "c0990f04929c14e0294dcefcfc80fab4", + getRequestUrl: "7ee4471ded1f135c3cfce3544a56c3a2" + } + } +]); +$runtime.register(ServerFunctions, "getCountAsOne"); +$runtime.register(ServerFunctions, "getCount"); +$runtime.register(ServerFunctions, "getDate"); +$runtime.register(ServerFunctions, "useNodeFileSystem"); +$runtime.register(ServerFunctions, "useFetchInNode"); +$runtime.register(ServerFunctions, "getDoublePlusOne"); +$runtime.register(ServerFunctions, "getEncodedString"); +$runtime.register(ServerFunctions, "getPrivateFunction"); +$runtime.register(ServerFunctions, "getRequestUrl"); +$runtime.register(ServerFunctions); diff --git a/tests/src/Application.njs b/tests/src/Application.njs index b83a28ea..30a1a726 100644 --- a/tests/src/Application.njs +++ b/tests/src/Application.njs @@ -1,7 +1,6 @@ import Nullstack from 'nullstack' import AnchorModifiers from './AnchorModifiers' -import './Application.css' import ArrayAttributes from './ArrayAttributes' import BodyFragment from './BodyFragment' import CatchError from './CatchError' @@ -30,7 +29,6 @@ import InstanceSelf from './InstanceSelf' import IsomorphicImport from './IsomorphicImport' import IsomorphicStartup from './IsomorphicStartup' import JavaScriptExtension from './JavaScriptExtension' -// import LazyComponentLoader from './LazyComponentLoader' import Logo from './Logo' import MetatagState from './MetatagState' import NestedProxy from './NestedProxy' @@ -57,22 +55,11 @@ import UndefinedNodes from './UndefinedNodes' import UnderscoredAttributes from './UnderscoredAttributes' import Vunerability from './Vunerability' import WebpackCustomPlugin from './WebpackCustomPlugin' -import WindowDependency from './WindowDependency' import WorkerVerbs from './WorkerVerbs' -import LazyComponent from './LazyComponent.njs' - -let CachedLazyComponent -function LazyImporter() { - if (!CachedLazyComponent) { - CachedLazyComponent = import('./LazyComponent') - CachedLazyComponent.then((mod) => CachedLazyComponent = mod.default) - return false - } - if (CachedLazyComponent instanceof Promise) { - return false - } - return -} +import LazyComponent from './LazyComponent' +import LazyComponentLoader from './LazyComponentLoader' +import './Application.css' +import NestedFolder from './nested/NestedFolder.njs' class Application extends Nullstack { @@ -80,10 +67,6 @@ class Application extends Nullstack { await instances.instanceable.customMethod() } - static async safelist() { - console.log(LazyComponent) - } - prepare(context) { context.string = 'nullstack' context.refInstanceCount = 0 @@ -91,7 +74,7 @@ class Application extends Nullstack { render({ project, page, environment, refInstanceCount }) { return ( - +

{project.name}

{page.status !== 200 &&
}
@@ -107,7 +90,6 @@ class Application extends Nullstack { error-on-child-node?serialization=true #bottom
- @@ -142,7 +124,7 @@ class Application extends Nullstack { - {/* */} + @@ -164,6 +146,8 @@ class Application extends Nullstack { + + ) diff --git a/tests/src/Application.test.js b/tests/src/Application.test.js index 8a4dd3b4..bb563435 100644 --- a/tests/src/Application.test.js +++ b/tests/src/Application.test.js @@ -15,9 +15,4 @@ describe('Application', () => { const element = await page.$('[rel="stylesheet"]') expect(element).toBeTruthy() }) - - test('simple scripts depending on window should be importable on server', async () => { - const element = await page.$('[data-window="shim"]') - expect(element).toBeTruthy() - }) }) diff --git a/tests/src/BodyFragment.test.js b/tests/src/BodyFragment.test.js index b95624bf..6860906d 100644 --- a/tests/src/BodyFragment.test.js +++ b/tests/src/BodyFragment.test.js @@ -52,9 +52,9 @@ describe('BodyFragment', () => { test('the body removes events when the fragment leaves the tree', async () => { await page.waitForSelector('body[data-hydrated]') await page.click('[href="/"]') - await page.waitForSelector('[data-window="shim"]:not([data-count])') + await page.waitForSelector('[data-application-hydrated]:not([data-count])') await page.click('body') - const element = await page.$('[data-window="shim"]:not([data-count])') + const element = await page.$('[data-application-hydrated]:not([data-count])') expect(element).toBeTruthy() }) }) diff --git a/tests/src/LazyComponent.njs b/tests/src/LazyComponent.njs index 8d40289a..62b6605c 100644 --- a/tests/src/LazyComponent.njs +++ b/tests/src/LazyComponent.njs @@ -4,17 +4,19 @@ import './LazyComponent.css' class LazyComponent extends Nullstack { static async serverFunctionWorks() { - return 1 + return true } async initiate() { this.safelisted = await this.serverFunctionWorks() } - render() { + render({ prop }) { return ( -
- LazyComponent: {this.safelisted}!!! +
+

safelisted: {this.safelisted}

+

prop: {prop}

+ home
) } diff --git a/tests/src/WindowDependency.js b/tests/src/WindowDependency.js deleted file mode 100644 index b1806955..00000000 --- a/tests/src/WindowDependency.js +++ /dev/null @@ -1,3 +0,0 @@ -window.key = 'shim' - -export default window diff --git a/tests/src/nested/NestedFolder.njs b/tests/src/nested/NestedFolder.njs new file mode 100644 index 00000000..fac716cc --- /dev/null +++ b/tests/src/nested/NestedFolder.njs @@ -0,0 +1,17 @@ +import Nullstack from 'nullstack' +import LazyComponent from '../LazyComponent' + +class NestedFolder extends Nullstack { + + render() { + return ( +
+ NestedFolder + +
+ ) + } + +} + +export default NestedFolder; \ No newline at end of file diff --git a/webpack/experiments.js b/webpack/experiments.js index 8732bde1..1762de2e 100644 --- a/webpack/experiments.js +++ b/webpack/experiments.js @@ -1,5 +1,5 @@ function experiments(options) { - if (options.environment !== 'development') return false + if (options.environment !== 'development') return return { lazyCompilation: { entries: false, diff --git a/webpack/module.js b/webpack/module.js index 7e50d631..c5532fcd 100644 --- a/webpack/module.js +++ b/webpack/module.js @@ -162,10 +162,19 @@ function runtime(options) { } } +function debug(options) { + if (options.target !== 'server') return + return { + test: /\.(nts|tsx|njs|jsx)$/, + loader: path.posix.join(options.configFolder, 'loaders', 'debug.js'), + } +} + function rules(options) { return [ css(options), scss(options), + debug(options), js(options), ts(options), njs(options),