diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml
index 30df7a2e..75248b85 100644
--- a/.github/workflows/pr-tests.yml
+++ b/.github/workflows/pr-tests.yml
@@ -21,13 +21,16 @@ jobs:
steps:
- uses: actions/checkout@v2
+ - uses: pnpm/action-setup@v2
+ with:
+ version: 7
# cache the dependencies from any node_modules directory
- name: Cache dependencies
uses: actions/cache@v2
with:
path: |
**/node_modules
- **/package-lock.json
+ **/pnpm-lock.yaml
key: node_modules-${{ matrix.node-version }}-${{ hashFiles('**/package.json') }}
- name: Use Node.js ${{ matrix.node-version }}
@@ -37,15 +40,15 @@ jobs:
- name: Install and link main deps
run: |
- npm install
- npm link
+ pnpm install
+ pnpm link --global
- name: Install deps at tests folder
working-directory: ./tests
run: |
- npm link nullstack
- npm install
+ pnpm link nullstack --global
+ pnpm install
- name: Run tests
working-directory: ./tests
- run: npm test
\ No newline at end of file
+ run: pnpm test
\ No newline at end of file
diff --git a/client/client.js b/client/client.js
index 18c993fa..a95b8e5b 100644
--- a/client/client.js
+++ b/client/client.js
@@ -19,8 +19,8 @@ client.selector = null
client.events = {}
client.generateContext = generateContext
client.renderQueue = null
-client.currentBody = {}
-client.nextBody = {}
+client.currentMeta = { body: {}, html: {}, window: {} }
+client.nextMeta = { body: {}, html: {}, window: {} }
client.currentHead = []
client.nextHead = []
client.head = document.head
diff --git a/client/events.js b/client/events.js
index cab01448..718c2850 100644
--- a/client/events.js
+++ b/client/events.js
@@ -6,12 +6,19 @@ 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 })
+export function generateSubject(selector, attributes, name) {
+ if (Array.isArray(attributes[name])) {
+ for (let i = 0; i < attributes[name].length; i++) {
+ if (typeof attributes[name][i] === 'object') {
+ let changeset = attributes[name][i]
+ attributes[name][i] = () => Object.assign(attributes.source, changeset)
+ }
+ }
+ } else if (typeof attributes[name] === 'object') {
+ let changeset = attributes[name]
+ attributes[name] = () => Object.assign(attributes.source, changeset)
}
+ eventSubjects.set(selector, attributes)
}
function debounce(selector, name, time, callback) {
@@ -60,6 +67,12 @@ export function generateCallback(selector, name) {
object[property] = event.target[valueName] === 'true'
} else if (typeof object[property] === 'number') {
object[property] = +event.target[valueName] || 0
+ } else if (object[property] instanceof Date) {
+ const [yyyy, mm, dd] = event.target[valueName].split('-')
+ object[property].setFullYear(yyyy)
+ object[property].setMonth(+mm - 1)
+ object[property].setDate(dd)
+ object[property] = object[property]
} else {
object[property] = event.target[valueName]
}
@@ -67,10 +80,10 @@ export function generateCallback(selector, name) {
if (subject[name] === noop) return
if (Array.isArray(subject[name])) {
for (const subcallback of subject[name]) {
- executeEvent(subcallback, subject, event, data)
+ subcallback({ ...subject, event, data })
}
} else {
- executeEvent(subject[name], subject, event, data)
+ subject[name]({ ...subject, event, data })
}
})
}
diff --git a/client/index.js b/client/index.js
index 60660942..202868fc 100644
--- a/client/index.js
+++ b/client/index.js
@@ -68,9 +68,9 @@ export default class Nullstack {
} else {
client.virtualDom = await generateTree(client.initializer(), scope)
hydrate(client.selector, client.virtualDom)
- client.currentBody = client.nextBody
+ client.currentMeta = client.nextMeta
client.currentHead = client.nextHead
- client.nextBody = {}
+ client.nextMeta = { body: {}, html: {}, window: {} }
client.nextHead = []
context.environment = environment
scope.plugins = loadPlugins(scope)
diff --git a/client/render.js b/client/render.js
index 14673b10..7fee3434 100644
--- a/client/render.js
+++ b/client/render.js
@@ -2,7 +2,7 @@ import { sanitizeInnerHtml } from '../shared/sanitizeString'
import generateTruthyString from '../shared/generateTruthyString'
import { isFalse, isText } from '../shared/nodes'
import { anchorableElement } from './anchorableNode'
-import { eventSubjects, generateCallback } from './events'
+import { generateCallback, generateSubject } from './events'
import { ref } from './ref'
export default function render(node, options) {
@@ -36,7 +36,7 @@ export default function render(node, options) {
const eventName = name.substring(2)
const callback = generateCallback(node.element, name)
node.element.addEventListener(eventName, callback)
- eventSubjects.set(node.element, node.attributes)
+ generateSubject(node.element, node.attributes, name)
}
} else {
let nodeValue
diff --git a/client/rerender.js b/client/rerender.js
index 34f93f4d..cd788f67 100644
--- a/client/rerender.js
+++ b/client/rerender.js
@@ -3,7 +3,7 @@ import generateTruthyString from '../shared/generateTruthyString'
import { isFalse, isText, isUndefined } from '../shared/nodes'
import { anchorableElement } from './anchorableNode'
import client from './client'
-import { eventCallbacks, eventSubjects, generateCallback } from './events'
+import { eventCallbacks, eventSubjects, generateCallback, generateSubject } from './events'
import { reref } from './ref'
import render from './render'
@@ -36,6 +36,7 @@ function updateAttributes(selector, currentAttributes, nextAttributes) {
if (!callback) {
selector.addEventListener(eventName, generateCallback(selector, name))
}
+ generateSubject(selector, nextAttributes, name)
eventSubjects.set(selector, nextAttributes)
}
}
@@ -156,12 +157,14 @@ function _rerender(current, next) {
export default function rerender() {
_rerender(client.virtualDom, client.nextVirtualDom)
- updateAttributes(client.body, client.currentBody, client.nextBody)
+ updateAttributes(client.body, client.currentMeta.body, client.nextMeta.body)
+ updateAttributes(window, client.currentMeta.window, client.nextMeta.window)
+ updateAttributes(client.body.parentElement, client.currentMeta.html, client.nextMeta.html)
updateHeadChildren(client.currentHead, client.nextHead)
client.virtualDom = client.nextVirtualDom
client.nextVirtualDom = null
- client.currentBody = client.nextBody
- client.nextBody = {}
+ client.currentMeta = client.nextMeta
+ client.nextMeta = { body: {}, html: {}, window: {} }
client.currentHead = client.nextHead
client.nextHead = []
}
diff --git a/package.json b/package.json
index 9e7e4ba1..f8178938 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "nullstack",
- "version": "0.18.0",
- "description": "Full Stack Javascript Components for one-dev armies",
+ "version": "0.19.0",
+ "description": "Feature-Driven Full Stack JavaScript Components",
"main": "./types/index.d.ts",
"author": "Mortaro",
"repository": "github:nullstack/nullstack",
@@ -32,7 +32,7 @@
"css-loader": "6.7.3",
"css-minimizer-webpack-plugin": "^4.2.2",
"dotenv": "16.0.3",
- "eslint-plugin-nullstack": "0.0.12",
+ "eslint-plugin-nullstack": "0.0.26",
"express": "4.18.2",
"fs-extra": "11.1.0",
"lightningcss": "^1.19.0",
@@ -43,7 +43,7 @@
"sass-loader": "13.2.0",
"style-loader": "^3.3.1",
"swc-loader": "0.2.3",
- "swc-plugin-nullstack": "0.1.2",
+ "swc-plugin-nullstack": "0.1.3",
"terser-webpack-plugin": "5.3.6",
"time-analytics-webpack-plugin": "^0.1.20",
"webpack": "^5.0.0",
diff --git a/plugins/bindable.js b/plugins/bindable.js
index b3424084..1f8b27c7 100644
--- a/plugins/bindable.js
+++ b/plugins/bindable.js
@@ -12,6 +12,11 @@ function transform({ node, environment }) {
node.children = [object[property] ?? '']
} else if (node.type === 'input' && node.attributes.type === 'checkbox') {
node.attributes.checked = object[property]
+ } else if (node.type === 'input' && node.attributes.type === 'date' && object[property] instanceof Date) {
+ const yyyy = object[property].getFullYear()
+ const mm = (object[property].getMonth() + 1).toString().padStart(2, '0')
+ const dd = object[property].getDate().toString().padStart(2, '0')
+ node.attributes.value = `${yyyy}-${mm}-${dd}`
} else {
node.attributes.value = object[property] ?? ''
}
diff --git a/server/prerender.js b/server/prerender.js
index b6507080..be6f8ea2 100644
--- a/server/prerender.js
+++ b/server/prerender.js
@@ -31,7 +31,7 @@ export async function prerender(request, response) {
scope.body = ''
scope.context = context
scope.generateContext = generateContext(context)
- scope.nextBody = {}
+ scope.nextMeta = { body: {}, html: {}, window: {} }
scope.nextHead = []
scope.plugins = loadPlugins(scope)
@@ -48,7 +48,7 @@ export async function prerender(request, response) {
context.page.status = 500
} finally {
if (context.page.status !== 200) {
- scope.nextBody = {}
+ scope.nextMeta = {body: {}, html: {}, window: {}}
scope.nextHead = []
for (const key in context.router._routes) {
delete context.router._routes[key]
diff --git a/server/template.js b/server/template.js
index 8d9277b7..88a954b4 100644
--- a/server/template.js
+++ b/server/template.js
@@ -6,7 +6,7 @@ import project from './project'
import renderAttributes from './renderAttributes'
import settings from './settings'
-export default function ({ head, body, nextBody, context, instances }) {
+export default function ({ head, body, nextMeta, context, instances }) {
const { page, router, worker, params } = context
const canonical = absolute(page.canonical || router.url)
const image = cdnOrAbsolute(page.image)
@@ -39,7 +39,7 @@ export default function ({ head, body, nextBody, context, instances }) {
context: environment.mode === 'spa' ? {} : serializableContext,
}
return `
-
+
@@ -75,7 +75,7 @@ export default function ({ head, body, nextBody, context, instances }) {
integrities['client.js'] || ''
}" defer crossorigin="anonymous">
-
+
${environment.mode === 'spa' ? '' : body}
`
diff --git a/shared/generateTree.js b/shared/generateTree.js
index c084133a..fae3986d 100644
--- a/shared/generateTree.js
+++ b/shared/generateTree.js
@@ -105,27 +105,36 @@ async function generateBranch(siblings, node, depth, scope) {
return
}
- if (node.type === 'body') {
+ if (node.type === 'body' || node.type === 'html' || node.type === 'window') {
+ const tagName = node.type
node.type = fragment
for (const attribute in node.attributes) {
if (attribute === 'children' || attribute.startsWith('_')) continue
- if (attribute === 'class' || attribute === 'style') {
- if (!scope.nextBody[attribute]) {
- scope.nextBody[attribute] = []
- }
- scope.nextBody[attribute].push(node.attributes[attribute])
- } else if (attribute.startsWith('on')) {
+ if (attribute.startsWith('on')) {
if (scope.context.environment.server) continue
- if (!scope.nextBody[attribute]) {
- scope.nextBody[attribute] = []
+ if (!scope.nextMeta[tagName][attribute]) {
+ scope.nextMeta[tagName][attribute] = []
}
if (Array.isArray(node.attributes[attribute])) {
- scope.nextBody[attribute].push(...node.attributes[attribute])
+ for(const callback of node.attributes[attribute]) {
+ if (typeof callback === 'object') {
+ scope.nextMeta[tagName][attribute].push(() => Object.assign(node.attributes.source, callback))
+ } else {
+ scope.nextMeta[tagName][attribute].push(callback)
+ }
+ }
} else {
- scope.nextBody[attribute].push(node.attributes[attribute])
+ scope.nextMeta[tagName][attribute].push(node.attributes[attribute])
+ }
+ } else if (tagName !== 'window') {
+ if (attribute === 'class' || attribute === 'style') {
+ if (!scope.nextMeta[tagName][attribute]) {
+ scope.nextMeta[tagName][attribute] = []
+ }
+ scope.nextMeta[tagName][attribute].push(node.attributes[attribute])
+ } else {
+ scope.nextMeta[tagName][attribute] = node.attributes[attribute]
}
- } else {
- scope.nextBody[attribute] = node.attributes[attribute]
}
}
}
diff --git a/tests/src/Application.njs b/tests/src/Application.njs
index f79960d4..58691b9e 100644
--- a/tests/src/Application.njs
+++ b/tests/src/Application.njs
@@ -3,6 +3,8 @@ import Nullstack from 'nullstack'
import AnchorModifiers from './AnchorModifiers'
import ArrayAttributes from './ArrayAttributes'
import BodyFragment from './BodyFragment'
+import HtmlFragment from './HtmlFragment'
+import WindowFragment from './WindowFragment'
import CatchError from './CatchError'
import ChildComponent from './ChildComponent'
import ComponentTernary from './ComponentTernary'
@@ -60,6 +62,7 @@ import LazyComponent from './LazyComponent'
import LazyComponentLoader from './LazyComponentLoader'
import NestedFolder from './nested/NestedFolder'
import ChildComponentWithoutServerFunctions from './ChildComponentWithoutServerFunctions'
+import ObjectEventScope from './ObjectEventScope'
import './Application.css'
class Application extends Nullstack {
@@ -140,6 +143,8 @@ class Application extends Nullstack {
+
+
@@ -150,6 +155,7 @@ class Application extends Nullstack {
+
)
@@ -157,4 +163,4 @@ class Application extends Nullstack {
}
-export default Application
+export default Application
\ No newline at end of file
diff --git a/tests/src/Head.jsx b/tests/src/Head.jsx
new file mode 100644
index 00000000..11f41af7
--- /dev/null
+++ b/tests/src/Head.jsx
@@ -0,0 +1,41 @@
+// deprecate project.type
+// deprecate project.viewport
+// deprecate page.schema
+
+export default function Head({ router, project, page }) {
+ const image = page.image
+ const canonical = page.canonical || router.url
+ const favicon = project.favicon // cdn
+ return (
+
+
+ {page.title} - {project.title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/tests/src/HtmlFragment.njs b/tests/src/HtmlFragment.njs
new file mode 100644
index 00000000..e782469b
--- /dev/null
+++ b/tests/src/HtmlFragment.njs
@@ -0,0 +1,57 @@
+import Nullstack from 'nullstack'
+
+class HtmlFragment extends Nullstack {
+
+ count = 0
+ visible = false
+ objected = false
+
+ increment() {
+ this.count++
+ }
+
+ reveal() {
+ this.visible = !this.visible
+ }
+
+ countDataKeys({ data }) {
+ this.hasDataKeys = Object.keys(data).length > 0
+ }
+
+ render() {
+ return (
+
+
+
+ HtmlFragment
+
+ {this.visible && (
+
+ HtmlFragment2
+
+ )}
+
home
+
+
+ )
+ }
+
+}
+
+export default HtmlFragment
diff --git a/tests/src/HtmlFragment.test.js b/tests/src/HtmlFragment.test.js
new file mode 100644
index 00000000..6c67128f
--- /dev/null
+++ b/tests/src/HtmlFragment.test.js
@@ -0,0 +1,60 @@
+beforeEach(async () => {
+ await page.goto('http://localhost:6969/html-fragment')
+})
+
+describe('HtmlFragment', () => {
+ test('the html behaves as a fragment and creates no markup', async () => {
+ const element = await page.$('[data-html-parent] > [data-html-child]')
+ expect(element).toBeTruthy()
+ })
+
+ test('when the html is nested regular attributes are overwritten by the last one in the tree', async () => {
+ const element = await page.$('html[data-chars="b"]')
+ expect(element).toBeTruthy()
+ })
+
+ test('when the html is nested classes are merged togheter', async () => {
+ const element = await page.$('html[class="class-one class-two class-three class-four"]')
+ expect(element).toBeTruthy()
+ })
+
+ test('when the html is nested styles are merged togheter', async () => {
+ const element = await page.$('html[style="background-color: black; color: white;"]')
+ expect(element).toBeTruthy()
+ })
+
+ test('when the html is nested events are invoked sequentially', async () => {
+ await page.waitForSelector('html[data-hydrated]')
+ await page.click('html')
+ await page.waitForSelector('[data-keys][data-objected][data-visible]')
+ const element = await page.$('[data-keys][data-objected][data-visible]')
+ expect(element).toBeTruthy()
+ })
+
+ test('when a html is added to the vdom attributes are added', async () => {
+ await page.waitForSelector('html[data-hydrated]')
+ await page.click('html')
+ await page.waitForSelector('html[data-visible]')
+ const element = await page.$('html[data-visible]')
+ expect(element).toBeTruthy()
+ })
+
+ test('when a html is removed from the vdom attributes are removed', async () => {
+ await page.waitForSelector('html[data-hydrated]')
+ await page.click('html')
+ await page.waitForSelector('html[data-visible]')
+ await page.click('html')
+ await page.waitForSelector('html:not([data-visible])')
+ const element = await page.$('html:not([data-visible])')
+ expect(element).toBeTruthy()
+ })
+
+ test('the html removes events when the fragment leaves the tree', async () => {
+ await page.waitForSelector('html[data-hydrated]')
+ await page.click('[href="/"]')
+ await page.waitForSelector('[data-application-hydrated]:not([data-count])')
+ await page.click('html')
+ const element = await page.$('[data-application-hydrated]:not([data-count])')
+ expect(element).toBeTruthy()
+ })
+})
diff --git a/tests/src/ObjectEventScope.njs b/tests/src/ObjectEventScope.njs
new file mode 100644
index 00000000..f63510ff
--- /dev/null
+++ b/tests/src/ObjectEventScope.njs
@@ -0,0 +1,43 @@
+import Nullstack from 'nullstack';
+
+class NestedComponent extends Nullstack {
+
+ boolean = false
+
+ render({ source, onchange, id }) {
+ return (
+
+
+
+ )
+ }
+
+}
+
+class ObjectEventScope extends Nullstack {
+
+ boolean = false
+ visible = false
+
+ render() {
+ return (
+
+
+
+
----
+
+ {this.visible &&
+
+
+
+
+ }
+
----
+
Outter: {this.boolean.toString()}
+
+ )
+ }
+
+}
+
+export default ObjectEventScope;
\ No newline at end of file
diff --git a/tests/src/ObjectEventScope.test.js b/tests/src/ObjectEventScope.test.js
new file mode 100644
index 00000000..043325e9
--- /dev/null
+++ b/tests/src/ObjectEventScope.test.js
@@ -0,0 +1,38 @@
+describe('OptimizedEvents', () => {
+ beforeEach(async () => {
+ await page.goto('http://localhost:6969/object-event-scope')
+ await page.waitForSelector('[data-hydrated]')
+ })
+
+ test('single object events run in declaration scope on render', async () => {
+ await page.click('#render-single-object')
+ await page.waitForSelector('[data-boolean]')
+ const element = await page.$('[data-boolean]')
+ expect(element).toBeTruthy()
+ })
+
+ test('array object events run in declaration scope on render', async () => {
+ await page.click('#render-object-array')
+ await page.waitForSelector('[data-boolean]')
+ const element = await page.$('[data-boolean]')
+ expect(element).toBeTruthy()
+ })
+
+ test('single object events run in declaration scope on rerender', async () => {
+ await page.click('#rerender')
+ await page.waitForSelector('#rerender-single-object')
+ await page.click('#rerender-single-object')
+ await page.waitForSelector('[data-boolean]')
+ const element = await page.$('[data-boolean]')
+ expect(element).toBeTruthy()
+ })
+
+ test('array object events run in declaration scope on rerender', async () => {
+ await page.click('#rerender')
+ await page.waitForSelector('#rerender-object-array')
+ await page.click('#render-object-array')
+ await page.waitForSelector('[data-boolean]')
+ const element = await page.$('[data-boolean]')
+ expect(element).toBeTruthy()
+ })
+})
diff --git a/tests/src/TwoWayBindings.njs b/tests/src/TwoWayBindings.njs
index 64a77596..a7a4d352 100644
--- a/tests/src/TwoWayBindings.njs
+++ b/tests/src/TwoWayBindings.njs
@@ -9,6 +9,7 @@ class TwoWayBindings extends Nullstack {
boolean = true
character = 'a'
text = 'aaaa'
+ date = new Date()
object = { count: 1 }
array = ['a', 'b', 'c']
@@ -143,6 +144,14 @@ class TwoWayBindings extends Nullstack {
>
rebounce
+
)
}
diff --git a/tests/src/TwoWayBindings.test.js b/tests/src/TwoWayBindings.test.js
index 885486f9..26991660 100644
--- a/tests/src/TwoWayBindings.test.js
+++ b/tests/src/TwoWayBindings.test.js
@@ -257,4 +257,19 @@ describe('TwoWayBindings', () => {
const element = await page.$('[data-render-clicked-and-inputed]')
expect(element).toBeTruthy()
})
+
+ test('dates can be bound to date inputs', async () => {
+ await page.$eval('[data-instanceof-date]', (element) => {
+ element.focus()
+ element.value = '1992-10-16'
+ element.blur()
+ const event = new Event('input')
+ element.dispatchEvent(event)
+ })
+ await page.waitForSelector('[data-date="16"]')
+ await page.waitForSelector('[data-month="10"]')
+ await page.waitForSelector('[data-year="1992"]')
+ const element = await page.$('[data-date="16"]')
+ expect(element).toBeTruthy()
+ })
})
diff --git a/tests/src/WindowFragment.njs b/tests/src/WindowFragment.njs
new file mode 100644
index 00000000..4e135937
--- /dev/null
+++ b/tests/src/WindowFragment.njs
@@ -0,0 +1,42 @@
+import Nullstack from 'nullstack'
+
+class WindowFragment extends Nullstack {
+
+ count = 0
+ visible = false
+ objected = false
+
+ increment() {
+ this.count++
+ }
+
+ reveal() {
+ this.visible = !this.visible
+ }
+
+ render() {
+ return (
+
+
console.log('hi'), {objected: true}]}
+ >
+ {this.hydrated && }
+
+ WindowFragment
+
+ {this.visible && (
+
+
+ WindowFragment2
+
+
+ )}
+ home
+
+
+ )
+ }
+
+}
+
+export default WindowFragment
diff --git a/tests/src/WindowFragment.test.js b/tests/src/WindowFragment.test.js
new file mode 100644
index 00000000..ff3d399e
--- /dev/null
+++ b/tests/src/WindowFragment.test.js
@@ -0,0 +1,32 @@
+beforeEach(async () => {
+ await page.goto('http://localhost:6969/window-fragment')
+})
+
+describe('WindowFragment', () => {
+ test('the window behaves as a fragment and creates no markup', async () => {
+ const element = await page.$('[data-window-parent] > [data-window-child]')
+ expect(element).toBeTruthy()
+ })
+
+ test('the window ignores non attribute events', async () => {
+ const element = await page.$('[data-window-parent] > [data-window-child]')
+ expect(element).toBeTruthy()
+ })
+
+ test('when the window is nested events are invoked sequentially', async () => {
+ await page.waitForSelector('[data-hydrated]')
+ await page.click('body')
+ await page.waitForSelector('[data-count="1"][data-objected]')
+ const element = await page.$('[data-count="1"][data-objected]')
+ expect(element).toBeTruthy()
+ })
+
+ test('the window removes events when the fragment leaves the tree', async () => {
+ await page.waitForSelector('[data-hydrated]')
+ await page.click('[href="/"]')
+ await page.waitForSelector('[data-application-hydrated]:not([data-count])')
+ await page.click('body')
+ const element = await page.$('[data-application-hydrated]:not([data-count])')
+ expect(element).toBeTruthy()
+ })
+})
diff --git a/types/ClientContext.d.ts b/types/ClientContext.d.ts
index f780bb89..9caace1b 100644
--- a/types/ClientContext.d.ts
+++ b/types/ClientContext.d.ts
@@ -1,5 +1,6 @@
import { NullstackEnvironment } from './Environment'
-import { NullstackNode } from './JSX'
+import { NullstackInstances } from './Instances'
+import { NullstackFragment } from './JSX'
import { NullstackPage } from './Page'
import { NullstackParams } from './Params'
import { NullstackProject } from './Project'
@@ -10,7 +11,7 @@ import { NullstackWorker } from './Worker'
/**
* @see https://nullstack.app/context
*/
-export type NullstackClientContext = TProps & {
+interface BaseNullstackClientContext {
/**
* Callback function that bootstrap the context for the application.
*/
@@ -53,7 +54,7 @@ export type NullstackClientContext = TProps & {
* @scope client
* @see https://nullstack.app/context-instances
*/
- instances: Record
+ instances: NullstackInstances
/**
* It gives you information about the current environment.
@@ -98,7 +99,7 @@ export type NullstackClientContext = TProps & {
*
* @see https://nullstack.app/renderable-components#components-with-children
*/
- children: NullstackNode
+ children: NullstackFragment
/**
* Bind object.
@@ -129,3 +130,5 @@ export type NullstackClientContext = TProps & {
element?: HTMLElement
}
+
+export type NullstackClientContext = BaseNullstackClientContext & TProps
\ No newline at end of file
diff --git a/types/Environment.d.ts b/types/Environment.d.ts
index db962150..f761598d 100644
--- a/types/Environment.d.ts
+++ b/types/Environment.d.ts
@@ -1,4 +1,4 @@
-export type NullstackEnvironment = {
+export interface NullstackEnvironment {
client: boolean
server: boolean
diff --git a/types/Instances.d.ts b/types/Instances.d.ts
new file mode 100644
index 00000000..1b5d7dc9
--- /dev/null
+++ b/types/Instances.d.ts
@@ -0,0 +1 @@
+export interface NullstackInstances extends Record {}
\ No newline at end of file
diff --git a/types/JSX.d.ts b/types/JSX.d.ts
index c8bcc4fd..8995bee4 100644
--- a/types/JSX.d.ts
+++ b/types/JSX.d.ts
@@ -577,10 +577,14 @@ type AriaRole =
| 'treeitem'
| (string & {})
+type Falsy = false | 0 | '' | null | undefined
+type CssClass = string | Falsy | Array
+type CssStyle = string | Falsy | Array
+
export interface HTMLAttributes extends AriaAttributes, DOMAttributes {
// Standard HTML Attributes
accesskey?: string
- class?: string | string[]
+ class?: CssClass
contenteditable?: Booleanish | 'inherit'
contextmenu?: string
dir?: string
@@ -591,7 +595,7 @@ export interface HTMLAttributes extends AriaAttributes, DOMAttributes {
placeholder?: string
slot?: string
spellcheck?: Booleanish
- style?: string
+ style?: CssStyle
tabindex?: number | string
title?: string
translate?: 'yes' | 'no'
@@ -1188,7 +1192,7 @@ export interface VideoHTMLAttributes extends MediaHTMLAttributes {
// - union of string literals
export interface SVGAttributes extends AriaAttributes, DOMAttributes {
// Attributes which also defined in HTMLAttributes
- class?: string | string[]
+ class?: CssClass
color?: string
height?: number | string
id?: string
@@ -1198,7 +1202,7 @@ export interface SVGAttributes extends AriaAttributes, DOMAttributes {
method?: string
min?: number | string
name?: string
- style?: string
+ style?: CssStyle
target?: string
type?: string
width?: number | string
@@ -1426,5 +1430,9 @@ declare global {
}
interface IntrinsicElements extends ExoticElements, AllElements {}
+
+ interface ElementChildrenAttribute {
+ children: NullstackNode
+ }
}
}
diff --git a/types/Page.d.ts b/types/Page.d.ts
index c78e5db6..0d3a0747 100644
--- a/types/Page.d.ts
+++ b/types/Page.d.ts
@@ -1,4 +1,4 @@
-export type NullstackPage = {
+export interface NullstackPage {
/**
* Current page title
*
diff --git a/types/Plugin.d.ts b/types/Plugin.d.ts
index bf680ac2..dbdcefb6 100644
--- a/types/Plugin.d.ts
+++ b/types/Plugin.d.ts
@@ -1,7 +1,7 @@
import { NullstackClientContext } from './ClientContext'
import { AllHTMLAttributes, NullstackAttributes } from './JSX'
-type NullstackPluginNode = {
+interface NullstackPluginNode {
type: string | boolean
attributes: AllHTMLAttributes & NullstackAttributes
children: NullstackClientContext['children']
@@ -11,7 +11,7 @@ interface NullstackNodeContext extends NullstackClientContext {
node: NullstackPluginNode
}
-export type NullstackPlugin = {
+export interface NullstackPlugin {
/**
* Runs transformation to node element
* @param context Context with node attributes
diff --git a/types/Project.d.ts b/types/Project.d.ts
index a20b04d9..cd282da4 100644
--- a/types/Project.d.ts
+++ b/types/Project.d.ts
@@ -1,4 +1,4 @@
-export type NullstackProject = {
+export interface NullstackProject {
domain: string
/**
diff --git a/types/Request.d.ts b/types/Request.d.ts
new file mode 100644
index 00000000..ea864563
--- /dev/null
+++ b/types/Request.d.ts
@@ -0,0 +1,3 @@
+import { Request } from 'express'
+
+export interface NullstackRequest extends Request {}
\ No newline at end of file
diff --git a/types/Response.d.ts b/types/Response.d.ts
new file mode 100644
index 00000000..15458b80
--- /dev/null
+++ b/types/Response.d.ts
@@ -0,0 +1,3 @@
+import { Response } from 'express'
+
+export interface NullstackResponse extends Response {}
\ No newline at end of file
diff --git a/types/Router.d.ts b/types/Router.d.ts
index 216b36f4..38dab493 100644
--- a/types/Router.d.ts
+++ b/types/Router.d.ts
@@ -1,4 +1,4 @@
-export type NullstackRouter = {
+export interface NullstackRouter {
/**
* The router path including query params.
* Does not contain the domain and port.
diff --git a/types/Secrets.d.ts b/types/Secrets.d.ts
index 349e2f5c..d1b8ecb5 100644
--- a/types/Secrets.d.ts
+++ b/types/Secrets.d.ts
@@ -1 +1 @@
-export type NullstackSecrets = Record
+export interface NullstackSecrets extends Record {}
diff --git a/types/Server.d.ts b/types/Server.d.ts
index b6470951..56007c19 100644
--- a/types/Server.d.ts
+++ b/types/Server.d.ts
@@ -1,4 +1,4 @@
-export type NullstackServer = {
+export interface NullstackServer {
get(...args)
post(...args)
diff --git a/types/ServerContext.d.ts b/types/ServerContext.d.ts
index 8ec90589..5a84f660 100644
--- a/types/ServerContext.d.ts
+++ b/types/ServerContext.d.ts
@@ -1,5 +1,7 @@
import { NullstackEnvironment } from './Environment'
import { NullstackProject } from './Project'
+import { NullstackRequest } from './Request'
+import { NullstackResponse } from './Response'
import { NullstackSecrets } from './Secrets'
import { NullstackServer } from './Server'
import { NullstackSettings } from './Settings'
@@ -8,7 +10,7 @@ import { NullstackWorker } from './Worker'
/**
* @see https://nullstack.app/context
*/
-export type NullstackServerContext = TProps & {
+interface BaseNullstackServerContext {
/**
* Callback function that bootstrap the context for the application.
*/
@@ -49,7 +51,7 @@ export type NullstackServerContext = TProps & {
* @scope server
* @see https://nullstack.app/server-request-and-response
*/
- request?: Record
+ request?: NullstackRequest
/**
* Original `response` object from [Express](https://expressjs.com/)
@@ -57,7 +59,7 @@ export type NullstackServerContext = TProps & {
* @scope server
* @see https://nullstack.app/server-request-and-response
*/
- response?: Record
+ response?: NullstackResponse
/**
* You can assign any key with any type of public information.
@@ -83,3 +85,5 @@ export type NullstackServerContext = TProps & {
*/
secrets: NullstackSecrets
}
+
+export type NullstackServerContext = BaseNullstackServerContext & TProps
\ No newline at end of file
diff --git a/types/Settings.d.ts b/types/Settings.d.ts
index efe88216..cf699bcc 100644
--- a/types/Settings.d.ts
+++ b/types/Settings.d.ts
@@ -1 +1 @@
-export type NullstackSettings = Record
+export interface NullstackSettings extends Record {}
diff --git a/types/Worker.d.ts b/types/Worker.d.ts
index b4999345..77ac02e2 100644
--- a/types/Worker.d.ts
+++ b/types/Worker.d.ts
@@ -1,4 +1,4 @@
-export type NullstackWorker = {
+export interface NullstackWorker {
/**
* - keys: server functions names
* - values: array of these functions arguments
diff --git a/types/index.d.ts b/types/index.d.ts
index 1a46f414..b2243c41 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -5,10 +5,13 @@ import { NullstackServerContext } from './ServerContext'
export * from './ClientContext'
export * from './Environment'
+export * from './Instances'
export * from './Page'
export * from './Params'
export * from './Plugin'
export * from './Project'
+export * from './Request'
+export * from './Response'
export * from './Router'
export * from './Secrets'
export * from './Server'
diff --git a/webpack/entry.js b/webpack/entry.js
index cfa6f024..afca360d 100644
--- a/webpack/entry.js
+++ b/webpack/entry.js
@@ -16,7 +16,7 @@ function server(options) {
return options.entry
}
return [
- 'webpack/hot/poll?100',
+ `${require.resolve('webpack/hot/poll')}?100`,
path.posix.join(options.configFolder, 'shared', 'accept.js'),
options.entry
]