Modular router && remove $$
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 9s

This commit is contained in:
2026-05-05 16:27:53 +02:00
parent ab0e6e0697
commit 439809b1e7
20 changed files with 2007 additions and 722 deletions

View File

@@ -1,6 +1,6 @@
<div class="w-full -mt-10"><section class="relative py-20 overflow-hidden border-b border-base-200/30 text-center flex flex-col items-center"><div class="relative z-10 max-w-5xl mx-auto px-6 flex flex-col items-center"><div class="flex justify-center mb-10"><img src="logo.svg" alt="SigPro Logo" class="w-48 h-48 md:w-64 md:h-64 object-contain drop-shadow-2xl"></div><h1 class="text-7xl md:text-9xl font-black tracking-tighter mb-4 bg-clip-text text-transparent bg-linear-to-r from-primary via-secondary to-accent !text-center w-full">SigPro</h1><div class="text-2xl md:text-3xl font-bold text-base-content/90 mb-4 !text-center w-full">Atomic Unified Reactive Engine</div><div class="text-xl text-base-content/60 max-w-3xl mx-auto mb-10 leading-relaxed italic text-balance font-light !text-center w-full">"The efficiency of direct DOM manipulation with the elegance of functional reactivity."</div><div class="flex flex-wrap justify-center gap-4 w-full"><a href="#/install" class="btn btn-primary btn-lg shadow-xl shadow-primary/20 group px-10 border-none">Get Started <span class="group-hover:translate-x-1 transition-transform inline-block">→</span></a><button onclick="window.open('https://git.natxocc.com/natxocc/sigpro')" class="btn btn-outline btn-lg border-base-300 hover:bg-base-300 hover:text-base-content">Gitea</button></div></div><div class="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full -z-0 opacity-10 pointer-events-none"><div class="absolute top-10 left-1/4 w-96 h-96 bg-primary filter blur-3xl rounded-full animate-pulse"></div><div class="absolute bottom-10 right-1/4 w-96 h-96 bg-accent filter blur-3xl rounded-full animate-pulse" style="animation-delay: 2.5s"></div></div></section></div>
<section class="max-w-6xl mx-auto px-6 py-16"><div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 items-stretch"><div class="card bg-base-200/30 border border-base-300 hover:border-primary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-primary italic">FUNCTIONAL</h3><p class="text-sm opacity-70">No strings. No templates. Pure JS function calls for instant DOM mounting.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-secondary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-secondary italic">ATOMIC</h3><p class="text-sm opacity-70">Finegrained signals update exactly what changes. No VDOM diffing overhead.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-accent/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-accent italic">ULTRATHIN</h3><p class="text-sm opacity-70">Sub3KB runtime. Infinitely smaller bundle than React, Vue or even Svelte.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-base-content/20 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black italic text-base-content">COMPILERFREE</h3><p class="text-sm opacity-70">Standard Vanilla JS. What you write is what the browser executes. Period.</p></div></div></div></section>
<section class="max-w-6xl mx-auto px-6 py-16"><div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 items-stretch"><div class="card bg-base-200/30 border border-base-300 hover:border-primary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-primary italic">FUNCTIONAL</h3><p class="text-sm opacity-70">No strings. No templates. Pure JS function calls for instant DOM mounting.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-secondary/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-secondary italic">ATOMIC</h3><p class="text-sm opacity-70">Finegrained signals update exactly what changes. No VDOM diffing overhead.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-accent/40 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black text-accent italic">ULTRATHIN</h3><p class="text-sm opacity-70">less than 3KB runtime. Infinitely smaller bundle than React, Vue or even Svelte.</p></div></div><div class="card bg-base-200/30 border border-base-300 hover:border-base-content/20 transition-all p-1"><div class="card-body p-6"><h3 class="card-title text-xl font-black italic text-base-content">COMPILERFREE</h3><p class="text-sm opacity-70">Standard Vanilla JS. What you write is what the browser executes. Period.</p></div></div></div></section>
<div class="max-w-6xl mx-auto px-6 py-8"><h2 class="text-4xl font-black mb-6">Functional DOM Construction</h2><p class="text-lg opacity-80 mb-6">SigPro replaces slow "Template Parsing" with <strong>HighEfficiency Function Calls</strong>. While other frameworks force the browser to parse strings of HTML or execute complex JSX transformations, SigPro uses a direct functional approach.</p>

View File

@@ -2,15 +2,14 @@
* **Introduction**
* [Installation](install.md)
* [Vite Plugin](vite/plugin.md)
* [Router](router.md)
* **API Reference**
* [Quick Start](api/quick.md)
* [Signals & Proxies](api/signal.md)
* [$ignal](api/signal.md)
* [watch](api/watch.md)
* [when](api/when.md)
* [each](api/each.md)
* [router](api/router.md)
* [mount](api/mount.md)
* [h](api/h.md)

View File

@@ -169,7 +169,7 @@ batch(() => {
Hashbased SPA router. Returns a DOM node that renders the current route.
```javascript
import { router } from 'sigpro'
import { router } from 'sigpro/router' // import router
const routes = [
{ path: '/', component: () => div({}, 'Home') },

View File

@@ -1,6 +1,6 @@
# The Signal Function: `$( )`
# The Signal Function: `$()`
The `$( )` function is the core constructor of SigPro. It defines how data is stored, computed, and persisted.
The `$()` function is the **only** reactive primitive in SigPro. It defines how data is stored, computed, and persisted. For complex nested objects, you compose signals naturally.
## Function Signature
@@ -99,175 +99,181 @@ When calling the setter, you can pass an **updater function** to access the curr
---
# The Reactive Object: `$$( )`
## Composing Signals for Complex State
The `$$( )` function creates a reactive proxy for complex nested objects. Unlike `$()`, which tracks a single value, `$$()` tracks **every property access** automatically.
## Function Signature
```typescript
$$<T extends object>(obj: T): T
```
| Parameter | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **`obj`** | `object` | Yes | The object to make reactive. Properties are tracked recursively. |
**Returns:** A reactive proxy that behaves like the original object but triggers updates when any property changes.
---
## Usage Patterns
For nested objects, **compose signals** instead of using magic proxies. This gives you explicit control over reactivity and memory.
### 1. Simple Object
<div id="demo-dollar-simple"></div>
<div id="demo-compose-simple"></div>
```javascript
{
const state = $$({ count: 0, name: "Juan" });
watch(() => console.log(`Count is now ${state.count}`));
const count = $(0);
const name = $("Juan");
// Optionally create a derived combined state
const state = $(() => ({ count: count(), name: name() }));
const App = () => div([
p(() => `Count: ${state.count}, Name: ${state.name}`),
button({ onClick: () => state.count++ }, "Increment count"),
button({ onClick: () => state.name = state.name === "Juan" ? "Ana" : "Juan" }, "Toggle name")
p(() => `Count: ${count()}, Name: ${name()}`),
button({ onClick: () => count(count() + 1) }, "Increment count"),
button({ onClick: () => name(name() === "Juan" ? "Ana" : "Juan") }, "Toggle name")
]);
setTimeout(() => mount(App, '#demo-dollar-simple'), 50);
setTimeout(() => mount(App, '#demo-compose-simple'), 50);
}
```
### 2. Deep Reactivity
### 2. Deeply Nested State
<div id="demo-dollar-deep"></div>
<div id="demo-compose-deep"></div>
```javascript
{
const user = $$({
profile: {
name: "Juan",
address: { city: "Madrid", zip: "28001" }
}
});
const profileName = $("Juan");
const profileCity = $("Madrid");
const profileZip = $("28001");
watch(() => user.profile.address.city, () => console.log("City changed"));
// Computed derived values
const fullAddress = $(() => `${profileCity()}, ${profileZip()}`);
watch(profileCity, () => console.log("City changed to:", profileCity()));
const App = () => div([
p(() => `City: ${user.profile.address.city}`),
button({ onClick: () => user.profile.address.city = "Barcelona" }, "Change to Barcelona")
p(() => `Name: ${profileName()}`),
p(() => `City: ${profileCity()}`),
p(() => `Full address: ${fullAddress()}`),
button({ onClick: () => profileCity("Barcelona") }, "Change to Barcelona")
]);
setTimeout(() => mount(App, '#demo-dollar-deep'), 50);
setTimeout(() => mount(App, '#demo-compose-deep'), 50);
}
```
### 3. Arrays
<div id="demo-dollar-array"></div>
<div id="demo-compose-array"></div>
```javascript
{
const todos = $$([
const todos = $([
{ id: 1, text: "Learn SigPro", done: false },
{ id: 2, text: "Build an app", done: false }
]);
watch(() => todos.length, () => console.log(`You have ${todos.length} todos`));
const todoCount = $(() => todos().length);
watch(todoCount, () => console.log(`You have ${todoCount()} todos`));
const App = () => div([
ul(() => todos.map(todo => li(todo.text + (todo.done ? " ✓" : "")))),
button({ onClick: () => todos.push({ id: Date.now(), text: "New todo", done: false }) }, "Add todo"),
button({ onClick: () => todos[0].done = !todos[0].done }, "Toggle first todo")
ul(() => todos().map(todo => li(todo.text + (todo.done ? " ✓" : "")))),
button({ onClick: () => todos(prev => [...prev, { id: Date.now(), text: "New todo", done: false }]) }, "Add todo"),
button({ onClick: () => {
const updated = [...todos()];
updated[0] = { ...updated[0], done: !updated[0].done };
todos(updated);
}}, "Toggle first todo")
]);
setTimeout(() => mount(App, '#demo-dollar-array'), 50);
setTimeout(() => mount(App, '#demo-compose-array'), 50);
}
```
### 4. Mixed with Signals
### 4. Complete Form Example
<div id="demo-dollar-mixed"></div>
<div id="demo-compose-form"></div>
```javascript
{
const form = $$({
fields: { email: "", password: "" },
isValid: $(false)
});
const email = $("");
const password = $("");
const isValid = $(() => email().includes("@") && password().length > 6);
const canSubmit = $(() =>
form.fields.email.includes("@") &&
form.fields.password.length > 6
);
watch(canSubmit, valid => form.isValid(valid));
watch(isValid, valid => console.log("Form valid:", valid));
const App = () => div([
input({ type: "email", placeholder: "Email", value: () => form.fields.email, onInput: e => form.fields.email = e.target.value }),
input({ type: "password", placeholder: "Password", value: () => form.fields.password, onInput: e => form.fields.password = e.target.value }),
p(() => `Form valid: ${form.isValid() ? "Yes" : "No"}`)
input({
type: "email",
placeholder: "Email",
value: email,
onInput: e => email(e.target.value)
}),
input({
type: "password",
placeholder: "Password",
value: password,
onInput: e => password(e.target.value)
}),
p(() => `Form valid: ${isValid() ? "Yes" : "No"}`)
]);
setTimeout(() => mount(App, '#demo-dollar-mixed'), 50);
setTimeout(() => mount(App, '#demo-compose-form'), 50);
}
```
---
## Key Differences: `$()` vs `$$()`
## Best Practices for Complex State
| Feature | `$()` Signal | `$$()` Reactive |
| :--- | :--- | :--- |
| **Primitives** | ✅ Works directly | ❌ Needs wrapper object |
| **Objects** | Manual tracking | ✅ Automatic deep tracking |
| **Nested properties** | ❌ Not reactive | ✅ Fully reactive |
| **Arrays** | Requires reassignment | ✅ Methods (push, pop, etc.) work |
| **Syntax** | `count()` / `count(5)` | `state.count = 5` |
| **LocalStorage** | ✅ Built-in | ❌ (use `$()` for persistence) |
| **Performance** | Lighter | Slightly heavier (Proxy) |
| **Destructuring** | ✅ Safe | ❌ Breaks reactivity |
---
## When to Use Each
### Use `$()` when:
<div id="demo-use-dollar"></div>
### ✅ DO: Compose signals explicitly
```javascript
{
const count = $(0);
const firstName = $("John");
const lastName = $("Doe");
const fullName = $(() => `${firstName()} ${lastName()}`);
const App = () => div([
p(() => `Count: ${count()}`),
button({ onClick: () => count(count() + 1) }, "Count up"),
p(() => `Full name: ${fullName()}`),
input({ value: firstName, placeholder: "First name" }),
input({ value: lastName, placeholder: "Last name" })
]);
setTimeout(() => mount(App, '#demo-use-dollar'), 50);
// Clear, predictable, and memory-safe
const user = {
name: $("Juan"),
email: $("juan@example.com"),
preferences: {
theme: $("dark"),
notifications: $(true)
}
}
// Computed values derived from composition
const userDisplay = $(() => `${user.name()} <${user.email()}>`)
```
### Use `$$()` when:
<div id="demo-use-dollar-dollar"></div>
### ✅ DO: Create store patterns
```javascript
{
const form = $$({ email: "", password: "" });
const settings = $$({ theme: "dark", notifications: true });
const App = () => div([
input({ placeholder: "Email", onInput: e => form.email = e.target.value }),
input({ placeholder: "Password", type: "password", onInput: e => form.password = e.target.value }),
p(() => `Email: ${form.email}, Password: ${form.password}`),
button({ onClick: () => settings.theme = settings.theme === "dark" ? "light" : "dark" }, "Toggle theme"),
p(() => `Current theme: ${settings.theme}`)
]);
setTimeout(() => mount(App, '#demo-use-dollar-dollar'), 50);
const createUserStore = () => {
const name = $("")
const email = $("")
const isValid = $(() => name().length > 0 && email().includes("@"))
const actions = {
setName: (value) => name(value),
setEmail: (value) => email(value),
reset: () => {
name("")
email("")
}
}
return { name, email, isValid, ...actions }
}
const userStore = createUserStore()
```
### ❌ DON'T: Try to wrap objects with signals
```javascript
// Wrong - loses reactivity on nested properties
const user = $({ name: "Juan", email: "..." })
user().name = "Ana" // ❌ Not reactive!
// Correct - each property its own signal
const userName = $("Juan")
const userEmail = $("...")
```
### ❌ DON'T: Destructure signals in reactive contexts
```javascript
// Wrong - breaks tracking
const { name, email } = user
watch(() => name(), ...) // ❌ 'name' is not tracked properly
// Correct - use the original signal
watch(() => user.name(), ...) // ✅
```
---
@@ -276,115 +282,121 @@ $$<T extends object>(obj: T): T
### ✅ DO:
```javascript
// Access properties directly
state.count = 10;
state.user.name = "Ana";
todos.push(newItem);
// Update by recreating objects for arrays
todos(prev => [...prev, newTodo])
// Track in effects
watch(() => state.count, () => {});
watch(() => state.user.name, () => {});
// Update objects immutably
const current = user()
user({ ...current, name: "Ana" })
// Track individual signals
watch(() => user.name(), () => {})
watch(() => user.email(), () => {})
```
### ❌ DON'T:
```javascript
// Destructuring breaks reactivity
const { count, user } = state; // ❌ count and user are not reactive
// Mutate objects directly
user().name = "Ana" // ❌ Not reactive
// Reassigning the whole object
state = { count: 10 }; // ❌ Loses reactivity
// Mutate arrays in place
todos().push(newTodo) // ❌ Not reactive
// Using primitive directly
const count = $$(0); // ❌ Doesn't work (use $() instead)
// Destructure in component bodies
const { name, email } = user // ❌ Breaks reactivity
```
---
## Automatic Cleanup
Like all SigPro reactive primitives, `$$()` integrates with the cleanup system:
All signals integrate with the cleanup system:
- Effects tracking reactive properties are automatically disposed
- No manual cleanup needed
- Works with `watch`, `when`, and `each`
```javascript
// Effects are automatically disposed when components unmount
const name = $("Juan")
watch(name, () => console.log("Name changed"))
---
## Technical Comparison
| Aspect | `$()` | `$$()` |
| :--- | :--- | :--- |
| **Implementation** | Closure with Set | Proxy with WeakMap |
| **Tracking** | Explicit (function call) | Implicit (property access) |
| **Memory** | Minimal | Slightly more (WeakMap cache) |
| **Use Case** | Simple state | Complex state |
| **Learning Curve** | Low | Low (feels like plain JS) |
// Manual cleanup if needed
const stop = watch(name, callback)
stop() // Clean up manually
```
---
## Complete Example
<div id="demo-complete"></div>
<div id="demo-complete-final"></div>
```javascript
{
const app = {
theme: $("dark", "theme_complete"),
sidebarOpen: $(true),
user: $$({ name: "", email: "", preferences: { notifications: true, language: "es" } }),
isLoggedIn: $(() => !!app.user.name),
login(name, email) {
app.user.name = name;
app.user.email = email;
},
logout() {
app.user.name = "";
app.user.email = "";
app.user.preferences.notifications = true;
}
};
// All state as explicit signals
const theme = $("dark", "theme_complete")
const sidebarOpen = $(true)
const userName = $("")
const userEmail = $("")
const notifications = $(true)
const language = $("es")
// Computed signals
const isLoggedIn = $(() => !!userName() && !!userEmail())
// Actions as plain functions
const login = (name, email) => {
userName(name)
userEmail(email)
}
const logout = () => {
userName("")
userEmail("")
notifications(true) // Reset on logout
}
// Components using signals directly
const LoginForm = () => div([
input({ placeholder: "Name", onInput: e => app.user.name = e.target.value }),
input({ placeholder: "Email", onInput: e => app.user.email = e.target.value }),
button({ onClick: () => app.login(app.user.name, app.user.email) }, "Login")
]);
input({
placeholder: "Name",
onInput: e => userName(e.target.value)
}),
input({
placeholder: "Email",
onInput: e => userEmail(e.target.value)
}),
button({
onClick: () => login(userName(), userEmail())
}, "Login")
])
const UserProfile = () => div([
h2(() => `Welcome ${app.user.name}`),
p(() => `Email: ${app.user.email}`),
p(() => `Notifications: ${app.user.preferences.notifications ? "ON" : "OFF"}`),
button({ onClick: () => app.user.preferences.notifications = !app.user.preferences.notifications }, "Toggle Notifications"),
button({ onClick: app.logout }, "Logout")
]);
h2(() => `Welcome ${userName()}`),
p(() => `Email: ${userEmail()}`),
p(() => `Notifications: ${notifications() ? "ON" : "OFF"}`),
p(() => `Language: ${language()}`),
button({
onClick: () => notifications(!notifications())
}, "Toggle Notifications"),
button({ onClick: logout }, "Logout")
])
const App = () => div({ class: "complete-example" }, [
when(() => app.isLoggedIn(), () => UserProfile(), () => LoginForm())
]);
when(() => isLoggedIn(), () => UserProfile(), () => LoginForm())
])
setTimeout(() => mount(App, '#demo-complete'), 50);
setTimeout(() => mount(App, '#demo-complete-final'), 50)
}
```
---
## Migration from `$()` to `$$()`
## Summary
If you have code using nested signals:
With **only `$()`** as your reactive primitive:
```javascript
// Before - Manual nesting
const user = $({
name: $(""),
email: $("")
});
user().name("Juan"); // Need to call inner signal
// After - Automatic nesting
const user = $$({
name: "",
email: ""
});
user.name = "Juan"; // Direct assignment
```
-**Explicit** - You know exactly what's reactive
-**Memory safe** - No hidden proxies or WeakMap caches
-**Predictable** - No magic, just signals
-**Performant** - Minimal overhead
-**Debuggable** - Clear data flow
Complex state is built by **composing signals**, not by wrapping objects. This gives you the same power as reactive proxies but with better control and fewer surprises.

View File

@@ -182,7 +182,7 @@ SigPro stands out by removing the "Build Step" tax and the "Virtual DOM" overhea
| Feature | **SigPro** | **SolidJS** | **Svelte** | **React** | **Vue** |
| :----------------- | :--------------- | :----------- | :----------- | :---------- | :---------- |
| **Bundle Size** | **~3KB** | ~7KB | ~4KB | ~40KB+ | ~30KB |
| **Bundle Size** | **<3KB** | ~7KB | ~4KB | ~40KB+ | ~30KB |
| **DOM Strategy** | **Direct DOM** | Direct DOM | Compiled DOM | Virtual DOM | Virtual DOM |
| **Reactivity** | **Fine-grained** | Fine-grained | Compiled | Re-renders | Proxies |
| **Build Step** | **Optional** | Required | Required | Required | Optional |

View File

@@ -17,7 +17,7 @@ router(routes: Route[]): HTMLElement
**Returns:** A `div` element (with class `"router-hook"`) that acts as the router outlet. The router automatically destroys the previous view and mounts the matched component when the hash changes.
> **Availability:** `router` and its helper methods (`router.to`, `router.back`, `router.path`, `router.params`) are exported from the SigPro module. In **ESM** you must import them (`import { router } from 'sigpro'`). In the **IIFE** classic script, they are automatically available on `window`. The examples below assume the functions are already in scope.
> **Availability:** `router` and its helper methods (`router.to`, `router.back`, `router.path`, `router.params`) are exported from the SigPro module. In **ESM** you must import them (`import { router } from 'sigpro/router'`). In the **IIFE** classic script, they are automatically available on `window`. The examples below assume the functions are already in scope.
---
@@ -187,3 +187,152 @@ mount(App, "#app");
| `router.back()` | Goes back in history. |
| `router.path()` | Returns the current path without `#`. |
| `router.params()` | Reactive signal of the current route parameters. |
# Vite Plugin: File-based Routing
The `sigproRouter` plugin for Vite automates route generation by scanning your `pages` directory. It creates a **virtual module** that you can import directly into your code, eliminating the need to maintain a manual routes array.
## 1. Project Structure
To use the plugin, organize your files within the `src/pages` directory. The folder hierarchy directly determines your application's URL structure. SigPro uses brackets `[param]` for dynamic segments.
<div class="mockup-code bg-base-300 text-base-content shadow-xl my-8">
<pre><code>my-sigpro-app/
├── src/
│ ├── pages/
│ │ ├── index.js → #/
│ │ ├── about.js → #/about
│ │ ├── users/
│ │ │ └── [id].js → #/users/:id
│ │ └── blog/
│ │ ├── index.js → #/blog
│ │ └── [slug].js → #/blog/:slug
│ ├── App.js (Main Layout)
│ └── main.js (Entry Point)
├── vite.config.js
└── package.json</code></pre>
</div>
---
## 2. Setup & Configuration
Add the plugin to your `vite.config.js`. It works out of the box with zero configuration.
```javascript
// vite.config.js
import { defineConfig } from 'vite';
import { sigproRouter } from 'sigpro/router';
export default defineConfig({
plugins: [sigproRouter()]
});
```
---
## 3. Implementation
Thanks to **SigPro's synchronous initialization**, you no longer need to wrap your mounting logic in `.then()` blocks.
<div class="tabs tabs-box w-full mt-8 mb-12 bg-base-200/50 p-2 border border-base-300">
<input type="radio" name="route_impl" class="tab" aria-label="Option A: Direct in main.js" checked />
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
```javascript
// src/main.js
import { mount } from 'sigpro';
import { router } from 'sigpro/router';
import { routes } from 'virtual:sigpro-routes';
// The Core already has Router ready
mount(router(routes), '#app');
```
</div>
<input type="radio" name="route_impl" class="tab" aria-label="Option B: Inside App.js (Persistent Layout)" />
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
```javascript
// src/App.js
import { routes } from 'virtual:sigpro-routes';
export default () => div({ class: 'layout' }, [
header([
h1("SigPro App"),
nav([
button({ onclick: () => Router.go('/') }, "Home"),
button({ onclick: () => Router.go('/blog') }, "Blog")
])
]),
// Only the content inside <main> will be swapped reactively
main(Router(routes))
]);
```
</div>
</div>
---
## 4. Route Mapping Reference
The plugin follows a simple convention to transform your file system into a routing map.
<div class="overflow-x-auto my-8">
<table class="table table-zebra w-full">
<thead class="bg-base-200">
<tr>
<th>File Path</th>
<th>Generated Path</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>index.js</code></td>
<td class="font-mono text-primary font-bold">/</td>
<td>The application root.</td>
</tr>
<tr>
<td><code>about.js</code></td>
<td class="font-mono text-primary font-bold">/about</td>
<td>A static page.</td>
</tr>
<tr>
<td><code>[id].js</code></td>
<td class="font-mono text-primary font-bold">/:id</td>
<td>Dynamic parameter (passed to the component).</td>
</tr>
<tr>
<td><code>blog/index.js</code></td>
<td class="font-mono text-primary font-bold">/blog</td>
<td>Folder index page.</td>
</tr>
<tr>
<td><code>_utils.js</code></td>
<td class="italic opacity-50 text-error">Ignored</td>
<td>Files starting with <code>_</code> are excluded from routing.</td>
</tr>
</tbody>
</table>
</div>
---
## 5. How it Works (Vite Virtual Module)
The plugin generates a virtual module named `virtual:sigpro-routes`. This module exports an array of objects compatible with `Router()`:
```javascript
// Internal representation generated by the plugin
export const routes = [
{ path: '/', component: () => import('/src/pages/index.js') },
{ path: '/users/:id', component: () => import('/src/pages/users/[id].js') },
// ...
];
```
Because it uses dynamic `import()`, Vite automatically performs **Code Splitting**, meaning each page is its own small JS file that only loads when the user navigates to it.

