Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
65fe280
feat: complete .rebase compiler pipeline
intensed-dev Sep 26, 2026
25d5931
feat: add component composition and built-in routing
intensed-dev Sep 26, 2026
9a50f23
feat: finalize Rebase 0.3 public API and tests
intensed-dev Sep 26, 2026
2732ee7
feat: finalize Rebase 0.3 public API and tests
intensed-dev Sep 26, 2026
f1faf32
test: finalize Rebase public API and compiler/runtime coverage
intensed-dev Sep 26, 2026
753561a
test: finalize Rebase public API and compiler/runtime coverage
intensed-dev Sep 26, 2026
1ad737c
test: finalize Rebase public API and compiler/runtime coverage
intensed-dev Sep 26, 2026
2bf55e0
test: finalize Rebase public API and compiler/runtime coverage
intensed-dev Sep 26, 2026
0a92e26
test: finalize Rebase public API and compiler/runtime coverage
intensed-dev Sep 26, 2026
5f87bfe
chore: remove legacy docs and roadmap from package repo
intensed-dev Sep 26, 2026
af69b48
chore: remove legacy docs and roadmap from package repo
intensed-dev Sep 26, 2026
3dbfbc2
chore: remove legacy docs and roadmap from package repo
intensed-dev Sep 26, 2026
5554c89
chore: remove legacy docs and roadmap from package repo
intensed-dev Sep 26, 2026
0cd53e7
chore: remove legacy docs and roadmap from package repo
intensed-dev Sep 26, 2026
e54369c
docs: keep completed Rebase roadmap without legacy docs site
intensed-dev Sep 26, 2026
183da26
fix: make router safe in Node and SSR environments
intensed-dev Sep 26, 2026
93a9ac6
ci: validate completed Rebase implementation
intensed-dev Sep 26, 2026
ca288c6
test: fix custom component assertion
intensed-dev Sep 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ https://rebase.js.org
## License

MIT


Rebase 0.3 includes reactive blocks, reusable components, and built-in routing.
24 changes: 12 additions & 12 deletions Roadmap.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Roadmap
# Rebase 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 `<CoolCard title="CardTitle" author="CardAuthor">`
- [ ] 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 `<script>...</script>`, then `<template>...</template>` and lastly `<style>...</style>` (you can also switch style and template if you want)
- [ ] Dynamic Content Loading using `[slug]` or so like in svelte, for blogs
- [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
1 change: 0 additions & 1 deletion docs/404.html

This file was deleted.

157 changes: 0 additions & 157 deletions docs/api.md

This file was deleted.

34 changes: 0 additions & 34 deletions docs/index.html

This file was deleted.

1 change: 0 additions & 1 deletion docs/style.css

This file was deleted.

9 changes: 3 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
1 change: 1 addition & 0 deletions packages/compiler/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { compile, compileFile } from "./src/compiler.js";
83 changes: 63 additions & 20 deletions packages/compiler/src/compiler.js
Original file line number Diff line number Diff line change
@@ -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(/<script[^>]*>([\\s\\S]*?)<\\/script>/i)?.[1]?.trim() ?? "";
const style = source.match(/<style[^>]*>([\\s\\S]*?)<\\/style>/i)?.[1]?.trim() ?? "";
const template = source
.replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, "")
.replace(/<style[^>]*>[\\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) {
Expand All @@ -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,
'<rebase-component data-component="$1"$2></rebase-component>')
.replace(/<([A-Z][A-Za-z0-9_$-]*)(\s[^>]*)>([\s\S]*?)<\/\1>/g,
'<rebase-component data-component="$1"$2>$3</rebase-component>');
}

function indent(value) {
return value ? value.split("\n").map(line => " " + line).join("\n") : " // no script";
}
25 changes: 12 additions & 13 deletions packages/compiler/test/compiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
<style>.app { color: red; }</style>
<template><p>{message}</p></template>
<script>const message = "hello";</script>
`);
test("compiles sections in any order", () => {
const result = compile("<style>.app { color: red; }</style><template><p>{message}</p></template><script>const message = \"hello\";</script>");
assert.equal(result.template, "<p>{message}</p>");
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(`
<script>import Card from "./Card.rebase";</script>
<template><Card title="Hello" /></template>
`);
assert.match(result.code, /from "\\.\\/Card\\.js"/);
assert.match(result.code, /data-component=\\\"Card\\\"/);
const result = compile("<script>import Card from \"./Card.rebase\";</script><template><Card title=\"Hello\" /></template>");
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("<template>{#if ok}<p>{#each items as item}{item}{/each}</p>{:else}<p>no</p>{/if}</template><script>const ok=true; const items=[1,2];</script>");
assert.match(result.template, /#each/);
assert.match(result.template, /#if/);
});
11 changes: 1 addition & 10 deletions packages/rebase/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading