round-core 0.0.3 → 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 +44 -0
- package/README.md +112 -54
- package/benchmarks/apps/react/index.html +9 -0
- package/benchmarks/apps/react/main.jsx +25 -0
- package/benchmarks/apps/react/vite.config.js +12 -0
- package/benchmarks/apps/round/index.html +11 -0
- package/benchmarks/apps/round/main.jsx +22 -0
- package/benchmarks/apps/round/vite.config.js +15 -0
- package/benchmarks/bun.lock +497 -0
- package/benchmarks/dist-bench/react/assets/index-9KGqIPOU.js +8 -0
- package/benchmarks/dist-bench/react/index.html +10 -0
- package/benchmarks/dist-bench/round/assets/index-CBBIRhox.js +52 -0
- package/benchmarks/dist-bench/round/index.html +8 -0
- package/benchmarks/package.json +22 -0
- package/benchmarks/scripts/measure-build.js +64 -0
- package/benchmarks/tests/runtime.bench.js +51 -0
- package/benchmarks/vitest.config.js +8 -0
- package/bun.lock +11 -0
- package/dist/cli.js +530 -0
- package/dist/index.js +2025 -0
- package/dist/vite-plugin.js +774 -0
- package/package.json +46 -39
- package/src/cli.js +11 -55
- package/src/compiler/vite-plugin.js +48 -1
- package/src/runtime/context.js +22 -11
- package/src/runtime/dom.js +23 -9
- package/src/runtime/lifecycle.js +1 -1
- package/src/runtime/router.js +83 -14
- package/vite.config.build.js +36 -0
- package/index.html +0 -19
package/package.json
CHANGED
|
@@ -1,40 +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": "
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
}
|
|
40
47
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import process from 'node:process';
|
|
@@ -6,6 +6,13 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { createServer, build as viteBuild, preview as vitePreview } from 'vite';
|
|
7
7
|
import RoundPlugin from './compiler/vite-plugin.js';
|
|
8
8
|
|
|
9
|
+
// Handle graceful shutdown
|
|
10
|
+
function onSignal() {
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
process.on('SIGINT', onSignal);
|
|
14
|
+
process.on('SIGTERM', onSignal);
|
|
15
|
+
|
|
9
16
|
function normalizePath(p) {
|
|
10
17
|
return p.replaceAll('\\', '/');
|
|
11
18
|
}
|
|
@@ -115,32 +122,6 @@ function resolveFrom(baseDir, p) {
|
|
|
115
122
|
return path.resolve(baseDir, p);
|
|
116
123
|
}
|
|
117
124
|
|
|
118
|
-
function ensureIndexHtml(rootDir, entryRel, title = 'Round') {
|
|
119
|
-
const indexPath = path.join(rootDir, 'index.html');
|
|
120
|
-
if (fs.existsSync(indexPath)) return;
|
|
121
|
-
|
|
122
|
-
const entryPath = entryRel.startsWith('/') ? entryRel : `/${entryRel}`;
|
|
123
|
-
fs.writeFileSync(indexPath, [
|
|
124
|
-
'<!DOCTYPE html>',
|
|
125
|
-
'<html lang="en">',
|
|
126
|
-
'<head>',
|
|
127
|
-
' <meta charset="UTF-8" />',
|
|
128
|
-
' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
|
|
129
|
-
` <title>${title}</title>`,
|
|
130
|
-
'</head>',
|
|
131
|
-
'<body>',
|
|
132
|
-
' <div id="app"></div>',
|
|
133
|
-
' <script type="module">',
|
|
134
|
-
" import { render } from 'round-core';",
|
|
135
|
-
` import App from '${entryPath}';`,
|
|
136
|
-
'',
|
|
137
|
-
' render(App, document.getElementById("app"));',
|
|
138
|
-
' </script>',
|
|
139
|
-
'</body>',
|
|
140
|
-
'</html>',
|
|
141
|
-
''
|
|
142
|
-
].join('\n'), 'utf8');
|
|
143
|
-
}
|
|
144
125
|
|
|
145
126
|
function parseArgs(argv) {
|
|
146
127
|
const args = { _: [] };
|
|
@@ -224,7 +205,6 @@ async function runInit({ name }) {
|
|
|
224
205
|
const pkgPath = path.join(projectDir, 'package.json');
|
|
225
206
|
const configPath = path.join(projectDir, 'round.config.json');
|
|
226
207
|
const viteConfigPath = path.join(projectDir, 'vite.config.js');
|
|
227
|
-
const indexHtmlPath = path.join(projectDir, 'index.html');
|
|
228
208
|
const appRoundPath = path.join(srcDir, 'app.round');
|
|
229
209
|
const counterRoundPath = path.join(srcDir, 'counter.round');
|
|
230
210
|
|
|
@@ -239,7 +219,7 @@ async function runInit({ name }) {
|
|
|
239
219
|
preview: 'round preview'
|
|
240
220
|
},
|
|
241
221
|
dependencies: {
|
|
242
|
-
'round-core': '^0.0.
|
|
222
|
+
'round-core': '^0.0.4'
|
|
243
223
|
},
|
|
244
224
|
devDependencies: {
|
|
245
225
|
vite: '^5.0.0'
|
|
@@ -272,7 +252,7 @@ async function runInit({ name }) {
|
|
|
272
252
|
|
|
273
253
|
writeFileIfMissing(viteConfigPath, [
|
|
274
254
|
"import { defineConfig } from 'vite';",
|
|
275
|
-
"import RoundPlugin from 'round-core/
|
|
255
|
+
"import RoundPlugin from 'round-core/vite-plugin';",
|
|
276
256
|
'',
|
|
277
257
|
'export default defineConfig({',
|
|
278
258
|
" plugins: [RoundPlugin({ configPath: './round.config.json' })],",
|
|
@@ -283,26 +263,6 @@ async function runInit({ name }) {
|
|
|
283
263
|
''
|
|
284
264
|
].join('\n'));
|
|
285
265
|
|
|
286
|
-
writeFileIfMissing(indexHtmlPath, [
|
|
287
|
-
'<!DOCTYPE html>',
|
|
288
|
-
'<html lang="en">',
|
|
289
|
-
'<head>',
|
|
290
|
-
' <meta charset="UTF-8" />',
|
|
291
|
-
' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
|
|
292
|
-
` <title>${name}</title>`,
|
|
293
|
-
'</head>',
|
|
294
|
-
'<body>',
|
|
295
|
-
' <div id="app"></div>',
|
|
296
|
-
' <script type="module">',
|
|
297
|
-
" import { render } from 'round-core';",
|
|
298
|
-
" import App from '/src/app.round';",
|
|
299
|
-
'',
|
|
300
|
-
" render(App, document.getElementById('app'));",
|
|
301
|
-
' </script>',
|
|
302
|
-
'</body>',
|
|
303
|
-
'</html>',
|
|
304
|
-
''
|
|
305
|
-
].join('\n'));
|
|
306
266
|
|
|
307
267
|
writeFileIfMissing(appRoundPath, [
|
|
308
268
|
"import { Route } from 'round-core';",
|
|
@@ -379,7 +339,6 @@ async function runDev({ rootDir, configPathAbs, config }) {
|
|
|
379
339
|
throw new Error(`Entry not found: ${entryAbs ?? '(missing entry)'} (config: ${configPathAbs})`);
|
|
380
340
|
}
|
|
381
341
|
const entryRel = normalizePath(path.relative(rootDir, entryAbs));
|
|
382
|
-
ensureIndexHtml(rootDir, entryRel, config?.name ?? 'Round');
|
|
383
342
|
|
|
384
343
|
let viteServer = null;
|
|
385
344
|
let restarting = false;
|
|
@@ -393,7 +352,6 @@ async function runDev({ rootDir, configPathAbs, config }) {
|
|
|
393
352
|
throw new Error(`Entry not found: ${entryAbs2 ?? '(missing entry)'} (config: ${configPathAbs})`);
|
|
394
353
|
}
|
|
395
354
|
const entryRel2 = normalizePath(path.relative(rootDir, entryAbs2));
|
|
396
|
-
ensureIndexHtml(rootDir, entryRel2, nextConfig?.name ?? 'Round');
|
|
397
355
|
|
|
398
356
|
const serverPort2 = coerceNumber(nextConfig?.dev?.port, 5173);
|
|
399
357
|
const open2 = Boolean(nextConfig?.dev?.open);
|
|
@@ -468,7 +426,6 @@ async function runBuild({ rootDir, configPathAbs, config }) {
|
|
|
468
426
|
throw new Error(`Entry not found: ${entryAbs ?? '(missing entry)'} (config: ${configPathAbs})`);
|
|
469
427
|
}
|
|
470
428
|
const entryRel = normalizePath(path.relative(rootDir, entryAbs));
|
|
471
|
-
ensureIndexHtml(rootDir, entryRel, config?.name ?? 'Round');
|
|
472
429
|
|
|
473
430
|
const outDir = config?.output ? resolveFrom(configDir, config.output) : resolveFrom(rootDir, './dist');
|
|
474
431
|
const base = config?.routing?.base ?? '/';
|
|
@@ -506,8 +463,7 @@ async function runPreview({ rootDir, configPathAbs, config }) {
|
|
|
506
463
|
|
|
507
464
|
const entryAbs = config?.entry ? resolveFrom(configDir, config.entry) : null;
|
|
508
465
|
if (entryAbs && fs.existsSync(entryAbs)) {
|
|
509
|
-
|
|
510
|
-
ensureIndexHtml(rootDir, entryRel, config?.name ?? 'Round');
|
|
466
|
+
// No physical index.html needed
|
|
511
467
|
}
|
|
512
468
|
|
|
513
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
|
};
|
|
@@ -131,7 +133,13 @@ export default function RoundPlugin(pluginOptions = {}) {
|
|
|
131
133
|
const trailingSlash = config?.routing?.trailingSlash;
|
|
132
134
|
state.routingTrailingSlash = trailingSlash !== undefined ? Boolean(trailingSlash) : true;
|
|
133
135
|
|
|
136
|
+
const customTags = config?.htmlTags;
|
|
137
|
+
state.customTags = Array.isArray(customTags) ? customTags : [];
|
|
138
|
+
|
|
139
|
+
state.name = config?.name ?? 'Round';
|
|
140
|
+
|
|
134
141
|
const entryRel = config?.entry;
|
|
142
|
+
state.entryRel = entryRel;
|
|
135
143
|
state.entryAbs = entryRel ? resolveMaybeRelative(configDir, entryRel) : null;
|
|
136
144
|
|
|
137
145
|
const include = pluginOptions.include ?? config?.include ?? [];
|
|
@@ -303,7 +311,8 @@ export default function RoundPlugin(pluginOptions = {}) {
|
|
|
303
311
|
|
|
304
312
|
return {
|
|
305
313
|
define: {
|
|
306
|
-
__ROUND_ROUTING_TRAILING_SLASH__: JSON.stringify(state.routingTrailingSlash)
|
|
314
|
+
__ROUND_ROUTING_TRAILING_SLASH__: JSON.stringify(state.routingTrailingSlash),
|
|
315
|
+
__ROUND_CUSTOM_TAGS__: JSON.stringify(state.customTags ?? [])
|
|
307
316
|
},
|
|
308
317
|
esbuild: {
|
|
309
318
|
include: /\.(round|js|jsx|ts|tsx)$/,
|
|
@@ -319,7 +328,45 @@ export default function RoundPlugin(pluginOptions = {}) {
|
|
|
319
328
|
};
|
|
320
329
|
},
|
|
321
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
|
+
|
|
322
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
|
+
|
|
323
370
|
if (!isMdRawRequest(id)) return;
|
|
324
371
|
|
|
325
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/dom.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { effect, untrack } from './signals.js';
|
|
2
|
-
import {
|
|
2
|
+
import { runInLifecycle, createComponentInstance, mountComponent, initLifecycleRoot } from './lifecycle.js';
|
|
3
3
|
import { reportErrorSafe } from './error-reporter.js';
|
|
4
4
|
import { captureContext, runInContext, readContext } from './context.js';
|
|
5
5
|
import { SuspenseContext } from './suspense.js';
|
|
@@ -76,14 +76,13 @@ export function createElement(tag, props = {}, ...children) {
|
|
|
76
76
|
|
|
77
77
|
if (typeof tag === 'string') {
|
|
78
78
|
const isCustomElement = tag.includes('-');
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (!isCustomElement && !isStandard && /^[a-z]/.test(tag)) {
|
|
79
|
+
|
|
80
|
+
const isStandard = /^(a|abbr|address|area|article|aside|audio|b|base|bdi|bdo|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|data|datalist|dd|del|details|dfn|dialog|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|label|legend|li|link|main|map|mark|meta|meter|nav|noscript|object|ol|optgroup|option|output|p|param|picture|pre|progress|q|rp|rt|ruby|s|samp|script|search|section|select|slot|small|source|span|strong|style|sub|summary|sup|svg|table|tbody|td|template|textarea|tfoot|th|thead|time|title|tr|track|u|ul|var|video|wbr|menu|animate|animateMotion|animateTransform|circle|clipPath|defs|desc|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|foreignObject|g|image|line|linearGradient|marker|mask|metadata|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|stop|switch|symbol|text|textPath|tspan|use|view)$/.test(tag);
|
|
81
|
+
|
|
82
|
+
// __ROUND_CUSTOM_TAGS__ is injected by the vite plugin from round.config.json
|
|
83
|
+
const isCustomConfigured = typeof __ROUND_CUSTOM_TAGS__ !== 'undefined' && __ROUND_CUSTOM_TAGS__.includes(tag);
|
|
84
|
+
|
|
85
|
+
if (!isCustomElement && !isStandard && !isCustomConfigured && /^[a-z]/.test(tag)) {
|
|
87
86
|
throw new Error(`Component names must start with an uppercase letter: <${tag} />`);
|
|
88
87
|
}
|
|
89
88
|
}
|
|
@@ -273,6 +272,21 @@ export function createElement(tag, props = {}, ...children) {
|
|
|
273
272
|
return;
|
|
274
273
|
}
|
|
275
274
|
|
|
275
|
+
if (key === 'classList') {
|
|
276
|
+
if (value && typeof value === 'object') {
|
|
277
|
+
Object.entries(value).forEach(([className, condition]) => {
|
|
278
|
+
if (typeof condition === 'function') {
|
|
279
|
+
effect(() => {
|
|
280
|
+
element.classList.toggle(className, !!condition());
|
|
281
|
+
}, { onLoad: false });
|
|
282
|
+
} else {
|
|
283
|
+
element.classList.toggle(className, !!condition);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
276
290
|
if (key === 'className') element.className = value;
|
|
277
291
|
else if (key === 'value') element.value = value;
|
|
278
292
|
else if (key === 'checked') element.checked = Boolean(value);
|
package/src/runtime/lifecycle.js
CHANGED
|
@@ -6,7 +6,7 @@ export function getCurrentComponent() {
|
|
|
6
6
|
return componentStack[componentStack.length - 1];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export function
|
|
9
|
+
export function runInLifecycle(componentInstance, fn) {
|
|
10
10
|
componentStack.push(componentInstance);
|
|
11
11
|
try {
|
|
12
12
|
return fn();
|
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);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
build: {
|
|
7
|
+
target: 'es2022',
|
|
8
|
+
outDir: 'dist',
|
|
9
|
+
emptyOutDir: true,
|
|
10
|
+
minify: false,
|
|
11
|
+
lib: {
|
|
12
|
+
entry: {
|
|
13
|
+
index: path.resolve(__dirname, 'src/index.js'),
|
|
14
|
+
cli: path.resolve(__dirname, 'src/cli.js'),
|
|
15
|
+
'vite-plugin': path.resolve(__dirname, 'src/compiler/vite-plugin.js')
|
|
16
|
+
},
|
|
17
|
+
formats: ['es']
|
|
18
|
+
},
|
|
19
|
+
rollupOptions: {
|
|
20
|
+
external: [
|
|
21
|
+
'vite',
|
|
22
|
+
'marked',
|
|
23
|
+
'node:fs', 'node:path', 'node:process', 'node:url', 'node:vm', 'node:util',
|
|
24
|
+
'fs', 'path', 'process', 'url', 'vm', 'util'
|
|
25
|
+
],
|
|
26
|
+
output: {
|
|
27
|
+
banner: (chunk) => {
|
|
28
|
+
if (chunk.name === 'cli' || chunk.fileName === 'cli.js') {
|
|
29
|
+
return '#!/usr/bin/env node';
|
|
30
|
+
}
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
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>
|