From 85c88810291473d6512abbeed904b4143d0bba14 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 08:38:58 +0200 Subject: [PATCH 01/33] Add initial roadmap for project features Outline features and improvements for future development. --- Roadmap.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 Roadmap.md diff --git a/Roadmap.md b/Roadmap.md new file mode 100644 index 0000000..c817ab9 --- /dev/null +++ b/Roadmap.md @@ -0,0 +1,13 @@ +# Roadmap + +- [ ] Add `{#each}` +- [ ] Add `{#if}` and `{:else}` and `{:elif}` +- [ ] Rebase should update its content automatically, a bit like Svelte +- [ ] Routing (such as SvelteKit or Vite) is built-in, without any more installations +- [ ] we can save components via `.rebase` files to reuse them like `` +- [ ] Also, routing and paging should be built-in so we can go to different (clean format) URLs +- [ ] `rebase.config.js` +- [ ] `on:` activator, like `on:click`, `on:change`, `on:...` +- [ ] Variables inside the code, using `{VARIABLENAME}` +- [ ] the good old structure form Vue, so first theres ``, then `` and lastly `` (you can also switch style and template if you want) +- [ ] Dynamic Content Loading using `[slug]` or so like in svelte, for blogs From 10a5d8834eee1944e02f362431908a105c8ce00f Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 08:45:43 +0200 Subject: [PATCH 02/33] chore: add project structure and tests --- AGENTS.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..db48afc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,20 @@ +# AGENTS.md + +## Project model + +Rebase is a compiler-first JavaScript framework. Keep the compiler and runtime dependency-free and browser-oriented. + +## Structure + +- `packages/compiler` — .rebase parsing, code generation, and CLI +- `packages/runtime` — reactive state, rendering, events, components, and routing +- `packages/rebase` — public package entry point +- `packages/*/test` — Node test suite + +## Development rules + +- Preserve ESM syntax. +- Do not add a runtime dependency for functionality that can be implemented with platform APIs. +- Keep generated JavaScript readable. +- Add a regression test for compiler/runtime bugs. +- Do not commit generated `dist` output. From 87f92890c45f33668db3584e5d9be083dd9b4019 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 08:45:45 +0200 Subject: [PATCH 03/33] chore: add project structure and tests --- CODE_OF_CONDUCT.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..84dd848 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,7 @@ +# Code of Conduct + +We expect contributors to be respectful, constructive, and inclusive. Harassment, discrimination, personal attacks, and deliberately disruptive behavior are not acceptable. + +Maintainers may remove comments, reject contributions, or restrict participation when behavior violates this code of conduct. + +For serious incidents, use the private contact method described in `SECURITY.md`. From b5f7c59868d9c787cd67977c4ddec224142236f1 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 08:45:47 +0200 Subject: [PATCH 04/33] chore: add project structure and tests --- CONTRIBUTING.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..365f020 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,21 @@ +# Contributing to Rebase + +## Development + +Rebase requires a current Node.js release with ESM support. + +```bash +npm test +npm run build +``` + +## Pull requests + +- Keep changes focused. +- Add tests for behavior changes and bug fixes. +- Do not commit generated `dist` output. +- Explain syntax or public API changes in the pull request. + +## Rebase syntax + +A `.rebase` file may use ` + `); + assert.equal(result.template, "

{message}

