round-core 0.0.4 → 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/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,6 +613,9 @@ ${head.raw}`;
609
613
  }
610
614
  };
611
615
  },
616
+ resolveId(id) {
617
+ return null;
618
+ },
612
619
  load(id) {
613
620
  if (!isMdRawRequest(id)) return;
614
621
  const fileAbs = stripQuery(id);
package/package.json CHANGED
@@ -1,48 +1,48 @@
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.6",
4
+ "description": "A lightweight frontend framework for SPA with signals and fine grained reactivity",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": "./dist/index.js",
9
+ "./vite-plugin": "./dist/vite-plugin.js"
10
+ },
11
+ "type": "module",
12
+ "icon": "round.png",
13
+ "repository": {
14
+ "url": "https://github.com/ZtaMDev/RoundJS.git"
15
+ },
16
+ "bin": {
17
+ "round": "./dist/cli.js"
18
+ },
19
+ "scripts": {
20
+ "dev": "node ./src/cli.js dev --config ./test/main/round.config.json",
21
+ "build": "node ./src/cli.js build --config ./test/main/round.config.json",
22
+ "build:core": "vite build -c vite.config.build.js",
23
+ "test": "vitest run",
24
+ "bench": "bun run --cwd benchmarks bench",
25
+ "bench:build": "bun run --cwd benchmarks bench:build",
26
+ "bench:runtime": "bun run --cwd benchmarks bench:runtime"
27
+ },
28
+ "keywords": [
29
+ "framework",
30
+ "spa",
31
+ "signals",
32
+ "vite"
33
+ ],
34
+ "author": "Round Framework Team",
35
+ "license": "MIT",
36
+ "dependencies": {
37
+ "marked": "^12.0.2",
38
+ "vite": "^5.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "bun-types": "latest",
42
+ "@types/node": "latest",
43
+ "vitest": "^1.6.0"
44
+ },
45
+ "peerDependencies": {
46
+ "bun": ">=1.0.0"
47
+ }
48
48
  }
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('\\', '/');
@@ -122,32 +133,6 @@ function resolveFrom(baseDir, p) {
122
133
  return path.resolve(baseDir, p);
123
134
  }
124
135
 
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
136
 
152
137
  function parseArgs(argv) {
153
138
  const args = { _: [] };
@@ -231,7 +216,6 @@ async function runInit({ name }) {
231
216
  const pkgPath = path.join(projectDir, 'package.json');
232
217
  const configPath = path.join(projectDir, 'round.config.json');
233
218
  const viteConfigPath = path.join(projectDir, 'vite.config.js');
234
- const indexHtmlPath = path.join(projectDir, 'index.html');
235
219
  const appRoundPath = path.join(srcDir, 'app.round');
236
220
  const counterRoundPath = path.join(srcDir, 'counter.round');
237
221
 
@@ -290,26 +274,6 @@ async function runInit({ name }) {
290
274
  ''
291
275
  ].join('\n'));
292
276
 
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
277
 
314
278
  writeFileIfMissing(appRoundPath, [
315
279
  "import { Route } from 'round-core';",
@@ -377,6 +341,44 @@ function coerceNumber(v, fallback) {
377
341
  return Number.isFinite(n) ? n : fallback;
378
342
  }
379
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
+
380
382
  async function runDev({ rootDir, configPathAbs, config }) {
381
383
  const startedAt = Date.now();
382
384
  const configDir = path.dirname(configPathAbs);
@@ -386,7 +388,6 @@ async function runDev({ rootDir, configPathAbs, config }) {
386
388
  throw new Error(`Entry not found: ${entryAbs ?? '(missing entry)'} (config: ${configPathAbs})`);
387
389
  }
388
390
  const entryRel = normalizePath(path.relative(rootDir, entryAbs));
389
- ensureIndexHtml(rootDir, entryRel, config?.name ?? 'Round');
390
391
 
391
392
  let viteServer = null;
392
393
  let restarting = false;
@@ -400,12 +401,13 @@ async function runDev({ rootDir, configPathAbs, config }) {
400
401
  throw new Error(`Entry not found: ${entryAbs2 ?? '(missing entry)'} (config: ${configPathAbs})`);
401
402
  }
402
403
  const entryRel2 = normalizePath(path.relative(rootDir, entryAbs2));
403
- ensureIndexHtml(rootDir, entryRel2, nextConfig?.name ?? 'Round');
404
404
 
405
405
  const serverPort2 = coerceNumber(nextConfig?.dev?.port, 5173);
406
406
  const open2 = Boolean(nextConfig?.dev?.open);
407
407
  const base2 = nextConfig?.routing?.base ?? '/';
408
408
 
409
+ const isTemp = writeTemporaryIndex(rootDir, nextConfig);
410
+
409
411
  if (showBanner) {
410
412
  banner('Dev Server');
411
413
  process.stdout.write(`${c(' Config', 'gray')} ${configPathAbs}\n`);
@@ -475,11 +477,12 @@ async function runBuild({ rootDir, configPathAbs, config }) {
475
477
  throw new Error(`Entry not found: ${entryAbs ?? '(missing entry)'} (config: ${configPathAbs})`);
476
478
  }
477
479
  const entryRel = normalizePath(path.relative(rootDir, entryAbs));
478
- ensureIndexHtml(rootDir, entryRel, config?.name ?? 'Round');
479
480
 
480
481
  const outDir = config?.output ? resolveFrom(configDir, config.output) : resolveFrom(rootDir, './dist');
481
482
  const base = config?.routing?.base ?? '/';
482
483
 
484
+ const isTemp = writeTemporaryIndex(rootDir, config);
485
+
483
486
  banner('Build');
484
487
  process.stdout.write(`${c(' Config', 'gray')} ${configPathAbs}\n`);
485
488
  process.stdout.write(`${c(' OutDir', 'gray')} ${outDir}\n`);
@@ -513,8 +516,7 @@ async function runPreview({ rootDir, configPathAbs, config }) {
513
516
 
514
517
  const entryAbs = config?.entry ? resolveFrom(configDir, config.entry) : null;
515
518
  if (entryAbs && fs.existsSync(entryAbs)) {
516
- const entryRel = normalizePath(path.relative(rootDir, entryAbs));
517
- ensureIndexHtml(rootDir, entryRel, config?.name ?? 'Round');
519
+ // No physical index.html needed
518
520
  }
519
521
 
520
522
  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,6 +328,9 @@ export default function RoundPlugin(pluginOptions = {}) {
323
328
  };
324
329
  },
325
330
 
331
+ resolveId(id) {
332
+ return null;
333
+ },
326
334
  load(id) {
327
335
  if (!isMdRawRequest(id)) return;
328
336