From bbca71c8c539359fcfa1428a189ba9ba2de537c1 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 20:48:08 +0200 Subject: [PATCH 01/38] feat: add rebase core package --- packages/core/package.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 packages/core/package.json diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..920f12b --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,8 @@ +{ + "name": "@rebase/core", + "version": "1.0.0-alpha.1", + "description": "Framework-agnostic JavaScript core and plugin API for Rebase.", + "type": "module", + "exports": { ".": "./index.js" }, + "license": "MIT" +} \ No newline at end of file From 984ed1ce272e970c56ee80545c46a0767fdebe05 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 20:48:10 +0200 Subject: [PATCH 02/38] feat: add extensible non-invasive JavaScript core --- packages/core/index.js | 330 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 packages/core/index.js diff --git a/packages/core/index.js b/packages/core/index.js new file mode 100644 index 0000000..3db74a3 --- /dev/null +++ b/packages/core/index.js @@ -0,0 +1,330 @@ +const BUILT_INS = new Set(["expression", "if", "each", "else", "elif"]); + +export class Rebase { + constructor(options = {}) { + this.options = options; + this.scope = options.scope ?? {}; + this.syntax = new SyntaxRegistry(); + this.plugins = new Map(); + this.roots = new Set(); + + if (options.host) this.host(options.host); + if (options.builtIns !== false) registerBuiltIns(this); + } + + host(nameOrOptions) { + const hosts = typeof nameOrOptions === "string" + ? { name: nameOrOptions } + : nameOrOptions; + + const disabled = HOST_RESERVED[hosts.name] ?? []; + for (const syntax of disabled) this.syntax.disable(syntax.type, syntax.name); + return this; + } + + use(plugin, options = {}) { + const normalized = plugin?.default ?? plugin; + if (!normalized) throw new TypeError("Rebase.use(): plugin is required."); + + if (typeof normalized === "function") { + normalized(this.api(), options); + } else if (typeof normalized.install === "function") { + normalized.install(this.api(), options); + } else { + throw new TypeError("Rebase.use(): plugin must be a function or expose install()."); + } + + if (normalized.name) this.plugins.set(normalized.name, normalized); + return this; + } + + directive(name, handler, options = {}) { + return this.syntax.directive(name, handler, options); + } + + expression(name, handler, options = {}) { + return this.syntax.expression(name, handler, options); + } + + block(name, handler, options = {}) { + return this.syntax.block(name, handler, options); + } + + hook(name, handler) { + return this.syntax.hook(name, handler); + } + + api() { + return Object.freeze({ + version, + rebase: this, + directive: this.directive.bind(this), + expression: this.expression.bind(this), + block: this.block.bind(this), + hook: this.hook.bind(this), + syntax: this.syntax + }); + } + + mount(target, options = {}) { + const root = resolveTarget(target); + if (!root) throw new Error("Rebase.mount(): target not found."); + + const record = { + root, + source: options.template ?? root.innerHTML, + scope: { ...this.scope, ...(options.scope ?? {}) }, + options + }; + + this.roots.add(record); + this.render(record); + + return { + update: () => this.render(record), + unmount: () => { + this.roots.delete(record); + root.innerHTML = record.source; + } + }; + } + + render(record) { + let html = record.source; + html = this.syntax.process(html, record.scope, this); + record.root.innerHTML = html; + bindEvents(record.root, record.scope); + this.hook("render", { root: record.root, scope: record.scope }); + return html; + } + + update() { + for (const record of this.roots) this.render(record); + return this; + } +} + +export class SyntaxRegistry { + constructor() { + this.directives = new Map(); + this.expressions = new Map(); + this.blocks = new Map(); + this.hooks = new Map(); + this.disabled = new Map(); + } + + directive(name, handler, options = {}) { + assertName(name); + this.directives.set(name, { handler, options }); + return this; + } + + expression(name, handler, options = {}) { + assertName(name); + this.expressions.set(name, { handler, options }); + return this; + } + + block(name, handler, options = {}) { + assertName(name); + this.blocks.set(name, { handler, options }); + return this; + } + + hook(name, handler) { + const list = this.hooks.get(name) ?? []; + list.push(handler); + this.hooks.set(name, list); + return this; + } + + disable(type, name) { + const set = this.disabled.get(type) ?? new Set(); + set.add(name); + this.disabled.set(type, set); + return this; + } + + isEnabled(type, name) { + return !this.disabled.get(type)?.has(name); + } + + process(source, scope, rebase) { + let output = source; + + output = processBlocks(output, this.blocks, scope, rebase, this); + output = processDirectives(output, this.directives, scope, rebase); + output = processExpressions(output, this.expressions, scope, rebase); + return interpolate(output, scope); + } +} + +export function plugin(definition) { + return definition; +} + +export function createRebase(options) { + return new Rebase(options); +} + +export const hosts = { + svelte: { + name: "svelte", + reserved: HOST_RESERVED.svelte + }, + vue: { + name: "vue", + reserved: HOST_RESERVED.vue + }, + react: { + name: "react", + reserved: HOST_RESERVED.react + } +}; + +function registerBuiltIns(rebase) { + rebase.expression("value", ({ expression, scope }) => evaluate(expression, scope)); + rebase.block("if", ({ expression, body, scope }) => + evaluate(expression, scope) ? body : "" + ); + rebase.block("each", ({ expression, body, scope }) => { + const match = expression.match(/^(.+?)\\s+as\\s+([A-Za-z_$][\\w$]*)(?:\\s*,\\s*([A-Za-z_$][\\w$]*))?$/); + if (!match) return ""; + const collection = evaluate(match[1], scope); + if (collection == null || typeof collection[Symbol.iterator] !== "function") return ""; + return [...collection].map((item, index) => { + const child = { ...scope, [match[2]]: item }; + if (match[3]) child[match[3]] = index; + return processBuiltInBody(body, child); + }).join(""); + }); +} + +function processBuiltInBody(body, scope) { + return body.replace(/\\{\\s*([^{}]+?)\\s*\\}/g, (_, expression) => + escapeHtml(evaluate(expression, scope)) + ); +} + +function processBlocks(source, blocks, scope, rebase, registry) { + const pattern = /\\{#([A-Za-z_$][\\w$-]*)(?:\\s+([^}]*))?\\}([\\s\\S]*?)\\{\\/([A-Za-z_$][\\w$-]*)\\}/g; + + return source.replace(pattern, (full, name, expression = "", body, closing) => { + if (name !== closing || !registry.isEnabled("block", name)) return full; + const entry = blocks.get(name); + if (!entry) return full; + + const branches = splitBranches(body); + const result = entry.handler({ + name, + expression: expression.trim(), + body: branches.length > 1 ? branches : body, + scope, + rebase + }); + + return result == null ? "" : String(result); + }); +} + +function processDirectives(source, directives, scope, rebase) { + return source.replace(/\\{@([A-Za-z_$][\\w$:-]*)(?:\\s+([^{}]*?))?\\}/g, (full, name, expression = "") => { + const entry = directives.get(name); + if (!entry) return full; + const result = entry.handler({ + name, + expression: expression.trim(), + scope, + rebase + }); + return result == null ? "" : String(result); + }); +} + +function processExpressions(source, expressions, scope, rebase) { + return source.replace(/\\{([A-Za-z_$][\\w$]*(?:\\.[A-Za-z_$][\\w$]*)*)\\}/g, (full, name) => { + const entry = expressions.get(name); + if (!entry) return full; + const result = entry.handler({ name, expression: name, scope, rebase }); + return escapeHtml(result); + }); +} + +function interpolate(source, scope) { + return source.replace(/\\{\\{([^{}]+)\\}\\}/g, (_, expression) => + escapeHtml(evaluate(expression.trim(), scope)) + ); +} + +function splitBranches(body) { + const marker = /\\{:(else|elif)\\b([^}]*)\\}/g; + const branches = []; + let cursor = 0; + let expression = null; + let match; + + while ((match = marker.exec(body))) { + branches.push({ expression, content: body.slice(cursor, match.index) }); + expression = match[1] === "elif" ? match[2].trim() : null; + cursor = marker.lastIndex; + } + + branches.push({ expression, content: body.slice(cursor) }); + return branches; +} + +function bindEvents(root, scope) { + for (const element of root.querySelectorAll("*")) { + for (const attribute of [...element.attributes]) { + if (!attribute.name.startsWith("on:")) continue; + const eventName = attribute.name.slice(3); + const expression = attribute.value.replace(/^\\{|\\}$/g, "").trim(); + element.removeAttribute(attribute.name); + element.addEventListener(eventName, event => + evaluate(expression, { ...scope, event }) + ); + } + } +} + +function evaluate(expression, scope) { + try { + return Function("scope", "with (scope) { return (" + expression + "); }")(scope); + } catch { + return undefined; + } +} + +function escapeHtml(value) { + return String(value ?? "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function resolveTarget(target) { + if (typeof document === "undefined") throw new Error("Rebase.mount() requires a browser."); + return typeof target === "string" ? document.querySelector(target) : target; +} + +function assertName(name) { + if (!/^[A-Za-z_$][\\w$-]*$/.test(name)) { + throw new TypeError("Invalid Rebase syntax name: " + name); + } +} + +const HOST_RESERVED = { + svelte: [ + { type: "block", name: "if" }, + { type: "block", name: "each" }, + { type: "block", name: "await" }, + { type: "block", name: "key" }, + { type: "block", name: "snippet" } + ], + vue: [], + react: [] +}; + +export const version = "1.0.0-alpha.1"; From 2ac5a82e3488cb0e705a65cb1d72584ffc45ac1a Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 20:48:41 +0200 Subject: [PATCH 03/38] refactor: expose core through public package --- packages/rebase/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rebase/index.js b/packages/rebase/index.js index 0724f7d..278855b 100644 --- a/packages/rebase/index.js +++ b/packages/rebase/index.js @@ -1 +1 @@ -export { component, mount, createApp, state, computed, effect, watch, render, html, createRouter } from "../runtime/src/index.js"; \ No newline at end of file +export * from "../core/index.js"; From 30691be0ebf68a402e97234a90ffb6a4f0470b33 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 20:48:43 +0200 Subject: [PATCH 04/38] refactor: update public package metadata --- packages/rebase/package.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/rebase/package.json b/packages/rebase/package.json index db83163..d78e166 100644 --- a/packages/rebase/package.json +++ b/packages/rebase/package.json @@ -1,7 +1,10 @@ { "name": "rebase", - "version": "0.3.0", + "version": "1.0.0-alpha.1", + "description": "A framework-agnostic JavaScript enhancement layer with an extensible syntax API.", "type": "module", - "main": "./index.js", - "exports": "./index.js" + "main": "index.js", + "module": "index.js", + "exports": { ".": "./index.js" }, + "license": "MIT" } \ No newline at end of file From 899e36c998e7ef2b9a6b28982cf969d3014b8f32 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 20:48:45 +0200 Subject: [PATCH 05/38] docs: redefine Rebase as a non-invasive JavaScript layer --- README.md | 125 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 113 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 18dfe90..fe14810 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,127 @@ # Rebase -Rebase is an experimental compiler-first JavaScript framework for building web interfaces. +Rebase is a small **JavaScript enhancement layer for the web**. -> Build the web. On your terms. +It does not replace your framework. It sits on top of HTML, JavaScript, Svelte, Vue, React, and other web stacks and adds opt-in functionality through a plugin API. -## Status +> Rebase doesn't replace your stack. It adds to it. + +## Design principles + +- **JavaScript first** +- **Framework agnostic** +- **Opt-in syntax** +- **Plugins before features** +- **Never consume host syntax by accident** +- Works from a CDN, local files, or a package installation + +## Syntax + +Rebase has three extension points: + +- `{name}` — simple registered expressions +- `{@name ...}` — directives +- `{#name ...}...{/name}` — blocks + +A plugin can register any of them: + +```js +export default { + name: "@rebase/example", + + install(rebase) { + rebase.expression("version", () => "1.0.0"); + + rebase.directive("hello", ({ expression }) => + `Hello ${expression}` + ); + + rebase.block("feature", ({ body }) => + `
${body}
` + ); + } +}; +``` + +Then: + +```html +{@hello World} + +{#feature} + Rebase content +{/feature} +``` + +## Framework compatibility + +Rebase does **not** automatically reinterpret framework syntax. + +For Svelte, use the Svelte adapter: -Rebase is in early development. The component syntax, compiler, runtime, and public API are experimental. +```js +import { createSvelteRebase } from "@rebase/adapters/svelte"; -## Documentation +const rebase = createSvelteRebase(); +rebase.directive("date", ({ expression }) => + new Date(expression).toLocaleString() +); +``` -https://rebase.js.org +The adapter reserves Svelte's native blocks such as `{#if}`, `{#each}`, `{#await}`, `{#key}`, and `{#snippet}`. + +Vue and React are similarly treated as host frameworks. Rebase should only consume syntax explicitly registered by the Rebase instance. + +## Plugin API + +The public API is intentionally small: + +```js +rebase.expression(name, handler) +rebase.directive(name, handler) +rebase.block(name, handler) +rebase.hook(name, handler) +rebase.use(plugin) +``` + +Handlers receive: + +```js +{ + name, + expression, + body, + scope, + rebase +} +``` + +This makes new Rebase syntax possible without changing the core parser. + +## Example + +```html +

{title}

+ +{@icon lucide:arrow-up} + +{#feature} +

Plugin-provided content.

+{/feature} +``` + +The important distinction is that `{#if}` is not inherently "Rebase syntax". It is only Rebase syntax when the active Rebase instance has registered it and the host framework has not reserved it. ## Packages -- `packages/compiler` — .rebase compiler prototype -- `packages/runtime` — browser runtime -- `packages/rebase` — public package entry +- `@rebase/core` — JavaScript core and syntax/plugin API +- `rebase` — public package +- `@rebase/adapters` — framework host adapters -## License +## Status -MIT +Experimental alpha. The API is intentionally small and likely to change. +## License -Rebase 0.3 includes reactive blocks, reusable components, and built-in routing. +MIT From 0c57a637762a13fa37e521d7cb672d9f129adc72 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 20:48:46 +0200 Subject: [PATCH 06/38] refactor: update project metadata --- package.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e1bff35..dcdd261 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,11 @@ { "name": "rebase", - "version": "0.3.0", - "description": "A compiler-first JavaScript framework for building web interfaces.", + "version": "1.0.0-alpha.1", + "description": "A framework-agnostic JavaScript enhancement layer for the web.", "type": "module", "private": true, - "scripts": {"build": "node packages/compiler/src/cli.js", "test": "node --test"}, + "scripts": { + "test": "node --test packages/core/test packages/compiler/test packages/runtime/test" + }, "license": "MIT" } \ No newline at end of file From f653f4d26aab5cddc99eb2a20d3baf466c41700a Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 20:48:48 +0200 Subject: [PATCH 07/38] feat: add non-invasive Svelte adapter --- packages/adapters/svelte.js | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/adapters/svelte.js diff --git a/packages/adapters/svelte.js b/packages/adapters/svelte.js new file mode 100644 index 0000000..d5ba398 --- /dev/null +++ b/packages/adapters/svelte.js @@ -0,0 +1,4 @@ +import { createRebase } from "../core/index.js"; +export function createSvelteRebase(options = {}) { + return createRebase({ ...options, host: "svelte" }); +} \ No newline at end of file From 88c1343830b38e689395cc9be92f7e3fa42c8472 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 20:48:50 +0200 Subject: [PATCH 08/38] feat: add non-invasive Vue adapter --- packages/adapters/vue.js | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/adapters/vue.js diff --git a/packages/adapters/vue.js b/packages/adapters/vue.js new file mode 100644 index 0000000..5e76506 --- /dev/null +++ b/packages/adapters/vue.js @@ -0,0 +1,4 @@ +import { createRebase } from "../core/index.js"; +export function createVueRebase(options = {}) { + return createRebase({ ...options, host: "vue" }); +} \ No newline at end of file From 4febd9a24a693ef18cee1ac61f5bf3eeb993e110 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 20:48:52 +0200 Subject: [PATCH 09/38] feat: add non-invasive React adapter --- packages/adapters/react.js | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 packages/adapters/react.js diff --git a/packages/adapters/react.js b/packages/adapters/react.js new file mode 100644 index 0000000..24af7fc --- /dev/null +++ b/packages/adapters/react.js @@ -0,0 +1,4 @@ +import { createRebase } from "../core/index.js"; +export function createReactRebase(options = {}) { + return createRebase({ ...options, host: "react" }); +} \ No newline at end of file From 7156075cf96b130a62c227edd403121e29dbd339 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 20:48:54 +0200 Subject: [PATCH 10/38] test: cover plugin API and framework compatibility --- packages/core/test/api.test.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 packages/core/test/api.test.js diff --git a/packages/core/test/api.test.js b/packages/core/test/api.test.js new file mode 100644 index 0000000..47ef2c8 --- /dev/null +++ b/packages/core/test/api.test.js @@ -0,0 +1,19 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createRebase } from "../index.js"; + +test("plugins can register directives and blocks", () => { + const rebase = createRebase(); + rebase.directive("hello", ({ expression }) => "Hello " + expression); + rebase.block("feature", ({ body }) => "FEATURE:" + body); + assert.equal(rebase.syntax.process("{@hello world} {#feature}works{/feature}", {}, rebase), "Hello world FEATURE:works"); +}); + +test("Svelte native blocks remain untouched", () => { + const rebase = createRebase({ host: "svelte" }); + rebase.directive("custom", ({ expression }) => expression); + assert.equal( + rebase.syntax.process("{#if visible}

{name}

{/if} {@custom test}", { visible: true, name: "A" }, rebase), + "{#if visible}

A

{/if} test" + ); +}); \ No newline at end of file From 686968e6f87949d310e4c0f5fc267b296914bd28 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 20:49:09 +0200 Subject: [PATCH 11/38] feat: add host-aware source transformation --- packages/core/index.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/core/index.js b/packages/core/index.js index 3db74a3..ea4072b 100644 --- a/packages/core/index.js +++ b/packages/core/index.js @@ -89,6 +89,12 @@ export class Rebase { }; } + transform(source, options = {}) { + const host = options.host ?? this.options.host; + const scope = options.scope ?? this.scope; + return this.syntax.process(source, scope, this, { host }); + } + render(record) { let html = record.source; html = this.syntax.process(html, record.scope, this); @@ -149,13 +155,15 @@ export class SyntaxRegistry { return !this.disabled.get(type)?.has(name); } - process(source, scope, rebase) { + process(source, scope, rebase, options = {}) { let output = source; + const host = options.host ?? rebase.options.host; - output = processBlocks(output, this.blocks, scope, rebase, this); + output = processBlocks(output, this.blocks, scope, rebase, this, host); output = processDirectives(output, this.directives, scope, rebase); - output = processExpressions(output, this.expressions, scope, rebase); - return interpolate(output, scope); + if (!host) output = processExpressions(output, this.expressions, scope, rebase); + if (!host) output = interpolate(output, scope); + return output; } } @@ -206,11 +214,12 @@ function processBuiltInBody(body, scope) { ); } -function processBlocks(source, blocks, scope, rebase, registry) { +function processBlocks(source, blocks, scope, rebase, registry, host) { const pattern = /\\{#([A-Za-z_$][\\w$-]*)(?:\\s+([^}]*))?\\}([\\s\\S]*?)\\{\\/([A-Za-z_$][\\w$-]*)\\}/g; return source.replace(pattern, (full, name, expression = "", body, closing) => { if (name !== closing || !registry.isEnabled("block", name)) return full; + if (host && !registry.isEnabled("block", name)) return full; const entry = blocks.get(name); if (!entry) return full; From e1f10a6b82d1ad418daa8e214b5dda49d25a5bb0 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 20:49:14 +0200 Subject: [PATCH 12/38] feat: export framework adapters --- packages/adapters/index.js | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/adapters/index.js diff --git a/packages/adapters/index.js b/packages/adapters/index.js new file mode 100644 index 0000000..35a9df8 --- /dev/null +++ b/packages/adapters/index.js @@ -0,0 +1,3 @@ +export { createSvelteRebase } from "./svelte.js"; +export { createVueRebase } from "./vue.js"; +export { createReactRebase } from "./react.js"; From 1fe488fde70572b107d0acddc3c83a0bbbbd3ee9 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 20:49:16 +0200 Subject: [PATCH 13/38] feat: add adapter package metadata --- packages/adapters/package.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 packages/adapters/package.json diff --git a/packages/adapters/package.json b/packages/adapters/package.json new file mode 100644 index 0000000..85f68dc --- /dev/null +++ b/packages/adapters/package.json @@ -0,0 +1,17 @@ +{ + "name": "@rebase/adapters", + "version": "1.0.0-alpha.1", + "description": "Non-invasive source adapters for Rebase host frameworks.", + "type": "module", + "main": "index.js", + "exports": { + ".": "./index.js", + "./svelte": "./svelte.js", + "./vue": "./vue.js", + "./react": "./react.js" + }, + "peerDependencies": { + "@rebase/core": "1.0.0-alpha.1" + }, + "license": "MIT" +} \ No newline at end of file From 667c2ba813744697738f0d4beba53d75236c6937 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:07:25 +0200 Subject: [PATCH 14/38] refactor: remove obsolete framework runtime --- packages/compiler/index.js | 1 - 1 file changed, 1 deletion(-) delete mode 100644 packages/compiler/index.js diff --git a/packages/compiler/index.js b/packages/compiler/index.js deleted file mode 100644 index fbb27bf..0000000 --- a/packages/compiler/index.js +++ /dev/null @@ -1 +0,0 @@ -export { compile, compileFile } from "./src/compiler.js"; \ No newline at end of file From e083d895beed473fb608b9b81427b2781b97c1a3 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:07:30 +0200 Subject: [PATCH 15/38] refactor: remove obsolete rebase config --- rebase.config.js | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 rebase.config.js diff --git a/rebase.config.js b/rebase.config.js deleted file mode 100644 index af9fb5f..0000000 --- a/rebase.config.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - root: "src", - outDir: "dist", - clean: true -}; From 4cf0f6823bb8568b9578f7c44499d6afe184fe26 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:07:39 +0200 Subject: [PATCH 16/38] refactor: remove obsolete framework code --- packages/compiler/src/cli.js | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 packages/compiler/src/cli.js diff --git a/packages/compiler/src/cli.js b/packages/compiler/src/cli.js deleted file mode 100644 index c693404..0000000 --- a/packages/compiler/src/cli.js +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env node -import fs from "node:fs"; -import path from "node:path"; -import { compileFile } from "./compiler.js"; - -const [input, output = input.replace(/\\.rebase$/i, ".js")] = process.argv.slice(2); -if (!input) { - console.error("Usage: rebase [output.js]"); - process.exit(1); -} -compileFile(input, output); -console.log(`Compiled ${path.resolve(input)} -> ${path.resolve(output)}`); From 1f809de33f2d601b61e3d2d0356fd9a212a2878f Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:07:43 +0200 Subject: [PATCH 17/38] refactor: remove obsolete framework code --- packages/compiler/test/compiler.test.js | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 packages/compiler/test/compiler.test.js diff --git a/packages/compiler/test/compiler.test.js b/packages/compiler/test/compiler.test.js deleted file mode 100644 index 023c24e..0000000 --- a/packages/compiler/test/compiler.test.js +++ /dev/null @@ -1,22 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { compile } from "../src/compiler.js"; - -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 \};/); -}); - -test("rewrites .rebase imports and custom components", () => { - 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 a10f71f6cdc13d5d427dcd5d6d31911091e39a8c Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:07:45 +0200 Subject: [PATCH 18/38] refactor: remove obsolete framework code --- packages/runtime/package.json | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 packages/runtime/package.json diff --git a/packages/runtime/package.json b/packages/runtime/package.json deleted file mode 100644 index b0e81e7..0000000 --- a/packages/runtime/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "@rebasejs/runtime", - "version": "0.3.0", - "type": "module", - "exports": "./src/index.js" -} From 074daa453a5958395038d6fbcfa25863d8de5c6d Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:07:49 +0200 Subject: [PATCH 19/38] refactor: remove obsolete framework code --- packages/runtime/test/runtime.test.js | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 packages/runtime/test/runtime.test.js diff --git a/packages/runtime/test/runtime.test.js b/packages/runtime/test/runtime.test.js deleted file mode 100644 index 18d9ea6..0000000 --- a/packages/runtime/test/runtime.test.js +++ /dev/null @@ -1,27 +0,0 @@ -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", () => { - 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 f08c64cf8636e3ab0130077829da5d265040d2d0 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:08:31 +0200 Subject: [PATCH 20/38] fix: replace parser with stable JavaScript core --- packages/core/index.js | 375 +++++------------------------------------ 1 file changed, 40 insertions(+), 335 deletions(-) diff --git a/packages/core/index.js b/packages/core/index.js index ea4072b..0682cc7 100644 --- a/packages/core/index.js +++ b/packages/core/index.js @@ -1,339 +1,44 @@ -const BUILT_INS = new Set(["expression", "if", "each", "else", "elif"]); - -export class Rebase { - constructor(options = {}) { - this.options = options; - this.scope = options.scope ?? {}; - this.syntax = new SyntaxRegistry(); - this.plugins = new Map(); - this.roots = new Set(); - - if (options.host) this.host(options.host); - if (options.builtIns !== false) registerBuiltIns(this); - } - - host(nameOrOptions) { - const hosts = typeof nameOrOptions === "string" - ? { name: nameOrOptions } - : nameOrOptions; - - const disabled = HOST_RESERVED[hosts.name] ?? []; - for (const syntax of disabled) this.syntax.disable(syntax.type, syntax.name); - return this; - } - - use(plugin, options = {}) { - const normalized = plugin?.default ?? plugin; - if (!normalized) throw new TypeError("Rebase.use(): plugin is required."); - - if (typeof normalized === "function") { - normalized(this.api(), options); - } else if (typeof normalized.install === "function") { - normalized.install(this.api(), options); - } else { - throw new TypeError("Rebase.use(): plugin must be a function or expose install()."); - } - - if (normalized.name) this.plugins.set(normalized.name, normalized); - return this; - } - - directive(name, handler, options = {}) { - return this.syntax.directive(name, handler, options); - } - - expression(name, handler, options = {}) { - return this.syntax.expression(name, handler, options); - } - - block(name, handler, options = {}) { - return this.syntax.block(name, handler, options); - } - - hook(name, handler) { - return this.syntax.hook(name, handler); - } - - api() { - return Object.freeze({ - version, - rebase: this, - directive: this.directive.bind(this), - expression: this.expression.bind(this), - block: this.block.bind(this), - hook: this.hook.bind(this), - syntax: this.syntax - }); - } - - mount(target, options = {}) { - const root = resolveTarget(target); - if (!root) throw new Error("Rebase.mount(): target not found."); - - const record = { - root, - source: options.template ?? root.innerHTML, - scope: { ...this.scope, ...(options.scope ?? {}) }, - options - }; - - this.roots.add(record); - this.render(record); - - return { - update: () => this.render(record), - unmount: () => { - this.roots.delete(record); - root.innerHTML = record.source; - } - }; - } - - transform(source, options = {}) { - const host = options.host ?? this.options.host; - const scope = options.scope ?? this.scope; - return this.syntax.process(source, scope, this, { host }); - } - - render(record) { - let html = record.source; - html = this.syntax.process(html, record.scope, this); - record.root.innerHTML = html; - bindEvents(record.root, record.scope); - this.hook("render", { root: record.root, scope: record.scope }); - return html; - } - - update() { - for (const record of this.roots) this.render(record); - return this; - } -} +export const version = "1.0.0-alpha.2"; export class SyntaxRegistry { - constructor() { - this.directives = new Map(); - this.expressions = new Map(); - this.blocks = new Map(); - this.hooks = new Map(); - this.disabled = new Map(); - } - - directive(name, handler, options = {}) { - assertName(name); - this.directives.set(name, { handler, options }); - return this; - } - - expression(name, handler, options = {}) { - assertName(name); - this.expressions.set(name, { handler, options }); - return this; - } - - block(name, handler, options = {}) { - assertName(name); - this.blocks.set(name, { handler, options }); - return this; - } - - hook(name, handler) { - const list = this.hooks.get(name) ?? []; - list.push(handler); - this.hooks.set(name, list); - return this; - } - - disable(type, name) { - const set = this.disabled.get(type) ?? new Set(); - set.add(name); - this.disabled.set(type, set); - return this; - } - - isEnabled(type, name) { - return !this.disabled.get(type)?.has(name); - } - - process(source, scope, rebase, options = {}) { - let output = source; - const host = options.host ?? rebase.options.host; - - output = processBlocks(output, this.blocks, scope, rebase, this, host); - output = processDirectives(output, this.directives, scope, rebase); - if (!host) output = processExpressions(output, this.expressions, scope, rebase); - if (!host) output = interpolate(output, scope); - return output; - } -} - -export function plugin(definition) { - return definition; -} - -export function createRebase(options) { - return new Rebase(options); + constructor(){this.expressions=new Map();this.directives=new Map();this.blocks=new Map();this.hooks=new Map();this.disabled=new Map();} + expression(name,handler,options={}){add(this.expressions,name,handler,options);return this} + directive(name,handler,options={}){add(this.directives,name,handler,options);return this} + block(name,handler,options={}){add(this.blocks,name,handler,options);return this} + hook(name,handler){if(typeof handler!=="function")throw new TypeError("Invalid hook");(this.hooks.get(name)||this.hooks.set(name,[]).get(name)).push(handler);return this} + disable(type,name){(this.disabled.get(type)||this.disabled.set(type,new Set()).get(type)).add(name);return this} + enabled(type,name){return !this.disabled.get(type)?.has(name)} + emit(name,value){for(const fn of this.hooks.get(name)||[])fn(value)} } -export const hosts = { - svelte: { - name: "svelte", - reserved: HOST_RESERVED.svelte - }, - vue: { - name: "vue", - reserved: HOST_RESERVED.vue - }, - react: { - name: "react", - reserved: HOST_RESERVED.react - } -}; - -function registerBuiltIns(rebase) { - rebase.expression("value", ({ expression, scope }) => evaluate(expression, scope)); - rebase.block("if", ({ expression, body, scope }) => - evaluate(expression, scope) ? body : "" - ); - rebase.block("each", ({ expression, body, scope }) => { - const match = expression.match(/^(.+?)\\s+as\\s+([A-Za-z_$][\\w$]*)(?:\\s*,\\s*([A-Za-z_$][\\w$]*))?$/); - if (!match) return ""; - const collection = evaluate(match[1], scope); - if (collection == null || typeof collection[Symbol.iterator] !== "function") return ""; - return [...collection].map((item, index) => { - const child = { ...scope, [match[2]]: item }; - if (match[3]) child[match[3]] = index; - return processBuiltInBody(body, child); - }).join(""); - }); -} - -function processBuiltInBody(body, scope) { - return body.replace(/\\{\\s*([^{}]+?)\\s*\\}/g, (_, expression) => - escapeHtml(evaluate(expression, scope)) - ); -} - -function processBlocks(source, blocks, scope, rebase, registry, host) { - const pattern = /\\{#([A-Za-z_$][\\w$-]*)(?:\\s+([^}]*))?\\}([\\s\\S]*?)\\{\\/([A-Za-z_$][\\w$-]*)\\}/g; - - return source.replace(pattern, (full, name, expression = "", body, closing) => { - if (name !== closing || !registry.isEnabled("block", name)) return full; - if (host && !registry.isEnabled("block", name)) return full; - const entry = blocks.get(name); - if (!entry) return full; - - const branches = splitBranches(body); - const result = entry.handler({ - name, - expression: expression.trim(), - body: branches.length > 1 ? branches : body, - scope, - rebase - }); - - return result == null ? "" : String(result); - }); -} - -function processDirectives(source, directives, scope, rebase) { - return source.replace(/\\{@([A-Za-z_$][\\w$:-]*)(?:\\s+([^{}]*?))?\\}/g, (full, name, expression = "") => { - const entry = directives.get(name); - if (!entry) return full; - const result = entry.handler({ - name, - expression: expression.trim(), - scope, - rebase - }); - return result == null ? "" : String(result); - }); -} - -function processExpressions(source, expressions, scope, rebase) { - return source.replace(/\\{([A-Za-z_$][\\w$]*(?:\\.[A-Za-z_$][\\w$]*)*)\\}/g, (full, name) => { - const entry = expressions.get(name); - if (!entry) return full; - const result = entry.handler({ name, expression: name, scope, rebase }); - return escapeHtml(result); - }); -} - -function interpolate(source, scope) { - return source.replace(/\\{\\{([^{}]+)\\}\\}/g, (_, expression) => - escapeHtml(evaluate(expression.trim(), scope)) - ); -} - -function splitBranches(body) { - const marker = /\\{:(else|elif)\\b([^}]*)\\}/g; - const branches = []; - let cursor = 0; - let expression = null; - let match; - - while ((match = marker.exec(body))) { - branches.push({ expression, content: body.slice(cursor, match.index) }); - expression = match[1] === "elif" ? match[2].trim() : null; - cursor = marker.lastIndex; - } - - branches.push({ expression, content: body.slice(cursor) }); - return branches; -} - -function bindEvents(root, scope) { - for (const element of root.querySelectorAll("*")) { - for (const attribute of [...element.attributes]) { - if (!attribute.name.startsWith("on:")) continue; - const eventName = attribute.name.slice(3); - const expression = attribute.value.replace(/^\\{|\\}$/g, "").trim(); - element.removeAttribute(attribute.name); - element.addEventListener(eventName, event => - evaluate(expression, { ...scope, event }) - ); - } - } -} - -function evaluate(expression, scope) { - try { - return Function("scope", "with (scope) { return (" + expression + "); }")(scope); - } catch { - return undefined; - } -} - -function escapeHtml(value) { - return String(value ?? "") - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """) - .replaceAll("'", "'"); -} - -function resolveTarget(target) { - if (typeof document === "undefined") throw new Error("Rebase.mount() requires a browser."); - return typeof target === "string" ? document.querySelector(target) : target; -} - -function assertName(name) { - if (!/^[A-Za-z_$][\\w$-]*$/.test(name)) { - throw new TypeError("Invalid Rebase syntax name: " + name); - } -} - -const HOST_RESERVED = { - svelte: [ - { type: "block", name: "if" }, - { type: "block", name: "each" }, - { type: "block", name: "await" }, - { type: "block", name: "key" }, - { type: "block", name: "snippet" } - ], - vue: [], - react: [] -}; - -export const version = "1.0.0-alpha.1"; +export class Rebase { + constructor(options={}){this.options={...options};this.scope=options.scope||{};this.syntax=new SyntaxRegistry();this.plugins=new Map();this.roots=new Set();if(options.host)this.host(options.host);if(options.builtIns!==false)builtIns(this)} + host(name){this.options.host=name;for(const x of HOSTS[name]||[])this.syntax.disable(x[0],x[1]);return this} + use(plugin,options={}){const p=plugin?.default||plugin;if(typeof p==="function")p(this.api(),options);else if(p?.install)p.install(this.api(),options);else throw new TypeError("Invalid Rebase plugin");if(p?.name)this.plugins.set(p.name,p);return this} + expression(n,h,o){this.syntax.expression(n,h,o);return this} + directive(n,h,o){this.syntax.directive(n,h,o);return this} + block(n,h,o){this.syntax.block(n,h,o);return this} + hook(n,h){this.syntax.hook(n,h);return this} + api(){return Object.freeze({version,rebase:this,expression:this.expression.bind(this),directive:this.directive.bind(this),block:this.block.bind(this),hook:this.hook.bind(this),syntax:this.syntax})} + async transform(source,{scope={},host=this.options.host}={}){return render(String(source),{...this.scope,...scope},this,host)} + mount(target,{template,scope={}}={}){const root=typeof target==="string"?document.querySelector(target):target;if(!root)throw new Error("Rebase mount target not found");const record={root,source:template??root.innerHTML,scope:{...this.scope,...scope}};this.roots.add(record);this.render(record);return{update:()=>this.render(record),unmount:()=>{this.roots.delete(record);root.innerHTML=record.source}}} + render(record){const html=renderSync(record.source,record.scope,this,this.options.host);record.root.innerHTML=html;bind(record.root,record.scope);this.syntax.emit("render",{root:record.root,scope:record.scope});return html} + update(){for(const r of this.roots)this.render(r);return this} +} + +export const createRebase=options=>new Rebase(options); +export const plugin=definition=>definition; + +function builtIns(r){r.expression("value",x=>evalIn(x.expression,x.scope));r.block("if",x=>evalIn(x.expression,x.scope)?x.body:"");r.block("each",x=>{const m=x.expression.match(/^(.+?)\s+as\s+([A-Za-z_$][\w$]*)(?:\s*,\s*([A-Za-z_$][\w$]*))?$/);if(!m)return"";const list=evalIn(m[1],x.scope);if(!list?.[Symbol.iterator])return"";return[...list].map((v,i)=>renderSync(x.body,{...x.scope,[m[2]]:v,...m[3]?[m[3]]:[],...(m[3]?{[m[3]]:i}:{})},x.rebase)).join("")})} + +function renderSync(s,scope,r,host){let out=s;out=blocks(out,r,scope,host);out=directives(out,r,scope);if(!host)out=out.replace(/\{\{([^{}]+)\}\}/g,(_,e)=>esc(evalIn(e.trim(),scope)));return out} +async function render(s,scope,r,host){let out=s;out=blocks(out,r,scope,host);out=await directivesAsync(out,r,scope);if(!host)out=out.replace(/\{\{([^{}]+)\}\}/g,(_,e)=>esc(evalIn(e.trim(),scope)));return out} + +function blocks(s,r,scope,host){const re=/\{#([\w$-]+)(?:\s+([^}]*))?\}([\s\S]*?)\{\/\1\}/g;return s.replace(re,(full,n,e,b)=>{if(!r.syntax.enabled("block",n)||host&&HOSTS[host]?.some(x=>x[0]==="block"&&x[1]===n))return full;const h=r.syntax.blocks.get(n);if(!h)return full;return String(h.handler({name:n,expression:(e||"").trim(),body:blocks(b,r,scope,host),scope,rebase:r})??"")})} +function directives(s,r,scope){return s.replace(/\{@([\w$:-]+)(?:\s+([^{}]*))?\}/g,(full,n,e)=>{const h=r.syntax.directives.get(n);if(!h)return full;const v=h.handler({name:n,expression:(e||"").trim(),scope,rebase:r});if(v?.then)throw new Error("Async Rebase plugin requires transform()");return v==null?"":String(v)})} +async function directivesAsync(s,r,scope){let out="",i=0;for(const m of s.matchAll(/\{@([\w$:-]+)(?:\s+([^{}]*))?\}/g)){out+=s.slice(i,m.index);const h=r.syntax.directives.get(m[1]);if(!h)out+=m[0];else out+=String(await h.handler({name:m[1],expression:(m[2]||"").trim(),scope,rebase:r})??"");i=m.index+m[0].length}return out+s.slice(i)} +function bind(root,scope){for(const el of root.querySelectorAll("*"))for(const a of [...el.attributes])if(a.name.startsWith("on:")){const e=a.name.slice(3),x=a.value.replace(/^\{|\}$/g,"");el.removeAttribute(a.name);el.addEventListener(e,ev=>evalIn(x,{...scope,event:ev}))}} +function evalIn(e,s){try{return Function("scope","with(scope)return ("+e+")")(s)}catch{return undefined}} +function esc(v){return String(v??"").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")} +function add(map,name,handler,options){if(!/^[A-Za-z_$][\w$-]*$/.test(name)||typeof handler!=="function")throw new TypeError("Invalid Rebase syntax registration");map.set(name,{handler,options})} +const HOSTS={svelte:[["block","if"],["block","each"],["block","await"],["block","key"],["block","snippet"]],vue:[],react:[]}; From 47416ac1561e96a3c8f9073efed1866d64492447 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:08:48 +0200 Subject: [PATCH 21/38] chore: update test script and version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index dcdd261..dbe838c 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "rebase", - "version": "1.0.0-alpha.1", + "version": "1.0.0-alpha.2", "description": "A framework-agnostic JavaScript enhancement layer for the web.", "type": "module", "private": true, "scripts": { - "test": "node --test packages/core/test packages/compiler/test packages/runtime/test" + "test": "node --test packages/core/test" }, "license": "MIT" } \ No newline at end of file From 1baaa9a756cef65b5bc34bd38a589d28d60d48f9 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:14:35 +0200 Subject: [PATCH 22/38] Delete packages/compiler directory --- packages/compiler/package.json | 9 ---- packages/compiler/src/compiler.js | 78 ------------------------------- 2 files changed, 87 deletions(-) delete mode 100644 packages/compiler/package.json delete mode 100644 packages/compiler/src/compiler.js diff --git a/packages/compiler/package.json b/packages/compiler/package.json deleted file mode 100644 index 6c29905..0000000 --- a/packages/compiler/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "@rebasejs/compiler", - "version": "0.3.0", - "type": "module", - "bin": { - "rebase": "./src/cli.js" - }, - "exports": "./src/compiler.js" -} diff --git a/packages/compiler/src/compiler.js b/packages/compiler/src/compiler.js deleted file mode 100644 index a08ca78..0000000 --- a/packages/compiler/src/compiler.js +++ /dev/null @@ -1,78 +0,0 @@ -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 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() {", - 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) { - const result = compile(fs.readFileSync(input, "utf8")); - fs.mkdirSync(path.dirname(output), { recursive: true }); - 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 2920559b9b1aae35eb434dcfea5dc5d20f3aec7c Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:14:43 +0200 Subject: [PATCH 23/38] Delete packages/runtime directory --- packages/runtime/src/index.js | 250 --------------------------- packages/runtime/test/router.test.js | 12 -- 2 files changed, 262 deletions(-) delete mode 100644 packages/runtime/src/index.js delete mode 100644 packages/runtime/test/router.test.js diff --git a/packages/runtime/src/index.js b/packages/runtime/src/index.js deleted file mode 100644 index c42c336..0000000 --- a/packages/runtime/src/index.js +++ /dev/null @@ -1,250 +0,0 @@ -const stateMarker = Symbol("rebase.state"); - -export function state(initialValue) { - let value = initialValue; - const subscribers = new Set(); - return { - [stateMarker]: true, - get value() { return value; }, - set value(next) { - const previous = value; - value = typeof next === "function" ? next(value) : next; - if (Object.is(previous, value)) return; - subscribers.forEach(fn => fn(value, previous)); - }, - subscribe(fn) { subscribers.add(fn); return () => subscribers.delete(fn); } - }; -} - -export function computed(getter, dependencies = []) { - const result = state(getter()); - 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)); - return () => stops.forEach(stop => stop()); -} - -export function watch(source, callback) { - if (source?.subscribe) return source.subscribe(callback); - let previous = source(); - return effect(() => { - const next = source(); - if (!Object.is(next, previous)) { - const old = previous; - previous = next; - callback(next, old); - } - }); -} - -export function component(definition = {}) { - return Object.freeze({ template: "", style: "", setup: undefined, components: {}, ...definition }); -} - -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(options.props ?? {}) : { ...(options.props ?? {}) }; - let childCleanups = []; - - const render = () => { - 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 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() { - subscriptions.forEach(stop => stop?.()); - childCleanups.forEach(stop => stop?.()); - definition.onUnmount?.(instance); - element.replaceChildren(); - } - }; -} - -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, options); 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])) : ""), ""); -} - -export function createRouter(routes = {}, options = {}) { - const browser = typeof window !== "undefined"; - let currentPath = normalizePath(options.initial ?? (browser ? window.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; - if (browser) history.pushState({}, "", next); - currentPath = next; - subscribers.forEach(fn => fn(router)); - }, - subscribe(fn) { subscribers.add(fn); return () => subscribers.delete(fn); }, - destroy() { if (browser) removeEventListener("popstate", pop); subscribers.clear(); } - }; - function pop() { - currentPath = normalizePath(browser ? window.location.pathname : currentPath); - subscribers.forEach(fn => fn(router)); - } - if (browser) addEventListener("popstate", pop); - return router; -} - -function renderTemplate(template, scope) { - let output = resolveEach(template, scope); - output = resolveIf(output, scope); - 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 = unwrap(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, 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*([^{}]+?)\s*\}/g, (_, expression) => - /^[#:\/]/.test(expression) ? "{" + expression + "}" : escapeHtml(evaluate(expression, scope) ?? "") - ); -} - -function evaluate(expression, scope) { - try { return Function("scope", "with (scope) { return (" + expression + "); }")(scope); } - catch { return ""; } -} - -function bindEvents(element, scope) { - for (const node of element.querySelectorAll("*")) { - for (const attribute of [...node.attributes]) { - if (!attribute.name.startsWith("on:")) continue; - const event = attribute.name.slice(3); - const handler = attribute.value.replace(/^\{|\}$/g, "").trim(); - node.removeAttribute(attribute.name); - 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 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])])); -} - -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("'","'"); -} diff --git a/packages/runtime/test/router.test.js b/packages/runtime/test/router.test.js deleted file mode 100644 index 5adb943..0000000 --- a/packages/runtime/test/router.test.js +++ /dev/null @@ -1,12 +0,0 @@ -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 348d85bed4a000fd998afe70ba1869a7741ff627 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:15:05 +0200 Subject: [PATCH 24/38] Delete Roadmap.md --- Roadmap.md | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 Roadmap.md diff --git a/Roadmap.md b/Roadmap.md deleted file mode 100644 index 1dcf958..0000000 --- a/Roadmap.md +++ /dev/null @@ -1,13 +0,0 @@ -# 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 ddb460535209d3d858c215f65cd0741d3ce21f18 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:15:39 +0200 Subject: [PATCH 25/38] Delete AGENTS.md --- AGENTS.md | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index db48afc..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,20 +0,0 @@ -# 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 18eaf3133db611fb41ce2a17a07b1d629392bb87 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:19:00 +0200 Subject: [PATCH 26/38] docs: prepare beta README --- README.md | 199 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 140 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index fe14810..514ad95 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,135 @@ # Rebase -Rebase is a small **JavaScript enhancement layer for the web**. +[![Beta](https://img.shields.io/badge/status-beta-7c3aed.svg)](https://github.com/intensed-dev/code) +[![License](https://img.shields.io/github/license/intensed-dev/code.svg)](LICENSE) +[![Tests](https://img.shields.io/github/actions/workflow/status/intensed-dev/code/ci.yml?label=tests)](https://github.com/intensed-dev/code/actions) -It does not replace your framework. It sits on top of HTML, JavaScript, Svelte, Vue, React, and other web stacks and adds opt-in functionality through a plugin API. +**A JavaScript-first enhancement layer for the web.** -> Rebase doesn't replace your stack. It adds to it. +Rebase adds small, opt-in features to HTML and existing web stacks without trying to become another framework. -## Design principles +> **Rebase doesn't replace your stack. It adds to it.** -- **JavaScript first** -- **Framework agnostic** -- **Opt-in syntax** -- **Plugins before features** -- **Never consume host syntax by accident** -- Works from a CDN, local files, or a package installation +## What Rebase is -## Syntax +Rebase is a small runtime and plugin API for extending web markup with explicitly registered syntax. -Rebase has three extension points: +It is designed to work alongside: + +- HTML +- JavaScript +- Svelte +- Vue +- React +- other build systems and frameworks + +Rebase only owns syntax that the active Rebase instance explicitly registers. Host-framework syntax is left alone. -- `{name}` — simple registered expressions -- `{@name ...}` — directives -- `{#name ...}...{/name}` — blocks +## Installation + +### Package + +```bash +npm install rebase +``` -A plugin can register any of them: +Then: ```js -export default { - name: "@rebase/example", +import { createRebase } from "rebase"; - install(rebase) { - rebase.expression("version", () => "1.0.0"); +const rebase = createRebase(); - rebase.directive("hello", ({ expression }) => - `Hello ${expression}` - ); +rebase.directive("hello", ({ expression }) => + `Hello ${expression}` +); +``` - rebase.block("feature", ({ body }) => - `
    ${body}
    ` - ); - } -}; +### Core package + +For library authors and integrations: + +```bash +npm install @rebase/core ``` -Then: +### Documentation + +Full documentation and API reference: + +**https://js-rebase.github.io** + +## Syntax + +Rebase has three extension points: + +| Syntax | Purpose | +| --- | --- | +| `{@name ...}` | Directive | +| `{#name ...}{/name}` | Block | +| `{{ expression }}` | Runtime interpolation | + +A plugin registers its own names. Rebase does not reserve arbitrary syntax globally. + +Example: ```html -{@hello World} +{@icon lucide:arrow-up} {#feature} - Rebase content +

    Plugin-provided content.

    {/feature} ``` -## Framework compatibility +## Plugins + +A plugin is just a normal JavaScript package. + +```js +export default { + name: "rebase-lucide", + + install(rebase) { + rebase.directive("lucide", ({ expression }) => { + // return the generated HTML/SVG + }); + } +}; +``` -Rebase does **not** automatically reinterpret framework syntax. +Users install plugins separately: -For Svelte, use the Svelte adapter: +```bash +npm install rebase rebase-lucide +``` + +and enable them explicitly: ```js -import { createSvelteRebase } from "@rebase/adapters/svelte"; +import { createRebase } from "rebase"; +import lucide from "rebase-lucide"; -const rebase = createSvelteRebase(); -rebase.directive("date", ({ expression }) => - new Date(expression).toLocaleString() -); +const rebase = createRebase(); +rebase.use(lucide); ``` -The adapter reserves Svelte's native blocks such as `{#if}`, `{#each}`, `{#await}`, `{#key}`, and `{#snippet}`. +This means a plugin cannot silently change a user's Rebase instance merely by existing in `node_modules`. + +## Framework compatibility + +Rebase is intentionally non-invasive. + +For Svelte, the adapter protects native blocks such as: + +- `{#if}` +- `{#each}` +- `{#await}` +- `{#key}` +- `{#snippet}` -Vue and React are similarly treated as host frameworks. Rebase should only consume syntax explicitly registered by the Rebase instance. +The Vue and React adapters provide the same host-aware entry point. Framework-specific source transformations remain the responsibility of the corresponding integration. -## Plugin API +## API The public API is intentionally small: @@ -82,9 +139,11 @@ rebase.directive(name, handler) rebase.block(name, handler) rebase.hook(name, handler) rebase.use(plugin) +rebase.transform(source, options) +rebase.mount(target, options) ``` -Handlers receive: +Plugin handlers receive a context containing: ```js { @@ -96,31 +155,53 @@ Handlers receive: } ``` -This makes new Rebase syntax possible without changing the core parser. +## Repository structure + +```text +. +├── .github/ +│ ├── ISSUE_TEMPLATE/ +│ └── workflows/ +├── packages/ +│ ├── core/ +│ │ ├── index.js +│ │ └── test/ +│ ├── adapters/ +│ │ ├── svelte.js +│ │ ├── vue.js +│ │ └── react.js +│ └── rebase/ +│ └── index.js +├── AGENTS.md +├── CONTRIBUTING.md +├── CODE_OF_CONDUCT.md +├── LICENSE +├── README.md +├── ROADMAP.md +├── SECURITY.md +└── package.json +``` -## Example +## Development -```html -

    {title}

    +Requirements: -{@icon lucide:arrow-up} +- Node.js 20+ +- npm -{#feature} -

    Plugin-provided content.

    -{/feature} -``` +Run the test suite: -The important distinction is that `{#if}` is not inherently "Rebase syntax". It is only Rebase syntax when the active Rebase instance has registered it and the host framework has not reserved it. +```bash +npm test +``` -## Packages +## Status -- `@rebase/core` — JavaScript core and syntax/plugin API -- `rebase` — public package -- `@rebase/adapters` — framework host adapters +**Beta.** -## Status +The API is usable for experimentation and plugin development, but minor API changes may still happen before 1.0. -Experimental alpha. The API is intentionally small and likely to change. +See the [roadmap](ROADMAP.md) and the [documentation](https://js-rebase.github.io). ## License From e3a41ce9a9a169b478e8c3803c4c3c97fe0c6b1e Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:19:14 +0200 Subject: [PATCH 27/38] chore: prepare beta release structure --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index dbe838c..04254c3 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "rebase", - "version": "1.0.0-alpha.2", - "description": "A framework-agnostic JavaScript enhancement layer for the web.", + "version": "1.0.0-beta.1", + "description": "A JavaScript-first enhancement layer for the web.", "type": "module", "private": true, + "workspaces": ["packages/*"], "scripts": { "test": "node --test packages/core/test" }, From 685f16d3afda76afad3e5204d4ced2d20135fd39 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:19:16 +0200 Subject: [PATCH 28/38] chore: prepare beta release structure --- packages/core/package.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 920f12b..c61ff96 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,8 +1,10 @@ { "name": "@rebase/core", - "version": "1.0.0-alpha.1", - "description": "Framework-agnostic JavaScript core and plugin API for Rebase.", + "version": "1.0.0-beta.1", + "description": "The core runtime and plugin API for Rebase.", "type": "module", - "exports": { ".": "./index.js" }, + "exports": { + ".": "./index.js" + }, "license": "MIT" } \ No newline at end of file From f34581722d1b5d760e0209afd79ee795776ada63 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:19:19 +0200 Subject: [PATCH 29/38] chore: prepare beta release structure --- packages/rebase/package.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/rebase/package.json b/packages/rebase/package.json index d78e166..b67c1a7 100644 --- a/packages/rebase/package.json +++ b/packages/rebase/package.json @@ -1,10 +1,15 @@ { "name": "rebase", - "version": "1.0.0-alpha.1", - "description": "A framework-agnostic JavaScript enhancement layer with an extensible syntax API.", + "version": "1.0.0-beta.1", + "description": "A JavaScript-first enhancement layer for the web.", "type": "module", "main": "index.js", "module": "index.js", - "exports": { ".": "./index.js" }, + "exports": { + ".": "./index.js" + }, + "dependencies": { + "@rebase/core": "1.0.0-beta.1" + }, "license": "MIT" } \ No newline at end of file From 20b4c6b14cae546b28e72ab84665011be57f9c73 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:19:21 +0200 Subject: [PATCH 30/38] chore: prepare beta release structure --- packages/adapters/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/adapters/package.json b/packages/adapters/package.json index 85f68dc..73605e2 100644 --- a/packages/adapters/package.json +++ b/packages/adapters/package.json @@ -1,7 +1,7 @@ { "name": "@rebase/adapters", - "version": "1.0.0-alpha.1", - "description": "Non-invasive source adapters for Rebase host frameworks.", + "version": "1.0.0-beta.1", + "description": "Host-framework adapters for Rebase.", "type": "module", "main": "index.js", "exports": { @@ -11,7 +11,7 @@ "./react": "./react.js" }, "peerDependencies": { - "@rebase/core": "1.0.0-alpha.1" + "@rebase/core": "1.0.0-beta.1" }, "license": "MIT" } \ No newline at end of file From 73fc7f98f33fd449bf212c304bb84c4cf297e2f4 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:19:23 +0200 Subject: [PATCH 31/38] chore: prepare beta release structure --- packages/rebase/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rebase/index.js b/packages/rebase/index.js index 278855b..dd33c50 100644 --- a/packages/rebase/index.js +++ b/packages/rebase/index.js @@ -1 +1 @@ -export * from "../core/index.js"; +export * from "@rebase/core"; \ No newline at end of file From cacdd921ba2bf86e202694d9a13caef307026d8a Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:19:25 +0200 Subject: [PATCH 32/38] chore: prepare beta release structure --- packages/core/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/index.js b/packages/core/index.js index 0682cc7..bf8ac48 100644 --- a/packages/core/index.js +++ b/packages/core/index.js @@ -1,4 +1,4 @@ -export const version = "1.0.0-alpha.2"; +export const version = "1.0.0-beta.1"; export class SyntaxRegistry { constructor(){this.expressions=new Map();this.directives=new Map();this.blocks=new Map();this.hooks=new Map();this.disabled=new Map();} From 9fb56f2063281a6e4ef8ab43f7c3b60d4717e903 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:19:28 +0200 Subject: [PATCH 33/38] chore: prepare beta release structure --- packages/core/test/api.test.js | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/packages/core/test/api.test.js b/packages/core/test/api.test.js index 47ef2c8..7aa30a5 100644 --- a/packages/core/test/api.test.js +++ b/packages/core/test/api.test.js @@ -2,18 +2,35 @@ import test from "node:test"; import assert from "node:assert/strict"; import { createRebase } from "../index.js"; -test("plugins can register directives and blocks", () => { +test("plugins can register directives and blocks", async () => { const rebase = createRebase(); + rebase.directive("hello", ({ expression }) => "Hello " + expression); rebase.block("feature", ({ body }) => "FEATURE:" + body); - assert.equal(rebase.syntax.process("{@hello world} {#feature}works{/feature}", {}, rebase), "Hello world FEATURE:works"); + + assert.equal( + await rebase.transform("{@hello world} {#feature}works{/feature}"), + "Hello world FEATURE:works" + ); }); -test("Svelte native blocks remain untouched", () => { +test("Svelte native blocks remain untouched", async () => { const rebase = createRebase({ host: "svelte" }); + rebase.directive("custom", ({ expression }) => expression); + assert.equal( - rebase.syntax.process("{#if visible}

    {name}

    {/if} {@custom test}", { visible: true, name: "A" }, rebase), - "{#if visible}

    A

    {/if} test" + await rebase.transform("{#if visible}

    {name}

    {/if} {@custom test}", { + scope: { visible: true, name: "A" } + }), + "{#if visible}

    {name}

    {/if} test" ); -}); \ No newline at end of file +}); + +test("async directives work through transform", async () => { + const rebase = createRebase(); + + rebase.directive("async", async ({ expression }) => expression.toUpperCase()); + + assert.equal(await rebase.transform("{@async hello}"), "HELLO"); +}); From bee35782451847590925b4cc72f29bd1935a768e Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:19:38 +0200 Subject: [PATCH 34/38] docs: add beta roadmap --- ROADMAP.md | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 ROADMAP.md diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..a51e63b --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,87 @@ +# Rebase Roadmap + +Rebase is a small, framework-agnostic JavaScript enhancement layer. + +The roadmap describes direction, not guaranteed release dates. + +## Beta + +### Core +- [x] Public Rebase API +- [x] Plugin registration +- [x] Expressions, directives and blocks +- [x] Hooks +- [x] Browser mounting +- [x] Async source transformation +- [x] Host-aware syntax protection +- [ ] Nested block parser +- [ ] Diagnostics and useful parser errors +- [ ] Stable public API documentation +- [ ] TypeScript declarations + +### Distribution +- [x] `@rebase/core` +- [x] `rebase` +- [x] `@rebase/adapters` +- [x] Consistent beta versions +- [ ] npm release workflow +- [ ] Browser/CDN build +- [ ] ESM/browser compatibility matrix + +### Plugins +- [ ] Official plugin specification +- [ ] Plugin naming and compatibility rules +- [ ] Official example plugins +- [ ] Plugin author guide +- [ ] Plugin test utilities +- [ ] Plugin discovery page + +### Frameworks +- [x] Svelte host protection +- [x] Svelte adapter entry point +- [x] Vue adapter entry point +- [x] React adapter entry point +- [ ] Build-time Svelte integration +- [ ] Build-time Vue integration +- [ ] Build-time React integration + +### Documentation +- [ ] API reference +- [ ] Plugin guide +- [ ] Framework guides +- [ ] Examples +- [ ] Migration guide +- [ ] Versioned documentation + +## 1.0 + +- [ ] Stable parser +- [ ] Stable plugin contract +- [ ] Stable adapter contract +- [ ] CDN/browser distribution +- [ ] TypeScript declarations +- [ ] Security model documentation +- [ ] Performance baseline +- [ ] Automated releases +- [ ] Versioned docs + +## Possible future plugins + +- icons +- dates and formatting +- i18n +- Markdown +- math +- accessibility helpers +- development diagnostics + +These should normally remain plugins rather than becoming core features. + +## Non-goals + +Rebase is not intended to become: + +- a replacement for Svelte, Vue or React +- a full templating language +- a dependency-heavy meta-framework +- a mandatory global syntax standard \ No newline at end of file From ef8b619d81bcbf21db6b695419bc34d914cda5fb Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:19:43 +0200 Subject: [PATCH 35/38] docs: add repository agent guidance --- AGENTS.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..12cabbe --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,55 @@ +# AGENTS.md + +## Project + +Rebase is a JavaScript-first, framework-agnostic enhancement layer for the web. + +Core principles: +1. Rebase adds to existing stacks; it does not replace them. +2. Syntax is opt-in and explicitly registered. +3. Framework-native syntax must never be consumed accidentally. +4. Plugins are first-class extension points. +5. Keep the core small; optional features belong in plugins. +6. Prefer standards-based JavaScript and small dependencies. +7. Document public APIs before calling them stable. + +## Repository layout + +- `packages/core`: core runtime, parser and plugin API. +- `packages/rebase`: user-facing `rebase` package. +- `packages/adapters`: host-framework integration entry points. +- `packages/*/test`: package tests. +- Documentation: https://js-rebase.github.io + +## API changes + +When changing public behavior: +- update tests; +- update README usage when necessary; +- update ROADMAP.md when scope changes; +- update the documentation site when applicable; +- provide a migration path for breaking plugin changes. + +## Plugins + +Plugins are independent JavaScript packages and must be explicitly enabled by applications. + +Do not add automatic plugin discovery to the core. + +Official packages use the `@rebase/*` scope. Community packages should use names such as `rebase-foo` or `@author/rebase-foo`. + +Plugins should declare the Rebase version range they support. + +## Testing + +Run: + +```bash +npm test +``` + +Tests should focus on public behavior rather than private implementation details. + +## Releases + +Beta releases use `1.0.0-beta.N`. Do not call the API stable until the plugin and adapter contracts are documented. \ No newline at end of file From 0541b53c8665e1abab84830841e4b560872b8c33 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:29:18 +0200 Subject: [PATCH 36/38] Update test script to run all test files --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 04254c3..63d2b4b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "private": true, "workspaces": ["packages/*"], "scripts": { - "test": "node --test packages/core/test" + "test": "node --test \"packages/*/test/*.test.js\"" }, "license": "MIT" -} \ No newline at end of file +} From aedb156827aaaa3f86b58890421c76bcdde35a55 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:31:27 +0200 Subject: [PATCH 37/38] Update test script to target core package tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 63d2b4b..7835139 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "private": true, "workspaces": ["packages/*"], "scripts": { - "test": "node --test \"packages/*/test/*.test.js\"" + "test": "node --test packages/core/test/*.test.js" }, "license": "MIT" } From 41b3fca55e276bb08678927ed681d8b1d3992de8 Mon Sep 17 00:00:00 2001 From: Intense Date: Sat, 26 Sep 2026 22:33:59 +0200 Subject: [PATCH 38/38] ci: remove obsolete build step --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad571d3..12b66b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,4 +16,3 @@ jobs: with: node-version: 22 - run: npm test - - run: npm run build