View File

@@ -1,4 +1,42 @@
(() => {
var __create = Object.create;
var __getProtoOf = Object.getPrototypeOf;
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
function __accessProp(key) {
return this[key];
}
var __toESMCache_node;
var __toESMCache_esm;
var __toESM = (mod, isNodeMode, target) => {
var canCache = mod != null && typeof mod === "object";
if (canCache) {
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
var cached = cache.get(mod);
if (cached)
return cached;
}
target = mod != null ? __create(__getProtoOf(mod)) : {};
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key))
__defProp(to, key, {
get: __accessProp.bind(mod, key),
enumerable: true
});
if (canCache)
cache.set(mod, to);
return to;
};
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined")
return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
// src/sigpro.js
var isFunc = (f) => typeof f === "function";
var isObj = (o) => o && typeof o === "object";
@@ -10,8 +48,6 @@
var isFlushing = false;
var batchDepth = 0;
var effectQueue = new Set;
var proxyCache = new WeakMap;
var ITER = Symbol("iter");
var MOUNTED_NODES = new WeakMap;
var SVG_NS = "http://www.w3.org/2000/svg";
var XLINK_NS = "http://www.w3.org/1999/xlink";
@@ -172,52 +208,6 @@
return val;
};
};
var $$ = (target) => {
if (!isObj(target))
return target;
const cached = proxyCache.get(target);
if (cached)
return cached;
const subs = new Map;
const getSubs = (key) => {
let set = subs.get(key);
if (!set)
subs.set(key, set = new Set);
return set;
};
const proxy = new Proxy(target, {
get(target2, key, receiver) {
if (typeof key !== "symbol")
trackUpdate(getSubs(key));
return $$(Reflect.get(target2, key, receiver));
},
set(target2, key, value, receiver) {
const hadKey = Reflect.has(target2, key);
const oldValue = Reflect.get(target2, key, receiver);
const result = Reflect.set(target2, key, value, receiver);
if (result && !Object.is(oldValue, value)) {
trackUpdate(getSubs(key), true);
if (!hadKey)
trackUpdate(getSubs(ITER), true);
}
return result;
},
deleteProperty(target2, key) {
const result = Reflect.deleteProperty(target2, key);
if (result) {
trackUpdate(getSubs(key), true);
trackUpdate(getSubs(ITER), true);
}
return result;
},
ownKeys(target2) {
trackUpdate(getSubs(ITER));
return Reflect.ownKeys(target2);
}
});
proxyCache.set(target, proxy);
return proxy;
};
var watch = (sources, cb) => {
if (cb === undefined) {
const effect2 = createEffect(sources);
@@ -467,6 +457,348 @@
});
return root;
};
var Fragment = (props) => props.children;
var mount = (comp, target) => {
const t = typeof target === "string" ? doc.querySelector(target) : target;
if (!t)
return;
if (MOUNTED_NODES.has(t))
MOUNTED_NODES.get(t).destroy();
const inst = render(isFunc(comp) ? comp : () => comp);
t.replaceChildren(inst.container);
MOUNTED_NODES.set(t, inst);
return inst;
};
if (typeof window !== "undefined") {
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video".split(" ").forEach((tag) => {
window[tag] = (props, children) => h(tag, props, children);
});
}
// src/router.js
var import_node_fs = (() => ({}));
// node:path
function assertPath(path) {
if (typeof path !== "string")
throw TypeError("Path must be a string. Received " + JSON.stringify(path));
}
function normalizeStringPosix(path, allowAboveRoot) {
var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code;
for (var i = 0;i <= path.length; ++i) {
if (i < path.length)
code = path.charCodeAt(i);
else if (code === 47)
break;
else
code = 47;
if (code === 47) {
if (lastSlash === i - 1 || dots === 1)
;
else if (lastSlash !== i - 1 && dots === 2) {
if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) {
if (res.length > 2) {
var lastSlashIndex = res.lastIndexOf("/");
if (lastSlashIndex !== res.length - 1) {
if (lastSlashIndex === -1)
res = "", lastSegmentLength = 0;
else
res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
lastSlash = i, dots = 0;
continue;
}
} else if (res.length === 2 || res.length === 1) {
res = "", lastSegmentLength = 0, lastSlash = i, dots = 0;
continue;
}
}
if (allowAboveRoot) {
if (res.length > 0)
res += "/..";
else
res = "..";
lastSegmentLength = 2;
}
} else {
if (res.length > 0)
res += "/" + path.slice(lastSlash + 1, i);
else
res = path.slice(lastSlash + 1, i);
lastSegmentLength = i - lastSlash - 1;
}
lastSlash = i, dots = 0;
} else if (code === 46 && dots !== -1)
++dots;
else
dots = -1;
}
return res;
}
function _format(sep, pathObject) {
var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "");
if (!dir)
return base;
if (dir === pathObject.root)
return dir + base;
return dir + sep + base;
}
function resolve() {
var resolvedPath = "", resolvedAbsolute = false, cwd;
for (var i = arguments.length - 1;i >= -1 && !resolvedAbsolute; i--) {
var path;
if (i >= 0)
path = arguments[i];
else {
if (cwd === undefined)
cwd = process.cwd();
path = cwd;
}
if (assertPath(path), path.length === 0)
continue;
resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47;
}
if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute)
if (resolvedPath.length > 0)
return "/" + resolvedPath;
else
return "/";
else if (resolvedPath.length > 0)
return resolvedPath;
else
return ".";
}
function normalize(path) {
if (assertPath(path), path.length === 0)
return ".";
var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47;
if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute)
path = ".";
if (path.length > 0 && trailingSeparator)
path += "/";
if (isAbsolute)
return "/" + path;
return path;
}
function isAbsolute(path) {
return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47;
}
function join() {
if (arguments.length === 0)
return ".";
var joined;
for (var i = 0;i < arguments.length; ++i) {
var arg = arguments[i];
if (assertPath(arg), arg.length > 0)
if (joined === undefined)
joined = arg;
else
joined += "/" + arg;
}
if (joined === undefined)
return ".";
return normalize(joined);
}
function relative(from, to) {
if (assertPath(from), assertPath(to), from === to)
return "";
if (from = resolve(from), to = resolve(to), from === to)
return "";
var fromStart = 1;
for (;fromStart < from.length; ++fromStart)
if (from.charCodeAt(fromStart) !== 47)
break;
var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1;
for (;toStart < to.length; ++toStart)
if (to.charCodeAt(toStart) !== 47)
break;
var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0;
for (;i <= length; ++i) {
if (i === length) {
if (toLen > length) {
if (to.charCodeAt(toStart + i) === 47)
return to.slice(toStart + i + 1);
else if (i === 0)
return to.slice(toStart + i);
} else if (fromLen > length) {
if (from.charCodeAt(fromStart + i) === 47)
lastCommonSep = i;
else if (i === 0)
lastCommonSep = 0;
}
break;
}
var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i);
if (fromCode !== toCode)
break;
else if (fromCode === 47)
lastCommonSep = i;
}
var out = "";
for (i = fromStart + lastCommonSep + 1;i <= fromEnd; ++i)
if (i === fromEnd || from.charCodeAt(i) === 47)
if (out.length === 0)
out += "..";
else
out += "/..";
if (out.length > 0)
return out + to.slice(toStart + lastCommonSep);
else {
if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47)
++toStart;
return to.slice(toStart);
}
}
function _makeLong(path) {
return path;
}
function dirname(path) {
if (assertPath(path), path.length === 0)
return ".";
var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true;
for (var i = path.length - 1;i >= 1; --i)
if (code = path.charCodeAt(i), code === 47) {
if (!matchedSlash) {
end = i;
break;
}
} else
matchedSlash = false;
if (end === -1)
return hasRoot ? "/" : ".";
if (hasRoot && end === 1)
return "//";
return path.slice(0, end);
}
function basename(path, ext) {
if (ext !== undefined && typeof ext !== "string")
throw TypeError('"ext" argument must be a string');
assertPath(path);
var start = 0, end = -1, matchedSlash = true, i;
if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
if (ext.length === path.length && ext === path)
return "";
var extIdx = ext.length - 1, firstNonSlashEnd = -1;
for (i = path.length - 1;i >= 0; --i) {
var code = path.charCodeAt(i);
if (code === 47) {
if (!matchedSlash) {
start = i + 1;
break;
}
} else {
if (firstNonSlashEnd === -1)
matchedSlash = false, firstNonSlashEnd = i + 1;
if (extIdx >= 0)
if (code === ext.charCodeAt(extIdx)) {
if (--extIdx === -1)
end = i;
} else
extIdx = -1, end = firstNonSlashEnd;
}
}
if (start === end)
end = firstNonSlashEnd;
else if (end === -1)
end = path.length;
return path.slice(start, end);
} else {
for (i = path.length - 1;i >= 0; --i)
if (path.charCodeAt(i) === 47) {
if (!matchedSlash) {
start = i + 1;
break;
}
} else if (end === -1)
matchedSlash = false, end = i + 1;
if (end === -1)
return "";
return path.slice(start, end);
}
}
function extname(path) {
assertPath(path);
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0;
for (var i = path.length - 1;i >= 0; --i) {
var code = path.charCodeAt(i);
if (code === 47) {
if (!matchedSlash) {
startPart = i + 1;
break;
}
continue;
}
if (end === -1)
matchedSlash = false, end = i + 1;
if (code === 46) {
if (startDot === -1)
startDot = i;
else if (preDotState !== 1)
preDotState = 1;
} else if (startDot !== -1)
preDotState = -1;
}
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
return "";
return path.slice(startDot, end);
}
function format(pathObject) {
if (pathObject === null || typeof pathObject !== "object")
throw TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject);
return _format("/", pathObject);
}
function parse(path) {
assertPath(path);
var ret = { root: "", dir: "", base: "", ext: "", name: "" };
if (path.length === 0)
return ret;
var code = path.charCodeAt(0), isAbsolute2 = code === 47, start;
if (isAbsolute2)
ret.root = "/", start = 1;
else
start = 0;
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0;
for (;i >= start; --i) {
if (code = path.charCodeAt(i), code === 47) {
if (!matchedSlash) {
startPart = i + 1;
break;
}
continue;
}
if (end === -1)
matchedSlash = false, end = i + 1;
if (code === 46) {
if (startDot === -1)
startDot = i;
else if (preDotState !== 1)
preDotState = 1;
} else if (startDot !== -1)
preDotState = -1;
}
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
if (end !== -1)
if (startPart === 0 && isAbsolute2)
ret.base = ret.name = path.slice(1, end);
else
ret.base = ret.name = path.slice(startPart, end);
} else {
if (startPart === 0 && isAbsolute2)
ret.name = path.slice(1, startDot), ret.base = path.slice(1, end);
else
ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end);
ret.ext = path.slice(startDot, end);
}
if (startPart > 0)
ret.dir = path.slice(0, startPart - 1);
else if (isAbsolute2)
ret.dir = "/";
return ret;
}
var sep = "/";
var delimiter = ":";
var posix = ((p) => (p.posix = p, p))({ resolve, normalize, isAbsolute, join, relative, _makeLong, dirname, basename, extname, format, parse, sep, delimiter, win32: null, posix: null });
// src/router.js
var router = (routes) => {
const getHash = () => window.location.hash.slice(1) || "/";
const path = $(getHash());
@@ -500,26 +832,9 @@
router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/");
router.back = () => window.history.back();
router.path = () => window.location.hash.replace(/^#/, "") || "/";
var Fragment = (props) => props.children;
var mount = (comp, target) => {
const t = typeof target === "string" ? doc.querySelector(target) : target;
if (!t)
return;
if (MOUNTED_NODES.has(t))
MOUNTED_NODES.get(t).destroy();
const inst = render(isFunc(comp) ? comp : () => comp);
t.replaceChildren(inst.container);
MOUNTED_NODES.set(t, inst);
return inst;
};
if (typeof window !== "undefined") {
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video".split(" ").forEach((tag) => {
window[tag] = (props, children) => h(tag, props, children);
});
}
// src/build_umd.js
if (typeof window !== "undefined") {
Object.assign(window, { $, $$, watch, h, Fragment, when, each, router, mount, batch, onUnmount, isArr, isFunc, isObj });
Object.assign(window, { $, watch, h, Fragment, when, each, router, mount, batch, onUnmount, isArr, isFunc, isObj });
}
})();

