round-core 0.1.2 → 0.1.3
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/package.json +13 -8
- package/src/cli.js +609 -0
- package/src/compiler/index.js +2 -0
- package/src/compiler/transformer.js +525 -0
- package/src/compiler/vite-plugin.d.ts +8 -0
- package/src/compiler/vite-plugin.js +472 -0
- package/src/index.js +42 -0
- package/src/runtime/context.js +101 -0
- package/src/runtime/dom.js +401 -0
- package/src/runtime/error-boundary.js +48 -0
- package/src/runtime/error-reporter.js +13 -0
- package/src/runtime/error-store.js +85 -0
- package/src/runtime/errors.js +152 -0
- package/src/runtime/lifecycle.js +140 -0
- package/src/runtime/router.js +475 -0
- package/src/runtime/signals.js +484 -0
- package/src/runtime/store.js +215 -0
- package/src/runtime/suspense.js +128 -0
- package/dist/cli.js +0 -607
- package/dist/index.js +0 -2071
- package/dist/vite-plugin.js +0 -883
- /package/{dist → src}/index.d.ts +0 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { transform } from './transformer.js';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
function normalizePath(p) {
|
|
6
|
+
return p.replaceAll('\\', '/');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isMdRawRequest(id) {
|
|
10
|
+
return typeof id === 'string' && id.includes('.md') && id.includes('?raw');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function stripQuery(id) {
|
|
14
|
+
return id.split('?')[0];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function escapeForJsString(s) {
|
|
18
|
+
return String(s)
|
|
19
|
+
.replaceAll('\\', '\\\\')
|
|
20
|
+
.replaceAll('`', '\\`')
|
|
21
|
+
.replaceAll('${', '\\${');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveMaybeRelative(baseDir, p) {
|
|
25
|
+
if (!p) return null;
|
|
26
|
+
if (path.isAbsolute(p)) return p;
|
|
27
|
+
return path.resolve(baseDir, p);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function inlineMarkdownInRound(code, fileAbs, addWatchFile) {
|
|
31
|
+
if (typeof code !== 'string' || typeof fileAbs !== 'string') return code;
|
|
32
|
+
|
|
33
|
+
// Only handle simple self-closing tags with literal src: <Markdown src="./x.md" ... />
|
|
34
|
+
// This runs before the .round transformer, so it's safe string-level rewriting.
|
|
35
|
+
const dir = path.dirname(fileAbs);
|
|
36
|
+
|
|
37
|
+
// Match src="..." or src='...'
|
|
38
|
+
const re = /<Markdown\b([^>]*?)\bsrc\s*=\s*("([^"]+)"|'([^']+)')([^>]*)\/>/g;
|
|
39
|
+
return code.replace(re, (full, beforeAttrs, _q, dbl, sgl, afterAttrs) => {
|
|
40
|
+
const src = dbl ?? sgl;
|
|
41
|
+
if (!src || typeof src !== 'string') return full;
|
|
42
|
+
|
|
43
|
+
// Only inline relative paths; absolute/public URLs should remain runtime-resolved.
|
|
44
|
+
if (!src.startsWith('./') && !src.startsWith('../')) return full;
|
|
45
|
+
|
|
46
|
+
const mdAbs = path.resolve(dir, src);
|
|
47
|
+
try {
|
|
48
|
+
const raw = fs.readFileSync(mdAbs, 'utf8');
|
|
49
|
+
if (typeof addWatchFile === 'function') {
|
|
50
|
+
try { addWatchFile(mdAbs); } catch { }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const content = escapeForJsString(raw);
|
|
54
|
+
|
|
55
|
+
// Remove the src=... portion and inject content={...}
|
|
56
|
+
const rebuilt = `<Markdown${beforeAttrs}content={\`${content}\`} ${afterAttrs} />`;
|
|
57
|
+
return rebuilt.replace(/\s+\/>$/, ' />');
|
|
58
|
+
} catch (e) {
|
|
59
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
60
|
+
throw new Error(`Markdown file not found: ${src} (resolved: ${mdAbs})\n${msg}`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isExcluded(fileAbsPath, excludeAbs) {
|
|
66
|
+
const file = normalizePath(fileAbsPath);
|
|
67
|
+
for (const pat of excludeAbs) {
|
|
68
|
+
const patNorm = normalizePath(pat);
|
|
69
|
+
const prefix = patNorm.endsWith('/**') ? patNorm.slice(0, -3) : patNorm;
|
|
70
|
+
if (file.startsWith(prefix)) return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isIncluded(fileAbsPath, includeAbs) {
|
|
76
|
+
if (!includeAbs.length) return true;
|
|
77
|
+
const file = normalizePath(fileAbsPath);
|
|
78
|
+
for (const pat of includeAbs) {
|
|
79
|
+
const patNorm = normalizePath(pat);
|
|
80
|
+
const prefix = patNorm.endsWith('/**') ? patNorm.slice(0, -3) : patNorm;
|
|
81
|
+
if (file.startsWith(prefix)) return true;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default function RoundPlugin(pluginOptions = {}) {
|
|
87
|
+
const state = {
|
|
88
|
+
rootDir: process.cwd(),
|
|
89
|
+
includeAbs: [],
|
|
90
|
+
excludeAbs: [],
|
|
91
|
+
configLoaded: false,
|
|
92
|
+
routingTrailingSlash: true,
|
|
93
|
+
configPathAbs: null,
|
|
94
|
+
configDir: null,
|
|
95
|
+
entryAbs: null,
|
|
96
|
+
entryRel: null,
|
|
97
|
+
name: 'Round',
|
|
98
|
+
startHead: null,
|
|
99
|
+
startHeadHtml: null
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
let lastRuntimeErrorKey = null;
|
|
103
|
+
let lastRuntimeErrorAt = 0;
|
|
104
|
+
|
|
105
|
+
const runtimeImport = pluginOptions.runtimeImport ?? 'round-core';
|
|
106
|
+
const restartOnConfigChange = pluginOptions.restartOnConfigChange !== undefined
|
|
107
|
+
? Boolean(pluginOptions.restartOnConfigChange)
|
|
108
|
+
: true;
|
|
109
|
+
|
|
110
|
+
function loadConfigOnce(rootDir) {
|
|
111
|
+
if (state.configLoaded) return;
|
|
112
|
+
state.configLoaded = true;
|
|
113
|
+
|
|
114
|
+
const configPath = pluginOptions.configPath
|
|
115
|
+
? resolveMaybeRelative(rootDir, pluginOptions.configPath)
|
|
116
|
+
: resolveMaybeRelative(rootDir, './round.config.json');
|
|
117
|
+
|
|
118
|
+
state.configPathAbs = configPath;
|
|
119
|
+
|
|
120
|
+
const configDir = configPath ? path.dirname(configPath) : rootDir;
|
|
121
|
+
state.configDir = configDir;
|
|
122
|
+
|
|
123
|
+
let config = null;
|
|
124
|
+
if (configPath && fs.existsSync(configPath)) {
|
|
125
|
+
try {
|
|
126
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
127
|
+
config = JSON.parse(raw);
|
|
128
|
+
} catch {
|
|
129
|
+
config = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const trailingSlash = config?.routing?.trailingSlash;
|
|
134
|
+
state.routingTrailingSlash = trailingSlash !== undefined ? Boolean(trailingSlash) : true;
|
|
135
|
+
|
|
136
|
+
const customTags = config?.htmlTags;
|
|
137
|
+
state.customTags = Array.isArray(customTags) ? customTags : [];
|
|
138
|
+
|
|
139
|
+
state.name = config?.name ?? 'Round';
|
|
140
|
+
|
|
141
|
+
const entryRel = config?.entry;
|
|
142
|
+
state.entryRel = entryRel;
|
|
143
|
+
state.entryAbs = entryRel ? resolveMaybeRelative(configDir, entryRel) : null;
|
|
144
|
+
|
|
145
|
+
const include = pluginOptions.include ?? config?.include ?? [];
|
|
146
|
+
const exclude = pluginOptions.exclude ?? config?.exclude ?? ['./node_modules', './dist'];
|
|
147
|
+
|
|
148
|
+
const includeBase = pluginOptions.include ? rootDir : configDir;
|
|
149
|
+
const excludeBase = pluginOptions.exclude ? rootDir : configDir;
|
|
150
|
+
|
|
151
|
+
state.includeAbs = Array.isArray(include) ? include.map(p => resolveMaybeRelative(includeBase, p)).filter(Boolean) : [];
|
|
152
|
+
state.excludeAbs = Array.isArray(exclude) ? exclude.map(p => resolveMaybeRelative(excludeBase, p)).filter(Boolean) : [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function findBlock(str, startIndex) {
|
|
156
|
+
let open = 0;
|
|
157
|
+
let inSingle = false;
|
|
158
|
+
let inDouble = false;
|
|
159
|
+
let inTemplate = false;
|
|
160
|
+
|
|
161
|
+
let start = -1;
|
|
162
|
+
for (let i = startIndex; i < str.length; i++) {
|
|
163
|
+
const ch = str[i];
|
|
164
|
+
const prev = i > 0 ? str[i - 1] : '';
|
|
165
|
+
|
|
166
|
+
if (!inDouble && !inTemplate && ch === '\'' && prev !== '\\') inSingle = !inSingle;
|
|
167
|
+
else if (!inSingle && !inTemplate && ch === '"' && prev !== '\\') inDouble = !inDouble;
|
|
168
|
+
else if (!inSingle && !inDouble && ch === '`' && prev !== '\\') inTemplate = !inTemplate;
|
|
169
|
+
|
|
170
|
+
if (inSingle || inDouble || inTemplate) continue;
|
|
171
|
+
|
|
172
|
+
if (ch === '{') {
|
|
173
|
+
if (open === 0) start = i;
|
|
174
|
+
open++;
|
|
175
|
+
} else if (ch === '}') {
|
|
176
|
+
open--;
|
|
177
|
+
if (open === 0 && start !== -1) {
|
|
178
|
+
return { start, end: i };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseStartHeadCallArgument(str, fromIndex) {
|
|
186
|
+
const idx = str.indexOf('startHead', fromIndex);
|
|
187
|
+
if (idx === -1) return null;
|
|
188
|
+
|
|
189
|
+
const callIdx = str.indexOf('(', idx);
|
|
190
|
+
if (callIdx === -1) return null;
|
|
191
|
+
|
|
192
|
+
let i = callIdx;
|
|
193
|
+
let paren = 0;
|
|
194
|
+
let inSingle = false;
|
|
195
|
+
let inDouble = false;
|
|
196
|
+
let inTemplate = false;
|
|
197
|
+
|
|
198
|
+
for (; i < str.length; i++) {
|
|
199
|
+
const ch = str[i];
|
|
200
|
+
const prev = i > 0 ? str[i - 1] : '';
|
|
201
|
+
|
|
202
|
+
if (!inDouble && !inTemplate && ch === '\'' && prev !== '\\') inSingle = !inSingle;
|
|
203
|
+
else if (!inSingle && !inTemplate && ch === '"' && prev !== '\\') inDouble = !inDouble;
|
|
204
|
+
else if (!inSingle && !inDouble && ch === '`' && prev !== '\\') inTemplate = !inTemplate;
|
|
205
|
+
|
|
206
|
+
if (inSingle || inDouble || inTemplate) continue;
|
|
207
|
+
|
|
208
|
+
if (ch === '(') paren++;
|
|
209
|
+
else if (ch === ')') {
|
|
210
|
+
paren--;
|
|
211
|
+
if (paren === 0) {
|
|
212
|
+
const inner = str.slice(callIdx + 1, i).trim();
|
|
213
|
+
return { arg: inner, start: idx, end: i + 1 };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function parseStartHeadInDefaultExport(code) {
|
|
222
|
+
// Find `export default function ... { ... }`
|
|
223
|
+
const m = code.match(/export\s+default\s+function\b/);
|
|
224
|
+
const hasAnyCall = /\bstartHead\s*\(/.test(code);
|
|
225
|
+
if (!m || typeof m.index !== 'number') return { headExpr: null, hasAny: hasAnyCall };
|
|
226
|
+
|
|
227
|
+
const fnStart = m.index;
|
|
228
|
+
const braceIdx = code.indexOf('{', fnStart);
|
|
229
|
+
if (braceIdx === -1) return { headExpr: null, hasAny: hasAnyCall };
|
|
230
|
+
|
|
231
|
+
const block = findBlock(code, braceIdx);
|
|
232
|
+
if (!block) return { headExpr: null, hasAny: hasAnyCall };
|
|
233
|
+
|
|
234
|
+
const body = code.slice(block.start + 1, block.end);
|
|
235
|
+
const call = parseStartHeadCallArgument(body, 0);
|
|
236
|
+
return { headExpr: call ? call.arg : null, hasAny: hasAnyCall, hasOutside: hasAnyCall && !call };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function headToHtml(head) {
|
|
240
|
+
if (!head || typeof head !== 'object') return '';
|
|
241
|
+
|
|
242
|
+
let out = '';
|
|
243
|
+
if (typeof head.title === 'string') {
|
|
244
|
+
out += `\n <title>${head.title}</title>`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const meta = head.meta;
|
|
248
|
+
const links = head.links;
|
|
249
|
+
|
|
250
|
+
const renderAttrs = (attrs) => {
|
|
251
|
+
if (!attrs || typeof attrs !== 'object') return '';
|
|
252
|
+
return Object.entries(attrs)
|
|
253
|
+
.filter(([, v]) => v !== null && v !== undefined)
|
|
254
|
+
.map(([k, v]) => ` ${k}="${String(v).replaceAll('"', '"')}"`)
|
|
255
|
+
.join('');
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
if (Array.isArray(meta)) {
|
|
259
|
+
meta.forEach((m) => {
|
|
260
|
+
if (!m) return;
|
|
261
|
+
if (Array.isArray(m) && m.length >= 2) {
|
|
262
|
+
out += `\n <meta name="${String(m[0]).replaceAll('"', '"')}" content="${String(m[1] ?? '').replaceAll('"', '"')}">`;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (typeof m === 'object') {
|
|
266
|
+
out += `\n <meta${renderAttrs(m)}>`;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
} else if (meta && typeof meta === 'object') {
|
|
270
|
+
Object.entries(meta).forEach(([name, content]) => {
|
|
271
|
+
out += `\n <meta name="${String(name).replaceAll('"', '"')}" content="${String(content ?? '').replaceAll('"', '"')}">`;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (Array.isArray(links)) {
|
|
276
|
+
links.forEach((l) => {
|
|
277
|
+
if (!l || typeof l !== 'object') return;
|
|
278
|
+
out += `\n <link${renderAttrs(l)}>`;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// allow raw html injection (advanced escape hatch)
|
|
283
|
+
if (typeof head.raw === 'string' && head.raw.trim()) {
|
|
284
|
+
out += `\n${head.raw}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
name: 'vite-plugin-round',
|
|
292
|
+
enforce: 'pre',
|
|
293
|
+
|
|
294
|
+
transformIndexHtml(html) {
|
|
295
|
+
if (!state.startHeadHtml) return html;
|
|
296
|
+
if (!html.includes('</head>')) return html;
|
|
297
|
+
|
|
298
|
+
// Remove existing <title> to avoid duplicates if we set it.
|
|
299
|
+
let next = html;
|
|
300
|
+
if (state.startHead && typeof state.startHead.title === 'string') {
|
|
301
|
+
next = next.replace(/<title>[\s\S]*?<\/title>/i, '');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return next.replace('</head>', `${state.startHeadHtml}\n</head>`);
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
config(userConfig, env) {
|
|
308
|
+
const rootDir = path.resolve(process.cwd(), userConfig.root ?? '.');
|
|
309
|
+
state.rootDir = rootDir;
|
|
310
|
+
loadConfigOnce(rootDir);
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
define: {
|
|
314
|
+
__ROUND_ROUTING_TRAILING_SLASH__: JSON.stringify(state.routingTrailingSlash),
|
|
315
|
+
__ROUND_CUSTOM_TAGS__: JSON.stringify(state.customTags ?? [])
|
|
316
|
+
},
|
|
317
|
+
esbuild: {
|
|
318
|
+
include: /\.(round|js|jsx|ts|tsx)$/,
|
|
319
|
+
loader: 'jsx',
|
|
320
|
+
jsxFactory: 'createElement',
|
|
321
|
+
jsxFragment: 'Fragment'
|
|
322
|
+
// NOTE: Inject the runtime import in transform() to avoid
|
|
323
|
+
},
|
|
324
|
+
// Ensure .round files are treated as JS/JSX
|
|
325
|
+
resolve: {
|
|
326
|
+
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.round']
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
resolveId(id) {
|
|
332
|
+
return null;
|
|
333
|
+
},
|
|
334
|
+
load(id) {
|
|
335
|
+
if (!isMdRawRequest(id)) return;
|
|
336
|
+
|
|
337
|
+
const fileAbs = stripQuery(id);
|
|
338
|
+
try {
|
|
339
|
+
const raw = fs.readFileSync(fileAbs, 'utf8');
|
|
340
|
+
this.addWatchFile(fileAbs);
|
|
341
|
+
return `export default \`${escapeForJsString(raw)}\`;`;
|
|
342
|
+
} catch {
|
|
343
|
+
this.addWatchFile(fileAbs);
|
|
344
|
+
return 'export default ``;';
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
configureServer(server) {
|
|
349
|
+
loadConfigOnce(server.config.root ?? process.cwd());
|
|
350
|
+
|
|
351
|
+
if (state.configPathAbs) {
|
|
352
|
+
server.watcher.add(state.configPathAbs);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
server.middlewares.use((req, res, next) => {
|
|
356
|
+
if (!req.url) return next();
|
|
357
|
+
const [urlPath] = req.url.split('?');
|
|
358
|
+
if (urlPath && urlPath.endsWith('.md')) {
|
|
359
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
360
|
+
}
|
|
361
|
+
next();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
server.ws.on('round:runtime-error', (payload = {}) => {
|
|
365
|
+
try {
|
|
366
|
+
const message = typeof payload.message === 'string' ? payload.message : 'Runtime error';
|
|
367
|
+
const phase = typeof payload.phase === 'string' && payload.phase ? ` (${payload.phase})` : '';
|
|
368
|
+
const component = typeof payload.component === 'string' && payload.component ? ` in ${payload.component}` : '';
|
|
369
|
+
const header = `[round] Runtime error${component}${phase}: ${message}`;
|
|
370
|
+
|
|
371
|
+
const stack = payload.stack ? String(payload.stack) : '';
|
|
372
|
+
const key = `${header}\n${stack}`;
|
|
373
|
+
const now = Date.now();
|
|
374
|
+
if (lastRuntimeErrorKey === key && (now - lastRuntimeErrorAt) < 2000) return;
|
|
375
|
+
lastRuntimeErrorKey = key;
|
|
376
|
+
lastRuntimeErrorAt = now;
|
|
377
|
+
|
|
378
|
+
server.config.logger.error(header);
|
|
379
|
+
if (stack) server.config.logger.error(stack);
|
|
380
|
+
} catch {
|
|
381
|
+
server.config.logger.error('[round] Runtime error');
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
handleHotUpdate(ctx) {
|
|
387
|
+
if (state.configPathAbs && ctx.file === state.configPathAbs) {
|
|
388
|
+
if (!restartOnConfigChange) return [];
|
|
389
|
+
try {
|
|
390
|
+
if (typeof ctx.server.restart === 'function') {
|
|
391
|
+
ctx.server.restart();
|
|
392
|
+
} else {
|
|
393
|
+
ctx.server.ws.send({ type: 'full-reload' });
|
|
394
|
+
}
|
|
395
|
+
} catch {
|
|
396
|
+
ctx.server.ws.send({ type: 'full-reload' });
|
|
397
|
+
}
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
configurePreviewServer(server) {
|
|
403
|
+
server.middlewares.use((req, res, next) => {
|
|
404
|
+
if (!req.url) return next();
|
|
405
|
+
const [urlPath] = req.url.split('?');
|
|
406
|
+
if (urlPath && urlPath.endsWith('.md')) {
|
|
407
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
408
|
+
}
|
|
409
|
+
next();
|
|
410
|
+
});
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
transform(code, id) {
|
|
414
|
+
if (id.endsWith('.round')) {
|
|
415
|
+
const fileAbs = path.isAbsolute(id) ? id : path.resolve(state.rootDir, id);
|
|
416
|
+
if (!isIncluded(fileAbs, state.includeAbs)) return;
|
|
417
|
+
if (isExcluded(fileAbs, state.excludeAbs)) return;
|
|
418
|
+
|
|
419
|
+
const isEntry = state.entryAbs && normalizePath(fileAbs) === normalizePath(state.entryAbs);
|
|
420
|
+
const parsedHead = parseStartHeadInDefaultExport(code);
|
|
421
|
+
|
|
422
|
+
if (parsedHead.hasAny && !isEntry) {
|
|
423
|
+
this.error(new Error(`startHead() can only be used in the entry module's export default function: ${state.entryAbs ?? '(unknown entry)'}\nFound in: ${fileAbs}`));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (isEntry && parsedHead.hasOutside) {
|
|
427
|
+
this.error(new Error(`startHead() must be called inside the entry module's export default function body (not at top-level).\nEntry: ${fileAbs}`));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (isEntry && parsedHead.headExpr) {
|
|
431
|
+
const trimmed = parsedHead.headExpr.trim();
|
|
432
|
+
if (!trimmed.startsWith('{')) {
|
|
433
|
+
this.error(new Error(`startHead(...) expects an object literal. Example: startHead({ title: 'Home' })\nFound: ${trimmed.slice(0, 60)}...`));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (/\bfunction\b|=>|\bimport\b|\brequire\b|\bprocess\b|\bglobal\b/.test(trimmed)) {
|
|
437
|
+
this.error(new Error('startHead object must be static data (no functions/imports).'));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
let headObj = null;
|
|
441
|
+
try {
|
|
442
|
+
headObj = Function(`"use strict"; return (${trimmed});`)();
|
|
443
|
+
} catch (e) {
|
|
444
|
+
this.error(new Error(`Failed to parse startHead(...) object in ${fileAbs}: ${String(e?.message ?? e)}`));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
state.startHead = headObj;
|
|
448
|
+
state.startHeadHtml = headToHtml(headObj);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
let nextCode = code;
|
|
452
|
+
try {
|
|
453
|
+
nextCode = inlineMarkdownInRound(nextCode, fileAbs, (p) => this.addWatchFile(p));
|
|
454
|
+
} catch (e) {
|
|
455
|
+
// Fail fast in build and show the file that triggered the problem.
|
|
456
|
+
this.error(e);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
let transformedCode = transform(nextCode);
|
|
460
|
+
|
|
461
|
+
if (!/^\s*import\s+\{\s*createElement\s*,\s*Fragment\s*\}\s+from\s+['"][^'"]+['"];?/m.test(transformedCode)) {
|
|
462
|
+
transformedCode = `import { createElement, Fragment } from '${runtimeImport}';\n` + transformedCode;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
code: transformedCode,
|
|
467
|
+
map: null
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export * from './runtime/signals.js';
|
|
2
|
+
export * from './runtime/dom.js';
|
|
3
|
+
export * from './runtime/lifecycle.js';
|
|
4
|
+
export * from './runtime/router.js';
|
|
5
|
+
export * from './runtime/errors.js';
|
|
6
|
+
export * from './runtime/error-store.js';
|
|
7
|
+
export * from './runtime/error-boundary.js';
|
|
8
|
+
export * from './runtime/suspense.js';
|
|
9
|
+
export * from './runtime/context.js';
|
|
10
|
+
export * from './runtime/store.js';
|
|
11
|
+
|
|
12
|
+
import * as Signals from './runtime/signals.js';
|
|
13
|
+
import * as DOM from './runtime/dom.js';
|
|
14
|
+
import * as Lifecycle from './runtime/lifecycle.js';
|
|
15
|
+
import * as Router from './runtime/router.js';
|
|
16
|
+
import * as Errors from './runtime/errors.js';
|
|
17
|
+
import * as Suspense from './runtime/suspense.js';
|
|
18
|
+
import * as Context from './runtime/context.js';
|
|
19
|
+
import * as Store from './runtime/store.js';
|
|
20
|
+
|
|
21
|
+
export function render(Component, container) {
|
|
22
|
+
Lifecycle.initLifecycleRoot(container);
|
|
23
|
+
Errors.initErrorHandling(container);
|
|
24
|
+
try {
|
|
25
|
+
const root = DOM.createElement(Component);
|
|
26
|
+
container.appendChild(root);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
Errors.reportError(e, { phase: 'render', component: Component?.name ?? 'App' });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default {
|
|
33
|
+
...Signals,
|
|
34
|
+
...DOM,
|
|
35
|
+
...Lifecycle,
|
|
36
|
+
...Router,
|
|
37
|
+
...Errors,
|
|
38
|
+
...Suspense,
|
|
39
|
+
...Context,
|
|
40
|
+
...Store,
|
|
41
|
+
render
|
|
42
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createElement } from './dom.js';
|
|
2
|
+
|
|
3
|
+
let nextContextId = 1;
|
|
4
|
+
const contextStack = [];
|
|
5
|
+
|
|
6
|
+
function pushContext(values) {
|
|
7
|
+
contextStack.push(values);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function popContext() {
|
|
11
|
+
contextStack.pop();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read the current value of a context from the tree.
|
|
16
|
+
* @template T
|
|
17
|
+
* @param {Context<T>} ctx The context object.
|
|
18
|
+
* @returns {T} The current context value.
|
|
19
|
+
*/
|
|
20
|
+
export function readContext(ctx) {
|
|
21
|
+
for (let i = contextStack.length - 1; i >= 0; i--) {
|
|
22
|
+
const layer = contextStack[i];
|
|
23
|
+
if (layer && Object.prototype.hasOwnProperty.call(layer, ctx.id)) {
|
|
24
|
+
return layer[ctx.id];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return ctx.defaultValue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a new Context object for sharing state between components.
|
|
32
|
+
* @template T
|
|
33
|
+
* @param {T} [defaultValue] The value used when no provider is found.
|
|
34
|
+
* @returns {Context<T>} The context object with a `Provider` component.
|
|
35
|
+
*/
|
|
36
|
+
export function createContext(defaultValue) {
|
|
37
|
+
const ctx = {
|
|
38
|
+
id: nextContextId++,
|
|
39
|
+
defaultValue,
|
|
40
|
+
Provider: null
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function Provider(props = {}) {
|
|
44
|
+
const children = props.children;
|
|
45
|
+
|
|
46
|
+
// Push context now so that any createElement/appendChild called
|
|
47
|
+
// during the instantiation of this Provider branch picks it up immediately.
|
|
48
|
+
pushContext({ [ctx.id]: props.value });
|
|
49
|
+
try {
|
|
50
|
+
// We use a span to handle reactive value updates and dynamic children.
|
|
51
|
+
return createElement('span', { style: { display: 'contents' } }, () => {
|
|
52
|
+
// Read current value (reactive if it's a signal)
|
|
53
|
+
const val = (typeof props.value === 'function' && props.value.peek) ? props.value() : props.value;
|
|
54
|
+
|
|
55
|
+
// Push it during the effect run too! This ensures that anything returned
|
|
56
|
+
// from this callback (which might trigger more appendChild calls) sees the context.
|
|
57
|
+
pushContext({ [ctx.id]: val });
|
|
58
|
+
try {
|
|
59
|
+
return children;
|
|
60
|
+
} finally {
|
|
61
|
+
popContext();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
} finally {
|
|
65
|
+
popContext();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
ctx.Provider = Provider;
|
|
70
|
+
return ctx;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function bindContext(ctx) {
|
|
74
|
+
return () => {
|
|
75
|
+
const provided = readContext(ctx);
|
|
76
|
+
if (typeof provided === 'function') {
|
|
77
|
+
try {
|
|
78
|
+
return provided();
|
|
79
|
+
} catch {
|
|
80
|
+
return provided;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return provided;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function captureContext() {
|
|
88
|
+
return contextStack.slice();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function runInContext(snapshot, fn) {
|
|
92
|
+
const prev = contextStack.slice();
|
|
93
|
+
contextStack.length = 0;
|
|
94
|
+
contextStack.push(...snapshot);
|
|
95
|
+
try {
|
|
96
|
+
return fn();
|
|
97
|
+
} finally {
|
|
98
|
+
contextStack.length = 0;
|
|
99
|
+
contextStack.push(...prev);
|
|
100
|
+
}
|
|
101
|
+
}
|