round-core 0.0.5 → 0.0.6
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/README.md +2 -2
- package/dist/cli.js +49 -0
- package/dist/index.d.ts +326 -0
- package/dist/vite-plugin.js +0 -31
- package/package.json +2 -1
- package/src/cli.js +53 -0
- package/src/compiler/vite-plugin.js +0 -35
- package/src/index.d.ts +326 -0
- package/src/runtime/context.js +12 -0
- package/src/runtime/dom.js +10 -0
- package/src/runtime/router.js +28 -0
- package/src/runtime/signals.js +38 -0
- package/src/runtime/store.js +7 -0
- package/vite.config.build.js +12 -0
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
npm install round-core
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Instead of a Virtual DOM diff, Round updates the UI by subscribing DOM updates directly to reactive primitives (**signals**)
|
|
19
|
+
Instead of a Virtual DOM diff, Round updates the UI by subscribing DOM updates directly to reactive primitives (**signals**) and **bindables**. This keeps rendering predictable, small, and fast for interactive apps.
|
|
20
20
|
|
|
21
21
|
## What Round is focused on
|
|
22
22
|
|
|
@@ -93,7 +93,7 @@ This scaffolds a minimal Round app with `src/app.round` and an example `src/coun
|
|
|
93
93
|
|
|
94
94
|
## `.round` files
|
|
95
95
|
|
|
96
|
-
A `.round` file is a JSX-based component module (ESM) compiled by the Round toolchain.
|
|
96
|
+
A `.round` file is a JSX-based component module (ESM) compiled by the Round toolchain. you can also use .jsx files but you wont get the round JSX superset features like conditional rendering and other features.
|
|
97
97
|
|
|
98
98
|
Example `src/app.round`:
|
|
99
99
|
|
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,19 @@ function onSignal() {
|
|
|
10
10
|
}
|
|
11
11
|
process.on("SIGINT", onSignal);
|
|
12
12
|
process.on("SIGTERM", onSignal);
|
|
13
|
+
process.on("exit", () => {
|
|
14
|
+
cleanupTemporaryFiles();
|
|
15
|
+
});
|
|
16
|
+
const temporaryFiles = /* @__PURE__ */ new Set();
|
|
17
|
+
function cleanupTemporaryFiles() {
|
|
18
|
+
for (const f of temporaryFiles) {
|
|
19
|
+
try {
|
|
20
|
+
if (fs.existsSync(f)) fs.unlinkSync(f);
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
temporaryFiles.clear();
|
|
25
|
+
}
|
|
13
26
|
function normalizePath(p) {
|
|
14
27
|
return p.replaceAll("\\", "/");
|
|
15
28
|
}
|
|
@@ -323,6 +336,40 @@ function coerceNumber(v, fallback) {
|
|
|
323
336
|
const n = Number(v);
|
|
324
337
|
return Number.isFinite(n) ? n : fallback;
|
|
325
338
|
}
|
|
339
|
+
function generateIndexHtml(config) {
|
|
340
|
+
const name = config?.name ?? "Round";
|
|
341
|
+
const rawEntry = config?.entry ?? "./src/app.round";
|
|
342
|
+
let entry = rawEntry;
|
|
343
|
+
if (entry.startsWith("./")) entry = entry.slice(1);
|
|
344
|
+
if (!entry.startsWith("/")) entry = "/" + entry;
|
|
345
|
+
return [
|
|
346
|
+
"<!DOCTYPE html>",
|
|
347
|
+
'<html lang="en">',
|
|
348
|
+
"<head>",
|
|
349
|
+
' <meta charset="UTF-8" />',
|
|
350
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
|
|
351
|
+
` <title>${name}</title>`,
|
|
352
|
+
"</head>",
|
|
353
|
+
"<body>",
|
|
354
|
+
' <div id="app"></div>',
|
|
355
|
+
' <script type="module">',
|
|
356
|
+
" import { render } from 'round-core';",
|
|
357
|
+
` import App from '${entry}';`,
|
|
358
|
+
"",
|
|
359
|
+
" render(App, document.getElementById('app'));",
|
|
360
|
+
" <\/script>",
|
|
361
|
+
"</body>",
|
|
362
|
+
"</html>"
|
|
363
|
+
].join("\n");
|
|
364
|
+
}
|
|
365
|
+
function writeTemporaryIndex(rootDir, config) {
|
|
366
|
+
const indexPath = path.resolve(rootDir, "index.html");
|
|
367
|
+
if (fs.existsSync(indexPath)) return false;
|
|
368
|
+
const content = generateIndexHtml(config);
|
|
369
|
+
fs.writeFileSync(indexPath, content, "utf8");
|
|
370
|
+
temporaryFiles.add(indexPath);
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
326
373
|
async function runDev({ rootDir, configPathAbs, config }) {
|
|
327
374
|
const startedAt = Date.now();
|
|
328
375
|
const configDir = path.dirname(configPathAbs);
|
|
@@ -344,6 +391,7 @@ async function runDev({ rootDir, configPathAbs, config }) {
|
|
|
344
391
|
const serverPort2 = coerceNumber(nextConfig?.dev?.port, 5173);
|
|
345
392
|
const open2 = Boolean(nextConfig?.dev?.open);
|
|
346
393
|
const base2 = nextConfig?.routing?.base ?? "/";
|
|
394
|
+
writeTemporaryIndex(rootDir, nextConfig);
|
|
347
395
|
if (showBanner) {
|
|
348
396
|
banner();
|
|
349
397
|
process.stdout.write(`${c(" Config", "gray")} ${configPathAbs}
|
|
@@ -413,6 +461,7 @@ async function runBuild({ rootDir, configPathAbs, config }) {
|
|
|
413
461
|
normalizePath(path.relative(rootDir, entryAbs));
|
|
414
462
|
const outDir = config?.output ? resolveFrom(configDir, config.output) : resolveFrom(rootDir, "./dist");
|
|
415
463
|
const base = config?.routing?.base ?? "/";
|
|
464
|
+
writeTemporaryIndex(rootDir, config);
|
|
416
465
|
banner();
|
|
417
466
|
process.stdout.write(`${c(" Config", "gray")} ${configPathAbs}
|
|
418
467
|
`);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round Framework Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface RoundSignal<T> {
|
|
6
|
+
/**
|
|
7
|
+
* Get or set the current value.
|
|
8
|
+
*/
|
|
9
|
+
(newValue?: T): T;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get the current value (reactive).
|
|
13
|
+
*/
|
|
14
|
+
value: T;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the current value without tracking dependencies.
|
|
18
|
+
*/
|
|
19
|
+
peek(): T;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates a transformed view of this signal.
|
|
23
|
+
*/
|
|
24
|
+
transform<U>(fromInput: (v: U) => T, toOutput: (v: T) => U): RoundSignal<U>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Attaches validation logic to the signal.
|
|
28
|
+
*/
|
|
29
|
+
validate(validator: (next: T, prev: T) => string | boolean | undefined | null, options?: {
|
|
30
|
+
/** Timing of validation: 'input' (default) or 'blur'. */
|
|
31
|
+
validateOn?: 'input' | 'blur';
|
|
32
|
+
/** Whether to run validation immediately on startup. */
|
|
33
|
+
validateInitial?: boolean;
|
|
34
|
+
}): RoundSignal<T> & {
|
|
35
|
+
/** Signal containing the current validation error message. */
|
|
36
|
+
error: RoundSignal<string | null>;
|
|
37
|
+
/** Manually trigger validation check. Returns true if valid. */
|
|
38
|
+
check(): boolean
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates a read/write view of a specific property path.
|
|
43
|
+
*/
|
|
44
|
+
$pick<K extends keyof T>(path: K): RoundSignal<T[K]>;
|
|
45
|
+
$pick(path: string | string[]): RoundSignal<any>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Internal: marks the signal as bindable for two-way bindings.
|
|
49
|
+
*/
|
|
50
|
+
bind?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Creates a reactive signal.
|
|
55
|
+
*/
|
|
56
|
+
export function signal<T>(initialValue?: T): RoundSignal<T>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Creates a bindable signal intended for two-way DOM bindings.
|
|
60
|
+
*/
|
|
61
|
+
export function bindable<T>(initialValue?: T): RoundSignal<T>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Run a function without tracking any signals it reads.
|
|
65
|
+
* Any signals accessed inside the function will not become dependencies of the current effect.
|
|
66
|
+
*/
|
|
67
|
+
export function untrack<T>(fn: () => T): T;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a reactive side-effect that runs whenever its signal dependencies change.
|
|
71
|
+
*/
|
|
72
|
+
export function effect(fn: () => void | (() => void), options?: {
|
|
73
|
+
/** If false, the effect won't run immediately on creation. Defaults to true. */
|
|
74
|
+
onLoad?: boolean
|
|
75
|
+
}): () => void;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a reactive side-effect with explicit dependencies.
|
|
79
|
+
*/
|
|
80
|
+
export function effect(deps: any[], fn: () => void | (() => void), options?: {
|
|
81
|
+
/** If false, the effect won't run immediately on creation. Defaults to true. */
|
|
82
|
+
onLoad?: boolean
|
|
83
|
+
}): () => void;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a read-only computed signal derived from other signals.
|
|
87
|
+
*/
|
|
88
|
+
export function derive<T>(fn: () => T): () => T;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create a read/write view of a specific path within a signal object.
|
|
92
|
+
*/
|
|
93
|
+
export function pick<T = any>(root: RoundSignal<any>, path: string | string[]): RoundSignal<T>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Store API
|
|
97
|
+
*/
|
|
98
|
+
export interface RoundStore<T> {
|
|
99
|
+
/**
|
|
100
|
+
* Access a specific key from the store as a bindable signal.
|
|
101
|
+
*/
|
|
102
|
+
use<K extends keyof T>(key: K): RoundSignal<T[K]>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Update a specific key in the store.
|
|
106
|
+
*/
|
|
107
|
+
set<K extends keyof T>(key: K, value: T[K]): T[K];
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Batch update multiple keys in the store.
|
|
111
|
+
*/
|
|
112
|
+
patch(obj: Partial<T>): void;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get a snapshot of the current state.
|
|
116
|
+
*/
|
|
117
|
+
snapshot(options?: {
|
|
118
|
+
/** If true, the returned values will be reactive signals. */
|
|
119
|
+
reactive?: boolean
|
|
120
|
+
}): T;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Enable persistence for the store.
|
|
124
|
+
*/
|
|
125
|
+
persist(storageKey: string, options?: {
|
|
126
|
+
/** The storage implementation (defaults to localStorage). */
|
|
127
|
+
storage?: Storage;
|
|
128
|
+
/** Debounce time in milliseconds for writes. */
|
|
129
|
+
debounce?: number;
|
|
130
|
+
/** Array of keys to exclude from persistence. */
|
|
131
|
+
exclude?: string[];
|
|
132
|
+
}): RoundStore<T>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Action methods defined during store creation.
|
|
136
|
+
*/
|
|
137
|
+
actions: Record<string, Function>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create a shared global state store with actions and optional persistence.
|
|
142
|
+
*/
|
|
143
|
+
export function createStore<T, A extends Record<string, (state: T, ...args: any[]) => Partial<T> | void>>(
|
|
144
|
+
initialState: T,
|
|
145
|
+
actions?: A
|
|
146
|
+
): RoundStore<T> & { [K in keyof A]: (...args: Parameters<A[K]> extends [any, ...infer P] ? P : never) => any };
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Router API
|
|
150
|
+
*/
|
|
151
|
+
export interface RouteProps {
|
|
152
|
+
/** The path to match. Must start with a forward slash. */
|
|
153
|
+
route?: string;
|
|
154
|
+
/** If true, only matches if the path is exactly the same. */
|
|
155
|
+
exact?: boolean;
|
|
156
|
+
/** Page title to set in the document header when active. */
|
|
157
|
+
title?: string;
|
|
158
|
+
/** Meta description to set in the document header when active. */
|
|
159
|
+
description?: string;
|
|
160
|
+
/** Advanced head configuration including links and meta tags. */
|
|
161
|
+
head?: any;
|
|
162
|
+
/** Fragment or elements to render when matched. */
|
|
163
|
+
children?: any;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Define a route that renders its children when the path matches.
|
|
168
|
+
*/
|
|
169
|
+
export function Route(props: RouteProps): any;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* An alias for Route, typically used for top-level pages.
|
|
173
|
+
*/
|
|
174
|
+
export function Page(props: RouteProps): any;
|
|
175
|
+
|
|
176
|
+
export interface LinkProps {
|
|
177
|
+
/** The destination path. */
|
|
178
|
+
href: string;
|
|
179
|
+
/** Alias for href. */
|
|
180
|
+
to?: string;
|
|
181
|
+
/** Use SPA navigation (prevents full page reloads). Defaults to true. */
|
|
182
|
+
spa?: boolean;
|
|
183
|
+
/** Force a full page reload on navigation. */
|
|
184
|
+
reload?: boolean;
|
|
185
|
+
/** Custom click event handler. */
|
|
186
|
+
onClick?: (e: MouseEvent) => void;
|
|
187
|
+
/** Link content (text or elements). */
|
|
188
|
+
children?: any;
|
|
189
|
+
[key: string]: any;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A standard link component that performs SPA navigation.
|
|
194
|
+
*/
|
|
195
|
+
export function Link(props: LinkProps): any;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Define a fallback component or content for when no routes match.
|
|
199
|
+
*/
|
|
200
|
+
export function NotFound(props: {
|
|
201
|
+
/** Optional component to render for the 404 state. */
|
|
202
|
+
component?: any;
|
|
203
|
+
/** Fallback content. ignored if 'component' is provided. */
|
|
204
|
+
children?: any
|
|
205
|
+
}): any;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Navigate to a different path programmatically.
|
|
209
|
+
*/
|
|
210
|
+
export function navigate(to: string, options?: {
|
|
211
|
+
/** If true, replaces the current history entry instead of pushing. */
|
|
212
|
+
replace?: boolean
|
|
213
|
+
}): void;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Hook to get a reactive function returning the current normalized pathname.
|
|
217
|
+
*/
|
|
218
|
+
export function usePathname(): () => string;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get the current normalized pathname.
|
|
222
|
+
*/
|
|
223
|
+
export function getPathname(): string;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Hook to get a reactive function returning the current location object.
|
|
227
|
+
*/
|
|
228
|
+
export function useLocation(): () => { pathname: string; search: string; hash: string };
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get the current location object (pathname, search, hash).
|
|
232
|
+
*/
|
|
233
|
+
export function getLocation(): { pathname: string; search: string; hash: string };
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Hook to get a reactive function returning whether the current path has no matches.
|
|
237
|
+
*/
|
|
238
|
+
export function useIsNotFound(): () => boolean;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get whether the current path is NOT matched by any defined route.
|
|
242
|
+
*/
|
|
243
|
+
export function getIsNotFound(): boolean;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* DOM & Context API
|
|
247
|
+
*/
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Create a DOM element or instance a component.
|
|
251
|
+
*/
|
|
252
|
+
export function createElement(tag: any, props?: any, ...children: any[]): any;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* A grouping component that returns its children without a wrapper element.
|
|
256
|
+
*/
|
|
257
|
+
export function Fragment(props: { children?: any }): any;
|
|
258
|
+
|
|
259
|
+
export interface Context<T> {
|
|
260
|
+
/** Internal identifier for the context. */
|
|
261
|
+
id: number;
|
|
262
|
+
/** Default value used when no Provider is found in the tree. */
|
|
263
|
+
defaultValue: T;
|
|
264
|
+
/** Component that provides a value to all its descendants. */
|
|
265
|
+
Provider: (props: { value: T; children?: any }) => any;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Create a new Context object for sharing state between components.
|
|
270
|
+
*/
|
|
271
|
+
export function createContext<T>(defaultValue?: T): Context<T>;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Read the current value of a context from the component tree.
|
|
275
|
+
*/
|
|
276
|
+
export function readContext<T>(ctx: Context<T>): T;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Returns a reactive function that reads the current context value.
|
|
280
|
+
*/
|
|
281
|
+
export function bindContext<T>(ctx: Context<T>): () => T;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Async & Code Splitting
|
|
285
|
+
*/
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Mark a component for lazy loading (code-splitting).
|
|
289
|
+
* Expects a function returning a dynamic import promise.
|
|
290
|
+
*/
|
|
291
|
+
export function lazy<T>(fn: () => Promise<{ default: T }>): T;
|
|
292
|
+
|
|
293
|
+
export interface SuspenseProps {
|
|
294
|
+
/** Content to show while children (e.g. lazy components) are loading. */
|
|
295
|
+
fallback: any;
|
|
296
|
+
/** Content that might trigger a loading state. */
|
|
297
|
+
children?: any;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Component that boundaries async operations and renders a fallback while loading.
|
|
302
|
+
*/
|
|
303
|
+
export function Suspense(props: SuspenseProps): any;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Head Management
|
|
307
|
+
*/
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Define static head metadata (titles, meta tags, favicons, etc.).
|
|
311
|
+
*/
|
|
312
|
+
export function startHead(head: any): any;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Markdown
|
|
316
|
+
*/
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Component that renders Markdown content into HTML.
|
|
320
|
+
*/
|
|
321
|
+
export function Markdown(props: {
|
|
322
|
+
/** The markdown string or a function returning it. */
|
|
323
|
+
content: string | (() => string);
|
|
324
|
+
/** Remark/Rehype configuration options. */
|
|
325
|
+
options?: any
|
|
326
|
+
}): any;
|
package/dist/vite-plugin.js
CHANGED
|
@@ -614,40 +614,9 @@ ${head.raw}`;
|
|
|
614
614
|
};
|
|
615
615
|
},
|
|
616
616
|
resolveId(id) {
|
|
617
|
-
if (id === "/index.html" || id === "index.html") {
|
|
618
|
-
const fullPath = path.resolve(state.rootDir, "index.html");
|
|
619
|
-
if (!fs.existsSync(fullPath)) {
|
|
620
|
-
return "/index.html";
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
617
|
return null;
|
|
624
618
|
},
|
|
625
619
|
load(id) {
|
|
626
|
-
if (id === "/index.html" || id === "index.html") {
|
|
627
|
-
const fullPath = path.resolve(state.rootDir, "index.html");
|
|
628
|
-
if (fs.existsSync(fullPath)) return null;
|
|
629
|
-
const entry = state.entryRel ?? "./src/index.js";
|
|
630
|
-
const entryPath = entry.startsWith("/") ? entry : `/${entry}`;
|
|
631
|
-
return [
|
|
632
|
-
"<!DOCTYPE html>",
|
|
633
|
-
'<html lang="en">',
|
|
634
|
-
"<head>",
|
|
635
|
-
' <meta charset="UTF-8" />',
|
|
636
|
-
' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
|
|
637
|
-
` <title>${state.name}</title>`,
|
|
638
|
-
"</head>",
|
|
639
|
-
"<body>",
|
|
640
|
-
' <div id="app"></div>',
|
|
641
|
-
' <script type="module">',
|
|
642
|
-
" import { render } from 'round-core';",
|
|
643
|
-
` import App from '${entryPath}';`,
|
|
644
|
-
"",
|
|
645
|
-
" render(App, document.getElementById('app'));",
|
|
646
|
-
" <\/script>",
|
|
647
|
-
"</body>",
|
|
648
|
-
"</html>"
|
|
649
|
-
].join("\n");
|
|
650
|
-
}
|
|
651
620
|
if (!isMdRawRequest(id)) return;
|
|
652
621
|
const fileAbs = stripQuery(id);
|
|
653
622
|
try {
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "round-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "A lightweight frontend framework for SPA with signals and fine grained reactivity",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
6
7
|
"exports": {
|
|
7
8
|
".": "./dist/index.js",
|
|
8
9
|
"./vite-plugin": "./dist/vite-plugin.js"
|
package/src/cli.js
CHANGED
|
@@ -12,6 +12,17 @@ function onSignal() {
|
|
|
12
12
|
}
|
|
13
13
|
process.on('SIGINT', onSignal);
|
|
14
14
|
process.on('SIGTERM', onSignal);
|
|
15
|
+
process.on('exit', () => {
|
|
16
|
+
cleanupTemporaryFiles();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const temporaryFiles = new Set();
|
|
20
|
+
function cleanupTemporaryFiles() {
|
|
21
|
+
for (const f of temporaryFiles) {
|
|
22
|
+
try { if (fs.existsSync(f)) fs.unlinkSync(f); } catch { }
|
|
23
|
+
}
|
|
24
|
+
temporaryFiles.clear();
|
|
25
|
+
}
|
|
15
26
|
|
|
16
27
|
function normalizePath(p) {
|
|
17
28
|
return p.replaceAll('\\', '/');
|
|
@@ -330,6 +341,44 @@ function coerceNumber(v, fallback) {
|
|
|
330
341
|
return Number.isFinite(n) ? n : fallback;
|
|
331
342
|
}
|
|
332
343
|
|
|
344
|
+
function generateIndexHtml(config) {
|
|
345
|
+
const name = config?.name ?? 'Round';
|
|
346
|
+
const rawEntry = config?.entry ?? './src/app.round';
|
|
347
|
+
let entry = rawEntry;
|
|
348
|
+
if (entry.startsWith('./')) entry = entry.slice(1);
|
|
349
|
+
if (!entry.startsWith('/')) entry = '/' + entry;
|
|
350
|
+
|
|
351
|
+
return [
|
|
352
|
+
'<!DOCTYPE html>',
|
|
353
|
+
'<html lang="en">',
|
|
354
|
+
'<head>',
|
|
355
|
+
' <meta charset="UTF-8" />',
|
|
356
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
|
|
357
|
+
` <title>${name}</title>`,
|
|
358
|
+
'</head>',
|
|
359
|
+
'<body>',
|
|
360
|
+
' <div id="app"></div>',
|
|
361
|
+
' <script type="module">',
|
|
362
|
+
" import { render } from 'round-core';",
|
|
363
|
+
` import App from '${entry}';`,
|
|
364
|
+
'',
|
|
365
|
+
" render(App, document.getElementById('app'));",
|
|
366
|
+
' </script>',
|
|
367
|
+
'</body>',
|
|
368
|
+
'</html>'
|
|
369
|
+
].join('\n');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function writeTemporaryIndex(rootDir, config) {
|
|
373
|
+
const indexPath = path.resolve(rootDir, 'index.html');
|
|
374
|
+
if (fs.existsSync(indexPath)) return false;
|
|
375
|
+
|
|
376
|
+
const content = generateIndexHtml(config);
|
|
377
|
+
fs.writeFileSync(indexPath, content, 'utf8');
|
|
378
|
+
temporaryFiles.add(indexPath);
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
|
|
333
382
|
async function runDev({ rootDir, configPathAbs, config }) {
|
|
334
383
|
const startedAt = Date.now();
|
|
335
384
|
const configDir = path.dirname(configPathAbs);
|
|
@@ -357,6 +406,8 @@ async function runDev({ rootDir, configPathAbs, config }) {
|
|
|
357
406
|
const open2 = Boolean(nextConfig?.dev?.open);
|
|
358
407
|
const base2 = nextConfig?.routing?.base ?? '/';
|
|
359
408
|
|
|
409
|
+
const isTemp = writeTemporaryIndex(rootDir, nextConfig);
|
|
410
|
+
|
|
360
411
|
if (showBanner) {
|
|
361
412
|
banner('Dev Server');
|
|
362
413
|
process.stdout.write(`${c(' Config', 'gray')} ${configPathAbs}\n`);
|
|
@@ -430,6 +481,8 @@ async function runBuild({ rootDir, configPathAbs, config }) {
|
|
|
430
481
|
const outDir = config?.output ? resolveFrom(configDir, config.output) : resolveFrom(rootDir, './dist');
|
|
431
482
|
const base = config?.routing?.base ?? '/';
|
|
432
483
|
|
|
484
|
+
const isTemp = writeTemporaryIndex(rootDir, config);
|
|
485
|
+
|
|
433
486
|
banner('Build');
|
|
434
487
|
process.stdout.write(`${c(' Config', 'gray')} ${configPathAbs}\n`);
|
|
435
488
|
process.stdout.write(`${c(' OutDir', 'gray')} ${outDir}\n`);
|
|
@@ -329,44 +329,9 @@ export default function RoundPlugin(pluginOptions = {}) {
|
|
|
329
329
|
},
|
|
330
330
|
|
|
331
331
|
resolveId(id) {
|
|
332
|
-
if (id === '/index.html' || id === 'index.html') {
|
|
333
|
-
const fullPath = path.resolve(state.rootDir, 'index.html');
|
|
334
|
-
if (!fs.existsSync(fullPath)) {
|
|
335
|
-
return '/index.html'; // Virtual ID
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
332
|
return null;
|
|
339
333
|
},
|
|
340
|
-
|
|
341
334
|
load(id) {
|
|
342
|
-
if (id === '/index.html' || id === 'index.html') {
|
|
343
|
-
const fullPath = path.resolve(state.rootDir, 'index.html');
|
|
344
|
-
if (fs.existsSync(fullPath)) return null; // Fallback to disk
|
|
345
|
-
|
|
346
|
-
const entry = state.entryRel ?? './src/index.js';
|
|
347
|
-
const entryPath = entry.startsWith('/') ? entry : `/${entry}`;
|
|
348
|
-
|
|
349
|
-
return [
|
|
350
|
-
'<!DOCTYPE html>',
|
|
351
|
-
'<html lang="en">',
|
|
352
|
-
'<head>',
|
|
353
|
-
' <meta charset="UTF-8" />',
|
|
354
|
-
' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
|
|
355
|
-
` <title>${state.name}</title>`,
|
|
356
|
-
'</head>',
|
|
357
|
-
'<body>',
|
|
358
|
-
' <div id="app"></div>',
|
|
359
|
-
' <script type="module">',
|
|
360
|
-
" import { render } from 'round-core';",
|
|
361
|
-
` import App from '${entryPath}';`,
|
|
362
|
-
'',
|
|
363
|
-
" render(App, document.getElementById('app'));",
|
|
364
|
-
' </script>',
|
|
365
|
-
'</body>',
|
|
366
|
-
'</html>'
|
|
367
|
-
].join('\n');
|
|
368
|
-
}
|
|
369
|
-
|
|
370
335
|
if (!isMdRawRequest(id)) return;
|
|
371
336
|
|
|
372
337
|
const fileAbs = stripQuery(id);
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round Framework Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface RoundSignal<T> {
|
|
6
|
+
/**
|
|
7
|
+
* Get or set the current value.
|
|
8
|
+
*/
|
|
9
|
+
(newValue?: T): T;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get the current value (reactive).
|
|
13
|
+
*/
|
|
14
|
+
value: T;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the current value without tracking dependencies.
|
|
18
|
+
*/
|
|
19
|
+
peek(): T;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates a transformed view of this signal.
|
|
23
|
+
*/
|
|
24
|
+
transform<U>(fromInput: (v: U) => T, toOutput: (v: T) => U): RoundSignal<U>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Attaches validation logic to the signal.
|
|
28
|
+
*/
|
|
29
|
+
validate(validator: (next: T, prev: T) => string | boolean | undefined | null, options?: {
|
|
30
|
+
/** Timing of validation: 'input' (default) or 'blur'. */
|
|
31
|
+
validateOn?: 'input' | 'blur';
|
|
32
|
+
/** Whether to run validation immediately on startup. */
|
|
33
|
+
validateInitial?: boolean;
|
|
34
|
+
}): RoundSignal<T> & {
|
|
35
|
+
/** Signal containing the current validation error message. */
|
|
36
|
+
error: RoundSignal<string | null>;
|
|
37
|
+
/** Manually trigger validation check. Returns true if valid. */
|
|
38
|
+
check(): boolean
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates a read/write view of a specific property path.
|
|
43
|
+
*/
|
|
44
|
+
$pick<K extends keyof T>(path: K): RoundSignal<T[K]>;
|
|
45
|
+
$pick(path: string | string[]): RoundSignal<any>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Internal: marks the signal as bindable for two-way bindings.
|
|
49
|
+
*/
|
|
50
|
+
bind?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Creates a reactive signal.
|
|
55
|
+
*/
|
|
56
|
+
export function signal<T>(initialValue?: T): RoundSignal<T>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Creates a bindable signal intended for two-way DOM bindings.
|
|
60
|
+
*/
|
|
61
|
+
export function bindable<T>(initialValue?: T): RoundSignal<T>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Run a function without tracking any signals it reads.
|
|
65
|
+
* Any signals accessed inside the function will not become dependencies of the current effect.
|
|
66
|
+
*/
|
|
67
|
+
export function untrack<T>(fn: () => T): T;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a reactive side-effect that runs whenever its signal dependencies change.
|
|
71
|
+
*/
|
|
72
|
+
export function effect(fn: () => void | (() => void), options?: {
|
|
73
|
+
/** If false, the effect won't run immediately on creation. Defaults to true. */
|
|
74
|
+
onLoad?: boolean
|
|
75
|
+
}): () => void;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a reactive side-effect with explicit dependencies.
|
|
79
|
+
*/
|
|
80
|
+
export function effect(deps: any[], fn: () => void | (() => void), options?: {
|
|
81
|
+
/** If false, the effect won't run immediately on creation. Defaults to true. */
|
|
82
|
+
onLoad?: boolean
|
|
83
|
+
}): () => void;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a read-only computed signal derived from other signals.
|
|
87
|
+
*/
|
|
88
|
+
export function derive<T>(fn: () => T): () => T;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create a read/write view of a specific path within a signal object.
|
|
92
|
+
*/
|
|
93
|
+
export function pick<T = any>(root: RoundSignal<any>, path: string | string[]): RoundSignal<T>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Store API
|
|
97
|
+
*/
|
|
98
|
+
export interface RoundStore<T> {
|
|
99
|
+
/**
|
|
100
|
+
* Access a specific key from the store as a bindable signal.
|
|
101
|
+
*/
|
|
102
|
+
use<K extends keyof T>(key: K): RoundSignal<T[K]>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Update a specific key in the store.
|
|
106
|
+
*/
|
|
107
|
+
set<K extends keyof T>(key: K, value: T[K]): T[K];
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Batch update multiple keys in the store.
|
|
111
|
+
*/
|
|
112
|
+
patch(obj: Partial<T>): void;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get a snapshot of the current state.
|
|
116
|
+
*/
|
|
117
|
+
snapshot(options?: {
|
|
118
|
+
/** If true, the returned values will be reactive signals. */
|
|
119
|
+
reactive?: boolean
|
|
120
|
+
}): T;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Enable persistence for the store.
|
|
124
|
+
*/
|
|
125
|
+
persist(storageKey: string, options?: {
|
|
126
|
+
/** The storage implementation (defaults to localStorage). */
|
|
127
|
+
storage?: Storage;
|
|
128
|
+
/** Debounce time in milliseconds for writes. */
|
|
129
|
+
debounce?: number;
|
|
130
|
+
/** Array of keys to exclude from persistence. */
|
|
131
|
+
exclude?: string[];
|
|
132
|
+
}): RoundStore<T>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Action methods defined during store creation.
|
|
136
|
+
*/
|
|
137
|
+
actions: Record<string, Function>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create a shared global state store with actions and optional persistence.
|
|
142
|
+
*/
|
|
143
|
+
export function createStore<T, A extends Record<string, (state: T, ...args: any[]) => Partial<T> | void>>(
|
|
144
|
+
initialState: T,
|
|
145
|
+
actions?: A
|
|
146
|
+
): RoundStore<T> & { [K in keyof A]: (...args: Parameters<A[K]> extends [any, ...infer P] ? P : never) => any };
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Router API
|
|
150
|
+
*/
|
|
151
|
+
export interface RouteProps {
|
|
152
|
+
/** The path to match. Must start with a forward slash. */
|
|
153
|
+
route?: string;
|
|
154
|
+
/** If true, only matches if the path is exactly the same. */
|
|
155
|
+
exact?: boolean;
|
|
156
|
+
/** Page title to set in the document header when active. */
|
|
157
|
+
title?: string;
|
|
158
|
+
/** Meta description to set in the document header when active. */
|
|
159
|
+
description?: string;
|
|
160
|
+
/** Advanced head configuration including links and meta tags. */
|
|
161
|
+
head?: any;
|
|
162
|
+
/** Fragment or elements to render when matched. */
|
|
163
|
+
children?: any;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Define a route that renders its children when the path matches.
|
|
168
|
+
*/
|
|
169
|
+
export function Route(props: RouteProps): any;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* An alias for Route, typically used for top-level pages.
|
|
173
|
+
*/
|
|
174
|
+
export function Page(props: RouteProps): any;
|
|
175
|
+
|
|
176
|
+
export interface LinkProps {
|
|
177
|
+
/** The destination path. */
|
|
178
|
+
href: string;
|
|
179
|
+
/** Alias for href. */
|
|
180
|
+
to?: string;
|
|
181
|
+
/** Use SPA navigation (prevents full page reloads). Defaults to true. */
|
|
182
|
+
spa?: boolean;
|
|
183
|
+
/** Force a full page reload on navigation. */
|
|
184
|
+
reload?: boolean;
|
|
185
|
+
/** Custom click event handler. */
|
|
186
|
+
onClick?: (e: MouseEvent) => void;
|
|
187
|
+
/** Link content (text or elements). */
|
|
188
|
+
children?: any;
|
|
189
|
+
[key: string]: any;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A standard link component that performs SPA navigation.
|
|
194
|
+
*/
|
|
195
|
+
export function Link(props: LinkProps): any;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Define a fallback component or content for when no routes match.
|
|
199
|
+
*/
|
|
200
|
+
export function NotFound(props: {
|
|
201
|
+
/** Optional component to render for the 404 state. */
|
|
202
|
+
component?: any;
|
|
203
|
+
/** Fallback content. ignored if 'component' is provided. */
|
|
204
|
+
children?: any
|
|
205
|
+
}): any;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Navigate to a different path programmatically.
|
|
209
|
+
*/
|
|
210
|
+
export function navigate(to: string, options?: {
|
|
211
|
+
/** If true, replaces the current history entry instead of pushing. */
|
|
212
|
+
replace?: boolean
|
|
213
|
+
}): void;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Hook to get a reactive function returning the current normalized pathname.
|
|
217
|
+
*/
|
|
218
|
+
export function usePathname(): () => string;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get the current normalized pathname.
|
|
222
|
+
*/
|
|
223
|
+
export function getPathname(): string;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Hook to get a reactive function returning the current location object.
|
|
227
|
+
*/
|
|
228
|
+
export function useLocation(): () => { pathname: string; search: string; hash: string };
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get the current location object (pathname, search, hash).
|
|
232
|
+
*/
|
|
233
|
+
export function getLocation(): { pathname: string; search: string; hash: string };
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Hook to get a reactive function returning whether the current path has no matches.
|
|
237
|
+
*/
|
|
238
|
+
export function useIsNotFound(): () => boolean;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get whether the current path is NOT matched by any defined route.
|
|
242
|
+
*/
|
|
243
|
+
export function getIsNotFound(): boolean;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* DOM & Context API
|
|
247
|
+
*/
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Create a DOM element or instance a component.
|
|
251
|
+
*/
|
|
252
|
+
export function createElement(tag: any, props?: any, ...children: any[]): any;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* A grouping component that returns its children without a wrapper element.
|
|
256
|
+
*/
|
|
257
|
+
export function Fragment(props: { children?: any }): any;
|
|
258
|
+
|
|
259
|
+
export interface Context<T> {
|
|
260
|
+
/** Internal identifier for the context. */
|
|
261
|
+
id: number;
|
|
262
|
+
/** Default value used when no Provider is found in the tree. */
|
|
263
|
+
defaultValue: T;
|
|
264
|
+
/** Component that provides a value to all its descendants. */
|
|
265
|
+
Provider: (props: { value: T; children?: any }) => any;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Create a new Context object for sharing state between components.
|
|
270
|
+
*/
|
|
271
|
+
export function createContext<T>(defaultValue?: T): Context<T>;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Read the current value of a context from the component tree.
|
|
275
|
+
*/
|
|
276
|
+
export function readContext<T>(ctx: Context<T>): T;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Returns a reactive function that reads the current context value.
|
|
280
|
+
*/
|
|
281
|
+
export function bindContext<T>(ctx: Context<T>): () => T;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Async & Code Splitting
|
|
285
|
+
*/
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Mark a component for lazy loading (code-splitting).
|
|
289
|
+
* Expects a function returning a dynamic import promise.
|
|
290
|
+
*/
|
|
291
|
+
export function lazy<T>(fn: () => Promise<{ default: T }>): T;
|
|
292
|
+
|
|
293
|
+
export interface SuspenseProps {
|
|
294
|
+
/** Content to show while children (e.g. lazy components) are loading. */
|
|
295
|
+
fallback: any;
|
|
296
|
+
/** Content that might trigger a loading state. */
|
|
297
|
+
children?: any;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Component that boundaries async operations and renders a fallback while loading.
|
|
302
|
+
*/
|
|
303
|
+
export function Suspense(props: SuspenseProps): any;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Head Management
|
|
307
|
+
*/
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Define static head metadata (titles, meta tags, favicons, etc.).
|
|
311
|
+
*/
|
|
312
|
+
export function startHead(head: any): any;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Markdown
|
|
316
|
+
*/
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Component that renders Markdown content into HTML.
|
|
320
|
+
*/
|
|
321
|
+
export function Markdown(props: {
|
|
322
|
+
/** The markdown string or a function returning it. */
|
|
323
|
+
content: string | (() => string);
|
|
324
|
+
/** Remark/Rehype configuration options. */
|
|
325
|
+
options?: any
|
|
326
|
+
}): any;
|
package/src/runtime/context.js
CHANGED
|
@@ -11,6 +11,12 @@ function popContext() {
|
|
|
11
11
|
contextStack.pop();
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Read the current value of a context from the tree.
|
|
16
|
+
* @template T
|
|
17
|
+
* @param {Context<T>} ctx The context object.
|
|
18
|
+
* @returns {T} The current context value.
|
|
19
|
+
*/
|
|
14
20
|
export function readContext(ctx) {
|
|
15
21
|
for (let i = contextStack.length - 1; i >= 0; i--) {
|
|
16
22
|
const layer = contextStack[i];
|
|
@@ -21,6 +27,12 @@ export function readContext(ctx) {
|
|
|
21
27
|
return ctx.defaultValue;
|
|
22
28
|
}
|
|
23
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Create a new Context object for sharing state between components.
|
|
32
|
+
* @template T
|
|
33
|
+
* @param {T} [defaultValue] The value used when no provider is found.
|
|
34
|
+
* @returns {Context<T>} The context object with a `Provider` component.
|
|
35
|
+
*/
|
|
24
36
|
export function createContext(defaultValue) {
|
|
25
37
|
const ctx = {
|
|
26
38
|
id: nextContextId++,
|
package/src/runtime/dom.js
CHANGED
|
@@ -29,6 +29,13 @@ function warnSignalDirectUsage(fn, kind) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Create a DOM element or instance a component.
|
|
34
|
+
* @param {string | Function} tag HTML tag name or Component function.
|
|
35
|
+
* @param {object} [props] Element attributes or component props.
|
|
36
|
+
* @param {...any} children Child nodes.
|
|
37
|
+
* @returns {Node} The resulting DOM node.
|
|
38
|
+
*/
|
|
32
39
|
export function createElement(tag, props = {}, ...children) {
|
|
33
40
|
if (typeof tag === 'function') {
|
|
34
41
|
const componentInstance = createComponentInstance();
|
|
@@ -388,6 +395,9 @@ function appendChild(parent, child) {
|
|
|
388
395
|
}
|
|
389
396
|
}
|
|
390
397
|
|
|
398
|
+
/**
|
|
399
|
+
* A grouping component that returns its children without a wrapper element.
|
|
400
|
+
*/
|
|
391
401
|
export function Fragment(props) {
|
|
392
402
|
return props.children;
|
|
393
403
|
}
|
package/src/runtime/router.js
CHANGED
|
@@ -127,6 +127,11 @@ function mountAutoNotFound() {
|
|
|
127
127
|
root.appendChild(view);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Navigate to a different path programmatically.
|
|
132
|
+
* @param {string} to The destination URL or path.
|
|
133
|
+
* @param {object} [options] Navigation options (e.g., { replace: true }).
|
|
134
|
+
*/
|
|
130
135
|
export function navigate(to, options = {}) {
|
|
131
136
|
if (!hasWindow) return;
|
|
132
137
|
ensureListener();
|
|
@@ -264,6 +269,15 @@ export function setNotFound(Component) {
|
|
|
264
269
|
defaultNotFoundComponent = Component;
|
|
265
270
|
}
|
|
266
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Define a route that renders its children when the path matches.
|
|
274
|
+
* @param {object} props Route properties.
|
|
275
|
+
* @param {string} [props.route='/'] The path to match.
|
|
276
|
+
* @param {boolean} [props.exact] Whether to use exact matching.
|
|
277
|
+
* @param {string} [props.title] Page title to set when active.
|
|
278
|
+
* @param {string} [props.description] Meta description to set when active.
|
|
279
|
+
* @param {any} [props.children] Content to render.
|
|
280
|
+
*/
|
|
267
281
|
export function Route(props = {}) {
|
|
268
282
|
ensureListener();
|
|
269
283
|
|
|
@@ -319,6 +333,10 @@ export function Route(props = {}) {
|
|
|
319
333
|
});
|
|
320
334
|
}
|
|
321
335
|
|
|
336
|
+
/**
|
|
337
|
+
* An alias for Route, typically used for top-level pages.
|
|
338
|
+
* @param {object} props Page properties (same as Route).
|
|
339
|
+
*/
|
|
322
340
|
export function Page(props = {}) {
|
|
323
341
|
ensureListener();
|
|
324
342
|
|
|
@@ -371,6 +389,9 @@ export function Page(props = {}) {
|
|
|
371
389
|
});
|
|
372
390
|
}
|
|
373
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Define a fallback component or content for when no routes match.
|
|
394
|
+
*/
|
|
374
395
|
export function NotFound(props = {}) {
|
|
375
396
|
ensureListener();
|
|
376
397
|
|
|
@@ -402,6 +423,13 @@ export function NotFound(props = {}) {
|
|
|
402
423
|
});
|
|
403
424
|
}
|
|
404
425
|
|
|
426
|
+
/**
|
|
427
|
+
* A standard link component that performs SPA navigation.
|
|
428
|
+
* @param {object} props Link properties.
|
|
429
|
+
* @param {string} [props.href] The destination path.
|
|
430
|
+
* @param {boolean} [props.spa=true] Use SPA navigation (prevents reload).
|
|
431
|
+
* @param {any} [props.children] Link content.
|
|
432
|
+
*/
|
|
405
433
|
export function Link(props = {}) {
|
|
406
434
|
ensureListener();
|
|
407
435
|
|
package/src/runtime/signals.js
CHANGED
|
@@ -12,6 +12,13 @@ function subscribe(running, subscriptions) {
|
|
|
12
12
|
running.dependencies.add(subscriptions);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Run a function without tracking any signals it reads.
|
|
17
|
+
* Any signals accessed inside `fn` will not become dependencies of the current effect.
|
|
18
|
+
* @template T
|
|
19
|
+
* @param {() => T} fn The function to execute.
|
|
20
|
+
* @returns {T} The return value of `fn`.
|
|
21
|
+
*/
|
|
15
22
|
export function untrack(fn) {
|
|
16
23
|
context.push(null);
|
|
17
24
|
try {
|
|
@@ -21,6 +28,13 @@ export function untrack(fn) {
|
|
|
21
28
|
}
|
|
22
29
|
}
|
|
23
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Create a reactive side-effect that runs whenever its signal dependencies change.
|
|
33
|
+
* @param {(() => any) | any[]} arg1 Either the callback function or an array of explicit dependencies.
|
|
34
|
+
* @param {(() => any)} [arg2] The callback function if the first argument was explicit dependencies.
|
|
35
|
+
* @param {object} [arg3] Optional configuration (e.g., { onLoad: false }).
|
|
36
|
+
* @returns {() => void} A function to stop and cleanup the effect.
|
|
37
|
+
*/
|
|
24
38
|
export function effect(arg1, arg2, arg3) {
|
|
25
39
|
let callback;
|
|
26
40
|
let explicitDeps = null;
|
|
@@ -241,6 +255,12 @@ function attachHelpers(s) {
|
|
|
241
255
|
return s;
|
|
242
256
|
}
|
|
243
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Create a reactive signal.
|
|
260
|
+
* @template T
|
|
261
|
+
* @param {T} [initialValue] The starting value.
|
|
262
|
+
* @returns {RoundSignal<T>} A signal function that reads/writes the value.
|
|
263
|
+
*/
|
|
244
264
|
export function signal(initialValue) {
|
|
245
265
|
let value = initialValue;
|
|
246
266
|
const subscriptions = new Set();
|
|
@@ -285,6 +305,12 @@ export function signal(initialValue) {
|
|
|
285
305
|
return attachHelpers(signal);
|
|
286
306
|
}
|
|
287
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Create a bindable signal intended for two-way DOM bindings.
|
|
310
|
+
* @template T
|
|
311
|
+
* @param {T} [initialValue] The starting value.
|
|
312
|
+
* @returns {RoundSignal<T>} A signal function marked as bindable.
|
|
313
|
+
*/
|
|
288
314
|
export function bindable(initialValue) {
|
|
289
315
|
const s = signal(initialValue);
|
|
290
316
|
try {
|
|
@@ -345,6 +371,12 @@ function parsePath(path) {
|
|
|
345
371
|
return [String(path)];
|
|
346
372
|
}
|
|
347
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Create a read/write view of a specific path within a signal object.
|
|
376
|
+
* @param {RoundSignal<any>} root The source signal.
|
|
377
|
+
* @param {string | string[]} path The property path (e.g., 'user.profile.name' or ['user', 'profile', 'name']).
|
|
378
|
+
* @returns {RoundSignal<any>} A signal-like view of the path.
|
|
379
|
+
*/
|
|
348
380
|
export function pick(root, path) {
|
|
349
381
|
if (!isSignalLike(root)) {
|
|
350
382
|
throw new Error('[round] pick(root, path) expects root to be a signal (use bindable.object(...) or signal({...})).');
|
|
@@ -499,6 +531,12 @@ bindable.object = function (initialObject = {}) {
|
|
|
499
531
|
return createBindableObjectProxy(root, []);
|
|
500
532
|
};
|
|
501
533
|
|
|
534
|
+
/**
|
|
535
|
+
* Create a read-only computed signal derived from other signals.
|
|
536
|
+
* @template T
|
|
537
|
+
* @param {() => T} fn A function that computes the value.
|
|
538
|
+
* @returns {(() => T)} A function that returns the derived value.
|
|
539
|
+
*/
|
|
502
540
|
export function derive(fn) {
|
|
503
541
|
const derived = signal();
|
|
504
542
|
|
package/src/runtime/store.js
CHANGED
|
@@ -5,6 +5,13 @@ function hasWindow() {
|
|
|
5
5
|
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Create a shared global state store with actions and optional persistence.
|
|
10
|
+
* @template T
|
|
11
|
+
* @param {T} [initialState={}] Initial state object.
|
|
12
|
+
* @param {Record<string, (state: T, ...args: any[]) => any>} [actions] Action reducers.
|
|
13
|
+
* @returns {RoundStore<T>} The store object.
|
|
14
|
+
*/
|
|
8
15
|
export function createStore(initialState = {}, actions = null) {
|
|
9
16
|
const state = (initialState && typeof initialState === 'object') ? initialState : {};
|
|
10
17
|
const signals = Object.create(null);
|
package/vite.config.build.js
CHANGED
|
@@ -33,4 +33,16 @@ export default defineConfig({
|
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
},
|
|
36
|
+
plugins: [
|
|
37
|
+
{
|
|
38
|
+
name: 'copy-dts',
|
|
39
|
+
closeBundle() {
|
|
40
|
+
const src = path.resolve(__dirname, 'src/index.d.ts');
|
|
41
|
+
const dest = path.resolve(__dirname, 'dist/index.d.ts');
|
|
42
|
+
if (fs.existsSync(src)) {
|
|
43
|
+
fs.copyFileSync(src, dest);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
]
|
|
36
48
|
});
|