round-core 0.0.4 → 0.0.5
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/.github/workflows/benchmarks.yml +2 -2
- package/README.md +34 -6
- package/bun.lock +11 -0
- package/dist/cli.js +4 -56
- package/dist/index.js +71 -21
- package/dist/vite-plugin.js +38 -0
- package/package.json +46 -47
- package/src/cli.js +1 -52
- package/src/compiler/vite-plugin.js +43 -0
- package/src/runtime/context.js +22 -11
- package/src/runtime/router.js +83 -14
- package/vite.config.build.js +2 -13
- package/index.html +0 -19
|
@@ -12,7 +12,7 @@ jobs:
|
|
|
12
12
|
runs-on: ubuntu-latest
|
|
13
13
|
|
|
14
14
|
steps:
|
|
15
|
-
- uses: actions/checkout@
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
16
|
|
|
17
17
|
- name: Setup Bun
|
|
18
18
|
uses: oven-sh/setup-bun@v1
|
|
@@ -38,7 +38,7 @@ jobs:
|
|
|
38
38
|
run: bun run bench:runtime
|
|
39
39
|
|
|
40
40
|
- name: Upload Report
|
|
41
|
-
uses: actions/upload-artifact@
|
|
41
|
+
uses: actions/upload-artifact@v4
|
|
42
42
|
with:
|
|
43
43
|
name: benchmark-report
|
|
44
44
|
path: benchmarks/reports/build-bench.json
|
package/README.md
CHANGED
|
@@ -322,27 +322,55 @@ This compiles roughly to a `.map(...)` under the hood.
|
|
|
322
322
|
|
|
323
323
|
## Routing
|
|
324
324
|
|
|
325
|
-
Round includes router primitives intended for SPA navigation.
|
|
325
|
+
Round includes router primitives intended for SPA navigation. All route paths must start with a forward slash `/`.
|
|
326
326
|
|
|
327
|
-
|
|
327
|
+
### Basic Usage
|
|
328
328
|
|
|
329
329
|
```jsx
|
|
330
|
-
import { Route } from 'round-core';
|
|
330
|
+
import { Route, Link } from 'round-core';
|
|
331
331
|
|
|
332
332
|
export default function App() {
|
|
333
333
|
return (
|
|
334
334
|
<div>
|
|
335
|
-
<
|
|
336
|
-
<
|
|
335
|
+
<nav>
|
|
336
|
+
<Link href="/">Home</Link>
|
|
337
|
+
<Link href="/about">About</Link>
|
|
338
|
+
</nav>
|
|
339
|
+
|
|
340
|
+
<Route route="/" title="Home" exact>
|
|
341
|
+
<div>Welcome Home</div>
|
|
337
342
|
</Route>
|
|
338
343
|
<Route route="/about" title="About">
|
|
339
|
-
<div>About</div>
|
|
344
|
+
<div>About Us Content</div>
|
|
340
345
|
</Route>
|
|
341
346
|
</div>
|
|
342
347
|
);
|
|
343
348
|
}
|
|
344
349
|
```
|
|
345
350
|
|
|
351
|
+
### Nested Routing and Layouts
|
|
352
|
+
|
|
353
|
+
Routes can be nested to create hierarchical layouts. Child routes automatically inherit and combine paths with their parents.
|
|
354
|
+
|
|
355
|
+
- **Prefix Matching**: By default, routes use prefix matching (except for the root `/`). This allows a parent route to stay rendered as a "shell" or layout while its children are visited.
|
|
356
|
+
- **Exact Matching**: Use the `exact` prop to ensure a route only renders when the path matches precisely (default for root `/`).
|
|
357
|
+
|
|
358
|
+
```jsx
|
|
359
|
+
<Route route="/dashboard" title="Dashboard">
|
|
360
|
+
<h1>Dashboard Shell</h1>
|
|
361
|
+
|
|
362
|
+
{/* This route matches /dashboard/profile */}
|
|
363
|
+
<Route route="/dashboard/profile">
|
|
364
|
+
<h2>User Profile</h2>
|
|
365
|
+
</Route>
|
|
366
|
+
|
|
367
|
+
{/* This route matches /dashboard/settings */}
|
|
368
|
+
<Route route="/dashboard/settings">
|
|
369
|
+
<h2>Settings</h2>
|
|
370
|
+
</Route>
|
|
371
|
+
</Route>
|
|
372
|
+
```
|
|
373
|
+
|
|
346
374
|
## Suspense and lazy loading
|
|
347
375
|
|
|
348
376
|
Round supports `Suspense` for promise-based rendering and `lazy()` for code splitting.
|
package/bun.lock
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"": {
|
|
5
5
|
"name": "round-core",
|
|
6
6
|
"dependencies": {
|
|
7
|
+
"happy-dom": "^20.0.11",
|
|
7
8
|
"marked": "^12.0.2",
|
|
8
9
|
"vite": "^5.0.0",
|
|
9
10
|
"vitest": "^1.6.0",
|
|
@@ -153,6 +154,8 @@
|
|
|
153
154
|
|
|
154
155
|
"@types/node": ["@types/node@25.0.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA=="],
|
|
155
156
|
|
|
157
|
+
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
|
|
158
|
+
|
|
156
159
|
"@vitest/expect": ["@vitest/expect@1.6.1", "", { "dependencies": { "@vitest/spy": "1.6.1", "@vitest/utils": "1.6.1", "chai": "^4.3.10" } }, "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog=="],
|
|
157
160
|
|
|
158
161
|
"@vitest/runner": ["@vitest/runner@1.6.1", "", { "dependencies": { "@vitest/utils": "1.6.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" } }, "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA=="],
|
|
@@ -241,6 +244,8 @@
|
|
|
241
244
|
|
|
242
245
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
|
243
246
|
|
|
247
|
+
"happy-dom": ["happy-dom@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "@types/whatwg-mimetype": "^3.0.2", "whatwg-mimetype": "^3.0.0" } }, "sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g=="],
|
|
248
|
+
|
|
244
249
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
|
245
250
|
|
|
246
251
|
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
|
@@ -405,10 +410,16 @@
|
|
|
405
410
|
|
|
406
411
|
"cssstyle/rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="],
|
|
407
412
|
|
|
413
|
+
"happy-dom/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="],
|
|
414
|
+
|
|
415
|
+
"happy-dom/whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
|
416
|
+
|
|
408
417
|
"mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
|
409
418
|
|
|
410
419
|
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
|
411
420
|
|
|
412
421
|
"pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
|
422
|
+
|
|
423
|
+
"happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
|
413
424
|
}
|
|
414
425
|
}
|
package/dist/cli.js
CHANGED
|
@@ -122,31 +122,6 @@ function resolveFrom(baseDir, p) {
|
|
|
122
122
|
if (path.isAbsolute(p)) return p;
|
|
123
123
|
return path.resolve(baseDir, p);
|
|
124
124
|
}
|
|
125
|
-
function ensureIndexHtml(rootDir, entryRel, title = "Round") {
|
|
126
|
-
const indexPath = path.join(rootDir, "index.html");
|
|
127
|
-
if (fs.existsSync(indexPath)) return;
|
|
128
|
-
const entryPath = entryRel.startsWith("/") ? entryRel : `/${entryRel}`;
|
|
129
|
-
fs.writeFileSync(indexPath, [
|
|
130
|
-
"<!DOCTYPE html>",
|
|
131
|
-
'<html lang="en">',
|
|
132
|
-
"<head>",
|
|
133
|
-
' <meta charset="UTF-8" />',
|
|
134
|
-
' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
|
|
135
|
-
` <title>${title}</title>`,
|
|
136
|
-
"</head>",
|
|
137
|
-
"<body>",
|
|
138
|
-
' <div id="app"></div>',
|
|
139
|
-
' <script type="module">',
|
|
140
|
-
" import { render } from 'round-core';",
|
|
141
|
-
` import App from '${entryPath}';`,
|
|
142
|
-
"",
|
|
143
|
-
' render(App, document.getElementById("app"));',
|
|
144
|
-
" <\/script>",
|
|
145
|
-
"</body>",
|
|
146
|
-
"</html>",
|
|
147
|
-
""
|
|
148
|
-
].join("\n"), "utf8");
|
|
149
|
-
}
|
|
150
125
|
function parseArgs(argv) {
|
|
151
126
|
const args = { _: [] };
|
|
152
127
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -224,7 +199,6 @@ Usage:
|
|
|
224
199
|
const pkgPath = path.join(projectDir, "package.json");
|
|
225
200
|
const configPath = path.join(projectDir, "round.config.json");
|
|
226
201
|
const viteConfigPath = path.join(projectDir, "vite.config.js");
|
|
227
|
-
const indexHtmlPath = path.join(projectDir, "index.html");
|
|
228
202
|
const appRoundPath = path.join(srcDir, "app.round");
|
|
229
203
|
const counterRoundPath = path.join(srcDir, "counter.round");
|
|
230
204
|
writeFileIfMissing(pkgPath, JSON.stringify({
|
|
@@ -279,26 +253,6 @@ Usage:
|
|
|
279
253
|
"});",
|
|
280
254
|
""
|
|
281
255
|
].join("\n"));
|
|
282
|
-
writeFileIfMissing(indexHtmlPath, [
|
|
283
|
-
"<!DOCTYPE html>",
|
|
284
|
-
'<html lang="en">',
|
|
285
|
-
"<head>",
|
|
286
|
-
' <meta charset="UTF-8" />',
|
|
287
|
-
' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
|
|
288
|
-
` <title>${name}</title>`,
|
|
289
|
-
"</head>",
|
|
290
|
-
"<body>",
|
|
291
|
-
' <div id="app"></div>',
|
|
292
|
-
' <script type="module">',
|
|
293
|
-
" import { render } from 'round-core';",
|
|
294
|
-
" import App from '/src/app.round';",
|
|
295
|
-
"",
|
|
296
|
-
" render(App, document.getElementById('app'));",
|
|
297
|
-
" <\/script>",
|
|
298
|
-
"</body>",
|
|
299
|
-
"</html>",
|
|
300
|
-
""
|
|
301
|
-
].join("\n"));
|
|
302
256
|
writeFileIfMissing(appRoundPath, [
|
|
303
257
|
"import { Route } from 'round-core';",
|
|
304
258
|
'import { Counter } from "./counter"',
|
|
@@ -376,8 +330,7 @@ async function runDev({ rootDir, configPathAbs, config }) {
|
|
|
376
330
|
if (!entryAbs || !fs.existsSync(entryAbs)) {
|
|
377
331
|
throw new Error(`Entry not found: ${entryAbs ?? "(missing entry)"} (config: ${configPathAbs})`);
|
|
378
332
|
}
|
|
379
|
-
|
|
380
|
-
ensureIndexHtml(rootDir, entryRel, config?.name ?? "Round");
|
|
333
|
+
normalizePath(path.relative(rootDir, entryAbs));
|
|
381
334
|
let viteServer = null;
|
|
382
335
|
let restarting = false;
|
|
383
336
|
let restartTimer = null;
|
|
@@ -387,8 +340,7 @@ async function runDev({ rootDir, configPathAbs, config }) {
|
|
|
387
340
|
if (!entryAbs2 || !fs.existsSync(entryAbs2)) {
|
|
388
341
|
throw new Error(`Entry not found: ${entryAbs2 ?? "(missing entry)"} (config: ${configPathAbs})`);
|
|
389
342
|
}
|
|
390
|
-
|
|
391
|
-
ensureIndexHtml(rootDir, entryRel2, nextConfig?.name ?? "Round");
|
|
343
|
+
normalizePath(path.relative(rootDir, entryAbs2));
|
|
392
344
|
const serverPort2 = coerceNumber(nextConfig?.dev?.port, 5173);
|
|
393
345
|
const open2 = Boolean(nextConfig?.dev?.open);
|
|
394
346
|
const base2 = nextConfig?.routing?.base ?? "/";
|
|
@@ -458,8 +410,7 @@ async function runBuild({ rootDir, configPathAbs, config }) {
|
|
|
458
410
|
if (!entryAbs || !fs.existsSync(entryAbs)) {
|
|
459
411
|
throw new Error(`Entry not found: ${entryAbs ?? "(missing entry)"} (config: ${configPathAbs})`);
|
|
460
412
|
}
|
|
461
|
-
|
|
462
|
-
ensureIndexHtml(rootDir, entryRel, config?.name ?? "Round");
|
|
413
|
+
normalizePath(path.relative(rootDir, entryAbs));
|
|
463
414
|
const outDir = config?.output ? resolveFrom(configDir, config.output) : resolveFrom(rootDir, "./dist");
|
|
464
415
|
const base = config?.routing?.base ?? "/";
|
|
465
416
|
banner();
|
|
@@ -497,10 +448,7 @@ async function runPreview({ rootDir, configPathAbs, config }) {
|
|
|
497
448
|
const base = config?.routing?.base ?? "/";
|
|
498
449
|
const previewPort = coerceNumber(config?.dev?.port, 5173);
|
|
499
450
|
const entryAbs = config?.entry ? resolveFrom(configDir, config.entry) : null;
|
|
500
|
-
if (entryAbs && fs.existsSync(entryAbs))
|
|
501
|
-
const entryRel = normalizePath(path.relative(rootDir, entryAbs));
|
|
502
|
-
ensureIndexHtml(rootDir, entryRel, config?.name ?? "Round");
|
|
503
|
-
}
|
|
451
|
+
if (entryAbs && fs.existsSync(entryAbs)) ;
|
|
504
452
|
banner();
|
|
505
453
|
process.stdout.write(`${c("Config:", "cyan")} ${configPathAbs}
|
|
506
454
|
`);
|
package/dist/index.js
CHANGED
|
@@ -620,17 +620,21 @@ function createContext(defaultValue) {
|
|
|
620
620
|
Provider: null
|
|
621
621
|
};
|
|
622
622
|
function Provider(props = {}) {
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
623
|
+
const children = props.children;
|
|
624
|
+
pushContext({ [ctx.id]: props.value });
|
|
625
|
+
try {
|
|
626
|
+
return createElement("span", { style: { display: "contents" } }, () => {
|
|
627
|
+
const val = typeof props.value === "function" && props.value.peek ? props.value() : props.value;
|
|
628
|
+
pushContext({ [ctx.id]: val });
|
|
629
|
+
try {
|
|
630
|
+
return children;
|
|
631
|
+
} finally {
|
|
632
|
+
popContext();
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
} finally {
|
|
636
|
+
popContext();
|
|
637
|
+
}
|
|
634
638
|
}
|
|
635
639
|
ctx.Provider = Provider;
|
|
636
640
|
return ctx;
|
|
@@ -1120,6 +1124,7 @@ const pathEvalReady = signal(true);
|
|
|
1120
1124
|
let defaultNotFoundComponent = null;
|
|
1121
1125
|
let autoNotFoundMounted = false;
|
|
1122
1126
|
let userProvidedNotFound = false;
|
|
1127
|
+
const RoutingContext = createContext("");
|
|
1123
1128
|
function ensureListener() {
|
|
1124
1129
|
if (!hasWindow$1 || listenerInitialized) return;
|
|
1125
1130
|
listenerInitialized = true;
|
|
@@ -1163,12 +1168,14 @@ function useRouteReady() {
|
|
|
1163
1168
|
}
|
|
1164
1169
|
function getIsNotFound() {
|
|
1165
1170
|
const pathname = normalizePathname(currentPath());
|
|
1171
|
+
if (pathname === "/") return false;
|
|
1166
1172
|
if (!(Boolean(pathEvalReady()) && lastPathEvaluated === pathname)) return false;
|
|
1167
1173
|
return !Boolean(pathHasMatch());
|
|
1168
1174
|
}
|
|
1169
1175
|
function useIsNotFound() {
|
|
1170
1176
|
return () => {
|
|
1171
1177
|
const pathname = normalizePathname(currentPath());
|
|
1178
|
+
if (pathname === "/") return false;
|
|
1172
1179
|
if (!(Boolean(pathEvalReady()) && lastPathEvaluated === pathname)) return false;
|
|
1173
1180
|
return !Boolean(pathHasMatch());
|
|
1174
1181
|
};
|
|
@@ -1188,6 +1195,7 @@ function mountAutoNotFound() {
|
|
|
1188
1195
|
if (!ready) return null;
|
|
1189
1196
|
if (lastPathEvaluated !== pathname) return null;
|
|
1190
1197
|
if (hasMatch) return null;
|
|
1198
|
+
if (pathname === "/") return null;
|
|
1191
1199
|
const Comp = defaultNotFoundComponent;
|
|
1192
1200
|
if (typeof Comp === "function") {
|
|
1193
1201
|
return createElement(Comp, { pathname });
|
|
@@ -1294,10 +1302,11 @@ function normalizeTo(to) {
|
|
|
1294
1302
|
if (!path.startsWith("/")) return String(to ?? "");
|
|
1295
1303
|
return normalizePathname(path) + suffix;
|
|
1296
1304
|
}
|
|
1297
|
-
function matchRoute(route, pathname) {
|
|
1305
|
+
function matchRoute(route, pathname, exact = true) {
|
|
1298
1306
|
const r = normalizePathname(route);
|
|
1299
1307
|
const p = normalizePathname(pathname);
|
|
1300
|
-
return r === p;
|
|
1308
|
+
if (exact) return r === p;
|
|
1309
|
+
return p === r || p.startsWith(r.endsWith("/") ? r : r + "/");
|
|
1301
1310
|
}
|
|
1302
1311
|
function beginPathEvaluation(pathname) {
|
|
1303
1312
|
if (pathname !== lastPathEvaluated) {
|
|
@@ -1316,11 +1325,31 @@ function setNotFound(Component) {
|
|
|
1316
1325
|
function Route(props = {}) {
|
|
1317
1326
|
ensureListener();
|
|
1318
1327
|
return createElement("span", { style: { display: "contents" } }, () => {
|
|
1328
|
+
const parentPath = readContext(RoutingContext) || "";
|
|
1319
1329
|
const pathname = normalizePathname(currentPath());
|
|
1320
1330
|
beginPathEvaluation(pathname);
|
|
1321
|
-
const
|
|
1322
|
-
if (!
|
|
1323
|
-
|
|
1331
|
+
const routeProp = props.route ?? "/";
|
|
1332
|
+
if (typeof routeProp === "string" && !routeProp.startsWith("/")) {
|
|
1333
|
+
throw new Error(`Invalid route: "${routeProp}". All routes must start with a forward slash "/". (Nested under: "${parentPath || "root"}")`);
|
|
1334
|
+
}
|
|
1335
|
+
let fullRoute = "";
|
|
1336
|
+
if (parentPath && parentPath !== "/") {
|
|
1337
|
+
const cleanParent = parentPath.endsWith("/") ? parentPath.slice(0, -1) : parentPath;
|
|
1338
|
+
const cleanChild = routeProp.startsWith("/") ? routeProp : "/" + routeProp;
|
|
1339
|
+
if (cleanChild.startsWith(cleanParent + "/") || cleanChild === cleanParent) {
|
|
1340
|
+
fullRoute = normalizePathname(cleanChild);
|
|
1341
|
+
} else {
|
|
1342
|
+
fullRoute = normalizePathname(cleanParent + cleanChild);
|
|
1343
|
+
}
|
|
1344
|
+
} else {
|
|
1345
|
+
fullRoute = normalizePathname(routeProp);
|
|
1346
|
+
}
|
|
1347
|
+
const isRoot = fullRoute === "/";
|
|
1348
|
+
const exact = props.exact !== void 0 ? Boolean(props.exact) : isRoot;
|
|
1349
|
+
if (!matchRoute(fullRoute, pathname, exact)) return null;
|
|
1350
|
+
if (matchRoute(fullRoute, pathname, true)) {
|
|
1351
|
+
pathHasMatch(true);
|
|
1352
|
+
}
|
|
1324
1353
|
const mergedHead = props.head && typeof props.head === "object" ? props.head : {};
|
|
1325
1354
|
const meta = props.description ? [{ name: "description", content: String(props.description) }].concat(mergedHead.meta ?? props.meta ?? []) : mergedHead.meta ?? props.meta;
|
|
1326
1355
|
const links = mergedHead.links ?? props.links;
|
|
@@ -1328,17 +1357,37 @@ function Route(props = {}) {
|
|
|
1328
1357
|
const icon = mergedHead.icon ?? props.icon;
|
|
1329
1358
|
const favicon = mergedHead.favicon ?? props.favicon;
|
|
1330
1359
|
applyHead({ title, meta, links, icon, favicon });
|
|
1331
|
-
return props.children;
|
|
1360
|
+
return createElement(RoutingContext.Provider, { value: fullRoute }, props.children);
|
|
1332
1361
|
});
|
|
1333
1362
|
}
|
|
1334
1363
|
function Page(props = {}) {
|
|
1335
1364
|
ensureListener();
|
|
1336
1365
|
return createElement("span", { style: { display: "contents" } }, () => {
|
|
1366
|
+
const parentPath = readContext(RoutingContext) || "";
|
|
1337
1367
|
const pathname = normalizePathname(currentPath());
|
|
1338
1368
|
beginPathEvaluation(pathname);
|
|
1339
|
-
const
|
|
1340
|
-
if (!
|
|
1341
|
-
|
|
1369
|
+
const routeProp = props.route ?? "/";
|
|
1370
|
+
if (typeof routeProp === "string" && !routeProp.startsWith("/")) {
|
|
1371
|
+
throw new Error(`Invalid route: "${routeProp}". All routes must start with a forward slash "/". (Nested under: "${parentPath || "root"}")`);
|
|
1372
|
+
}
|
|
1373
|
+
let fullRoute = "";
|
|
1374
|
+
if (parentPath && parentPath !== "/") {
|
|
1375
|
+
const cleanParent = parentPath.endsWith("/") ? parentPath.slice(0, -1) : parentPath;
|
|
1376
|
+
const cleanChild = routeProp.startsWith("/") ? routeProp : "/" + routeProp;
|
|
1377
|
+
if (cleanChild.startsWith(cleanParent + "/") || cleanChild === cleanParent) {
|
|
1378
|
+
fullRoute = normalizePathname(cleanChild);
|
|
1379
|
+
} else {
|
|
1380
|
+
fullRoute = normalizePathname(cleanParent + cleanChild);
|
|
1381
|
+
}
|
|
1382
|
+
} else {
|
|
1383
|
+
fullRoute = normalizePathname(routeProp);
|
|
1384
|
+
}
|
|
1385
|
+
const isRoot = fullRoute === "/";
|
|
1386
|
+
const exact = props.exact !== void 0 ? Boolean(props.exact) : isRoot;
|
|
1387
|
+
if (!matchRoute(fullRoute, pathname, exact)) return null;
|
|
1388
|
+
if (matchRoute(fullRoute, pathname, true)) {
|
|
1389
|
+
pathHasMatch(true);
|
|
1390
|
+
}
|
|
1342
1391
|
const mergedHead = props.head && typeof props.head === "object" ? props.head : {};
|
|
1343
1392
|
const meta = props.description ? [{ name: "description", content: String(props.description) }].concat(mergedHead.meta ?? props.meta ?? []) : mergedHead.meta ?? props.meta;
|
|
1344
1393
|
const links = mergedHead.links ?? props.links;
|
|
@@ -1346,7 +1395,7 @@ function Page(props = {}) {
|
|
|
1346
1395
|
const icon = mergedHead.icon ?? props.icon;
|
|
1347
1396
|
const favicon = mergedHead.favicon ?? props.favicon;
|
|
1348
1397
|
applyHead({ title, meta, links, icon, favicon });
|
|
1349
|
-
return props.children;
|
|
1398
|
+
return createElement(RoutingContext.Provider, { value: fullRoute }, props.children);
|
|
1350
1399
|
});
|
|
1351
1400
|
}
|
|
1352
1401
|
function NotFound(props = {}) {
|
|
@@ -1360,6 +1409,7 @@ function NotFound(props = {}) {
|
|
|
1360
1409
|
if (!ready) return null;
|
|
1361
1410
|
if (lastPathEvaluated !== pathname) return null;
|
|
1362
1411
|
if (hasMatch) return null;
|
|
1412
|
+
if (pathname === "/") return null;
|
|
1363
1413
|
const Comp = props.component ?? defaultNotFoundComponent;
|
|
1364
1414
|
if (typeof Comp === "function") {
|
|
1365
1415
|
return createElement(Comp, { pathname });
|
package/dist/vite-plugin.js
CHANGED
|
@@ -427,6 +427,8 @@ function RoundPlugin(pluginOptions = {}) {
|
|
|
427
427
|
configPathAbs: null,
|
|
428
428
|
configDir: null,
|
|
429
429
|
entryAbs: null,
|
|
430
|
+
entryRel: null,
|
|
431
|
+
name: "Round",
|
|
430
432
|
startHead: null,
|
|
431
433
|
startHeadHtml: null
|
|
432
434
|
};
|
|
@@ -454,7 +456,9 @@ function RoundPlugin(pluginOptions = {}) {
|
|
|
454
456
|
state.routingTrailingSlash = trailingSlash !== void 0 ? Boolean(trailingSlash) : true;
|
|
455
457
|
const customTags = config?.htmlTags;
|
|
456
458
|
state.customTags = Array.isArray(customTags) ? customTags : [];
|
|
459
|
+
state.name = config?.name ?? "Round";
|
|
457
460
|
const entryRel = config?.entry;
|
|
461
|
+
state.entryRel = entryRel;
|
|
458
462
|
state.entryAbs = entryRel ? resolveMaybeRelative(configDir, entryRel) : null;
|
|
459
463
|
const include = pluginOptions.include ?? config?.include ?? [];
|
|
460
464
|
const exclude = pluginOptions.exclude ?? config?.exclude ?? ["./node_modules", "./dist"];
|
|
@@ -609,7 +613,41 @@ ${head.raw}`;
|
|
|
609
613
|
}
|
|
610
614
|
};
|
|
611
615
|
},
|
|
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
|
+
return null;
|
|
624
|
+
},
|
|
612
625
|
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
|
+
}
|
|
613
651
|
if (!isMdRawRequest(id)) return;
|
|
614
652
|
const fileAbs = stripQuery(id);
|
|
615
653
|
try {
|
package/package.json
CHANGED
|
@@ -1,48 +1,47 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "round-core",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "A lightweight frontend framework for SPA with signals and fine grained reactivity",
|
|
5
|
-
"main": "./dist/index.js",
|
|
6
|
-
"exports": {
|
|
7
|
-
".": "./dist/index.js",
|
|
8
|
-
"./vite-plugin": "./dist/vite-plugin.js"
|
|
9
|
-
},
|
|
10
|
-
"type": "module",
|
|
11
|
-
"icon": "round.png",
|
|
12
|
-
"repository": {
|
|
13
|
-
"url": "https://github.com/ZtaMDev/RoundJS.git"
|
|
14
|
-
},
|
|
15
|
-
"bin": {
|
|
16
|
-
"round": "./dist/cli.js"
|
|
17
|
-
},
|
|
18
|
-
"scripts": {
|
|
19
|
-
"dev": "node ./src/cli.js dev --config ./test/main/round.config.json",
|
|
20
|
-
"build": "node ./src/cli.js build --config ./test/main/round.config.json",
|
|
21
|
-
"build:core": "vite build -c vite.config.build.js",
|
|
22
|
-
"test": "vitest run",
|
|
23
|
-
"bench": "bun run --cwd benchmarks bench",
|
|
24
|
-
"bench:build": "bun run --cwd benchmarks bench:build",
|
|
25
|
-
"bench:runtime": "bun run --cwd benchmarks bench:runtime"
|
|
26
|
-
},
|
|
27
|
-
"keywords": [
|
|
28
|
-
"framework",
|
|
29
|
-
"spa",
|
|
30
|
-
"signals",
|
|
31
|
-
"vite"
|
|
32
|
-
],
|
|
33
|
-
"author": "Round Framework Team",
|
|
34
|
-
"license": "MIT",
|
|
35
|
-
"dependencies": {
|
|
36
|
-
"marked": "^12.0.2",
|
|
37
|
-
"vite": "^5.0.0"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "round-core",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "A lightweight frontend framework for SPA with signals and fine grained reactivity",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./dist/index.js",
|
|
8
|
+
"./vite-plugin": "./dist/vite-plugin.js"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"icon": "round.png",
|
|
12
|
+
"repository": {
|
|
13
|
+
"url": "https://github.com/ZtaMDev/RoundJS.git"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"round": "./dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "node ./src/cli.js dev --config ./test/main/round.config.json",
|
|
20
|
+
"build": "node ./src/cli.js build --config ./test/main/round.config.json",
|
|
21
|
+
"build:core": "vite build -c vite.config.build.js",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"bench": "bun run --cwd benchmarks bench",
|
|
24
|
+
"bench:build": "bun run --cwd benchmarks bench:build",
|
|
25
|
+
"bench:runtime": "bun run --cwd benchmarks bench:runtime"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"framework",
|
|
29
|
+
"spa",
|
|
30
|
+
"signals",
|
|
31
|
+
"vite"
|
|
32
|
+
],
|
|
33
|
+
"author": "Round Framework Team",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"marked": "^12.0.2",
|
|
37
|
+
"vite": "^5.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"bun-types": "latest",
|
|
41
|
+
"@types/node": "latest",
|
|
42
|
+
"vitest": "^1.6.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"bun": ">=1.0.0"
|
|
46
|
+
}
|
|
48
47
|
}
|
package/src/cli.js
CHANGED
|
@@ -122,32 +122,6 @@ function resolveFrom(baseDir, p) {
|
|
|
122
122
|
return path.resolve(baseDir, p);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function ensureIndexHtml(rootDir, entryRel, title = 'Round') {
|
|
126
|
-
const indexPath = path.join(rootDir, 'index.html');
|
|
127
|
-
if (fs.existsSync(indexPath)) return;
|
|
128
|
-
|
|
129
|
-
const entryPath = entryRel.startsWith('/') ? entryRel : `/${entryRel}`;
|
|
130
|
-
fs.writeFileSync(indexPath, [
|
|
131
|
-
'<!DOCTYPE html>',
|
|
132
|
-
'<html lang="en">',
|
|
133
|
-
'<head>',
|
|
134
|
-
' <meta charset="UTF-8" />',
|
|
135
|
-
' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
|
|
136
|
-
` <title>${title}</title>`,
|
|
137
|
-
'</head>',
|
|
138
|
-
'<body>',
|
|
139
|
-
' <div id="app"></div>',
|
|
140
|
-
' <script type="module">',
|
|
141
|
-
" import { render } from 'round-core';",
|
|
142
|
-
` import App from '${entryPath}';`,
|
|
143
|
-
'',
|
|
144
|
-
' render(App, document.getElementById("app"));',
|
|
145
|
-
' </script>',
|
|
146
|
-
'</body>',
|
|
147
|
-
'</html>',
|
|
148
|
-
''
|
|
149
|
-
].join('\n'), 'utf8');
|
|
150
|
-
}
|
|
151
125
|
|
|
152
126
|
function parseArgs(argv) {
|
|
153
127
|
const args = { _: [] };
|
|
@@ -231,7 +205,6 @@ async function runInit({ name }) {
|
|
|
231
205
|
const pkgPath = path.join(projectDir, 'package.json');
|
|
232
206
|
const configPath = path.join(projectDir, 'round.config.json');
|
|
233
207
|
const viteConfigPath = path.join(projectDir, 'vite.config.js');
|
|
234
|
-
const indexHtmlPath = path.join(projectDir, 'index.html');
|
|
235
208
|
const appRoundPath = path.join(srcDir, 'app.round');
|
|
236
209
|
const counterRoundPath = path.join(srcDir, 'counter.round');
|
|
237
210
|
|
|
@@ -290,26 +263,6 @@ async function runInit({ name }) {
|
|
|
290
263
|
''
|
|
291
264
|
].join('\n'));
|
|
292
265
|
|
|
293
|
-
writeFileIfMissing(indexHtmlPath, [
|
|
294
|
-
'<!DOCTYPE html>',
|
|
295
|
-
'<html lang="en">',
|
|
296
|
-
'<head>',
|
|
297
|
-
' <meta charset="UTF-8" />',
|
|
298
|
-
' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
|
|
299
|
-
` <title>${name}</title>`,
|
|
300
|
-
'</head>',
|
|
301
|
-
'<body>',
|
|
302
|
-
' <div id="app"></div>',
|
|
303
|
-
' <script type="module">',
|
|
304
|
-
" import { render } from 'round-core';",
|
|
305
|
-
" import App from '/src/app.round';",
|
|
306
|
-
'',
|
|
307
|
-
" render(App, document.getElementById('app'));",
|
|
308
|
-
' </script>',
|
|
309
|
-
'</body>',
|
|
310
|
-
'</html>',
|
|
311
|
-
''
|
|
312
|
-
].join('\n'));
|
|
313
266
|
|
|
314
267
|
writeFileIfMissing(appRoundPath, [
|
|
315
268
|
"import { Route } from 'round-core';",
|
|
@@ -386,7 +339,6 @@ async function runDev({ rootDir, configPathAbs, config }) {
|
|
|
386
339
|
throw new Error(`Entry not found: ${entryAbs ?? '(missing entry)'} (config: ${configPathAbs})`);
|
|
387
340
|
}
|
|
388
341
|
const entryRel = normalizePath(path.relative(rootDir, entryAbs));
|
|
389
|
-
ensureIndexHtml(rootDir, entryRel, config?.name ?? 'Round');
|
|
390
342
|
|
|
391
343
|
let viteServer = null;
|
|
392
344
|
let restarting = false;
|
|
@@ -400,7 +352,6 @@ async function runDev({ rootDir, configPathAbs, config }) {
|
|
|
400
352
|
throw new Error(`Entry not found: ${entryAbs2 ?? '(missing entry)'} (config: ${configPathAbs})`);
|
|
401
353
|
}
|
|
402
354
|
const entryRel2 = normalizePath(path.relative(rootDir, entryAbs2));
|
|
403
|
-
ensureIndexHtml(rootDir, entryRel2, nextConfig?.name ?? 'Round');
|
|
404
355
|
|
|
405
356
|
const serverPort2 = coerceNumber(nextConfig?.dev?.port, 5173);
|
|
406
357
|
const open2 = Boolean(nextConfig?.dev?.open);
|
|
@@ -475,7 +426,6 @@ async function runBuild({ rootDir, configPathAbs, config }) {
|
|
|
475
426
|
throw new Error(`Entry not found: ${entryAbs ?? '(missing entry)'} (config: ${configPathAbs})`);
|
|
476
427
|
}
|
|
477
428
|
const entryRel = normalizePath(path.relative(rootDir, entryAbs));
|
|
478
|
-
ensureIndexHtml(rootDir, entryRel, config?.name ?? 'Round');
|
|
479
429
|
|
|
480
430
|
const outDir = config?.output ? resolveFrom(configDir, config.output) : resolveFrom(rootDir, './dist');
|
|
481
431
|
const base = config?.routing?.base ?? '/';
|
|
@@ -513,8 +463,7 @@ async function runPreview({ rootDir, configPathAbs, config }) {
|
|
|
513
463
|
|
|
514
464
|
const entryAbs = config?.entry ? resolveFrom(configDir, config.entry) : null;
|
|
515
465
|
if (entryAbs && fs.existsSync(entryAbs)) {
|
|
516
|
-
|
|
517
|
-
ensureIndexHtml(rootDir, entryRel, config?.name ?? 'Round');
|
|
466
|
+
// No physical index.html needed
|
|
518
467
|
}
|
|
519
468
|
|
|
520
469
|
banner('Preview');
|
|
@@ -93,6 +93,8 @@ export default function RoundPlugin(pluginOptions = {}) {
|
|
|
93
93
|
configPathAbs: null,
|
|
94
94
|
configDir: null,
|
|
95
95
|
entryAbs: null,
|
|
96
|
+
entryRel: null,
|
|
97
|
+
name: 'Round',
|
|
96
98
|
startHead: null,
|
|
97
99
|
startHeadHtml: null
|
|
98
100
|
};
|
|
@@ -134,7 +136,10 @@ export default function RoundPlugin(pluginOptions = {}) {
|
|
|
134
136
|
const customTags = config?.htmlTags;
|
|
135
137
|
state.customTags = Array.isArray(customTags) ? customTags : [];
|
|
136
138
|
|
|
139
|
+
state.name = config?.name ?? 'Round';
|
|
140
|
+
|
|
137
141
|
const entryRel = config?.entry;
|
|
142
|
+
state.entryRel = entryRel;
|
|
138
143
|
state.entryAbs = entryRel ? resolveMaybeRelative(configDir, entryRel) : null;
|
|
139
144
|
|
|
140
145
|
const include = pluginOptions.include ?? config?.include ?? [];
|
|
@@ -323,7 +328,45 @@ export default function RoundPlugin(pluginOptions = {}) {
|
|
|
323
328
|
};
|
|
324
329
|
},
|
|
325
330
|
|
|
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
|
+
return null;
|
|
339
|
+
},
|
|
340
|
+
|
|
326
341
|
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
|
+
|
|
327
370
|
if (!isMdRawRequest(id)) return;
|
|
328
371
|
|
|
329
372
|
const fileAbs = stripQuery(id);
|
package/src/runtime/context.js
CHANGED
|
@@ -29,18 +29,29 @@ export function createContext(defaultValue) {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
function Provider(props = {}) {
|
|
32
|
-
const
|
|
33
|
-
const child = Array.isArray(props.children) ? props.children[0] : props.children;
|
|
34
|
-
const childFn = typeof child === 'function' ? child : () => child;
|
|
32
|
+
const children = props.children;
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
// Push context now so that any createElement/appendChild called
|
|
35
|
+
// during the instantiation of this Provider branch picks it up immediately.
|
|
36
|
+
pushContext({ [ctx.id]: props.value });
|
|
37
|
+
try {
|
|
38
|
+
// We use a span to handle reactive value updates and dynamic children.
|
|
39
|
+
return createElement('span', { style: { display: 'contents' } }, () => {
|
|
40
|
+
// Read current value (reactive if it's a signal)
|
|
41
|
+
const val = (typeof props.value === 'function' && props.value.peek) ? props.value() : props.value;
|
|
42
|
+
|
|
43
|
+
// Push it during the effect run too! This ensures that anything returned
|
|
44
|
+
// from this callback (which might trigger more appendChild calls) sees the context.
|
|
45
|
+
pushContext({ [ctx.id]: val });
|
|
46
|
+
try {
|
|
47
|
+
return children;
|
|
48
|
+
} finally {
|
|
49
|
+
popContext();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
} finally {
|
|
53
|
+
popContext();
|
|
54
|
+
}
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
ctx.Provider = Provider;
|
package/src/runtime/router.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { signal, effect } from './signals.js';
|
|
2
2
|
import { createElement } from './dom.js';
|
|
3
|
+
import { createContext, readContext } from './context.js';
|
|
3
4
|
|
|
4
5
|
const hasWindow = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
5
6
|
|
|
@@ -20,6 +21,8 @@ let defaultNotFoundComponent = null;
|
|
|
20
21
|
let autoNotFoundMounted = false;
|
|
21
22
|
let userProvidedNotFound = false;
|
|
22
23
|
|
|
24
|
+
const RoutingContext = createContext('');
|
|
25
|
+
|
|
23
26
|
function ensureListener() {
|
|
24
27
|
if (!hasWindow || listenerInitialized) return;
|
|
25
28
|
listenerInitialized = true;
|
|
@@ -72,6 +75,7 @@ export function useRouteReady() {
|
|
|
72
75
|
|
|
73
76
|
export function getIsNotFound() {
|
|
74
77
|
const pathname = normalizePathname(currentPath());
|
|
78
|
+
if (pathname === '/') return false;
|
|
75
79
|
if (!(Boolean(pathEvalReady()) && lastPathEvaluated === pathname)) return false;
|
|
76
80
|
return !Boolean(pathHasMatch());
|
|
77
81
|
}
|
|
@@ -79,6 +83,7 @@ export function getIsNotFound() {
|
|
|
79
83
|
export function useIsNotFound() {
|
|
80
84
|
return () => {
|
|
81
85
|
const pathname = normalizePathname(currentPath());
|
|
86
|
+
if (pathname === '/') return false;
|
|
82
87
|
if (!(Boolean(pathEvalReady()) && lastPathEvaluated === pathname)) return false;
|
|
83
88
|
return !Boolean(pathHasMatch());
|
|
84
89
|
};
|
|
@@ -104,6 +109,10 @@ function mountAutoNotFound() {
|
|
|
104
109
|
if (lastPathEvaluated !== pathname) return null;
|
|
105
110
|
if (hasMatch) return null;
|
|
106
111
|
|
|
112
|
+
// Skip absolute 404 overlay for the root path if no match found,
|
|
113
|
+
// allowing the base app to render its non-routed content.
|
|
114
|
+
if (pathname === '/') return null;
|
|
115
|
+
|
|
107
116
|
const Comp = defaultNotFoundComponent;
|
|
108
117
|
if (typeof Comp === 'function') {
|
|
109
118
|
return createElement(Comp, { pathname });
|
|
@@ -229,10 +238,12 @@ function normalizeTo(to) {
|
|
|
229
238
|
return normalizePathname(path) + suffix;
|
|
230
239
|
}
|
|
231
240
|
|
|
232
|
-
function matchRoute(route, pathname) {
|
|
241
|
+
function matchRoute(route, pathname, exact = true) {
|
|
233
242
|
const r = normalizePathname(route);
|
|
234
243
|
const p = normalizePathname(pathname);
|
|
235
|
-
return r === p;
|
|
244
|
+
if (exact) return r === p;
|
|
245
|
+
// Prefix match: either exactly the same, or p starts with r plus a slash
|
|
246
|
+
return p === r || p.startsWith(r.endsWith('/') ? r : r + '/');
|
|
236
247
|
}
|
|
237
248
|
|
|
238
249
|
function beginPathEvaluation(pathname) {
|
|
@@ -257,13 +268,41 @@ export function Route(props = {}) {
|
|
|
257
268
|
ensureListener();
|
|
258
269
|
|
|
259
270
|
return createElement('span', { style: { display: 'contents' } }, () => {
|
|
271
|
+
const parentPath = readContext(RoutingContext) || '';
|
|
260
272
|
const pathname = normalizePathname(currentPath());
|
|
261
273
|
beginPathEvaluation(pathname);
|
|
262
|
-
const route = props.route ?? '/';
|
|
263
|
-
if (!matchRoute(route, pathname)) return null;
|
|
264
274
|
|
|
265
|
-
|
|
266
|
-
|
|
275
|
+
const routeProp = props.route ?? '/';
|
|
276
|
+
if (typeof routeProp === 'string' && !routeProp.startsWith('/')) {
|
|
277
|
+
throw new Error(`Invalid route: "${routeProp}". All routes must start with a forward slash "/". (Nested under: "${parentPath || 'root'}")`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let fullRoute = '';
|
|
281
|
+
if (parentPath && parentPath !== '/') {
|
|
282
|
+
const cleanParent = parentPath.endsWith('/') ? parentPath.slice(0, -1) : parentPath;
|
|
283
|
+
const cleanChild = routeProp.startsWith('/') ? routeProp : '/' + routeProp;
|
|
284
|
+
|
|
285
|
+
if (cleanChild.startsWith(cleanParent + '/') || cleanChild === cleanParent) {
|
|
286
|
+
fullRoute = normalizePathname(cleanChild);
|
|
287
|
+
} else {
|
|
288
|
+
fullRoute = normalizePathname(cleanParent + cleanChild);
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
fullRoute = normalizePathname(routeProp);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const isRoot = fullRoute === '/';
|
|
295
|
+
const exact = props.exact !== undefined ? Boolean(props.exact) : isRoot;
|
|
296
|
+
|
|
297
|
+
// For nested routing, we match as a prefix so parents stay rendered while children are active
|
|
298
|
+
if (!matchRoute(fullRoute, pathname, exact)) return null;
|
|
299
|
+
|
|
300
|
+
// If it's an exact match of the FULL segments, mark as matched for 404 purposes
|
|
301
|
+
if (matchRoute(fullRoute, pathname, true)) {
|
|
302
|
+
hasMatchForPath = true;
|
|
303
|
+
pathHasMatch(true);
|
|
304
|
+
}
|
|
305
|
+
|
|
267
306
|
const mergedHead = (props.head && typeof props.head === 'object') ? props.head : {};
|
|
268
307
|
const meta = props.description
|
|
269
308
|
? ([{ name: 'description', content: String(props.description) }].concat(mergedHead.meta ?? props.meta ?? []))
|
|
@@ -274,7 +313,9 @@ export function Route(props = {}) {
|
|
|
274
313
|
const favicon = mergedHead.favicon ?? props.favicon;
|
|
275
314
|
|
|
276
315
|
applyHead({ title, meta, links, icon, favicon });
|
|
277
|
-
|
|
316
|
+
|
|
317
|
+
// Provide the current full path to nested routes
|
|
318
|
+
return createElement(RoutingContext.Provider, { value: fullRoute }, props.children);
|
|
278
319
|
});
|
|
279
320
|
}
|
|
280
321
|
|
|
@@ -282,13 +323,39 @@ export function Page(props = {}) {
|
|
|
282
323
|
ensureListener();
|
|
283
324
|
|
|
284
325
|
return createElement('span', { style: { display: 'contents' } }, () => {
|
|
326
|
+
const parentPath = readContext(RoutingContext) || '';
|
|
285
327
|
const pathname = normalizePathname(currentPath());
|
|
286
328
|
beginPathEvaluation(pathname);
|
|
287
|
-
const route = props.route ?? '/';
|
|
288
|
-
if (!matchRoute(route, pathname)) return null;
|
|
289
329
|
|
|
290
|
-
|
|
291
|
-
|
|
330
|
+
const routeProp = props.route ?? '/';
|
|
331
|
+
if (typeof routeProp === 'string' && !routeProp.startsWith('/')) {
|
|
332
|
+
throw new Error(`Invalid route: "${routeProp}". All routes must start with a forward slash "/". (Nested under: "${parentPath || 'root'}")`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let fullRoute = '';
|
|
336
|
+
if (parentPath && parentPath !== '/') {
|
|
337
|
+
const cleanParent = parentPath.endsWith('/') ? parentPath.slice(0, -1) : parentPath;
|
|
338
|
+
const cleanChild = routeProp.startsWith('/') ? routeProp : '/' + routeProp;
|
|
339
|
+
|
|
340
|
+
if (cleanChild.startsWith(cleanParent + '/') || cleanChild === cleanParent) {
|
|
341
|
+
fullRoute = normalizePathname(cleanChild);
|
|
342
|
+
} else {
|
|
343
|
+
fullRoute = normalizePathname(cleanParent + cleanChild);
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
fullRoute = normalizePathname(routeProp);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const isRoot = fullRoute === '/';
|
|
350
|
+
const exact = props.exact !== undefined ? Boolean(props.exact) : isRoot;
|
|
351
|
+
|
|
352
|
+
if (!matchRoute(fullRoute, pathname, exact)) return null;
|
|
353
|
+
|
|
354
|
+
if (matchRoute(fullRoute, pathname, true)) {
|
|
355
|
+
hasMatchForPath = true;
|
|
356
|
+
pathHasMatch(true);
|
|
357
|
+
}
|
|
358
|
+
|
|
292
359
|
const mergedHead = (props.head && typeof props.head === 'object') ? props.head : {};
|
|
293
360
|
const meta = props.description
|
|
294
361
|
? ([{ name: 'description', content: String(props.description) }].concat(mergedHead.meta ?? props.meta ?? []))
|
|
@@ -299,7 +366,8 @@ export function Page(props = {}) {
|
|
|
299
366
|
const favicon = mergedHead.favicon ?? props.favicon;
|
|
300
367
|
|
|
301
368
|
applyHead({ title, meta, links, icon, favicon });
|
|
302
|
-
|
|
369
|
+
|
|
370
|
+
return createElement(RoutingContext.Provider, { value: fullRoute }, props.children);
|
|
303
371
|
});
|
|
304
372
|
}
|
|
305
373
|
|
|
@@ -318,6 +386,7 @@ export function NotFound(props = {}) {
|
|
|
318
386
|
if (lastPathEvaluated !== pathname) return null;
|
|
319
387
|
|
|
320
388
|
if (hasMatch) return null;
|
|
389
|
+
if (pathname === '/') return null;
|
|
321
390
|
|
|
322
391
|
const Comp = props.component ?? defaultNotFoundComponent;
|
|
323
392
|
if (typeof Comp === 'function') {
|
|
@@ -339,8 +408,8 @@ export function Link(props = {}) {
|
|
|
339
408
|
const rawHref = props.href ?? props.to ?? '#';
|
|
340
409
|
const href = spaNormalizeHref(rawHref);
|
|
341
410
|
|
|
342
|
-
|
|
343
|
-
|
|
411
|
+
const spa = props.spa !== undefined ? Boolean(props.spa) : true;
|
|
412
|
+
const reload = Boolean(props.reload);
|
|
344
413
|
|
|
345
414
|
const onClick = (e) => {
|
|
346
415
|
if (typeof props.onClick === 'function') props.onClick(e);
|
package/vite.config.build.js
CHANGED
|
@@ -2,32 +2,21 @@ import { defineConfig } from 'vite';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
|
|
5
|
-
// Custom plugin to move .d.ts files or raw assets if needed,
|
|
6
|
-
// for now we just handle JS bundling.
|
|
7
|
-
|
|
8
|
-
// Custom plugin to move .d.ts files or raw assets if needed,
|
|
9
|
-
// for now we just handle JS bundling.
|
|
10
|
-
|
|
11
5
|
export default defineConfig({
|
|
12
6
|
build: {
|
|
13
|
-
// Target modern environments
|
|
14
7
|
target: 'es2022',
|
|
15
8
|
outDir: 'dist',
|
|
16
9
|
emptyOutDir: true,
|
|
17
|
-
minify: false,
|
|
18
|
-
// Wait, user asked for "extremo rapido y liviano" (extremely fast and light).
|
|
19
|
-
// So I SHOULD minify.
|
|
10
|
+
minify: false,
|
|
20
11
|
lib: {
|
|
21
12
|
entry: {
|
|
22
13
|
index: path.resolve(__dirname, 'src/index.js'),
|
|
23
14
|
cli: path.resolve(__dirname, 'src/cli.js'),
|
|
24
|
-
// We expose the plugin separately so users can import it in their vite.config.js
|
|
25
15
|
'vite-plugin': path.resolve(__dirname, 'src/compiler/vite-plugin.js')
|
|
26
16
|
},
|
|
27
|
-
formats: ['es']
|
|
17
|
+
formats: ['es']
|
|
28
18
|
},
|
|
29
19
|
rollupOptions: {
|
|
30
|
-
// Externalize dependencies so they aren't bundled into the library
|
|
31
20
|
external: [
|
|
32
21
|
'vite',
|
|
33
22
|
'marked',
|
package/index.html
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
|
|
4
|
-
<head>
|
|
5
|
-
<meta charset="UTF-8">
|
|
6
|
-
<title>Round Vite Test</title>
|
|
7
|
-
</head>
|
|
8
|
-
|
|
9
|
-
<body>
|
|
10
|
-
<div id="app"></div>
|
|
11
|
-
<script type="module">
|
|
12
|
-
import { render } from '/index.js';
|
|
13
|
-
import TestApp from 'start_exmpl/TestApp.round';
|
|
14
|
-
|
|
15
|
-
render(TestApp, document.getElementById('app'));
|
|
16
|
-
</script>
|
|
17
|
-
</body>
|
|
18
|
-
|
|
19
|
-
</html>
|