Skip to content
Merged

Next #260

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
125 commits
Select commit Hold shift + click to select a range
0f580e8
:zap: optimized events
Mortaro May 9, 2022
d614ca1
:zap: optimized events
Mortaro May 9, 2022
b41d6e3
:zap: skip removing listeners
Mortaro May 9, 2022
235be93
:mute: remove console log
Mortaro May 10, 2022
7c8a6d1
:zap: weak ref events
Mortaro May 10, 2022
cff4275
Merge pull request #232 from nullstack/master
Mortaro May 10, 2022
3a4ad18
:construction: optimizing renderers
Mortaro May 12, 2022
dfb103e
Merge branch 'optimized-events' of https://github.com/nullstack/nulls…
Mortaro May 12, 2022
7e4034c
:construction: fixing render with element
Mortaro May 12, 2022
75b724e
Merge branch 'master' of https://github.com/nullstack/nullstack into …
Mortaro May 12, 2022
c9aa16f
:white_check_mark: tests for dynamic head
Mortaro May 13, 2022
4b758a9
:construction: head ternaries
Mortaro May 13, 2022
57480fe
:wrench: remove puppeteer default
Mortaro May 13, 2022
b9fe990
:zap: clean self elements faster
Mortaro May 14, 2022
49f9b29
:zap: faster depth
Mortaro May 14, 2022
e36d877
:zap: faster anchorables
Mortaro May 14, 2022
7758cca
:zap^Cfaster head and textarea
Mortaro May 14, 2022
8422f6c
:zap: faster branches
Mortaro May 14, 2022
8395790
:zap: faster element
Mortaro May 14, 2022
128a9ce
:zap: faster compare
Mortaro May 14, 2022
2a4407c
:recycle: cleaner ssr dom
Mortaro May 14, 2022
eed0986
:recycle: cleaner typeof
Mortaro May 14, 2022
83be1ce
:zap: faster text patches
Mortaro May 14, 2022
b68d3a0
:construction: body fragment and array attributes
Mortaro May 15, 2022
ad76377
:bug: fix service worker split
Mortaro May 15, 2022
04196c8
:zap: faster object events
Mortaro May 15, 2022
37f4b53
:construction: style children
Mortaro May 15, 2022
9ed196c
:adhesive_bandage: small style patches
Mortaro May 15, 2022
152815d
:white_check_mark: body fragment tests
Mortaro May 16, 2022
4aaa805
:white_check_mark: tests for style children
Mortaro May 16, 2022
d4f0cbd
:white_check_mark: tests for array attributes
Mortaro May 16, 2022
a098019
:white_check_mark: advanced anchor tests
Mortaro May 16, 2022
31762f9
:fire: remove hydration from spa
Mortaro May 21, 2022
a0421bd
:sparkles: spa dynamic head
Mortaro May 21, 2022
3eb376b
:zap: faster spa hydration
Mortaro May 22, 2022
908ecaf
:zap: faster head ternaries
Mortaro May 22, 2022
7e1af3e
:sparkles: null returning components
Mortaro May 22, 2022
217d73e
:zap: faster bindings
Mortaro May 22, 2022
df19c3f
:zap: faster plugins
Mortaro May 22, 2022
94f166c
:zap: faster heads
Mortaro May 22, 2022
f030e4b
:zap: faster attribute updates
Mortaro May 22, 2022
23bafc9
Merge branch 'master' of https://github.com/nullstack/nullstack into …
Mortaro May 24, 2022
faf2f83
Add support for absolute links and router paths
brunolm May 25, 2022
aaf30d0
Update tests/src/RoutesAndParams.test.js
brunolm May 26, 2022
99dc39e
Update tests/src/RoutesAndParams.test.js
brunolm May 26, 2022
d45da65
Rename ssr
brunolm May 26, 2022
53c3b77
Merge pull request #245 from brunolm/feature/absolute-routes-and-rout…
Mortaro May 26, 2022
edac106
Merge branch 'extremely-unstable-next' of https://github.com/nullstac…
Mortaro May 26, 2022
500b9f9
:poop: temporarily remove url schema tests
Mortaro May 26, 2022
cd26df9
:construction: scroll to hash
Mortaro May 26, 2022
ec815fb
:wrench: reenable headless tests
Mortaro May 27, 2022
3dfaa9d
:white_check_mark: fix tests on upgraded puppeteer
Mortaro May 27, 2022
418b4e3
:bookmark: version 0.16.0
Mortaro May 28, 2022
b76fc78
:bookmark: version 0.16.0 but without typos
Mortaro May 28, 2022
6eeb79a
:construction: hot module replacement
Mortaro May 30, 2022
811a01c
:sparkles: inject hmr
Mortaro May 30, 2022
3add8a2
:wrench: custom middleware that breaks tests
Mortaro May 30, 2022
cd71a57
:white_check_mark: moving tests to prod mode
Mortaro May 30, 2022
4990dec
:sparkles: hot module replacement
Mortaro Jun 1, 2022
8ae229b
:sparkles: hrm over a single server
Mortaro Jun 3, 2022
42f36fe
:lipstick: better console messages
Mortaro Jun 3, 2022
8f5225d
:recycle: better build errors
Mortaro Jun 4, 2022
1679c2d
:sparkles: new binds and refs
Mortaro Jun 5, 2022
6c33ccc
:recycle: deprecate data
Mortaro Jun 5, 2022
2c56ba6
:bug: fixing port print
Mortaro Jun 5, 2022
cd430c9
:bug: fix server start
Mortaro Jun 6, 2022
e324cf7
:white_check_mark: zero attributes test
Mortaro Jun 6, 2022
661916c
:sparkles: optional start modes
Mortaro Jun 6, 2022
cf33c55
Merge pull request #247 from nullstack/breaking-bind
Mortaro Jun 6, 2022
6401bdd
:adhesive_bandage: full reload after error
Mortaro Jun 7, 2022
71be3de
:fire: remove unused hot error
Mortaro Jun 7, 2022
4e26540
Merge pull request #246 from nullstack/unstable-hmr
Mortaro Jun 7, 2022
d2cb59a
Merge pull request #237 from nullstack/extremely-unstable-next
Mortaro Jun 7, 2022
31d8be7
:bug: truthy client render
Mortaro Jun 7, 2022
0fa10e5
:pushpin: lower node loader version
Mortaro Jun 7, 2022
013d4e4
:pushpin: lower script version
Mortaro Jun 7, 2022
0a355e8
:wrench: move script test to production
Mortaro Jun 7, 2022
a4e7481
fix external components binding
jayremias Jun 7, 2022
3cb5de4
update input name, add test case
jayremias Jun 7, 2022
72b5716
frontend test
jayremias Jun 8, 2022
2fb2287
Merge pull request #249 from jayremias/unstable-next
Mortaro Jun 8, 2022
713eeaf
:white_check_mark: update order of dom for tests
Mortaro Jun 8, 2022
2a46d10
:sparkles: debounced events
Mortaro Jun 9, 2022
d737897
:sparkles: remove debounce from dom
Mortaro Jun 11, 2022
d0307f2
Merge pull request #250 from nullstack/debounced-events
Mortaro Jun 11, 2022
92e69c1
:recycle: more consistent underscored attributes
Mortaro Jun 11, 2022
edfb535
:sparkles: bind noop and inner references
Mortaro Jun 15, 2022
4459591
:sparkles: improved refs
Mortaro Jun 19, 2022
edb4904
:white_check_mark: instance ref tests
Mortaro Jun 19, 2022
5e3b80e
:zap: faster simpler proxies
Mortaro Jun 19, 2022
f3efaaa
Fix ref undefined
jayremias Jun 21, 2022
c05952d
Fix mistaken Comparison Operators
jayremias Jun 21, 2022
c0ecf8c
better approach
jayremias Jun 21, 2022
892b83c
Merge pull request #251 from jayremias/unstable-next
Mortaro Jun 21, 2022
a8c7a04
Fix bindable
jayremias Jul 8, 2022
065bb09
:sparkles: static server functions with context
Mortaro Jul 24, 2022
c759076
:wrench: webpack dependency
Mortaro Jul 24, 2022
d712523
Merge pull request #253 from jayremias/fix-bindable
Mortaro Jul 24, 2022
2d26346
:white_check_mark: tests for nested undeclared bind
Mortaro Jul 24, 2022
9e88bcb
:construction: improved webpack servers
Mortaro Jul 30, 2022
77de101
:zap: improved dev server
Mortaro Jul 30, 2022
ae9d11b
:zap: fast ssr reload
Mortaro Jul 31, 2022
f753e93
:bug: fix static scopes
Mortaro Aug 2, 2022
30e3bdc
Merge pull request #256 from nullstack/ridiculously-unstable-next
Mortaro Aug 4, 2022
2916fd4
:construction: extension mode
Mortaro Aug 9, 2022
b96c6f4
:white_check_mark: tests
Mortaro Aug 9, 2022
ed2a45c
:sparkles: dev server port check
Mortaro Aug 9, 2022
4debd12
Merge pull request #257 from nullstack/unstable-extension
Mortaro Aug 9, 2022
c671ff9
:wrench: lower node requirement
Mortaro Aug 9, 2022
0cbb344
Fix undefined cb data breaking the server
jayremias Aug 9, 2022
508c6b2
Merge pull request #258 from jayremias/fix-undefined-data-callback-error
Mortaro Aug 9, 2022
5efe0ff
:wrench: wait for server ping
Mortaro Aug 14, 2022
2ab0a1c
:wrench: configure test timeout
Mortaro Aug 14, 2022
aa0e8d0
:wrench: configure test timeout
Mortaro Aug 14, 2022
0ad51a5
:wrench: configure test timeout
Mortaro Aug 14, 2022
f1e6dde
:sparkles: client.js hot reload
Mortaro Aug 16, 2022
8048850
allow zero when binding value + type fixes
mayconfsousa Aug 19, 2022
911ecc9
Merge branch 'unstable-next' into fix/bind_value_allow_zero
Mortaro Aug 19, 2022
3ee1d07
Merge pull request #259 from mayconfsousa/fix/bind_value_allow_zero
Mortaro Aug 19, 2022
9d5f83a
:fire: remove old segment logic
Mortaro Aug 20, 2022
d757f87
Merge branch 'unstable-next' of https://github.com/nullstack/nullstac…
Mortaro Aug 20, 2022
fd631de
:bug: fix livereload bug
Mortaro Aug 20, 2022
c1d09a2
:wrench: finalize cli options
Mortaro Aug 21, 2022
439b80f
:bug: skip cleaning non existing dir
Mortaro Aug 21, 2022
8039311
Merge pull request #248 from nullstack/unstable-next
Mortaro Aug 21, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<img src='https://raw.githubusercontent.com/nullstack/nullstack/master/nullstack.png' height='60' alt='Nullstack'>

