nuejs-core
Advanced tools
Comparing version 0.1.1 to 0.1.2
# Contributing to Nue | ||
Nues's codebase has two distinct parts: | ||
* The reactive client is under [src](src) directory | ||
* Server parts are under [ssr](ssr) directory (SSR: "Server Side Rendering") | ||
First and foremost: thank you for helping with Nue! ❤️❤️ | ||
[Bun](//bun.sh) is the preferred test and development environment because it's noticeably faster than Node or Deno. | ||
### Guidelines | ||
## Best practices | ||
1. **Most important** If you are adding a new feature, please _discuss it first_ by creating a new issue with the tag "New feature". You probably avoid doing redundant work, because not all features are automatically accepted. Nue JS strives for minimalism. | ||
1. If you are adding a new feature, please add a test. If you are fixing a bug, please add a test that fails before your fix and passes after your fix. | ||
2. Features that add lots of new code, complexity, or several new/heavy NPM packages are most likely rejected. Particularly if the first rule wasn't applied. | ||
2. Nue is a minimalistic project. The goal is to stay lean in all areas of development. Keep things simple and depend on as little dependencies as possible. | ||
3. Please add one fix/feature per pull request. Easier to accept and less potential merge conflicts. | ||
3. JavaScript is preferred over TypeScript because of minimalism, dynamic typing and it's standards-based. TypeScript should be used if there is a clear use case or a problem it solves. | ||
3. Please add a test for every bug fix | ||
4. New features add complexity. They must always be discussed before implemented. | ||
3. Please write JavaScript (Not TypeScript) | ||
### Running server test | ||
### Formatting rules | ||
Please try to use the original style in the codebase. Do not introduce new rules or patterns. The most notable rules are: | ||
1. No semicolons, because it's redundant | ||
2. Strings with single quotes | ||
3. Indent with two spaces | ||
4. Prefer `==` over `===`. Only strict equality only when truly needed, which is rarely | ||
Nue is not using Prettier or ESLint because they will increase the project size to 40MB. A single `.prettierrc.csj` file is preferred on the root directory. Not sure if [Biome](//biomejs.dev/) is better. | ||
### Nue JS codebase | ||
Nues JS codebase has two distinct parts: | ||
* The reactive client is under [src](src) directory | ||
* Server parts are under [ssr](ssr) directory (SSR: "Server Side Rendering") | ||
[Bun](//bun.sh) is the preferred test and development environment because it's noticeably faster than Node or Deno. | ||
### Running tests | ||
Nue uses [Bun](//bun.sh) for running tests: | ||
@@ -39,14 +60,3 @@ | ||
## FAQ | ||
### Why not the `===` operator? [equality] | ||
Strict typing is rarely needed in dynamically typed languages. Loose typing (`==`) is usually enough. | ||
### Why not semicolons or double quotes? | ||
Semicolons are optional and double is literally 2x. Less is more. | ||
### Why not TypeScript? | ||
This is answered on our [general FAQ page](//nuejs.org/faq/#ts) | ||
@@ -5,3 +5,3 @@ { | ||
"main": "ssr/index.js", | ||
"version": "0.1.1", | ||
"version": "0.1.2", | ||
"scripts": { | ||
@@ -14,3 +14,6 @@ "test": "cd test && bun test", | ||
"htmlparser2": "^9.0.0" | ||
}, | ||
"engines": { | ||
"bun": ">= 1" | ||
} | ||
} |
@@ -7,6 +7,6 @@ | ||
[Backstory](//nuejs.org/backstory/) • | ||
[Ecosystem](//nuejs.org/ecosystem/) • | ||
[Documentation](//nuejs.org/docs/nuejs/) • | ||
[Examples](//nuejs.org/docs/nuejs/examples/) • | ||
[Getting started](//nuejs.org/docs/nuejs/getting-started.html) | ||
[Rethinking reactivity](//nuejs.org/blog/blog/rethinking-reactivity/) • | ||
@@ -16,11 +16,11 @@ | ||
Nue JS is an exceptionally small (2.3kb minzipped) JavaScript library for building web interfaces. It is the core of the upcoming [Nue ecosystem](//nuejs.org/ecosystem/). It’s like **Vue.js, React.js**, or **Svelte** but there are no hooks, effects, props, portals, watchers, provides, injects, suspension, or other unusual abstractions on your way. Learn the basics of HTML, CSS, and JavaScript and you are good to go. | ||
Nue JS is an exceptionally small (2.3kb minzipped) JavaScript library for building web interfaces. It is the core of the upcoming [Nue toolset](//nuejs.org/tools/). It’s like **Vue.js, React.js**, or **Svelte** but there are no hooks, effects, props, portals, watchers, provides, injects, suspension, or other unusual abstractions on your way. Learn the basics of HTML, CSS, and JavaScript and you are good to go. | ||
## Build user interfaces with 10x less code | ||
The biggest benefit of Nue is that you need less code to do the same thing: | ||
## Build user interfaces with cleaner code | ||
With Nue your UI code is cleaner and usually smaller: | ||
![The amount of code required to build a basic listbox UI component](https://nuejs.org/docs/img/react-listbox-big.jpg) | ||
![The difference in coding style](https://nuejs.org/docs/img/react-listbox.jpg?1) | ||
It's not unusual to see 10x differences in the amount of code you need to write. For example, a listbox component written with Nue is around [ten times smaller](//nuejs.org/compare/component.html) than the [React version](https://headlessui.com/react/listbox) from the Headless UI project. | ||
It's not unusual to see [2x-10x differences](//nuejs.org/compare/component.html) in the amount of code you need to write. | ||
@@ -32,7 +32,7 @@ | ||
``` html | ||
<div @name="media-object" class="{ type }"> | ||
<div class="{ type }"> | ||
<img src="{ img }"> | ||
<aside> | ||
<h3>{ title }</h3> | ||
<p :if="desc">{ desc }</h3> | ||
<p :if="desc">{ desc }</p> | ||
<slot/> | ||
@@ -49,6 +49,6 @@ </aside> | ||
1. [Minimalism](//nuejs.org/why/#minimalism), a hundred lines of code is easier to scale than a thousand lines of code | ||
1. [Separation of concerns](//nuejs.org//why/#soc), easy-to-understand code is easier to scale than "spaghetti code" | ||
1. [Minimalism](//nuejs.org/why/#minimalism), a hundred lines of code is easier to scale than a thousand lines of code | ||
1. **Separation of talent**, when UX developers focus on the [front of the frontend][back] and JS/TS developers focus on the back of the frontend your team skills are optimally aligned: | ||
@@ -59,18 +59,7 @@ | ||
### Decoupled styling | ||
Nue does not promote the use of Scoped CSS, style attribute, Tailwind, or other CSS-in-JS gymnastics: | ||
1. **More reusable code**: When styling is not hardcoded to the component, the same component can look different depending on the page or context. | ||
1. **No spaghetti code**: pure HTML or pure CSS is easier to read than mixed spaghetti code | ||
1. **Faster page loads**: With decoupled styling it's easier to extract primary CSS from the secondary and keep your HTML page under the critical [14kb limit][fourteen]. | ||
Learn more about [styling](//nuejs.org/docs/nuejs/styling-components.html) | ||
## Reactive and isomorphic | ||
## Reactive, hybrid, and isomorphic | ||
Nue has a rich component model and it allows you to create all kinds of applications using different kinds of components: | ||
1. [Server components](//nuejs.org/docs/nuejs/server-components.html) are rendered on the server. They help you build content-focused websites that load faster without JavaScript and are crawlable by search engines. | ||
1. [Server components](//nuejs.org/docs/nuejs/server-components.html) are rendered on the server. They help you build content-focused websites that load faster without JavaScript and are crawled by search engines. | ||
@@ -115,5 +104,5 @@ 2. [Reactive components](//nuejs.org/docs/nuejs/reactive-components.html) are rendered on the client. They help you build dynamic islands or single-page applications. | ||
## Simpler tooling | ||
Nue JS comes with a simple `render` function for server-side rendering and a `compile` function to generate components for the browser. You don't need complex bundlers like Webpack or Vite to take control of your development environment. Just import Nue to your project and you are good to go. | ||
Nue JS comes with a simple `render` function for server-side rendering and a `compile` function to generate components for the browser. There is no need for toolchains like Webpack or Vite to hijack your natural workflow. Just import Nue to your project and you are good to go. | ||
You can of course use a bundler on the business model if your application becomes more complex with tons of dependencies. [Bun](//bun.sh) and [esbuild](//esbuild.github.io/) are great, performant options. | ||
You can of course use a bundler on the business model if your application becomes more complex with tons of dependencies. [Bun](//bun.sh) and [esbuild](//esbuild.github.io/) are great options. | ||
@@ -120,0 +109,0 @@ |
@@ -16,4 +16,4 @@ | ||
* @param { Component } component - a (compiled) component instance to be mounted | ||
* @param { Object } data? - optional data or data model for the component | ||
* @param { Array<Component> } deps - optional array of nested/dependant components | ||
* @param { Object } [data = {}] - optional data or data model for the component | ||
* @param { Array<Component> } [deps = {}] - optional array of nested/dependant components | ||
* @param { Object } $parent - (for internal use only) | ||
@@ -134,12 +134,12 @@ */ | ||
// dynamic attributes | ||
if (char == ':' && real != 'bind') { | ||
expr.push(_=> { | ||
let val = fn(ctx) | ||
setAttr(node, real, renderVal(val)) | ||
}) | ||
} | ||
// event handler | ||
if (char == '@') { | ||
if (char == ':') { | ||
if (real != 'bind') { | ||
// dynamic attributes | ||
expr.push(_ => { | ||
let val = fn(ctx) | ||
setAttr(node, real, renderVal(val)) | ||
}) | ||
} | ||
} else if (char == '@') { | ||
// event handler | ||
node[`on${real}`] = evt => { | ||
@@ -150,7 +150,5 @@ fn.call(ctx, ctx, evt) | ||
} | ||
} | ||
// boolean attribute | ||
if (char == '$') { | ||
expr.push(_=> { | ||
} else if (char == '$') { | ||
// boolean attribute | ||
expr.push(_ => { | ||
const flag = node[real] = !!fn(ctx) | ||
@@ -175,4 +173,5 @@ if (!flag) node.removeAttribute(real) | ||
function getAttr(node, key) { | ||
const fn = fns[node.getAttribute(':' + key)] | ||
return fn ? fn(ctx) : node.getAttribute(key) || node[key] || undefined | ||
const val = node.getAttribute(':' + key) | ||
const fn = fns[val] | ||
return fn ? fn(ctx) : ctx[val] || node.getAttribute(key) || node[key] || undefined | ||
} | ||
@@ -179,0 +178,0 @@ |
const VARIABLE = /(^|[\-\+\*\/\!\s\(\[]+)([\$,a-z,_]\w*)\b/g | ||
const VARIABLE = /(^|[\-\+\*\/\!\s\(\[]+)([\$a-z_]\w*)\b/g | ||
const STRING = /('[^']+'|"[^"]+")/ | ||
const EXPR = /\{([^}]+)\}/g | ||
const EXPR = /\{([^{}]+)\}/g | ||
@@ -20,3 +20,3 @@ | ||
const is_reserved = RESERVED.includes(varname) | ||
return prefix + (is_reserved ? varname == '$event' ? 'e' : varname : '_.' + varname.trimLeft()) | ||
return prefix + (is_reserved ? varname == '$event' ? 'e' : varname : '_.' + varname.trimStart()) | ||
}) | ||
@@ -23,0 +23,0 @@ } |
@@ -7,14 +7,25 @@ | ||
export const STD = 'a abbr acronym address applet area article aside audio b base basefont bdi bdo big\ | ||
blockquote body br button canvas caption center cite code col colgroup data datalist dd del\ | ||
details dfn dialog dir div dl dt em embed fieldset figcaption figure font footer form frame\ | ||
frameset head header hgroup h1 h2 h3 h4 h5 h6 hr html i iframe img input ins kbd keygen label\ | ||
legend li link main map mark menu menuitem meta meter nav noframes noscript object ol optgroup\ | ||
option output p param picture pre progress q rp rt ruby s samp script section select small\ | ||
source span strike strong style sub summary sup svg table tbody td template textarea tfoot th\ | ||
thead time title tr track tt u ul var video wbr'.split(' ') | ||
blockquote body br button canvas caption center circle cite clipPath code col colgroup data datalist\ | ||
dd defs del details dfn dialog dir div dl dt ellipse em embed fieldset figcaption figure font footer\ | ||
foreignObject form frame frameset g head header hgroup h1 h2 h3 h4 h5 h6 hr html i iframe image img\ | ||
input ins kbd keygen label legend li line link main map mark marker mask menu menuitem meta meter\ | ||
nav noframes noscript object ol optgroup option output p param path pattern picture polygon polyline\ | ||
pre progress q rect rp rt ruby s samp script section select small source span strike strong style sub\ | ||
summary sup svg switch symbol table tbody td template text textarea textPath tfoot th thead time\ | ||
title tr track tspan tt u ul use var video wbr'.split(' ') | ||
const BOOLEAN = `allowfullscreen async autofocus autoplay checked controls default | ||
defer disabled formnovalidate hidden ismap itemscope loop multiple muted nomodule novalidate | ||
open playsinline readonly required reversed selected truespeed`.trim().split(/\s+/) | ||
const SVG = 'animate animateMotion animateTransform circle clipPath defs desc ellipse\ | ||
feBlend feColorMatrix feComponentTransfer feComposite feConvolveMatrix feDiffuseLighting\ | ||
feDisplacementMap feDistantLight feDropShadow feFlood feFuncA feFuncB feFuncG feFuncR\ | ||
feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset fePointLight feSpecularLighting\ | ||
feSpotLight feTile feTurbulence filter foreignObject g hatch hatchpath image line linearGradient\ | ||
marker mask metadata mpath path pattern polygon polyline radialGradient rect set stop style svg\ | ||
switch symbol text textPath title tspan use view'.split(' ') | ||
STD.push(...SVG) | ||
const BOOLEAN = 'allowfullscreen async autofocus autoplay checked controls default\ | ||
defer disabled formnovalidate hidden ismap itemscope loop multiple muted nomodule\ | ||
novalidate open playsinline readonly required reversed selected truespeed'.split(/\s+/) | ||
export function isBoolean(key) { | ||
@@ -42,5 +53,7 @@ return BOOLEAN.includes(key) | ||
node = node.firstChild | ||
let next = null | ||
while (node) { | ||
next = node.nextSibling | ||
walk(node, fn) | ||
node = node.nextSibling | ||
node = next | ||
} | ||
@@ -47,0 +60,0 @@ } |
import { mkdom, getComponentName, mergeAttribs, exec, STD } from './fn.js' | ||
import { mkdom, getComponentName, mergeAttribs, isBoolean, exec, STD, walk } from './fn.js' | ||
import { parseExpr, parseFor, setContext } from './expr.js' | ||
@@ -43,6 +43,9 @@ import { parseDocument, DomUtils as DOM } from 'htmlparser2' | ||
// TODO: check all non-strings here | ||
if (val.constructor === Object) return | ||
// attributes must be strings | ||
if (1 * val) val = attribs[key] = '' + val | ||
const has_expr = val.includes('{') | ||
@@ -60,10 +63,18 @@ | ||
// expression | ||
if (key[0] == ':') { | ||
const name = key.slice(1) | ||
const value = has_expr ? renderExpr(val, data, name == 'class') : exec(setContext(val), data) | ||
if (value) attribs[name] = value | ||
if (key[0] != ':') return | ||
const name = key.slice(1) | ||
const value = has_expr ? renderExpr(val, data, name == 'class') : exec(setContext(val), data) | ||
// boolean attribute | ||
if (isBoolean(name)) { | ||
if (value != 'false') attribs[name] = '' | ||
else delete attribs[name] | ||
delete attribs[key] | ||
return delete attribs[key] | ||
} | ||
// other attribute | ||
if (value) attribs[name] = value | ||
else delete attribs[name] | ||
delete attribs[key] | ||
} | ||
@@ -82,5 +93,5 @@ | ||
function processIf(node, expr, data) { | ||
function processIf(node, expr, data, deps) { | ||
const blocks = getIfBlocks(node, expr) | ||
const flag = blocks.find(el => { | ||
const active = blocks.find(el => { | ||
const val = exec(setContext(el.expr), data) | ||
@@ -90,4 +101,8 @@ return val && val != 'false' | ||
blocks.forEach(el => { if (el != flag) removeElement(el.root) }) | ||
return flag | ||
blocks.forEach(el => { | ||
const { root } = el | ||
if (el == active) processNode({ root, data, deps }) | ||
else removeElement(root) | ||
}) | ||
return active | ||
} | ||
@@ -124,11 +139,12 @@ | ||
removeElement(node) | ||
// mark as dummy (removeElement(node) does not work here) | ||
node.attribs.__dummy = 'true' | ||
} | ||
// child component | ||
function processChild(comp, node, deps) { | ||
function processChild(comp, node, deps, data) { | ||
const { attribs } = node | ||
// merge attributes | ||
const child = comp.create(attribs, deps, node.children) | ||
const child = comp.create({ ...data, ...attribs }, deps, node.children) | ||
if (child.children.length == 1) mergeAttribs(child.firstChild.attribs, attribs) | ||
@@ -152,2 +168,7 @@ | ||
// setup empty attributes (:date --> :date="date") | ||
for (let key in attribs) { | ||
if (key[0] == ':' && attribs[key] == '') attribs[key] = key.slice(1) | ||
} | ||
// root | ||
@@ -162,7 +183,7 @@ if (type == 'root') { | ||
// element | ||
} else if (type == 'tag' || type == 'style') { | ||
} else if (type == 'tag' || type == 'style' || type == 'script') { | ||
// if | ||
let expr = getDel(':if', attribs) | ||
if (expr && !processIf(node, expr, data)) return nextSibling | ||
if (expr && !processIf(node, expr, data, deps)) return nextSibling | ||
@@ -189,8 +210,15 @@ // for | ||
// slot | ||
if (inner && name == 'slot') { | ||
while (inner[0]) DOM.prepend(node, inner[0]) | ||
removeElement(node) | ||
// slots | ||
if (name == 'slot') { | ||
if (attribs.for) { | ||
const html = data[attribs.for] | ||
if (html) DOM.replaceElement(node, mkdom(html)) | ||
} else if (inner) { | ||
while (inner[0]) DOM.prepend(node, inner[0]) | ||
removeElement(node) | ||
} | ||
} | ||
// custom component | ||
@@ -212,3 +240,3 @@ const is_custom = !STD.includes(name) | ||
// server side component | ||
if (component) processChild(component, node, deps) | ||
if (component) processChild(component, node, deps, data) | ||
@@ -256,2 +284,6 @@ } | ||
const node = create(data, deps) | ||
// cleanup / remove dummy elements | ||
walk(node, el => { if (el.attribs?.__dummy) removeElement(el) }) | ||
return getOuterHTML(node) | ||
@@ -306,5 +338,5 @@ } | ||
const src = await fs.readFile(path, 'utf-8') | ||
return render(path, data, deps) | ||
return render(src, data, deps) | ||
} | ||
@@ -14,3 +14,7 @@ | ||
function debug(tmpl, data) { | ||
console.info(render(tmpl, data)) | ||
} | ||
test('Expressions', () => { | ||
@@ -27,2 +31,4 @@ runTests({ | ||
'<input :type>': '<input type="bold">', | ||
// HTML | ||
@@ -40,5 +46,12 @@ '<h2>{ title }</h2>': '<h2>Hey <em>!</em></h2>', | ||
test('Comments', () => { | ||
runTests({ '<!-- hello --><b>World</b>': '<b>World</b>' }) | ||
runTests({ '<!-- hello --><!-- Another --><b>World</b>': '<b>World</b>' }) | ||
}) | ||
test('Conditionals', () => { | ||
runTests({ | ||
'<div><b :if="am > 100">No</b><p>Yes</p></div>': '<div><p>Yes</p></div>', | ||
'<a><em :if="flag"></em><b :else>{ val }</b></a>': '<a><b>A</b></a>', | ||
'<div><b :if="am > 100">No</b><b :else-if="am == 100">Yes</b><b :else>No</b></div>': '<div><b>Yes</b></div>', | ||
@@ -49,2 +62,3 @@ '<div><custom :if="bad"/></div> <b @name="custom">Hey</b>': '<div></div>', | ||
am: 100, | ||
val: 'A' | ||
}) | ||
@@ -77,3 +91,5 @@ }) | ||
test('Loops', () => { | ||
runTests({ | ||
'<p :for="n in nums">{ n }</p>': '<p>1</p><p>2</p><p>3</p>', | ||
@@ -94,2 +110,5 @@ | ||
// successive loops | ||
'<div><p :for="x in nums">{ x }</p><a :for="y in nums">{ y }</a></div>': | ||
'<div><p>1</p><p>2</p><p>3</p><a>1</a><a>2</a><a>3</a></div>', | ||
@@ -120,2 +139,4 @@ }, { | ||
// return debug('<html><slot for="page"/></html>', { page: '<main>Hello</main>' }) | ||
runTests({ | ||
@@ -126,2 +147,4 @@ | ||
'<hey :val/>': '<nue-island island="hey">\n <script type="application/json">{"val":"1"}</script>\n</nue-island>', | ||
// nue element | ||
@@ -131,2 +154,4 @@ // '<foo :nums="nums" :person="person" data-x="bar"/>': | ||
'<html><slot for="page"/></html>': '<html><main>Hello</main></html>', | ||
// custom tag and slots | ||
@@ -138,3 +163,5 @@ '<parent><p>{{ am }}</p><p>{ person.name }</p></parent><div @name="parent"><h3>Parent</h3><slot/></div>': | ||
person: { name: 'Nick', age: 10 }, | ||
page: '<main>Hello</main>', | ||
nums: [1, 2], | ||
val: 1 | ||
}) | ||
@@ -141,0 +168,0 @@ }) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
85865
1445
125