View File

@@ -1,146 +0,0 @@
# Vite Plugin: File-based Routing
The `sigproRouter` plugin for Vite automates route generation by scanning your `pages` directory. It creates a **virtual module** that you can import directly into your code, eliminating the need to maintain a manual routes array.
## 1. Project Structure
To use the plugin, organize your files within the `src/pages` directory. The folder hierarchy directly determines your application's URL structure. SigPro uses brackets `[param]` for dynamic segments.
<div class="mockup-code bg-base-300 text-base-content shadow-xl my-8">
<pre><code>my-sigpro-app/
├── src/
│ ├── pages/
│ │ ├── index.js → #/
│ │ ├── about.js → #/about
│ │ ├── users/
│ │ │ └── [id].js → #/users/:id
│ │ └── blog/
│ │ ├── index.js → #/blog
│ │ └── [slug].js → #/blog/:slug
│ ├── App.js (Main Layout)
│ └── main.js (Entry Point)
├── vite.config.js
└── package.json</code></pre>
</div>
---
## 2. Setup & Configuration
Add the plugin to your `vite.config.js`. It works out of the box with zero configuration.
```javascript
// vite.config.js
import { defineConfig } from 'vite';
import { sigproRouter } from 'sigpro/vite';
export default defineConfig({
plugins: [sigproRouter()]
});
```
---
## 3. Implementation
Thanks to **SigPro's synchronous initialization**, you no longer need to wrap your mounting logic in `.then()` blocks.
<div class="tabs tabs-box w-full mt-8 mb-12 bg-base-200/50 p-2 border border-base-300">
<input type="radio" name="route_impl" class="tab" aria-label="Option A: Direct in main.js" checked />
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
```javascript
// src/main.js
import { mount, router } from 'sigpro';
import { routes } from 'virtual:sigpro-routes';
// The Core already has Router ready
mount(router(routes), '#app');
```
</div>
<input type="radio" name="route_impl" class="tab" aria-label="Option B: Inside App.js (Persistent Layout)" />
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
```javascript
// src/App.js
import { routes } from 'virtual:sigpro-routes';
export default () => div({ class: 'layout' }, [
header([
h1("SigPro App"),
nav([
button({ onclick: () => Router.go('/') }, "Home"),
button({ onclick: () => Router.go('/blog') }, "Blog")
])
]),
// Only the content inside <main> will be swapped reactively
main(Router(routes))
]);
```
</div>
</div>
---
## 4. Route Mapping Reference
The plugin follows a simple convention to transform your file system into a routing map.
<div class="overflow-x-auto my-8">
<table class="table table-zebra w-full">
<thead class="bg-base-200">
<tr>
<th>File Path</th>
<th>Generated Path</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>index.js</code></td>
<td class="font-mono text-primary font-bold">/</td>
<td>The application root.</td>
</tr>
<tr>
<td><code>about.js</code></td>
<td class="font-mono text-primary font-bold">/about</td>
<td>A static page.</td>
</tr>
<tr>
<td><code>[id].js</code></td>
<td class="font-mono text-primary font-bold">/:id</td>
<td>Dynamic parameter (passed to the component).</td>
</tr>
<tr>
<td><code>blog/index.js</code></td>
<td class="font-mono text-primary font-bold">/blog</td>
<td>Folder index page.</td>
</tr>
<tr>
<td><code>_utils.js</code></td>
<td class="italic opacity-50 text-error">Ignored</td>
<td>Files starting with <code>_</code> are excluded from routing.</td>
</tr>
</tbody>
</table>
</div>
---
## 5. How it Works (Vite Virtual Module)
The plugin generates a virtual module named `virtual:sigpro-routes`. This module exports an array of objects compatible with `Router()`:
```javascript
// Internal representation generated by the plugin
export const routes = [
{ path: '/', component: () => import('/src/pages/index.js') },
{ path: '/users/:id', component: () => import('/src/pages/users/[id].js') },
// ...
];
```
Because it uses dynamic `import()`, Vite automatically performs **Code Splitting**, meaning each page is its own small JS file that only loads when the user navigates to it.