@blueshed/railroad 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/railroad/SKILL.md +86 -0
- package/.claude/skills/railroad/jsx.md +325 -0
- package/.claude/skills/railroad/routes.md +157 -0
- package/.claude/skills/railroad/shared.md +94 -0
- package/.claude/skills/railroad/signals.md +218 -0
- package/LICENSE +21 -0
- package/README.md +234 -0
- package/index.ts +18 -0
- package/jsx-dev-runtime.ts +4 -0
- package/jsx-runtime.ts +35 -0
- package/jsx.ts +381 -0
- package/logger.ts +91 -0
- package/package.json +40 -0
- package/routes.ts +110 -0
- package/shared.ts +32 -0
- package/signals.ts +186 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Signals — Reactive State
|
|
2
|
+
|
|
3
|
+
Signals are the foundation of railroad. Everything reactive flows from them.
|
|
4
|
+
|
|
5
|
+
## Creating Signals
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { signal } from "@blueshed/railroad";
|
|
9
|
+
|
|
10
|
+
const count = signal(0); // Signal<number>
|
|
11
|
+
const name = signal("World"); // Signal<string>
|
|
12
|
+
const items = signal<string[]>([]); // Signal<string[]>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Reading and Writing
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
count.get() // read (tracks dependency inside effect/computed)
|
|
19
|
+
count.set(5) // write (notifies listeners if value changed)
|
|
20
|
+
count.update(n => n + 1) // transform and write (caller must return a new value)
|
|
21
|
+
count.mutate(v => ...) // clone, mutate in place, notify (see below)
|
|
22
|
+
count.patch({ key: v }) // shallow merge for object signals (see below)
|
|
23
|
+
count.peek() // read WITHOUT tracking — use for one-off reads outside effects
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Equality Check
|
|
27
|
+
|
|
28
|
+
`set()` uses `Object.is()` to compare old and new values. If they are the same, no listeners are notified. This means:
|
|
29
|
+
|
|
30
|
+
- Primitives: setting the same number/string/boolean is a no-op.
|
|
31
|
+
- Objects/arrays: you must create a new reference to trigger updates.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const todos = signal([{ id: 1, text: "Buy milk" }]);
|
|
35
|
+
|
|
36
|
+
// WRONG — same array reference, no update:
|
|
37
|
+
todos.peek().push({ id: 2, text: "Walk dog" });
|
|
38
|
+
todos.set(todos.peek()); // Object.is says same reference, listeners NOT notified
|
|
39
|
+
|
|
40
|
+
// RIGHT — new array:
|
|
41
|
+
todos.update(arr => [...arr, { id: 2, text: "Walk dog" }]);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### `mutate(fn)` — In-Place Mutation
|
|
45
|
+
|
|
46
|
+
`mutate()` clones the current value with `structuredClone`, passes the clone to your function for in-place mutation, then notifies listeners. Use it when you want to modify objects or arrays naturally without manually creating a new reference.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const todos = signal([{ id: 1, text: "Buy milk" }]);
|
|
50
|
+
|
|
51
|
+
// Append:
|
|
52
|
+
todos.mutate(arr => arr.push({ id: 2, text: "Walk dog" }));
|
|
53
|
+
|
|
54
|
+
// Modify nested property:
|
|
55
|
+
const doc = signal({ title: "Draft", meta: { tags: ["a"] } });
|
|
56
|
+
doc.mutate(d => d.meta.tags.push("b"));
|
|
57
|
+
|
|
58
|
+
// Toggle in a Set:
|
|
59
|
+
const selected = signal(new Set([1, 2, 3]));
|
|
60
|
+
selected.mutate(s => s.has(4) ? s.delete(4) : s.add(4));
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`mutate()` always notifies listeners (the clone guarantees a new reference). Use `update()` when you can return a new value cheaply; use `mutate()` when in-place mutation is more natural.
|
|
64
|
+
|
|
65
|
+
### `patch(partial)` — Shallow Merge
|
|
66
|
+
|
|
67
|
+
`patch()` does a shallow merge for object signals — equivalent to `set({ ...current, ...partial })`:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
const filter = signal({ color: "red", size: 10, active: true });
|
|
71
|
+
|
|
72
|
+
filter.patch({ color: "blue" }); // { color: "blue", size: 10, active: true }
|
|
73
|
+
filter.patch({ size: 20, active: false }); // { color: "blue", size: 20, active: false }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Like `set()`, `patch()` uses `Object.is` — since the spread always creates a new reference, listeners are always notified.
|
|
77
|
+
|
|
78
|
+
## Computed Signals
|
|
79
|
+
|
|
80
|
+
Derived read-only signals that auto-update when dependencies change.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { computed } from "@blueshed/railroad";
|
|
84
|
+
|
|
85
|
+
const firstName = signal("John");
|
|
86
|
+
const lastName = signal("Doe");
|
|
87
|
+
const fullName = computed(() => `${firstName.get()} ${lastName.get()}`);
|
|
88
|
+
|
|
89
|
+
fullName.get(); // "John Doe"
|
|
90
|
+
firstName.set("Jane");
|
|
91
|
+
fullName.get(); // "Jane Doe"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Computed signals chain:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const a = signal(1);
|
|
98
|
+
const b = computed(() => a.get() + 1); // 2
|
|
99
|
+
const c = computed(() => b.get() * 10); // 20
|
|
100
|
+
a.set(3);
|
|
101
|
+
c.get(); // 40
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Effects
|
|
105
|
+
|
|
106
|
+
Run a function whenever its signal dependencies change. The function runs immediately on creation.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { effect } from "@blueshed/railroad";
|
|
110
|
+
|
|
111
|
+
const count = signal(0);
|
|
112
|
+
|
|
113
|
+
const dispose = effect(() => {
|
|
114
|
+
console.log(`count is ${count.get()}`);
|
|
115
|
+
});
|
|
116
|
+
// logs: "count is 0"
|
|
117
|
+
|
|
118
|
+
count.set(1);
|
|
119
|
+
// logs: "count is 1"
|
|
120
|
+
|
|
121
|
+
dispose(); // stop listening
|
|
122
|
+
count.set(2); // nothing logged
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Cleanup Functions
|
|
126
|
+
|
|
127
|
+
An effect can return a cleanup function. It runs before each re-execution and on dispose.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
effect(() => {
|
|
131
|
+
const id = setInterval(() => console.log(count.get()), 1000);
|
|
132
|
+
return () => clearInterval(id); // cleanup before re-run or on dispose
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Automatic Dependency Tracking
|
|
137
|
+
|
|
138
|
+
Effects only track signals read during execution. If a branch is not taken, those signals are not tracked:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const showDetail = signal(true);
|
|
142
|
+
const summary = signal("short");
|
|
143
|
+
const detail = signal("long");
|
|
144
|
+
|
|
145
|
+
effect(() => {
|
|
146
|
+
if (showDetail.get()) {
|
|
147
|
+
console.log(detail.get()); // tracked
|
|
148
|
+
} else {
|
|
149
|
+
console.log(summary.get()); // tracked only when showDetail is false
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// When showDetail is true, changing summary does NOT re-run the effect.
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Infinite Loop Protection
|
|
157
|
+
|
|
158
|
+
Effects are guarded against infinite loops (max depth 100). If an effect sets a signal that triggers itself in a cycle, it throws:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
Error: Maximum effect depth exceeded — possible infinite loop
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Batch
|
|
165
|
+
|
|
166
|
+
Group multiple signal writes so effects run only once at the end.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import { batch } from "@blueshed/railroad";
|
|
170
|
+
|
|
171
|
+
const a = signal(1);
|
|
172
|
+
const b = signal(2);
|
|
173
|
+
|
|
174
|
+
effect(() => {
|
|
175
|
+
console.log(a.get() + b.get());
|
|
176
|
+
});
|
|
177
|
+
// logs: 3
|
|
178
|
+
|
|
179
|
+
batch(() => {
|
|
180
|
+
a.set(10);
|
|
181
|
+
b.set(20);
|
|
182
|
+
});
|
|
183
|
+
// logs: 30 (once, not twice)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Batches nest safely — effects flush only when the outermost batch completes.
|
|
187
|
+
|
|
188
|
+
## Dispose Pattern
|
|
189
|
+
|
|
190
|
+
`effect()` returns a dispose function. Call it to stop the effect and run its cleanup.
|
|
191
|
+
|
|
192
|
+
The JSX layer manages dispose scopes automatically — effects created during component rendering are collected and disposed when the component is removed (e.g., by the router or `when()`). You rarely need to manage dispose manually unless you create effects outside of JSX rendering.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// Manual dispose (outside JSX):
|
|
196
|
+
const dispose = effect(() => { ... });
|
|
197
|
+
// later:
|
|
198
|
+
dispose();
|
|
199
|
+
|
|
200
|
+
// Inside JSX components, effects are auto-collected.
|
|
201
|
+
// The router or when() calls dispose when swapping content.
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Where to Declare Signals
|
|
205
|
+
|
|
206
|
+
- **Module level** — shared state, lives for the app lifetime. Good for stores, global UI state.
|
|
207
|
+
- **Inside a component** — local state, created fresh each time the component mounts. Disposed when the component is removed.
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
// Module level — shared, persistent
|
|
211
|
+
const currentUser = signal<User | null>(null);
|
|
212
|
+
|
|
213
|
+
// Component level — local, ephemeral
|
|
214
|
+
function SearchBox() {
|
|
215
|
+
const query = signal("");
|
|
216
|
+
return <input value={query} oninput={(e) => query.set(e.target.value)} />;
|
|
217
|
+
}
|
|
218
|
+
```
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 blueshed
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Railroad
|
|
2
|
+
|
|
3
|
+
Signals, JSX, and routes — a micro UI framework for Bun.
|
|
4
|
+
|
|
5
|
+
~400 lines. Zero dependencies. Real DOM. No virtual DOM, no compiler, no build step.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bun add @blueshed/railroad
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### Automatic runtime (recommended)
|
|
16
|
+
|
|
17
|
+
No JSX imports needed — the compiler inserts them for you.
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
// tsconfig.json
|
|
21
|
+
{
|
|
22
|
+
"compilerOptions": {
|
|
23
|
+
"jsx": "react-jsx",
|
|
24
|
+
"jsxImportSource": "@blueshed/railroad"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// app.tsx
|
|
31
|
+
import { signal, routes } from "@blueshed/railroad";
|
|
32
|
+
|
|
33
|
+
const count = signal(0);
|
|
34
|
+
|
|
35
|
+
function Home() {
|
|
36
|
+
return (
|
|
37
|
+
<div>
|
|
38
|
+
<h1>Hello World</h1>
|
|
39
|
+
<button onclick={() => count.update(n => n + 1)}>
|
|
40
|
+
Count: {count}
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
routes(document.getElementById("app")!, {
|
|
47
|
+
"/": () => <Home />,
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Classic runtime
|
|
52
|
+
|
|
53
|
+
If you prefer explicit imports:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
// tsconfig.json
|
|
57
|
+
{
|
|
58
|
+
"compilerOptions": {
|
|
59
|
+
"jsx": "react",
|
|
60
|
+
"jsxFactory": "createElement",
|
|
61
|
+
"jsxFragmentFactory": "Fragment"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// app.tsx
|
|
68
|
+
import { createElement, signal, routes } from "@blueshed/railroad";
|
|
69
|
+
|
|
70
|
+
const count = signal(0);
|
|
71
|
+
|
|
72
|
+
function Home() {
|
|
73
|
+
return (
|
|
74
|
+
<div>
|
|
75
|
+
<h1>Hello World</h1>
|
|
76
|
+
<button onclick={() => count.update(n => n + 1)}>
|
|
77
|
+
Count: {count}
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
routes(document.getElementById("app")!, {
|
|
84
|
+
"/": () => <Home />,
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Server
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// server.ts
|
|
92
|
+
import home from "./index.html";
|
|
93
|
+
|
|
94
|
+
Bun.serve({
|
|
95
|
+
routes: { "/": home },
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Resources and routes. Server and client. Same pattern.
|
|
100
|
+
|
|
101
|
+
## API
|
|
102
|
+
|
|
103
|
+
### Signals
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { signal, computed, effect, batch } from "@blueshed/railroad";
|
|
107
|
+
|
|
108
|
+
const count = signal(0);
|
|
109
|
+
const doubled = computed(() => count.get() * 2);
|
|
110
|
+
|
|
111
|
+
const dispose = effect(() => {
|
|
112
|
+
console.log(`count is ${count.get()}`);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
count.set(1); // logs "count is 1"
|
|
116
|
+
count.update(n => n + 1); // logs "count is 2"
|
|
117
|
+
count.peek(); // read without tracking
|
|
118
|
+
|
|
119
|
+
// In-place mutation (auto-clones, always notifies):
|
|
120
|
+
const todos = signal([{ id: 1, text: "Buy milk" }]);
|
|
121
|
+
todos.mutate(arr => arr.push({ id: 2, text: "Walk dog" }));
|
|
122
|
+
|
|
123
|
+
// Shallow merge for object signals:
|
|
124
|
+
const filter = signal({ color: "red", size: 10 });
|
|
125
|
+
filter.patch({ color: "blue" }); // { color: "blue", size: 10 }
|
|
126
|
+
|
|
127
|
+
batch(() => {
|
|
128
|
+
count.set(10);
|
|
129
|
+
count.set(20); // effect runs once, not twice
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
dispose(); // stop listening
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### JSX
|
|
136
|
+
|
|
137
|
+
Components are functions that return DOM nodes. Signals in children and props auto-update.
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { createElement, text, when, list, signal } from "@blueshed/railroad";
|
|
141
|
+
|
|
142
|
+
const name = signal("World");
|
|
143
|
+
|
|
144
|
+
function Greeting() {
|
|
145
|
+
return <h1>Hello {name}</h1>; // updates when name changes
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### `text(fn)` — reactive computed text
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
<span>{text(() => count.get() > 5 ? "High" : "Low")}</span>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### `when(condition, truthy, falsy?)` — conditional rendering
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
{when(
|
|
159
|
+
() => loggedIn.get(),
|
|
160
|
+
() => <Dashboard />,
|
|
161
|
+
() => <Login />,
|
|
162
|
+
)}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### `list(items, keyFn?, render)` — keyed list rendering
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
// Keyed — render receives Signal<T> and Signal<number>:
|
|
169
|
+
{list(
|
|
170
|
+
todos,
|
|
171
|
+
(t) => t.id,
|
|
172
|
+
(todo, idx) => <li>{text(() => todo.get().name)}</li>,
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
// Non-keyed (index-based, raw values):
|
|
176
|
+
{list(items, (item, i) => <li>{item}</li>)}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Routes
|
|
180
|
+
|
|
181
|
+
Hash-based client router with automatic dispose scoping.
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
import { routes, navigate } from "@blueshed/railroad";
|
|
185
|
+
|
|
186
|
+
const dispose = routes(app, {
|
|
187
|
+
"/": () => <Home />,
|
|
188
|
+
"/users/:id": ({ id }) => <User id={id} />,
|
|
189
|
+
"*": () => <NotFound />,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
navigate("/users/42");
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Shared
|
|
196
|
+
|
|
197
|
+
Typed dependency injection without prop threading.
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { key, provide, inject } from "@blueshed/railroad";
|
|
201
|
+
|
|
202
|
+
const STORE = key<AppStore>("store");
|
|
203
|
+
provide(STORE, createStore());
|
|
204
|
+
|
|
205
|
+
// anywhere:
|
|
206
|
+
const store = inject(STORE);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Design
|
|
210
|
+
|
|
211
|
+
- **Signals hold state** — reactive primitives with automatic dependency tracking
|
|
212
|
+
- **Effects update the DOM** — run when dependencies change, return cleanup
|
|
213
|
+
- **JSX creates the DOM** — real elements, not virtual. Signal-aware props and children
|
|
214
|
+
- **Routes swap the DOM** — hash-based, dispose-scoped, Bun.serve-style tables
|
|
215
|
+
|
|
216
|
+
No lifecycle methods. No hooks rules. No context providers. No `useCallback`. Just signals and the DOM.
|
|
217
|
+
|
|
218
|
+
## Claude Code
|
|
219
|
+
|
|
220
|
+
This package ships with a [Claude Code](https://claude.com/claude-code) skill in `.claude/skills/railroad/`. Copy it into your project so Claude generates correct railroad code — including API usage, patterns, and anti-patterns:
|
|
221
|
+
|
|
222
|
+
```sh
|
|
223
|
+
cp -r node_modules/@blueshed/railroad/.claude/skills/railroad .claude/skills/
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Or install it user-wide (available in all projects):
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
cp -r node_modules/@blueshed/railroad/.claude/skills/railroad ~/.claude/skills/
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Railroad — Signals, JSX, and Routes
|
|
2
|
+
|
|
3
|
+
export { Signal, signal, computed, effect, batch } from "./signals";
|
|
4
|
+
export type { Dispose } from "./signals";
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
createElement, Fragment,
|
|
8
|
+
text, when, list,
|
|
9
|
+
pushDisposeScope, popDisposeScope,
|
|
10
|
+
} from "./jsx";
|
|
11
|
+
|
|
12
|
+
export { routes, route, navigate, matchRoute } from "./routes";
|
|
13
|
+
|
|
14
|
+
export { key, provide, inject, tryInject } from "./shared";
|
|
15
|
+
export type { Key } from "./shared";
|
|
16
|
+
|
|
17
|
+
export { createLogger, setLogLevel, getLogLevel, loggedRequest } from "./logger";
|
|
18
|
+
export type { LogLevel } from "./logger";
|
package/jsx-runtime.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic JSX Runtime for Railroad
|
|
3
|
+
*
|
|
4
|
+
* Enables "jsx": "react-jsx" / jsxImportSource so consumers
|
|
5
|
+
* can write JSX without importing createElement.
|
|
6
|
+
*
|
|
7
|
+
* tsconfig.json:
|
|
8
|
+
* { "jsx": "react-jsx", "jsxImportSource": "@blueshed/railroad" }
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createElement, Fragment } from "./jsx";
|
|
12
|
+
|
|
13
|
+
export { Fragment };
|
|
14
|
+
|
|
15
|
+
export function jsx(
|
|
16
|
+
type: string | Function,
|
|
17
|
+
props: Record<string, any> | null,
|
|
18
|
+
_key?: string,
|
|
19
|
+
): Node {
|
|
20
|
+
if (!props) return createElement(type, null);
|
|
21
|
+
const { children, ...rest } = props;
|
|
22
|
+
if (children === undefined) return createElement(type, rest);
|
|
23
|
+
if (Array.isArray(children)) return createElement(type, rest, ...children);
|
|
24
|
+
return createElement(type, rest, children);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { jsx as jsxs };
|
|
28
|
+
|
|
29
|
+
// Re-export JSX namespace so TypeScript react-jsx mode finds the types
|
|
30
|
+
export namespace JSX {
|
|
31
|
+
export type Element = globalThis.Node;
|
|
32
|
+
export interface IntrinsicElements {
|
|
33
|
+
[tag: string]: any;
|
|
34
|
+
}
|
|
35
|
+
}
|