From b5a94b8cfa9cd6fdeadf0ec4a5b1bdab67788873 Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Sat, 10 Jun 2023 02:41:40 -0300 Subject: [PATCH 01/27] :sparkles: request based context --- server/context.js | 50 +++++++++++++++++++++++++++++---- server/exposeServerFunctions.js | 10 ++----- server/project.js | 7 +++-- server/registry.js | 8 ++---- server/reqres.js | 20 ------------- server/server.js | 29 +++++++------------ tests/server.js | 5 ++++ tests/src/ServerFunctions.njs | 5 +++- 8 files changed, 74 insertions(+), 60 deletions(-) delete mode 100644 server/reqres.js diff --git a/server/context.js b/server/context.js index e3b87d4c..f93c7246 100644 --- a/server/context.js +++ b/server/context.js @@ -1,14 +1,54 @@ -const context = {} +import { AsyncLocalStorage } from 'async_hooks' + +const internalContext = {} const contextProxyHandler = { set(target, name, value, receiver) { - context[name] = value - return Reflect.set(target, name, value, receiver) + const currentContext = asyncLocalStorage.getStore() + if (currentContext) { + currentContext[name] = value + return true + } else { + return Reflect.set(target, name, value, receiver) + } }, + get(target, name, receiver) { + const currentContext = asyncLocalStorage.getStore() + if (currentContext) { + return currentContext[name] + } else { + return Reflect.get(target, name, receiver) + } + } } +const context = new Proxy(internalContext, contextProxyHandler) + +const asyncLocalStorage = new AsyncLocalStorage() + export function generateContext(temporary) { - return new Proxy({ ...context, ...temporary }, contextProxyHandler) + if(temporary) { + Object.assign(context, temporary) + } + return context +} + +export function generateCurrentContext(temporary, callback) { + const currentContext = {...internalContext, ...temporary} + asyncLocalStorage.run(currentContext, () => { + callback(currentContext); + }); +} + +export function getCurrentContext(temporary) { + const currentContext = asyncLocalStorage.getStore() + if (!currentContext) { + return generateContext() + } + if(temporary) { + Object.assign(currentContext, temporary) + } + return currentContext } -export default context +export default context \ No newline at end of file diff --git a/server/exposeServerFunctions.js b/server/exposeServerFunctions.js index 45e55af0..1710d40e 100644 --- a/server/exposeServerFunctions.js +++ b/server/exposeServerFunctions.js @@ -3,10 +3,9 @@ import fs from 'fs' import bodyParser from 'body-parser' import path from 'path' import deserialize from '../shared/deserialize' -import { generateContext } from './context' +import { getCurrentContext } from './context' import printError from './printError' import registry from './registry' -import reqres from './reqres' export default function exposeServerFunctions(server) { for (const method of ['get', 'post', 'put', 'patch', 'delete', 'all']) { @@ -14,7 +13,6 @@ export default function exposeServerFunctions(server) { server[method] = function (...args) { if (typeof args[1] === 'function' && args[1].name === '_invoke') { return original(args[0], bodyParser.text({ limit: server.maximumPayloadSize }), async (request, response) => { - reqres.set(request, response) const params = {} for (const key of Object.keys(request.params)) { params[key] = extractParamValue(request.params[key]) @@ -27,14 +25,12 @@ export default function exposeServerFunctions(server) { Object.assign(params, deserialize(payload)) } try { - const subcontext = generateContext({ request, response, ...params }) + const currentContext = getCurrentContext(params) const exposedFunction = module.hot ? registry[args[1].hash] : args[1] - const result = await exposedFunction(subcontext) - reqres.clear() + const result = await exposedFunction(currentContext) response.json(result) } catch (error) { printError(error) - reqres.clear() response.status(500).json({}) } }) diff --git a/server/project.js b/server/project.js index b3c19e75..7f961c9b 100644 --- a/server/project.js +++ b/server/project.js @@ -1,7 +1,7 @@ import { ICONS } from 'nullstack/project' import environment from './environment' -import reqres from './reqres' import worker from './worker' +import { getCurrentContext } from './context' const project = {} @@ -21,8 +21,9 @@ project.disallow = [] project.icons = ICONS function getHost() { - if (reqres.request?.headers?.host) { - return reqres.request.headers.host + const currentContext = getCurrentContext() + if (currentContext.request?.headers?.host) { + return currentContext.request.headers.host } if (project.domain === 'localhost') { return `localhost:${process.env.NULLSTACK_SERVER_PORT}` diff --git a/server/registry.js b/server/registry.js index f4c0c354..003d6812 100644 --- a/server/registry.js +++ b/server/registry.js @@ -1,7 +1,6 @@ const registry = {} export default registry -import reqres from "./reqres" -import { generateContext } from "./context" +import { getCurrentContext } from "./context" import Nullstack from '.' import { load } from "./lazy" @@ -34,10 +33,9 @@ function bindStaticProps(klass) { return klass[propName].call(klass, ...args) } const params = args[0] || {} - const { request, response } = reqres - const subcontext = generateContext({ request, response, ...params }) + const currentContext = getCurrentContext(params) await load(klass.hash) - return klass[propName].call(klass, subcontext) + return klass[propName].call(klass, currentContext) } if (module.hot) { _invoke.hash = klass[prop].hash diff --git a/server/reqres.js b/server/reqres.js deleted file mode 100644 index 13cdb10c..00000000 --- a/server/reqres.js +++ /dev/null @@ -1,20 +0,0 @@ -class ReqRes { - - request = null - response = null - - set(request, response) { - this.request = request - this.response = response - } - - clear() { - this.request = null - this.response = null - } - -} - -const reqres = new ReqRes() - -export default reqres diff --git a/server/server.js b/server/server.js index 3203483c..8f8cc480 100644 --- a/server/server.js +++ b/server/server.js @@ -3,7 +3,7 @@ import express from 'express' import path from 'path' import deserialize from '../shared/deserialize' import prefix from '../shared/prefix' -import context, { generateContext } from './context' +import context, { getCurrentContext, generateCurrentContext } from './context' import emulatePrerender from './emulatePrerender' import environment from './environment' import exposeServerFunctions from './exposeServerFunctions' @@ -13,7 +13,6 @@ import generateManifest from './manifest' import { prerender } from './prerender' import printError from './printError' import registry from './registry' -import reqres from './reqres' import generateRobots from './robots' import template from './template' import { generateServiceWorker } from './worker' @@ -31,7 +30,9 @@ server.use(async (request, response, next) => { typeof context.start === 'function' && (await context.start()) contextStarted = true } - next() + generateCurrentContext({request, response}, () => { + next() + }) }) emulatePrerender(server) @@ -120,7 +121,6 @@ server.start = function () { server.all(`/${prefix}/:hash/:methodName.json`, async (request, response) => { const payload = request.method === 'GET' ? request.query.payload : request.body - reqres.set(request, response) const args = deserialize(payload) const { hash, methodName } = request.params const [invokerHash, boundHash] = hash.split('-') @@ -137,17 +137,15 @@ server.start = function () { const method = registry[key] if (method !== undefined) { try { - const subcontext = generateContext({ request, response, ...args }) - const result = await method.call(boundKlass, subcontext) - reqres.clear() + const currentContext = getCurrentContext(args) + console.log({currentContext}) + const result = await method.call(boundKlass, currentContext) response.json({ result }) } catch (error) { printError(error) - reqres.clear() response.status(500).json({}) } } else { - reqres.clear() response.status(404).json({}) } }) @@ -155,7 +153,6 @@ server.start = function () { if (module.hot) { server.all(`/${prefix}/:version/:hash/:methodName.json`, async (request, response) => { const payload = request.method === 'GET' ? request.query.payload : request.body - reqres.set(request, response) const args = deserialize(payload) const { version, hash, methodName } = request.params const [invokerHash, boundHash] = hash.split('-') @@ -178,17 +175,15 @@ server.start = function () { const method = registry[key] if (method !== undefined) { try { - const subcontext = generateContext({ request, response, ...args }) - const result = await method.call(boundKlass, subcontext) - reqres.clear() + console.log("DEVY") + const currentContext = getCurrentContext(args) + const result = await method.call(boundKlass, currentContext) response.json({ result }) } catch (error) { printError(error) - reqres.clear() response.status(500).json({}) } } else { - reqres.clear() response.status(404).json({}) } } @@ -210,15 +205,11 @@ server.start = function () { if (request.originalUrl.split('?')[0].indexOf('.') > -1) { return next() } - reqres.set(request, response) const scope = await prerender(request, response) if (!response.headersSent) { const status = scope.context.page.status const html = template(scope) - reqres.clear() response.status(status).send(html) - } else { - reqres.clear() } }) diff --git a/tests/server.js b/tests/server.js index 345659d1..7b479e6f 100644 --- a/tests/server.js +++ b/tests/server.js @@ -73,6 +73,11 @@ context.startIncrementalValue = 0 setExternalRoute(context.server) +context.server.use((request, response, next) => { + context.url = request.originalUrl + next() +}) + context.start = async function () { await ContextProject.start(context) await ContextSecrets.start(context) diff --git a/tests/src/ServerFunctions.njs b/tests/src/ServerFunctions.njs index d5081cce..c88c791d 100644 --- a/tests/src/ServerFunctions.njs +++ b/tests/src/ServerFunctions.njs @@ -21,7 +21,10 @@ class ServerFunctions extends Nullstack { this.count = await this.getCountAsOne() } - static async getCount({ to }) { + static async getCount(context) { + const { to, url } = context + console.log({url}) + setInterval(() => console.log("SERVERF:", context.url), 1000) return to } From 19f1902919cdd2bd02a95853554a68e1c4d1817a Mon Sep 17 00:00:00 2001 From: Luiz Felipe Moureau Date: Tue, 20 Jun 2023 23:38:31 -0300 Subject: [PATCH 02/27] feat: added element ref type --- types/JSX.d.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/types/JSX.d.ts b/types/JSX.d.ts index 8995bee4..8d38a83d 100644 --- a/types/JSX.d.ts +++ b/types/JSX.d.ts @@ -44,12 +44,12 @@ type Booleanish = boolean | 'true' | 'false' // Nullstack Elements // ---------------------------------------------------------------------- -export interface Attributes { +export interface Attributes { html?: string source?: object bind?: any debounce?: number - ref?: any + ref?: NodeType 'data-'?: any children?: NullstackNode route?: string @@ -57,9 +57,9 @@ export interface Attributes { [key: string]: any } -export interface NullstackAttributes extends Attributes {} +export interface NullstackAttributes extends Attributes {} -export interface ClassAttributes extends Attributes { +export interface ClassAttributes extends Attributes { key?: string } @@ -218,9 +218,9 @@ type WheelEventHandler = EventHandler> type DetailedHTMLProps, T> = E -export interface SVGProps extends SVGAttributes, ClassAttributes {} +export interface SVGProps extends SVGAttributes, ClassAttributes {} -export interface DOMAttributes extends Attributes { +export interface DOMAttributes extends Attributes { // Focus Events onfocus?: FocusEventHandler onblur?: FocusEventHandler @@ -1243,8 +1243,8 @@ declare global { namespace JSX { type Element = NullstackNode - type IntrinsicAttributes = NullstackAttributes - type IntrinsicClassAttributes = ClassAttributes + type IntrinsicAttributes = NullstackAttributes + type IntrinsicClassAttributes = ClassAttributes interface AllElements { // HTML @@ -1429,7 +1429,7 @@ declare global { element: ElementTagHTMLAttributes } - interface IntrinsicElements extends ExoticElements, AllElements {} + interface IntrinsicElements extends AllElements {} interface ElementChildrenAttribute { children: NullstackNode From 36973d874ab94af776c6837dd82293729679e102 Mon Sep 17 00:00:00 2001 From: Luiz Felipe Moureau Date: Tue, 20 Jun 2023 23:41:14 -0300 Subject: [PATCH 03/27] revert instrinsicelements change --- types/JSX.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/JSX.d.ts b/types/JSX.d.ts index 8d38a83d..52c2535d 100644 --- a/types/JSX.d.ts +++ b/types/JSX.d.ts @@ -1429,7 +1429,7 @@ declare global { element: ElementTagHTMLAttributes } - interface IntrinsicElements extends AllElements {} + interface IntrinsicElements extends ExoticElements, AllElements {} interface ElementChildrenAttribute { children: NullstackNode From 567a4622f3e2a48765eb29f3582125a83b33dc64 Mon Sep 17 00:00:00 2001 From: Luiz Felipe Moureau Date: Tue, 20 Jun 2023 23:46:39 -0300 Subject: [PATCH 04/27] better naming --- types/JSX.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/JSX.d.ts b/types/JSX.d.ts index 52c2535d..fea00751 100644 --- a/types/JSX.d.ts +++ b/types/JSX.d.ts @@ -44,12 +44,12 @@ type Booleanish = boolean | 'true' | 'false' // Nullstack Elements // ---------------------------------------------------------------------- -export interface Attributes { +export interface Attributes { html?: string source?: object bind?: any debounce?: number - ref?: NodeType + ref?: HTMLElementType 'data-'?: any children?: NullstackNode route?: string From 59a66dde25b40f97488f3f96f6b0f2091710f67d Mon Sep 17 00:00:00 2001 From: Luiz Felipe Moureau Date: Wed, 21 Jun 2023 18:07:30 -0300 Subject: [PATCH 05/27] feat(JSX.d.ts): add support for function refs in NullstackClientContext to be able to use functional components with refs --- types/JSX.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/JSX.d.ts b/types/JSX.d.ts index fea00751..5c26248e 100644 --- a/types/JSX.d.ts +++ b/types/JSX.d.ts @@ -49,7 +49,7 @@ export interface Attributes { source?: object bind?: any debounce?: number - ref?: HTMLElementType + ref?: HTMLElementType | ((context?: Partial) => void) 'data-'?: any children?: NullstackNode route?: string From 7979c1f82674c5d3a1653457e6cecd43c50d164c Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Fri, 23 Jun 2023 14:15:21 -0300 Subject: [PATCH 06/27] :mute: remove logs --- server/server.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/server.js b/server/server.js index 8f8cc480..45e73617 100644 --- a/server/server.js +++ b/server/server.js @@ -138,7 +138,6 @@ server.start = function () { if (method !== undefined) { try { const currentContext = getCurrentContext(args) - console.log({currentContext}) const result = await method.call(boundKlass, currentContext) response.json({ result }) } catch (error) { @@ -175,7 +174,6 @@ server.start = function () { const method = registry[key] if (method !== undefined) { try { - console.log("DEVY") const currentContext = getCurrentContext(args) const result = await method.call(boundKlass, currentContext) response.json({ result }) From 2deab818644e3e0ca56e54ad6455edf19792fbf2 Mon Sep 17 00:00:00 2001 From: Luiz Felipe Moureau Date: Tue, 27 Jun 2023 16:52:54 -0300 Subject: [PATCH 07/27] :bug: bug: change automatic scroll behavior when client changes route --- client/router.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/router.js b/client/router.js index ab0b9b12..ea2c54c5 100644 --- a/client/router.js +++ b/client/router.js @@ -71,7 +71,7 @@ class Router { } const absoluteUrl = new URL(target, document.baseURI) await this._update(absoluteUrl.pathname + absoluteUrl.search + absoluteUrl.hash, true) - window.scroll(0, 0) + window.scroll({ top: 0, left: 0, behavior: 'instant' }) } get url() { From 7c9370d6db239545b47eb526d8aa106b7b42c7c7 Mon Sep 17 00:00:00 2001 From: Guilherme Correia Date: Tue, 27 Jun 2023 13:09:58 -0300 Subject: [PATCH 08/27] :label: Sync `ref` types in JSX & Context --- types/ClientContext.d.ts | 10 ++++++---- types/JSX.d.ts | 16 ++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/types/ClientContext.d.ts b/types/ClientContext.d.ts index 9caace1b..2f2f2c81 100644 --- a/types/ClientContext.d.ts +++ b/types/ClientContext.d.ts @@ -11,7 +11,7 @@ import { NullstackWorker } from './Worker' /** * @see https://nullstack.app/context */ -interface BaseNullstackClientContext { +interface BaseNullstackClientContext { /** * Callback function that bootstrap the context for the application. */ @@ -120,15 +120,17 @@ interface BaseNullstackClientContext { * * @see https://nullstack.app/refs#complex-refable-components */ - ref?: { property?: string | number; object?: any } | ((context: NullstackClientContext) => void) + ref?: TProps extends { ref: any } ? TProps['ref'] : { + object: any + property: string | number + } /** * Ref element. * * @see https://nullstack.app/refs#complex-refable-components */ - element?: HTMLElement } -export type NullstackClientContext = BaseNullstackClientContext & TProps \ No newline at end of file +export type NullstackClientContext = BaseNullstackClientContext & TProps \ No newline at end of file diff --git a/types/JSX.d.ts b/types/JSX.d.ts index 5c26248e..da933d96 100644 --- a/types/JSX.d.ts +++ b/types/JSX.d.ts @@ -44,12 +44,12 @@ type Booleanish = boolean | 'true' | 'false' // Nullstack Elements // ---------------------------------------------------------------------- -export interface Attributes { +export interface Attributes { html?: string source?: object bind?: any debounce?: number - ref?: HTMLElementType | ((context?: Partial) => void) + ref?: HTMLElementType | ((context?: Partial) => void) | NullstackClientContext['ref'] 'data-'?: any children?: NullstackNode route?: string @@ -57,9 +57,9 @@ export interface Attributes { [key: string]: any } -export interface NullstackAttributes extends Attributes {} +export interface NullstackAttributes extends Attributes {} -export interface ClassAttributes extends Attributes { +export interface ClassAttributes extends Attributes { key?: string } @@ -1233,18 +1233,18 @@ export interface SVGAttributes extends AriaAttributes, DOMAttributes { y?: number | string } -export type ElementTagHTMLAttributes = AllHTMLAttributes<'div'> & { +export type ElementTagHTMLAttributes = AllHTMLAttributes & { tag?: string } -type ExoticElements = Record, HTMLElement>> +type ExoticElements = Record | SVGProps | ElementTagHTMLAttributes, HTMLElement>> declare global { namespace JSX { type Element = NullstackNode - type IntrinsicAttributes = NullstackAttributes - type IntrinsicClassAttributes = ClassAttributes + type IntrinsicAttributes = NullstackAttributes + type IntrinsicClassAttributes = ClassAttributes interface AllElements { // HTML From f2fa1b4b989bd4c2931a3d968e8c2b06a456654b Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Tue, 27 Jun 2023 23:00:55 -0300 Subject: [PATCH 09/27] :bookmark: version 0.20.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bf3c77ed..3317b859 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nullstack", - "version": "0.19.2", + "version": "0.20.0", "description": "Feature-Driven Full Stack JavaScript Components", "main": "./types/index.d.ts", "author": "Mortaro", From 25f1eb52143df22171428caad30da5b1f73cd9f4 Mon Sep 17 00:00:00 2001 From: Guilherme Correia Date: Wed, 28 Jun 2023 03:31:36 -0300 Subject: [PATCH 10/27] :white_check_mark: Avoid build 2x for tests & skipCache --- tests/jest-puppeteer.config.js | 4 ++-- tests/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/jest-puppeteer.config.js b/tests/jest-puppeteer.config.js index 6fa550bb..53a96c75 100644 --- a/tests/jest-puppeteer.config.js +++ b/tests/jest-puppeteer.config.js @@ -3,7 +3,7 @@ const defaultOptions = { headless: true, }, server: { - command: 'npm run build && node .production/server.js', + command: 'node .production/server.js', port: 6969, launchTimeout: 25000, }, @@ -23,7 +23,7 @@ const ciPipelineOptions = { ], }, server: { - command: 'npm run build && node .production/server.js', + command: 'node .production/server.js', port: 6969, launchTimeout: 25000, }, diff --git a/tests/package.json b/tests/package.json index 773810ac..9137875f 100644 --- a/tests/package.json +++ b/tests/package.json @@ -23,8 +23,8 @@ "terser": "npm:@swc/core" }, "scripts": { - "start": "node ../scripts/index.js start --port=6969 --name=test --disk", - "build": "node --enable-source-maps ../scripts/index.js build --name=test", + "start": "node ../scripts/index.js start --port=6969 --name=test --disk -sc", + "build": "node --enable-source-maps ../scripts/index.js build --name=test -sc", "clear": "rm -rf ../node_modules ../package-lock.json node_modules .development .production package-lock.json", "setup": "cd .. && npm install && cd tests && npm install", "test": "npm run build && jest --runInBand", From 275fd5d1b57ce20b3f26145225538fef34cb223b Mon Sep 17 00:00:00 2001 From: Guilherme Correia Date: Fri, 30 Jun 2023 16:54:51 -0300 Subject: [PATCH 11/27] :label: Add types to JSX Attributes['bind'] --- types/JSX.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/JSX.d.ts b/types/JSX.d.ts index da933d96..b8ff3691 100644 --- a/types/JSX.d.ts +++ b/types/JSX.d.ts @@ -47,7 +47,7 @@ type Booleanish = boolean | 'true' | 'false' export interface Attributes { html?: string source?: object - bind?: any + bind?: NullstackClientContext['bind'] | string | number | boolean debounce?: number ref?: HTMLElementType | ((context?: Partial) => void) | NullstackClientContext['ref'] 'data-'?: any From b2c99bbd0727806d38006a1a71635c445835a629 Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Sat, 1 Jul 2023 21:15:29 -0300 Subject: [PATCH 12/27] :bug: fix functional reload --- client/runtime.js | 4 +++- package.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/client/runtime.js b/client/runtime.js index 8942468e..01ebe75c 100644 --- a/client/runtime.js +++ b/client/runtime.js @@ -17,7 +17,9 @@ if (module.hot) { $runtime.dependencies = new Map() $runtime.accept = function accept(target, file, dependencies, declarations) { - target.hot.accept() + if (declarations.length > 0) { + target.hot.accept() + } let initiateQueue = [] const old = $runtime.dependencies.get(file) if (old) { diff --git a/package.json b/package.json index bf3c77ed..311332ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nullstack", - "version": "0.19.2", + "version": "0.19.3", "description": "Feature-Driven Full Stack JavaScript Components", "main": "./types/index.d.ts", "author": "Mortaro", From 48a5bf1fb05da5ee6222c7b0a98e14a029eee90b Mon Sep 17 00:00:00 2001 From: Guilherme Correia Date: Tue, 8 Aug 2023 22:01:45 -0300 Subject: [PATCH 13/27] =?UTF-8?q?=F0=9F=93=8C=20Fix=20dependencies=20above?= =?UTF-8?q?=20deprecations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 3317b859..75a446ad 100644 --- a/package.json +++ b/package.json @@ -16,29 +16,27 @@ }, "dependencies": { "@swc/core": "1.3.35", - "babel-loader": "9.1.2", "body-parser": "1.20.1", "commander": "10.0.0", - "copy-webpack-plugin": "^11.0.0", + "copy-webpack-plugin": "11.0.0", "css-loader": "6.7.3", - "css-minimizer-webpack-plugin": "^4.2.2", + "css-minimizer-webpack-plugin": "5.0.1", "dotenv": "16.0.3", "eslint-plugin-nullstack": "0.0.26", "express": "4.18.2", "fs-extra": "11.1.0", - "lightningcss": "^1.19.0", + "lightningcss": "1.21.5", "mini-css-extract-plugin": "2.7.2", "node-fetch": "2.6.7", - "nodemon-webpack-plugin": "^4.8.1", + "nodemon-webpack-plugin": "4.8.1", "sass": "1.58.0", "sass-loader": "13.2.0", - "style-loader": "^3.3.1", + "style-loader": "3.3.3", "swc-loader": "0.2.3", "swc-plugin-nullstack": "0.1.3", "terser-webpack-plugin": "5.3.6", - "webpack": "^5.0.0", - "webpack-dev-server": "4.11.1", - "webpack-hot-middleware": "^2.25.3" + "webpack": "5.88.1", + "webpack-hot-middleware": "2.25.4" }, "devDependencies": { "webpack-dev-middleware": "github:Mortaro/webpack-dev-middleware#fix-write-to-disk-cleanup" From 30f10d7c12b30049e485ffac019e0594a10d0411 Mon Sep 17 00:00:00 2001 From: Guilherme Correia Date: Mon, 14 Aug 2023 19:53:32 -0300 Subject: [PATCH 14/27] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20Add=20type=20to?= =?UTF-8?q?=20inner=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- types/index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/types/index.d.ts b/types/index.d.ts index b2243c41..380e297b 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -100,6 +100,8 @@ export default class Nullstack { render?(context: NullstackClientContext): NullstackNode + [_property: `render${Capitalize}`]: Nullstack['render'] + prerendered: boolean /** From 7a74807da0699c8bc21fbf8e4de57876294bd9be Mon Sep 17 00:00:00 2001 From: hallexcosta Date: Fri, 18 Aug 2023 00:30:39 -0300 Subject: [PATCH 15/27] add utility function symbolHashSplit --- shared/symbolHashSplit.js | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 shared/symbolHashSplit.js diff --git a/shared/symbolHashSplit.js b/shared/symbolHashSplit.js new file mode 100644 index 00000000..c395e330 --- /dev/null +++ b/shared/symbolHashSplit.js @@ -0,0 +1,3 @@ +export default function symbolHashSplit(hash) { + return hash.split('---') +} \ No newline at end of file From 05ec145346b409b64ddfaaaf8ad1b4e665ae7e9e Mon Sep 17 00:00:00 2001 From: hallexcosta Date: Fri, 18 Aug 2023 00:31:17 -0300 Subject: [PATCH 16/27] change seperator hash to --- --- server/server.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/server.js b/server/server.js index 45e73617..54349932 100644 --- a/server/server.js +++ b/server/server.js @@ -2,6 +2,7 @@ import bodyParser from 'body-parser' import express from 'express' import path from 'path' import deserialize from '../shared/deserialize' +import symbolHashSplit from '../shared/symbolHashSplit' import prefix from '../shared/prefix' import context, { getCurrentContext, generateCurrentContext } from './context' import emulatePrerender from './emulatePrerender' @@ -123,7 +124,7 @@ server.start = function () { const payload = request.method === 'GET' ? request.query.payload : request.body const args = deserialize(payload) const { hash, methodName } = request.params - const [invokerHash, boundHash] = hash.split('-') + const [invokerHash, boundHash] = symbolHashSplit(hash) const key = `${invokerHash}.${methodName}` await load(boundHash || invokerHash) const invokerKlass = registry[invokerHash] @@ -154,7 +155,7 @@ server.start = function () { const payload = request.method === 'GET' ? request.query.payload : request.body const args = deserialize(payload) const { version, hash, methodName } = request.params - const [invokerHash, boundHash] = hash.split('-') + const [invokerHash, boundHash] = symbolHashSplit(hash) let [filePath, klassName] = (invokerHash || boundHash).split("___") const file = path.resolve('..', filePath.replaceAll('__', '/')) console.info('\x1b[1;37m%s\x1b[0m', ` [${request.method}] ${request.path}`) From a4fe03be041485234c17ced679ac0f5e24182285 Mon Sep 17 00:00:00 2001 From: hallexcosta Date: Fri, 18 Aug 2023 00:31:44 -0300 Subject: [PATCH 17/27] use utility function symbolHashSplit --- client/invoke.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/invoke.js b/client/invoke.js index 83b6df1a..0c11a7ed 100644 --- a/client/invoke.js +++ b/client/invoke.js @@ -13,7 +13,7 @@ export default function invoke(name, hash) { } else { worker.queues[name] = [...worker.queues[name], params] } - let finalHash = hash === this.hash ? hash : `${hash}-${this.hash}` + let finalHash = hash === this.hash ? hash : `${hash}---${this.hash}` let url = `${worker.api}/${prefix}/${finalHash}/${name}.json` if (module.hot) { const version = client.klasses[hash].__hashes[name] From 707943407751391c165c52120da1fca3be7d2a0c Mon Sep 17 00:00:00 2001 From: hallexcosta Date: Fri, 18 Aug 2023 14:49:52 -0300 Subject: [PATCH 18/27] feat: add new func symbolHashJoin and rename shared file to symbolHash.js --- client/invoke.js | 3 ++- server/server.js | 2 +- shared/symbolHash.js | 7 +++++++ shared/symbolHashSplit.js | 3 --- 4 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 shared/symbolHash.js delete mode 100644 shared/symbolHashSplit.js diff --git a/client/invoke.js b/client/invoke.js index 0c11a7ed..315a06a6 100644 --- a/client/invoke.js +++ b/client/invoke.js @@ -1,5 +1,6 @@ import deserialize from '../shared/deserialize' import prefix from '../shared/prefix' +import { symbolHashJoin } from '../shared/symbolHash' import page from './page' import worker from './worker' import client from './client' @@ -13,7 +14,7 @@ export default function invoke(name, hash) { } else { worker.queues[name] = [...worker.queues[name], params] } - let finalHash = hash === this.hash ? hash : `${hash}---${this.hash}` + let finalHash = hash === this.hash ? hash : symbolHashJoin(hash, this.hash) let url = `${worker.api}/${prefix}/${finalHash}/${name}.json` if (module.hot) { const version = client.klasses[hash].__hashes[name] diff --git a/server/server.js b/server/server.js index 54349932..26721640 100644 --- a/server/server.js +++ b/server/server.js @@ -2,7 +2,7 @@ import bodyParser from 'body-parser' import express from 'express' import path from 'path' import deserialize from '../shared/deserialize' -import symbolHashSplit from '../shared/symbolHashSplit' +import { symbolHashSplit } from '../shared/symbolHash' import prefix from '../shared/prefix' import context, { getCurrentContext, generateCurrentContext } from './context' import emulatePrerender from './emulatePrerender' diff --git a/shared/symbolHash.js b/shared/symbolHash.js new file mode 100644 index 00000000..53d59ad0 --- /dev/null +++ b/shared/symbolHash.js @@ -0,0 +1,7 @@ +export function symbolHashSplit(hash) { + return hash.split('---') +} + +export function symbolHashJoin(hash, joinHash) { + return `${hash}---${joinHash}` +} \ No newline at end of file diff --git a/shared/symbolHashSplit.js b/shared/symbolHashSplit.js deleted file mode 100644 index c395e330..00000000 --- a/shared/symbolHashSplit.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function symbolHashSplit(hash) { - return hash.split('---') -} \ No newline at end of file From a9ec053c3867a224333e083973e63696d6cef1d1 Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Tue, 22 Aug 2023 19:26:29 -0300 Subject: [PATCH 19/27] :bookmark: version 0.20.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 75a446ad..005279b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nullstack", - "version": "0.20.0", + "version": "0.20.1", "description": "Feature-Driven Full Stack JavaScript Components", "main": "./types/index.d.ts", "author": "Mortaro", From de4df434839bfa7394b15b14a33fd9c6dbb31fa5 Mon Sep 17 00:00:00 2001 From: Guilherme Correia Date: Thu, 24 Aug 2023 00:23:48 -0300 Subject: [PATCH 20/27] =?UTF-8?q?=F0=9F=9A=91=20Move=20`webpack-dev-middle?= =?UTF-8?q?ware`=20to=20prod=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/package.json b/package.json index 005279b0..a26b0434 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,7 @@ "swc-plugin-nullstack": "0.1.3", "terser-webpack-plugin": "5.3.6", "webpack": "5.88.1", - "webpack-hot-middleware": "2.25.4" - }, - "devDependencies": { + "webpack-hot-middleware": "2.25.4", "webpack-dev-middleware": "github:Mortaro/webpack-dev-middleware#fix-write-to-disk-cleanup" } } \ No newline at end of file From 1f48102fcc948e173e814be30e76d6342effc32b Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Thu, 24 Aug 2023 12:03:09 -0300 Subject: [PATCH 21/27] :bookmark: version 0.20.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a26b0434..afd9f8b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nullstack", - "version": "0.20.1", + "version": "0.20.2", "description": "Feature-Driven Full Stack JavaScript Components", "main": "./types/index.d.ts", "author": "Mortaro", From 4acab9d496d0c890256e5f1c2543a7c0f7642f2f Mon Sep 17 00:00:00 2001 From: Guilherme Correia Date: Sat, 26 Aug 2023 21:04:48 -0300 Subject: [PATCH 22/27] =?UTF-8?q?=F0=9F=9A=91=E2=AC=86=EF=B8=8F=20Update?= =?UTF-8?q?=20webpack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index afd9f8b8..9a1a2097 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "swc-loader": "0.2.3", "swc-plugin-nullstack": "0.1.3", "terser-webpack-plugin": "5.3.6", - "webpack": "5.88.1", + "webpack": "5.88.2", "webpack-hot-middleware": "2.25.4", "webpack-dev-middleware": "github:Mortaro/webpack-dev-middleware#fix-write-to-disk-cleanup" } From 62d753c838720b44ebac1e9703abe2c72f75642c Mon Sep 17 00:00:00 2001 From: Guilherme Correia Date: Tue, 29 Aug 2023 06:32:52 -0300 Subject: [PATCH 23/27] =?UTF-8?q?=F0=9F=93=8C=20Move=20`webpack-dev-middle?= =?UTF-8?q?ware`=20from=20git=20to=20latest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index afd9f8b8..5fda4101 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,6 @@ "terser-webpack-plugin": "5.3.6", "webpack": "5.88.1", "webpack-hot-middleware": "2.25.4", - "webpack-dev-middleware": "github:Mortaro/webpack-dev-middleware#fix-write-to-disk-cleanup" + "webpack-dev-middleware": "6.1.1" } } \ No newline at end of file From fb4c5bf18c353ef5e9ef5c6ca9373818d87ce9ed Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Tue, 29 Aug 2023 16:53:47 -0300 Subject: [PATCH 24/27] :bookmark: version 0.20.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5db39879..83712d54 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nullstack", - "version": "0.20.2", + "version": "0.20.3", "description": "Feature-Driven Full Stack JavaScript Components", "main": "./types/index.d.ts", "author": "Mortaro", From 73df30ccd507347b65b9e46a1dd39ee6cb7c728d Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Mon, 1 Sep 2025 15:31:50 -0300 Subject: [PATCH 25/27] :sparkles: svg support --- client/render.js | 9 ++++--- client/rerender.js | 14 ++++++----- shared/nodes.js | 2 +- tests/src/Application.njs | 2 ++ tests/src/SvgSupport.njs | 46 ++++++++++++++++++++++++++++++++++++ tests/src/SvgSupport.test.js | 22 +++++++++++++++++ 6 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 tests/src/SvgSupport.njs create mode 100644 tests/src/SvgSupport.test.js diff --git a/client/render.js b/client/render.js index 7fee3434..e137652f 100644 --- a/client/render.js +++ b/client/render.js @@ -5,7 +5,7 @@ import { anchorableElement } from './anchorableNode' import { generateCallback, generateSubject } from './events' import { ref } from './ref' -export default function render(node, options) { +export default function render(node, isSvg = false) { if (isFalse(node) || node.type === 'head') { node.element = document.createComment('') return node.element @@ -16,9 +16,8 @@ export default function render(node, options) { return node.element } - const svg = (options && options.svg) || node.type === 'svg' - - if (svg) { + isSvg = isSvg || node.type === 'svg' + if (isSvg) { node.element = document.createElementNS('http://www.w3.org/2000/svg', node.type) } else { node.element = document.createElement(node.type) @@ -58,7 +57,7 @@ export default function render(node, options) { if (!node.attributes.html) { for (let i = 0; i < node.children.length; i++) { - const child = render(node.children[i], { svg }) + const child = render(node.children[i], isSvg) node.element.appendChild(child) } diff --git a/client/rerender.js b/client/rerender.js index cd788f67..512f9d69 100644 --- a/client/rerender.js +++ b/client/rerender.js @@ -100,7 +100,7 @@ function updateHeadChildren(currentChildren, nextChildren) { } } -function _rerender(current, next) { +function _rerender(current, next, isParentSvg = false) { const selector = current.element next.element = current.element @@ -108,8 +108,10 @@ function _rerender(current, next) { return } + const isSvg = isParentSvg || next.type === 'svg' + if (current.type !== next.type) { - const nextSelector = render(next) + const nextSelector = render(next, isSvg) selector.replaceWith(nextSelector) return } @@ -132,22 +134,22 @@ function _rerender(current, next) { const limit = Math.max(current.children.length, next.children.length) if (next.children.length > current.children.length) { for (let i = 0; i < current.children.length; i++) { - _rerender(current.children[i], next.children[i]) + _rerender(current.children[i], next.children[i], isSvg) } for (let i = current.children.length; i < next.children.length; i++) { - const nextSelector = render(next.children[i]) + const nextSelector = render(next.children[i], isSvg) selector.appendChild(nextSelector) } } else if (current.children.length > next.children.length) { for (let i = 0; i < next.children.length; i++) { - _rerender(current.children[i], next.children[i]) + _rerender(current.children[i], next.children[i], isSvg) } for (let i = current.children.length - 1; i >= next.children.length; i--) { selector.childNodes[i].remove() } } else { for (let i = limit - 1; i > -1; i--) { - _rerender(current.children[i], next.children[i]) + _rerender(current.children[i], next.children[i], isSvg) } } } diff --git a/shared/nodes.js b/shared/nodes.js index f1d3363e..4da25648 100644 --- a/shared/nodes.js +++ b/shared/nodes.js @@ -18,5 +18,5 @@ export function isFunction(node) { } export function isText(node) { - return node.type === 'text' + return node.type === 'text' && node.attributes === undefined } diff --git a/tests/src/Application.njs b/tests/src/Application.njs index 58691b9e..5bd8201a 100644 --- a/tests/src/Application.njs +++ b/tests/src/Application.njs @@ -63,6 +63,7 @@ import LazyComponentLoader from './LazyComponentLoader' import NestedFolder from './nested/NestedFolder' import ChildComponentWithoutServerFunctions from './ChildComponentWithoutServerFunctions' import ObjectEventScope from './ObjectEventScope' +import SvgSupport from './SvgSupport.njs' import './Application.css' class Application extends Nullstack { @@ -156,6 +157,7 @@ class Application extends Nullstack { + ) diff --git a/tests/src/SvgSupport.njs b/tests/src/SvgSupport.njs new file mode 100644 index 00000000..6dcc98e5 --- /dev/null +++ b/tests/src/SvgSupport.njs @@ -0,0 +1,46 @@ +import Nullstack from 'nullstack'; + +function Close({ size }) { + return ( + + + + + ) +} + +function Hamburger({ size }) { + return ( + + + + + + ) +} + +class SvgSupport extends Nullstack { + + open = false + visible = false + + render() { + return ( +
+ + I + love + my + cat! + + {this.open ? : } + + {this.visible && } + +
+ ) + } + +} + +export default SvgSupport; \ No newline at end of file diff --git a/tests/src/SvgSupport.test.js b/tests/src/SvgSupport.test.js new file mode 100644 index 00000000..0a966a6b --- /dev/null +++ b/tests/src/SvgSupport.test.js @@ -0,0 +1,22 @@ +describe('SvgSupport', () => { + beforeEach(async () => { + await page.goto('http://localhost:6969/svg-support') + await page.waitForSelector('[data-hydrated]') + }) + + test('svg can render text', async () => { + expect(true).toBeTruthy() + }) + + test('svg can add new paths while rerendering', async () => { + expect(true).toBeTruthy() + }) + + test('svg can render in short circuit statements', async () => { + expect(true).toBeTruthy() + }) + + test('svg can render in ternary statements', async () => { + expect(true).toBeTruthy() + }) +}) From 0b51beb8097a7610645d1882bc6d2c1a621a814e Mon Sep 17 00:00:00 2001 From: Aylon Muramatsu Date: Mon, 6 Oct 2025 17:19:40 -0300 Subject: [PATCH 26/27] =?UTF-8?q?feat:=20implementa=20testes=20e=20adicion?= =?UTF-8?q?a=20corre=C3=A7=C3=A3o=20para=20render=20do=20server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/render.js | 4 +- tests/src/SvgSupport.njs | 2 +- tests/src/SvgSupport.test.js | 96 ++++++++++++++++++++++++++++++++++-- 3 files changed, 95 insertions(+), 7 deletions(-) diff --git a/server/render.js b/server/render.js index e1b565d2..06e416cf 100644 --- a/server/render.js +++ b/server/render.js @@ -1,4 +1,4 @@ -import { isFalse } from '../shared/nodes' +import { isFalse, isText } from '../shared/nodes' import { sanitizeHtml } from '../shared/sanitizeString' import renderAttributes from './renderAttributes' @@ -25,7 +25,7 @@ function renderBody(node, scope, next) { if (isFalse(node)) { return '' } - if (node.type === 'text') { + if (isText(node)) { const text = node.text === '' ? ' ' : sanitizeHtml(node.text.toString()) return next && next.type === 'text' ? `${text}` : text } diff --git a/tests/src/SvgSupport.njs b/tests/src/SvgSupport.njs index 6dcc98e5..9080bc9d 100644 --- a/tests/src/SvgSupport.njs +++ b/tests/src/SvgSupport.njs @@ -26,7 +26,7 @@ class SvgSupport extends Nullstack { render() { return ( -
+
I love diff --git a/tests/src/SvgSupport.test.js b/tests/src/SvgSupport.test.js index 0a966a6b..b6510611 100644 --- a/tests/src/SvgSupport.test.js +++ b/tests/src/SvgSupport.test.js @@ -5,18 +5,106 @@ describe('SvgSupport', () => { }) test('svg can render text', async () => { - expect(true).toBeTruthy() + // Verifica se o SVG possui 4 elementos dentro dele + const svg = await page.$('svg'); + const texts = await svg.$$('text'); + expect(texts.length).toBe(4); }) test('svg can add new paths while rerendering', async () => { - expect(true).toBeTruthy() + // Verifica se o ícone Hamburger está presente inicialmente (3 paths) + const hamburgerPaths = await page.$$('svg[width="30"] path') + expect(hamburgerPaths.length).toBe(3) // Hamburger has 3 paths }) test('svg can render in short circuit statements', async () => { - expect(true).toBeTruthy() + // Verifica se o ícone de Hamburger está sendo exibido (3 paths) + const hamburgerPaths = await page.$$('svg[width="30"] path') + expect(hamburgerPaths.length).toBe(3) }) test('svg can render in ternary statements', async () => { - expect(true).toBeTruthy() + let bigHamburger = await page.$('svg[width="69"]') + expect(bigHamburger).toBeFalsy() + + // Clica no segundo botão (show) + const buttons = await page.$$('button') + await buttons[1].click() + + // Aguarda o Hamburger grande aparecer + await page.waitForSelector('svg[width="69"]') + + // Verifica se o Hamburger foi renderizado no ternário + bigHamburger = await page.$('svg[width="69"]') + expect(bigHamburger).toBeTruthy() + + }) + + test('icon toggle functionality works correctly', async () => { + // Primeiro verifica o estado inicial (deve ser Hamburger, 3 paths) + let iconPaths = await page.$$('svg[width="30"] path') + expect(iconPaths.length).toBe(3) // Hamburger tem 3 paths + + // Clica no primeiro botão (toggle) + const buttons = await page.$$('button') + await buttons[0].click() + + // Aguarda o ícone trocar (Close tem 2 paths) + await page.waitForFunction(() => { + const svg = document.querySelector('svg[width="30"]'); + return svg && svg.querySelectorAll('path').length === 2; + }); + + iconPaths = await page.$$('svg[width="30"] path') + expect(iconPaths.length).toBe(2) // Close tem 2 paths + + // Clica novamente para voltar ao Hamburger + await buttons[0].click() + + // Aguarda o ícone trocar de volta (Hamburger tem 3 paths) + await page.waitForFunction(() => { + const svg = document.querySelector('svg[width="30"]'); + return svg && svg.querySelectorAll('path').length === 3; + }); + + iconPaths = await page.$$('svg[width="30"] path') + expect(iconPaths.length).toBe(3) // Hamburger tem 3 paths + }) + + test('icon visibility toggle works correctly', async () => { + + // Verifica que o ícone grande não está visível inicialmente + let bigHamburger = await page.$('svg[width="69"]') + expect(bigHamburger).toBeFalsy() + + // Clica no segundo botão (show) + const buttons = await page.$$('button') + await buttons[1].click() + + // Aguarda o Hamburger grande aparecer + await page.waitForSelector('svg[width="69"]') + + // Verifica se o Hamburger grande apareceu + bigHamburger = await page.$('svg[width="69"]') + expect(bigHamburger).toBeTruthy() + + // Clica novamente no segundo botão (show) para esconder + await buttons[1].click() + + // Aguarda o Hamburger grande desaparecer do DOM + await page.waitForSelector('svg[width="69"]', { hidden: true }) + + // Verifica se o Hamburger grande desapareceu + bigHamburger = await page.$('svg[width="69"]') + expect(bigHamburger).toBeFalsy() + }) + + test('svg attributes are correctly applied', async () => { + // Verifica se os atributos SVG estão sendo aplicados corretamente + const svgElement = await page.$('svg[viewBox="0 0 240 80"]') + expect(svgElement).toBeTruthy() + + const xmlns = await page.$eval('svg[viewBox="0 0 240 80"]', el => el.getAttribute('xmlns')) + expect(xmlns).toBe('http://www.w3.org/2000/svg') }) }) From ec5e51be540a98bab311ab02eef5c9c0231ec017 Mon Sep 17 00:00:00 2001 From: Christian Mortaro Date: Thu, 9 Oct 2025 22:21:05 -0300 Subject: [PATCH 27/27] :bookmark: version 0.20.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 83712d54..970fece1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nullstack", - "version": "0.20.3", + "version": "0.20.4", "description": "Feature-Driven Full Stack JavaScript Components", "main": "./types/index.d.ts", "author": "Mortaro",