Modular router && remove $$
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 9s
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 9s
This commit is contained in:
@@ -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">Fine‑grained signals update exactly what changes. No V‑DOM 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">ULTRA‑THIN</h3><p class="text-sm opacity-70">Sub‑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">COMPILER‑FREE</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">Fine‑grained signals update exactly what changes. No V‑DOM 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">ULTRA‑THIN</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">COMPILER‑FREE</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>High‑Efficiency 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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ batch(() => {
|
||||
Hash‑based 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') },
|
||||
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
447
docs/sigpro.js
447
docs/sigpro.js
@@ -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 });
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user