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.
@@ -12,7 +12,7 @@ jobs:
12
12
  runs-on: ubuntu-latest
13
13
 
14
14
  steps:
15
- - uses: actions/checkout@v3
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@v3
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
- Typical usage:
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
- <Route route="/" title="Home">
336
- <div>Home</div>
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
- const entryRel = normalizePath(path.relative(rootDir, entryAbs));
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
- const entryRel2 = normalizePath(path.relative(rootDir, entryAbs2));
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
- const entryRel = normalizePath(path.relative(rootDir, entryAbs));
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 value = props.value;
624
- const child = Array.isArray(props.children) ? props.children[0] : props.children;
625
- const childFn = typeof child === "function" ? child : () => child;
626
- return createElement("span", { style: { display: "contents" } }, () => {
627
- pushContext({ [ctx.id]: value });
628
- try {
629
- return childFn();
630
- } finally {
631
- popContext();
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 route = props.route ?? "/";
1322
- if (!matchRoute(route, pathname)) return null;
1323
- pathHasMatch(true);
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 route = props.route ?? "/";
1340
- if (!matchRoute(route, pathname)) return null;
1341
- pathHasMatch(true);
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 });
@@ -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",
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
- "vitest": "^1.6.0"
39
- },
40
- "devDependencies": {
41
- "jsdom": "^24.0.0",
42
- "bun-types": "latest",
43
- "@types/node": "latest"
44
- },
45
- "peerDependencies": {
46
- "bun": ">=1.0.0"
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
- const entryRel = normalizePath(path.relative(rootDir, entryAbs));
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);
@@ -29,18 +29,29 @@ export function createContext(defaultValue) {
29
29
  };
30
30
 
31
31
  function Provider(props = {}) {
32
- const value = props.value;
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
- return createElement('span', { style: { display: 'contents' } }, () => {
37
- pushContext({ [ctx.id]: value });
38
- try {
39
- return childFn();
40
- } finally {
41
- popContext();
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;
@@ -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
- hasMatchForPath = true;
266
- pathHasMatch(true);
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
- return props.children;
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
- hasMatchForPath = true;
291
- pathHasMatch(true);
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
- return props.children;
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
- const spa = props.spa !== undefined ? Boolean(props.spa) : true;
343
- const reload = Boolean(props.reload);
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);
@@ -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, // User can enable if they want extreme minification, but for a lib readable code is nice.
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'] // ESM only is fine for modern "type": "module" package
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>