Feature-Driven Fullstack JavaScript Components
Feature-Driven Full Stack JavaScript Components

## What is Nullstack?

Nullstack is a Feature-Driven fullstack JavaScript framework that helps you build isomorphic applications and stay focused on shipping products to production.
Nullstack is a feature-driven full stack JavaScript framework that helps you build isomorphic applications and stay focused on shipping products to production.

Write the backend and frontend of a feature in a single component and let the framework decide where the code should run.

Expand Down
2 changes: 0 additions & 2 deletions client/anchorableNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import router from './router'
export function anchorableElement(element) {
const links = element.querySelectorAll('a[href^="/"]:not([target])')
for (const link of links) {
if (link.dataset.nullstack) return
link.dataset.nullstack = true
link.addEventListener('click', (event) => {
if (!event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) {
event.preventDefault()
Expand Down
39 changes: 27 additions & 12 deletions client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import router from './router'
const client = {}

client.initialized = false
client.hydrated = false
client.initializer = null
client.instances = {}
context.instances = client.instances
Expand All @@ -20,6 +19,12 @@ client.selector = null
client.events = {}
client.generateContext = generateContext
client.renderQueue = null
client.currentBody = {}
client.nextBody = {}
client.currentHead = []
client.nextHead = []
client.head = document.head
client.body = document.body

client.update = async function update() {
if (client.initialized) {
Expand All @@ -30,35 +35,45 @@ client.update = async function update() {
scope.plugins = loadPlugins(scope)
client.initialized = false
client.renewalQueue = []
client.nextVirtualDom = await generateTree(client.initializer(), scope)
rerender(client.selector)
client.virtualDom = client.nextVirtualDom
client.nextVirtualDom = null
client.processLifecycleQueues()
try {
client.nextVirtualDom = await generateTree(client.initializer(), scope)
rerender()
client.processLifecycleQueues()
} catch (e) {
client.skipHotReplacement = true
console.error(e)
}
}, 16)
}
}

client.processLifecycleQueues = async function processLifecycleQueues() {
if (!client.initialized) {
client.initialized = true
client.hydrated = true
}
let shouldUpdate = false
let shouldScroll = router._hash
while (client.initiationQueue.length) {
const instance = client.initiationQueue.shift()
instance.initiate && await instance.initiate()
instance._self.initiated = true
instance.initiated = true
instance.launch && instance.launch()
shouldUpdate = true
if (instance._attributes.route && shouldScroll) {
const element = document.getElementById(router._hash)
if (element) {
element.scrollIntoView({ behavior: 'smooth' })
}
shouldScroll = false
}
}
shouldUpdate && client.update()
shouldUpdate = false
while (client.realHydrationQueue.length) {
shouldUpdate = true
const instance = client.realHydrationQueue.shift()
instance.hydrate && await instance.hydrate()
instance._self.hydrated = true
instance.hydrated = true
}
shouldUpdate && client.update()
shouldUpdate = false
Expand All @@ -70,10 +85,10 @@ client.processLifecycleQueues = async function processLifecycleQueues() {
shouldUpdate && client.update()
for (const key in client.instances) {
const instance = client.instances[key]
if (!client.renewalQueue.includes(instance) && !instance._self.terminated) {
if (!client.renewalQueue.includes(instance) && !instance.terminated) {
instance.terminate && await instance.terminate()
if (instance._self.persistent) {
instance._self.terminated = true
if (instance.persistent) {
instance.terminated = true
} else {
delete client.instances[key]
}
Expand Down
8 changes: 6 additions & 2 deletions client/environment.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import state from './state';
const environment = { ...state.environment, client: true, server: false };

Object.freeze(environment);
const environment = {
...state.environment,
client: true,
server: false,
event: 'nullstack.environment'
};

export default environment;
72 changes: 72 additions & 0 deletions client/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import router from './router'
import { camelize } from '../shared/string';
import noop from '../shared/noop'

export const eventCallbacks = new WeakMap()
export const eventSubjects = new WeakMap()
export const eventDebouncer = new WeakMap()

function executeEvent(callback, subject, event, data) {
if (typeof callback === 'object') {
Object.assign(subject.source, callback);
} else {
callback({ ...subject, event, data });
}
}

function debounce(selector, name, time, callback) {
if (!time) {
callback()
} else {
const eventMap = eventDebouncer.get(selector) || {}
clearTimeout(eventMap[name])
eventMap[name] = setTimeout(callback, time)
eventDebouncer.set(selector, eventMap)
}
}

export function generateCallback(selector, name) {
return function eventCallback(event) {
const subject = eventSubjects.get(selector)
if (!subject) return
if (subject.href) {
if (!event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) {
event.preventDefault()
router.url = subject.href
}
} else if (subject.default !== true) {
event.preventDefault();
}
debounce(selector, name, subject.debounce, () => {
const data = { ...subject.data }
for (const attribute in subject) {
if (attribute.startsWith('data-')) {
const key = camelize(attribute.slice(5));
data[key] = subject[attribute];
}
}
if (subject?.bind !== undefined) {
const valueName = (subject.type === 'checkbox' || subject.type === 'radio') ? 'checked' : 'value'
const object = subject.bind.object
const property = subject.bind.property
if (valueName === 'checked') {
object[property] = event.target[valueName];
} else if (object[property] === true || object[property] === false) {
object[property] = event.target[valueName] === 'true';
} else if (typeof object[property] === 'number') {
object[property] = +event.target[valueName] || 0;
} else {
object[property] = event.target[valueName];
}
}
if (subject[name] === noop) return
if (Array.isArray(subject[name])) {
for (const subcallback of subject[name]) {
executeEvent(subcallback, subject, event, data)
}
} else {
executeEvent(subject[name], subject, event, data)
}
})
}
};
50 changes: 50 additions & 0 deletions client/hydrate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ref } from './ref';
import { isFalse } from '../shared/nodes';
import { anchorableElement } from './anchorableNode';
import client from './client';

let pool = []

function hydrateBody(selector, node) {
if (node?.attributes?.html) {
anchorableElement(selector);
}
node.element = selector
ref(node.attributes, selector)
for (const element of selector.childNodes) {
if ((element.tagName === 'TEXTAREA' || element.tagName === 'textarea') && element.childNodes.length === 0) {
element.appendChild(document.createTextNode(''));
} else if (element.COMMENT_NODE === 8 && element.textContent === '#') {
pool.push(element.remove())
}
}
if (!node.children) return
const limit = node.children.length;
for (let i = limit - 1; i > -1; i--) {
if (node.type !== 'head' && typeof selector?.childNodes?.[i] === 'undefined') {
console.error(
`${node.type.toUpperCase()} expected tag ${node.children[i].type.toUpperCase()} to be child at index ${i} but instead found undefined. This error usually happens because of an invalid HTML hierarchy or changes in comparisons after serialization.`,
selector
)
throw new Error('Virtual DOM does not match the DOM.')
}
hydrateBody(selector.childNodes[i], node.children[i])
}
}

function hydrateHead() {
for (const node of client.nextHead) {
if (isFalse(node)) {
node.element = pool.pop() || document.createComment("")
client.head.append(node.element)
} else {
node.element = document.getElementById(node.attributes.id)
}
}
pool = null
}

export default function hydrate(selector, node) {
hydrateBody(selector, node)
hydrateHead()
}
83 changes: 63 additions & 20 deletions client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@ import state from './state'
import element from '../shared/element';
import fragment from '../shared/fragment';
import generateTree from '../shared/generateTree';
import getProxyableMethods from '../shared/getProxyableMethods';
import { loadPlugins, usePlugins } from '../shared/plugins';
import { loadPlugins, useClientPlugins } from '../shared/plugins';
import client from './client';
import context, { generateContext } from './context';
import environment from './environment';
import instanceProxyHandler from './instanceProxyHandler';
import instanceProxyHandler, { instanceProxies } from './instanceProxyHandler';
import invoke from './invoke';
import './liveReload';
import page from './page';
import params, { updateParams } from './params';
import project from './project';
import render from './render';
import rerender from './rerender';
import hydrate from './hydrate';
import router from './router';
import settings from './settings';
import worker from './worker';
import klassMap from './klassMap';
import windowEvent from './windowEvent'

context.page = page;
context.router = router;
Expand All @@ -40,18 +41,25 @@ export default class Nullstack {
static element = element;
static invoke = invoke;
static fragment = fragment;
static use = usePlugins('client');
static use = useClientPlugins;
static klassMap = {}
static context = generateContext({})

static start(Starter) {
setTimeout(async () => {
window.addEventListener('popstate', () => {
router._popState();
});
if (client.initializer) {
client.initializer = () => element(Starter);
client.update()
return this.context
}
client.routes = {};
updateParams(router.url);
client.currentInstance = null;
client.initializer = () => element(Starter);
client.selector = document.querySelector('#application');
client.selector = document.getElementById('application');
if (environment.mode === 'spa') {
scope.plugins = loadPlugins(scope);
worker.online = navigator.onLine;
Expand All @@ -63,39 +71,74 @@ export default class Nullstack {
client.selector = body
} else {
client.virtualDom = await generateTree(client.initializer(), scope);
hydrate(client.selector, client.virtualDom)
client.currentBody = client.nextBody
client.currentHead = client.nextHead
client.nextBody = {}
client.nextHead = []
context.environment = environment;
scope.plugins = loadPlugins(scope);
worker.online = navigator.onLine;
typeof context.start === 'function' && await context.start(context);
client.nextVirtualDom = await generateTree(client.initializer(), scope);
rerender(client.selector);
client.virtualDom = client.nextVirtualDom;
client.nextVirtualDom = null;
rerender();
}
client.processLifecycleQueues();
delete state.context;
}, 0)
return generateContext({});
return this.context;
}

_self = {
prerendered: false,
initiated: false,
hydrated: false,
terminated: false,
}
prerendered = false
initiated = false
hydrated = false
terminated = false
key = null

constructor() {
const methods = getProxyableMethods(this);
const proxy = new Proxy(this, instanceProxyHandler);
for (const method of methods) {
this[method] = this[method].bind(proxy);
}
instanceProxies.set(this, proxy)
return proxy;
}

render() {
return false;
}

}

if (module.hot) {
const socket = new WebSocket('ws' + router.base.slice(4) + '/ws');
window.lastHash
socket.onmessage = async function (e) {
const data = JSON.parse(e.data)
if (data.type === 'NULLSTACK_SERVER_STARTED') {
(window.needsReload || !environment.hot) && window.location.reload()
} else if (data.type === 'hash') {
const newHash = data.data.slice(20)
if (newHash === window.lastHash) {
window.needsReload = true
} else {
window.lastHash = newHash
}
}
};
Nullstack.updateInstancesPrototypes = function updateInstancesPrototypes(hash, klass) {
for (const key in context.instances) {
const instance = context.instances[key]
if (instance.constructor.hash === hash) {
Object.setPrototypeOf(instance, klass.prototype);
}
}
klassMap[hash] = klass
}
Nullstack.hotReload = function hotReload(klass) {
if (client.skipHotReplacement) {
window.location.reload()
} else {
Nullstack.start(klass);
windowEvent('environment');
}
}
module.hot.decline()
}
Loading