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