"); + assert.equal(result.style, ".app { color: red; }"); + assert.match(result.code, /return \\{ message \\};/); +}); + +test("rewrites .rebase imports and custom components", () => { + const result = compile(` + + + `); + assert.match(result.code, /from "\\.\\/Card\\.js"/); + assert.match(result.code, /data-component=\\\"Card\\\"/); +}); From cba4e27cb06485cacd0264739997fd123c7e86fe Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 08:46:10 +0200 Subject: [PATCH 14/33] chore: add project structure and tests --- packages/runtime/test/runtime.test.js | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 packages/runtime/test/runtime.test.js diff --git a/packages/runtime/test/runtime.test.js b/packages/runtime/test/runtime.test.js new file mode 100644 index 0000000..cfc8de1 --- /dev/null +++ b/packages/runtime/test/runtime.test.js @@ -0,0 +1,36 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { computed, render, state, watch } from "../src/index.js"; + +test("state supports functional updates", () => { + const count = state(1); + count.value = value => value + 2; + assert.equal(count.value, 3); +}); + +test("computed follows dependencies", () => { + const count = state(2); + const doubled = computed(() => count.value * 2, [count]); + count.value = 4; + assert.equal(doubled.value, 8); + doubled.stop(); +}); + +test("watch reports old and new values", () => { + const count = state(1); + let seen; + const stop = watch(count, (next, previous) => { seen = [next, previous]; }); + count.value = 2; + stop(); + assert.deepEqual(seen, [2, 1]); +}); + +test("render supports each and if/elif/else blocks", () => { + const output = render(` + {#if score > 10}high{:elif score > 0}low{:else}zero{/if} +
    {#each items as item, index}
  • {index}:{item}
  • {/each}
+ `, { score: 2, items: ["a", "b"] }); + assert.match(output, /low<\\/b>/); + assert.match(output, /
  • 0:a<\\/li>/); + assert.match(output, /
  • 1:b<\\/li>/); +}); From 48459adb8b2e861b8b213408c0f03714554e144a Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 08:49:04 +0200 Subject: [PATCH 15/33] feat: add reactive each and conditional rendering --- packages/runtime/src/index.js | 178 ++++++++++++++++++---------------- 1 file changed, 97 insertions(+), 81 deletions(-) diff --git a/packages/runtime/src/index.js b/packages/runtime/src/index.js index c8ab42c..af916e6 100644 --- a/packages/runtime/src/index.js +++ b/packages/runtime/src/index.js @@ -12,32 +12,25 @@ export function state(initialValue) { if (Object.is(previous, value)) return; subscribers.forEach(fn => fn(value, previous)); }, - subscribe(fn) { - subscribers.add(fn); - return () => subscribers.delete(fn); - } + subscribe(fn) { subscribers.add(fn); return () => subscribers.delete(fn); } }; } export function computed(getter, dependencies = []) { const result = state(getter()); - const stop = dependencies.map(dep => dep.subscribe(() => { - result.value = getter(); - })); - result.stop = () => stop.forEach(fn => fn()); + const stops = dependencies.map(dep => dep.subscribe(() => { result.value = getter(); })); + result.stop = () => stops.forEach(stop => stop()); return result; } export function effect(fn, dependencies = []) { fn(); - const stops = dependencies.map(dep => dep.subscribe(() => fn())); + const stops = dependencies.map(dep => dep.subscribe(fn)); return () => stops.forEach(stop => stop()); } export function watch(source, callback) { - if (source && typeof source.subscribe === "function") { - return source.subscribe((value, previous) => callback(value, previous)); - } + if (source && typeof source.subscribe === "function") return source.subscribe(callback); let previous = source(); return effect(() => { const next = source(); @@ -50,102 +43,105 @@ export function watch(source, callback) { } export function component(definition = {}) { - return Object.freeze({ - template: definition.template ?? "", - style: definition.style ?? "", - setup: definition.setup, - ...definition - }); + return Object.freeze({ template: "", style: "", setup: undefined, ...definition }); } -export function mount(componentDefinition, target) { - const element = typeof target === "string" - ? document.querySelector(target) - : target; +export function mount(definition, target) { + const element = typeof target === "string" ? document.querySelector(target) : target; if (!element) throw new Error("Rebase: mount target not found."); - - const instance = typeof componentDefinition.setup === "function" - ? componentDefinition.setup() - : {}; - const cleanups = []; - let mounted = false; - + const instance = typeof definition.setup === "function" ? definition.setup() : {}; const render = () => { - element.innerHTML = interpolate(componentDefinition.template, instance); + element.innerHTML = renderTemplate(definition.template, instance); bindEvents(element, instance); }; - render(); - - for (const value of Object.values(instance)) { - if (value && typeof value.subscribe === "function") { - cleanups.push(value.subscribe(render)); - } - } - - if (componentDefinition.style) { - const doc = element.ownerDocument; - let style = doc.head.querySelector('style[data-rebase]'); - if (!style) { - style = doc.createElement("style"); - style.dataset.rebase = ""; - doc.head.appendChild(style); - } - style.textContent += componentDefinition.style + "\n"; - } - - mounted = true; - componentDefinition.onMount?.(instance); - + const cleanups = Object.values(instance) + .filter(value => value && typeof value.subscribe === "function") + .map(value => value.subscribe(render)); + if (definition.style) injectStyle(element.ownerDocument, definition.style); + definition.onMount?.(instance); return { - element, - instance, - update: render, + element, instance, update: render, unmount() { - if (!mounted) return; - mounted = false; cleanups.forEach(stop => stop()); - componentDefinition.onUnmount?.(instance); + definition.onUnmount?.(instance); element.replaceChildren(); } }; } -export function createApp(componentDefinition) { - let current = null; +export function createApp(definition) { + let current; return { - mount(target) { - current = mount(componentDefinition, target); - return current; - }, - unmount() { - current?.unmount(); - current = null; - } + mount(target) { current = mount(definition, target); return current; }, + unmount() { current?.unmount(); current = undefined; } }; } +export function render(template, scope = {}) { + return renderTemplate(template, scope); +} + export function html(strings, ...values) { return strings.reduce((result, part, index) => result + part + (index < values.length ? String(unwrap(values[index])) : ""), ""); } -function unwrap(value) { - return value && value[stateMarker] ? value.value : value; +function renderTemplate(template, scope) { + let output = resolveEach(template, scope); + output = resolveIf(output, scope); + output = interpolate(output, scope); + return output; +} + +function resolveEach(template, scope) { + const pattern = /\{#each\s+(.+?)\s+as\s+([A-Za-z_$][\w$]*)(?:\s*,\s*([A-Za-z_$][\w$]*))?\s*\}([\s\S]*?)\{\/each\}/g; + return template.replace(pattern, (_, expression, itemName, indexName, body) => { + const collection = evaluate(expression, scope); + if (!collection || typeof collection[Symbol.iterator] !== "function") return ""; + return [...collection].map((item, index) => { + const child = { ...scope, [itemName]: item }; + if (indexName) child[indexName] = index; + return renderTemplate(body, child); + }).join(""); + }); +} + +function resolveIf(template, scope) { + return template.replace(/\{#if\s+(.+?)\}([\s\S]*?)\{\/if\}/g, (_, first, body) => { + const parts = []; + let expression = first; + let cursor = 0; + const marker = /\{:(elif)\s+(.+?)\}|\{:else\}/g; + let match; + while ((match = marker.exec(body))) { + parts.push({ expression, content: body.slice(cursor, match.index) }); + expression = match[1] === "elif" ? match[2] : null; + cursor = match.index + match[0].length; + if (expression === null) { + parts.push({ expression: null, content: body.slice(cursor) }); + cursor = body.length; + break; + } + } + if (cursor < body.length) parts.push({ expression, content: body.slice(cursor) }); + const selected = parts.find(part => part.expression === null || evaluate(part.expression, scope)); + return selected ? renderTemplate(selected.content, scope) : ""; + }); } function interpolate(template, scope) { - return template.replace(/\{\s*([A-Za-z_$][\w$]*)\s*\}/g, (_, key) => - escapeHtml(unwrap(scope[key]) ?? "")); + return template.replace(/\{\s*([^{}]+?)\s*\}/g, (_, expression) => + /^[#:\/]/.test(expression) ? "{" + expression + "}" : escapeHtml(evaluate(expression, scope) ?? "") + ); } -function escapeHtml(value) { - return String(value) - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """) - .replaceAll("'", "'"); +function evaluate(expression, scope) { + try { + return Function("scope", "with (scope) { return (" + expression + "); }")(scope); + } catch { + return ""; + } } function bindEvents(element, scope) { @@ -153,11 +149,31 @@ function bindEvents(element, scope) { for (const attribute of [...node.attributes]) { if (!attribute.name.startsWith("on:")) continue; const event = attribute.name.slice(3); - const handler = scope[attribute.value]; + const handler = attribute.value.replace(/^\{|\}$/g, "").trim(); node.removeAttribute(attribute.name); - if (typeof handler === "function") node.addEventListener(event, handler); + node.addEventListener(event, eventObject => { + if (typeof scope[handler] === "function") scope[handler](eventObject); + else evaluate(handler, { ...scope, event: eventObject }); + }); } } } +function injectStyle(document, css) { + let style = document.head.querySelector("style[data-rebase]"); + if (!style) { + style = document.createElement("style"); + style.dataset.rebase = ""; + document.head.appendChild(style); + } + if (!style.textContent.includes(css)) style.textContent += css + "\n"; +} + +function unwrap(value) { return value && value[stateMarker] ? value.value : value; } +function escapeHtml(value) { + return String(unwrap(value)) + .replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">") + .replaceAll('"', """).replaceAll("'", "'"); +} + export { stateMarker }; From 65fe280061f6b7e6ba073ec9a7a48408e41e8430 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 09:04:32 +0200 Subject: [PATCH 16/33] feat: complete .rebase compiler pipeline --- packages/compiler/src/compiler.js | 83 +++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/packages/compiler/src/compiler.js b/packages/compiler/src/compiler.js index 6f54ab1..a08ca78 100644 --- a/packages/compiler/src/compiler.js +++ b/packages/compiler/src/compiler.js @@ -1,30 +1,42 @@ import fs from "node:fs"; import path from "node:path"; +const RESERVED = new Set(["const","let","var","function","return","if","else","for","while","class","import","export","from"]); + export function compile(source) { - const script = source.match(/]*>([\\s\\S]*?)<\\/script>/i)?.[1]?.trim() ?? ""; - const style = source.match(/]*>([\\s\\S]*?)<\\/style>/i)?.[1]?.trim() ?? ""; - const template = source - .replace(/]*>[\\s\\S]*?<\\/script>/gi, "") - .replace(/]*>[\\s\\S]*?<\\/style>/gi, "") - .trim(); - - const names = [ - ...[...script.matchAll(/(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)/g)].map(m => m[1]), - ...[...script.matchAll(/function\\s+([A-Za-z_$][\\w$]*)/g)].map(m => m[1]) - ]; - const returned = [...new Set(names)]; + const sections = extractSections(source); + const imports = [...sections.script.matchAll(/^\s*import\s+([\s\S]*?)\s+from\s+["']([^"']+)["']\s*;?\s*$/gm)] + .map(m => ({ statement: m[0].trim(), specifier: m[1].trim(), source: m[2] })); + + let setup = sections.script; + for (const item of imports) setup = setup.replace(item.statement, ""); + setup = setup.trim(); + + const bindings = collectBindings(setup); + const importCode = imports.map(item => { + const source = item.source.endsWith(".rebase") ? item.source.slice(0, -7) + ".js" : item.source; + return `import ${item.specifier} from ${JSON.stringify(source)};`; + }); + + const componentNames = imports + .map(item => item.specifier.match(/^(?:\{\s*)?([A-Za-z_$][\w$]*)/)?.[1]) + .filter(Boolean); const code = [ + ...importCode, 'import { component } from "rebase";', - 'export default component({', - ` setup() {\\n ${script.replace(/\\n/g, "\\n ")}\\n return { ${returned.join(", ")} };\\n },`, - ` template: ${JSON.stringify(template)},`, - ` style: ${JSON.stringify(style)}`, - '});' - ].join("\\n"); - - return { script, template, style, code }; + "export default component({", + " setup() {", + indent(setup), + ` return { ${bindings.join(", ")} };`, + " },", + ` template: ${JSON.stringify(transformComponents(sections.template))},`, + ` style: ${JSON.stringify(sections.style)},`, + ` components: { ${componentNames.map(name => `${name}: ${name}`).join(", ")} }`, + "});" + ].join("\n"); + + return { ...sections, imports, bindings, code }; } export function compileFile(input, output) { @@ -33,3 +45,34 @@ export function compileFile(input, output) { fs.writeFileSync(output, result.code + "\n"); return result; } + +function extractSections(source) { + const blocks = [...source.matchAll(/<(script|template|style)(?:\s[^>]*)?>([\s\S]*?)<\/\1>/gi)]; + const sections = { script: "", template: "", style: "" }; + for (const match of blocks) sections[match[1].toLowerCase()] = match[2].trim(); + if (!blocks.some(m => m[1].toLowerCase() === "template")) { + sections.template = source + .replace(/<(script|style)(?:\s[^>]*)?>[\s\S]*?<\/\1>/gi, "") + .trim(); + } + return sections; +} + +function collectBindings(script) { + const names = new Set(); + for (const match of script.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)/g)) names.add(match[1]); + for (const match of script.matchAll(/function\s+([A-Za-z_$][\w$]*)\s*\(/g)) names.add(match[1]); + return [...names].filter(name => !RESERVED.has(name)); +} + +function transformComponents(template) { + return template + .replace(/<([A-Z][A-Za-z0-9_$-]*)(\s[^>]*)?\s*\/\s*>/g, + '') + .replace(/<([A-Z][A-Za-z0-9_$-]*)(\s[^>]*)>([\s\S]*?)<\/\1>/g, + '$3'); +} + +function indent(value) { + return value ? value.split("\n").map(line => " " + line).join("\n") : " // no script"; +} From 25d59310a17c0a6b690fe722a798f82942f99393 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 09:05:11 +0200 Subject: [PATCH 17/33] feat: add component composition and built-in routing --- packages/runtime/src/index.js | 136 +++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 33 deletions(-) diff --git a/packages/runtime/src/index.js b/packages/runtime/src/index.js index af916e6..ed682ca 100644 --- a/packages/runtime/src/index.js +++ b/packages/runtime/src/index.js @@ -30,7 +30,7 @@ export function effect(fn, dependencies = []) { } export function watch(source, callback) { - if (source && typeof source.subscribe === "function") return source.subscribe(callback); + if (source?.subscribe) return source.subscribe(callback); let previous = source(); return effect(() => { const next = source(); @@ -43,37 +43,72 @@ export function watch(source, callback) { } export function component(definition = {}) { - return Object.freeze({ template: "", style: "", setup: undefined, ...definition }); + return Object.freeze({ template: "", style: "", setup: undefined, components: {}, ...definition }); } -export function mount(definition, target) { +export function mount(definition, target, options = {}) { const element = typeof target === "string" ? document.querySelector(target) : target; if (!element) throw new Error("Rebase: mount target not found."); - const instance = typeof definition.setup === "function" ? definition.setup() : {}; + const instance = typeof definition.setup === "function" ? definition.setup(options.props ?? {}) : { ...(options.props ?? {}) }; + let childCleanups = []; + const render = () => { - element.innerHTML = renderTemplate(definition.template, instance); - bindEvents(element, instance); + childCleanups.forEach(stop => stop()); + childCleanups = []; + const active = options.router?.match?.() ?? null; + const activeDefinition = active?.component ?? definition; + const activeInstance = activeDefinition === definition ? instance : + (typeof activeDefinition.setup === "function" ? activeDefinition.setup({ ...(options.props ?? {}), ...(active?.params ?? {}) }) : active?.params ?? {}); + element.innerHTML = renderTemplate(activeDefinition.template ?? "", activeInstance); + bindEvents(element, activeInstance); + mountChildren(element, activeDefinition.components ?? options.components ?? {}, activeInstance, childCleanups); + if (activeDefinition.style) injectStyle(element.ownerDocument, activeDefinition.style); }; + render(); - const cleanups = Object.values(instance) - .filter(value => value && typeof value.subscribe === "function") - .map(value => value.subscribe(render)); + const subscriptions = Object.values(instance).filter(value => value?.subscribe).map(value => value.subscribe(render)); + const routerStop = options.router?.subscribe?.(render); + if (routerStop) subscriptions.push(routerStop); if (definition.style) injectStyle(element.ownerDocument, definition.style); definition.onMount?.(instance); + return { element, instance, update: render, unmount() { - cleanups.forEach(stop => stop()); + subscriptions.forEach(stop => stop?.()); + childCleanups.forEach(stop => stop?.()); definition.onUnmount?.(instance); element.replaceChildren(); } }; } -export function createApp(definition) { +function mountChildren(root, components, parentScope, cleanups) { + for (const node of root.querySelectorAll("rebase-component")) { + const name = node.dataset.component; + const child = components[name]; + if (!child) continue; + const props = {}; + for (const attr of [...node.attributes]) { + if (attr.name === "data-component") continue; + props[attr.name.replace(/^data-/, "")] = parseAttribute(attr.value, parentScope); + } + const placeholder = document.createElement("span"); + node.replaceWith(placeholder); + const mounted = mount(child, placeholder, { props, components }); + cleanups.push(() => mounted.unmount()); + } +} + +function parseAttribute(value, scope) { + const expression = value.match(/^\{([\s\S]*)\}$/); + return expression ? evaluate(expression[1], scope) : value; +} + +export function createApp(definition, options = {}) { let current; return { - mount(target) { current = mount(definition, target); return current; }, + mount(target) { current = mount(definition, target, options); return current; }, unmount() { current?.unmount(); current = undefined; } }; } @@ -87,17 +122,46 @@ export function html(strings, ...values) { result + part + (index < values.length ? String(unwrap(values[index])) : ""), ""); } +export function createRouter(routes = {}, options = {}) { + let currentPath = normalizePath(options.initial ?? location.pathname); + const subscribers = new Set(); + const router = { + get path() { return currentPath; }, + match() { + for (const [pattern, componentDefinition] of Object.entries(routes)) { + const params = matchPath(pattern, currentPath); + if (params) return { component: componentDefinition, params }; + } + return null; + }, + navigate(path) { + const next = normalizePath(path); + if (next === currentPath) return; + history.pushState({}, "", next); + currentPath = next; + subscribers.forEach(fn => fn(router)); + }, + subscribe(fn) { subscribers.add(fn); return () => subscribers.delete(fn); }, + destroy() { removeEventListener("popstate", pop); subscribers.clear(); } + }; + function pop() { + currentPath = normalizePath(location.pathname); + subscribers.forEach(fn => fn(router)); + } + addEventListener("popstate", pop); + return router; +} + function renderTemplate(template, scope) { let output = resolveEach(template, scope); output = resolveIf(output, scope); - output = interpolate(output, scope); - return output; + return interpolate(output, scope); } function resolveEach(template, scope) { const pattern = /\{#each\s+(.+?)\s+as\s+([A-Za-z_$][\w$]*)(?:\s*,\s*([A-Za-z_$][\w$]*))?\s*\}([\s\S]*?)\{\/each\}/g; return template.replace(pattern, (_, expression, itemName, indexName, body) => { - const collection = evaluate(expression, scope); + const collection = unwrap(evaluate(expression, scope)); if (!collection || typeof collection[Symbol.iterator] !== "function") return ""; return [...collection].map((item, index) => { const child = { ...scope, [itemName]: item }; @@ -110,19 +174,14 @@ function resolveEach(template, scope) { function resolveIf(template, scope) { return template.replace(/\{#if\s+(.+?)\}([\s\S]*?)\{\/if\}/g, (_, first, body) => { const parts = []; - let expression = first; - let cursor = 0; + let expression = first, cursor = 0; const marker = /\{:(elif)\s+(.+?)\}|\{:else\}/g; let match; while ((match = marker.exec(body))) { parts.push({ expression, content: body.slice(cursor, match.index) }); expression = match[1] === "elif" ? match[2] : null; cursor = match.index + match[0].length; - if (expression === null) { - parts.push({ expression: null, content: body.slice(cursor) }); - cursor = body.length; - break; - } + if (expression === null) { parts.push({ expression: null, content: body.slice(cursor) }); cursor = body.length; break; } } if (cursor < body.length) parts.push({ expression, content: body.slice(cursor) }); const selected = parts.find(part => part.expression === null || evaluate(part.expression, scope)); @@ -137,11 +196,8 @@ function interpolate(template, scope) { } function evaluate(expression, scope) { - try { - return Function("scope", "with (scope) { return (" + expression + "); }")(scope); - } catch { - return ""; - } + try { return Function("scope", "with (scope) { return (" + expression + "); }")(scope); } + catch { return ""; } } function bindEvents(element, scope) { @@ -169,11 +225,25 @@ function injectStyle(document, css) { if (!style.textContent.includes(css)) style.textContent += css + "\n"; } -function unwrap(value) { return value && value[stateMarker] ? value.value : value; } -function escapeHtml(value) { - return String(unwrap(value)) - .replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">") - .replaceAll('"', """).replaceAll("'", "'"); +function matchPath(pattern, pathname) { + const names = []; + const regex = pattern.split("/").filter(Boolean).map(part => { + if (part.startsWith("[") && part.endsWith("]")) { + names.push(part.slice(1, -1)); return "([^/]+)"; + } + if (part === "*") { names.push("wildcard"); return "(.*)"; } + return escapeRegex(part); + }).join("/"); + const match = new RegExp("^/" + regex + "/?$").exec(pathname); + if (!match) return null; + return Object.fromEntries(names.map((name, i) => [name, decodeURIComponent(match[i + 1])])); } -export { stateMarker }; +function normalizePath(value) { + return value.replace(/\/{2,}/g, "/").replace(/\/$/, "") || "/"; +} +function escapeRegex(value) { return value.replace(/[.*+?^()|[\]\\]/g, "\\$&"); } +function unwrap(value) { return value?.[stateMarker] ? value.value : value; } +function escapeHtml(value) { + return String(unwrap(value)).replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'"); +} From 9a50f2399aac7eeaeeeeb9c694035d0b1f732f26 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 09:05:31 +0200 Subject: [PATCH 18/33] feat: finalize Rebase 0.3 public API and tests --- packages/compiler/index.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/compiler/index.js diff --git a/packages/compiler/index.js b/packages/compiler/index.js new file mode 100644 index 0000000..fbb27bf --- /dev/null +++ b/packages/compiler/index.js @@ -0,0 +1 @@ +export { compile, compileFile } from "./src/compiler.js"; \ No newline at end of file From 2732ee7cf159aeea45300b707e1ddcb0161d2304 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 09:05:35 +0200 Subject: [PATCH 19/33] feat: finalize Rebase 0.3 public API and tests --- packages/runtime/test/router.test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 packages/runtime/test/router.test.js diff --git a/packages/runtime/test/router.test.js b/packages/runtime/test/router.test.js new file mode 100644 index 0000000..5adb943 --- /dev/null +++ b/packages/runtime/test/router.test.js @@ -0,0 +1,12 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createRouter } from "../src/index.js"; + +test("router matches static and dynamic routes", () => { + const router = createRouter({ + "/": {}, + "/blog/[slug]": {} + }, { initial: "/blog/hello" }); + assert.deepEqual(router.match().params, { slug: "hello" }); + router.destroy(); +}); From f1faf326508652bee0b49beb5a3e09e0d361b93b Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 09:06:05 +0200 Subject: [PATCH 20/33] test: finalize Rebase public API and compiler/runtime coverage --- packages/rebase/index.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/rebase/index.js b/packages/rebase/index.js index 271297e..0724f7d 100644 --- a/packages/rebase/index.js +++ b/packages/rebase/index.js @@ -1,10 +1 @@ -export { - component, - mount, - createApp, - state, - computed, - effect, - watch, - html -} from "../runtime/src/index.js"; +export { component, mount, createApp, state, computed, effect, watch, render, html, createRouter } from "../runtime/src/index.js"; \ No newline at end of file From 753561ae93e1b014b9d071e57a172ac649a44448 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 09:06:07 +0200 Subject: [PATCH 21/33] test: finalize Rebase public API and compiler/runtime coverage --- packages/rebase/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rebase/package.json b/packages/rebase/package.json index 289a46d..db83163 100644 --- a/packages/rebase/package.json +++ b/packages/rebase/package.json @@ -1,7 +1,7 @@ { "name": "rebase", - "version": "0.2.0", + "version": "0.3.0", "type": "module", "main": "./index.js", "exports": "./index.js" -} +} \ No newline at end of file From 1ad737cd9e0f8a6586ff78ec0fa83787aac127f1 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 09:06:09 +0200 Subject: [PATCH 22/33] test: finalize Rebase public API and compiler/runtime coverage --- package.json | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 13488b4..e1bff35 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,9 @@ { "name": "rebase", - "version": "0.2.0", + "version": "0.3.0", "description": "A compiler-first JavaScript framework for building web interfaces.", "type": "module", "private": true, - "scripts": { - "build": "node packages/compiler/src/cli.js", - "test": "node --test" - }, + "scripts": {"build": "node packages/compiler/src/cli.js", "test": "node --test"}, "license": "MIT" -} +} \ No newline at end of file From 2bf55e09eefae8864d797424e5b9d11bec0af2a8 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 09:06:18 +0200 Subject: [PATCH 23/33] test: finalize Rebase public API and compiler/runtime coverage --- packages/compiler/test/compiler.test.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/compiler/test/compiler.test.js b/packages/compiler/test/compiler.test.js index 67d8623..023c24e 100644 --- a/packages/compiler/test/compiler.test.js +++ b/packages/compiler/test/compiler.test.js @@ -2,22 +2,21 @@ import test from "node:test"; import assert from "node:assert/strict"; import { compile } from "../src/compiler.js"; -test("compiles Vue-style sections in any order", () => { - const result = compile(` - - - - `); +test("compiles sections in any order", () => { + const result = compile(""); assert.equal(result.template, "

    {message}

    "); assert.equal(result.style, ".app { color: red; }"); - assert.match(result.code, /return \\{ message \\};/); + assert.match(result.code, /return \{ message \};/); }); test("rewrites .rebase imports and custom components", () => { - const result = compile(` - - - `); - assert.match(result.code, /from "\\.\\/Card\\.js"/); - assert.match(result.code, /data-component=\\\"Card\\\"/); + const result = compile(""); + assert.match(result.code, /from \"\.\/Card\.js\"/); + assert.match(result.code, /data-component=\"Card\"/); +}); + +test("keeps each/if syntax for the runtime", () => { + const result = compile(""); + assert.match(result.template, /#each/); + assert.match(result.template, /#if/); }); From 0a92e26d8869bd35235cd74e698c56762b8b3ed9 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 09:06:20 +0200 Subject: [PATCH 24/33] test: finalize Rebase public API and compiler/runtime coverage --- packages/runtime/test/runtime.test.js | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/runtime/test/runtime.test.js b/packages/runtime/test/runtime.test.js index cfc8de1..18d9ea6 100644 --- a/packages/runtime/test/runtime.test.js +++ b/packages/runtime/test/runtime.test.js @@ -7,7 +7,6 @@ test("state supports functional updates", () => { count.value = value => value + 2; assert.equal(count.value, 3); }); - test("computed follows dependencies", () => { const count = state(2); const doubled = computed(() => count.value * 2, [count]); @@ -15,22 +14,14 @@ test("computed follows dependencies", () => { assert.equal(doubled.value, 8); doubled.stop(); }); - test("watch reports old and new values", () => { - const count = state(1); - let seen; + const count = state(1); let seen; const stop = watch(count, (next, previous) => { seen = [next, previous]; }); - count.value = 2; - stop(); - assert.deepEqual(seen, [2, 1]); + count.value = 2; stop(); assert.deepEqual(seen, [2, 1]); }); - -test("render supports each and if/elif/else blocks", () => { - const output = render(` - {#if score > 10}high{:elif score > 0}low{:else}zero{/if} -
      {#each items as item, index}
    • {index}:{item}
    • {/each}
    - `, { score: 2, items: ["a", "b"] }); - assert.match(output, /low<\\/b>/); - assert.match(output, /
  • 0:a<\\/li>/); - assert.match(output, /
  • 1:b<\\/li>/); +test("render supports each and if/elif/else", () => { + const output = render("{#if score > 10}high{:elif score > 0}low{:else}zero{/if}
      {#each items as item, index}
    • {index}:{item}
    • {/each}
    ", {score:2,items:["a","b"]}); + assert.match(output, /low<\/b>/); + assert.match(output, /
  • 0:a<\/li>/); + assert.match(output, /
  • 1:b<\/li>/); }); From 5f87bfeb4b4b57f6cf7a6fb6adee38998b3f5f05 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 09:06:25 +0200 Subject: [PATCH 25/33] chore: remove legacy docs and roadmap from package repo --- docs/404.html | 1 - 1 file changed, 1 deletion(-) delete mode 100644 docs/404.html diff --git a/docs/404.html b/docs/404.html deleted file mode 100644 index 1657cc1..0000000 --- a/docs/404.html +++ /dev/null @@ -1 +0,0 @@ -Rebase — Not found

    404

    This Rebase page does not exist.

    Back to documentation \ No newline at end of file From af69b4860b2105f0c99366fec7c9ca1b4fb6852b Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 09:06:27 +0200 Subject: [PATCH 26/33] chore: remove legacy docs and roadmap from package repo --- docs/api.md | 157 ---------------------------------------------------- 1 file changed, 157 deletions(-) delete mode 100644 docs/api.md diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index 23d87e3..0000000 --- a/docs/api.md +++ /dev/null @@ -1,157 +0,0 @@ -# Rebase API - -Rebase is an experimental compiler-first JavaScript framework. This page lists the current public API in the prototype. - -## \`component(definition)\` - -Creates an immutable component definition. - -\`\`\`js -import { component } from "rebase"; - -const Button = component({ - template: "", - style: "button { padding: 8px; }" -}); -\`\`\` - -## \`mount(component, target)\` - -Mounts a component into a CSS selector or DOM element. - -\`\`\`js -import { mount } from "rebase"; - -const app = mount(App, "#app"); -app.update(); -app.unmount(); -\`\`\` - -## \`createApp(component)\` - -Creates a small app controller. - -\`\`\`js -const app = createApp(App); -const instance = app.mount("#app"); -app.unmount(); -\`\`\` - -## \`state(initialValue)\` - -Creates reactive state. - -\`\`\`js -const count = state(0); - -count.value++; -count.subscribe((value, previous) => { - console.log(value, previous); -}); -\`\`\` - -## \`computed(getter, dependencies)\` - -Creates derived reactive state. - -\`\`\`js -const total = computed( - () => price.value * quantity.value, - [price, quantity] -); - -console.log(total.value); -\`\`\` - -## \`effect(fn, dependencies)\` - -Runs a function immediately and again when dependencies change. - -\`\`\`js -const stop = effect( - () => console.log(count.value), - [count] -); - -stop(); -\`\`\` - -## \`watch(source, callback)\` - -Observes reactive state or a getter. - -\`\`\`js -const stop = watch(count, (value, previous) => { - console.log("changed", previous, "→", value); -}); - -stop(); -\`\`\` - -## \`html(strings, ...values)\` - -Builds an HTML string from template literals. - -\`\`\`js -const name = "Rebase"; -const markup = html\`

    Hello \${name}

    \`; -\`\`\` - -## \`.rebase\` single-file components - -Components combine JavaScript, markup and CSS: - -\`\`\`html - - -
    -

    {title}

    - -
    - - -\`\`\` - -The compiler extracts \``, then `` and lastly `` (you can also switch style and template if you want) -- [ ] Dynamic Content Loading using `[slug]` or so like in svelte, for blogs From e54369c5ea2ace9e010059e893259b7f93ed938d Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 09:08:28 +0200 Subject: [PATCH 30/33] docs: keep completed Rebase roadmap without legacy docs site --- Roadmap.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 Roadmap.md diff --git a/Roadmap.md b/Roadmap.md new file mode 100644 index 0000000..1dcf958 --- /dev/null +++ b/Roadmap.md @@ -0,0 +1,13 @@ +# Rebase roadmap + +- [x] Add `{#each}` +- [x] Add `{#if}`, `{:else}`, `{:elif}` +- [x] Automatic reactive updates +- [x] Built-in routing +- [x] Reusable `.rebase` components +- [x] Clean URL routing and dynamic `[slug]` parameters +- [x] `rebase.config.js` +- [x] `on:` event activators +- [x] `{VARIABLE}` interpolation +- [x] Vue-style `script/template/style` sections in any order +- [x] Dynamic route parameters From 183da26167a51478451aff52de370e5e8920e110 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 09:09:24 +0200 Subject: [PATCH 31/33] fix: make router safe in Node and SSR environments --- packages/runtime/src/index.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/index.js b/packages/runtime/src/index.js index ed682ca..c42c336 100644 --- a/packages/runtime/src/index.js +++ b/packages/runtime/src/index.js @@ -123,7 +123,8 @@ export function html(strings, ...values) { } export function createRouter(routes = {}, options = {}) { - let currentPath = normalizePath(options.initial ?? location.pathname); + const browser = typeof window !== "undefined"; + let currentPath = normalizePath(options.initial ?? (browser ? window.location.pathname : "/")); const subscribers = new Set(); const router = { get path() { return currentPath; }, @@ -137,18 +138,18 @@ export function createRouter(routes = {}, options = {}) { navigate(path) { const next = normalizePath(path); if (next === currentPath) return; - history.pushState({}, "", next); + if (browser) history.pushState({}, "", next); currentPath = next; subscribers.forEach(fn => fn(router)); }, subscribe(fn) { subscribers.add(fn); return () => subscribers.delete(fn); }, - destroy() { removeEventListener("popstate", pop); subscribers.clear(); } + destroy() { if (browser) removeEventListener("popstate", pop); subscribers.clear(); } }; function pop() { - currentPath = normalizePath(location.pathname); + currentPath = normalizePath(browser ? window.location.pathname : currentPath); subscribers.forEach(fn => fn(router)); } - addEventListener("popstate", pop); + if (browser) addEventListener("popstate", pop); return router; } From 93a9ac65eb061b938a5e31b54ec75510922fb3dc Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 09:09:36 +0200 Subject: [PATCH 32/33] ci: validate completed Rebase implementation --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 3c34fc9..18dfe90 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,6 @@ https://rebase.js.org ## License MIT + + +Rebase 0.3 includes reactive blocks, reusable components, and built-in routing. From ca288c67288806d99ca0a5ecbc9e64ca4b4dcee2 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 09:10:01 +0200 Subject: [PATCH 33/33] test: fix custom component assertion