round-core 0.0.1
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/README.md +354 -0
- package/Round.png +0 -0
- package/bun.lock +414 -0
- package/cli.js +2 -0
- package/index.html +19 -0
- package/index.js +2 -0
- package/package.json +40 -0
- package/src/cli.js +599 -0
- package/src/compiler/index.js +2 -0
- package/src/compiler/transformer.js +395 -0
- package/src/compiler/vite-plugin.js +461 -0
- package/src/index.js +45 -0
- package/src/runtime/context.js +62 -0
- package/src/runtime/dom.js +345 -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 +142 -0
- package/src/runtime/markdown.js +72 -0
- package/src/runtime/router.js +371 -0
- package/src/runtime/signals.js +510 -0
- package/src/runtime/store.js +208 -0
- package/src/runtime/suspense.js +106 -0
- package/vite.config.js +10 -0
- package/vitest.config.js +8 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { createServer, build as viteBuild, preview as vitePreview } from 'vite';
|
|
7
|
+
import RoundPlugin from './compiler/vite-plugin.js';
|
|
8
|
+
|
|
9
|
+
function normalizePath(p) {
|
|
10
|
+
return p.replaceAll('\\', '/');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const colors = {
|
|
14
|
+
reset: '\x1b[0m',
|
|
15
|
+
dim: '\x1b[2m',
|
|
16
|
+
bold: '\x1b[1m',
|
|
17
|
+
red: '\x1b[31m',
|
|
18
|
+
green: '\x1b[32m',
|
|
19
|
+
yellow: '\x1b[33m',
|
|
20
|
+
blue: '\x1b[34m',
|
|
21
|
+
magenta: '\x1b[35m',
|
|
22
|
+
cyan: '\x1b[36m',
|
|
23
|
+
gray: '\x1b[90m'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function c(text, color) {
|
|
27
|
+
return `${colors[color] ?? ''}${text}${colors.reset}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class CliError extends Error {
|
|
31
|
+
constructor(message, options = {}) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'CliError';
|
|
34
|
+
this.code = Number.isFinite(Number(options.code)) ? Number(options.code) : 1;
|
|
35
|
+
this.showHelp = Boolean(options.showHelp);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function printError(message) {
|
|
40
|
+
const msg = String(message ?? '').trimEnd();
|
|
41
|
+
if (!msg) return;
|
|
42
|
+
process.stderr.write(`${c('Error:', 'red')} ${msg}\n`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getRoundVersion() {
|
|
46
|
+
try {
|
|
47
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
const pkgPath = path.resolve(here, '..', 'package.json');
|
|
49
|
+
const raw = fs.readFileSync(pkgPath, 'utf8');
|
|
50
|
+
const json = JSON.parse(raw);
|
|
51
|
+
return typeof json?.version === 'string' ? json.version : null;
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function banner(title) {
|
|
58
|
+
const v = getRoundVersion();
|
|
59
|
+
const name = c('ROUND', 'cyan');
|
|
60
|
+
const version = v ? c(`v${v}`, 'gray') : '';
|
|
61
|
+
process.stdout.write(`\n ${name} ${version}`.trimEnd() + `\n`);
|
|
62
|
+
process.stdout.write(`\n`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function createViteLogger() {
|
|
66
|
+
let hasError = false;
|
|
67
|
+
const noop = () => { };
|
|
68
|
+
return {
|
|
69
|
+
hasErrorLogged: () => hasError,
|
|
70
|
+
info(msg) {
|
|
71
|
+
const s = String(msg ?? '');
|
|
72
|
+
if (!s) return;
|
|
73
|
+
if (s.includes('hmr update') || s.includes('page reload') || s.includes('hot updated') || s.includes('modules transformed')) {
|
|
74
|
+
process.stdout.write(`${c('[round]', 'cyan')} ${s.replace(/^\[vite\]\s*/i, '')}\n`);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
warn(msg) {
|
|
78
|
+
process.stderr.write(String(msg) + '\n');
|
|
79
|
+
},
|
|
80
|
+
warnOnce(msg) {
|
|
81
|
+
process.stderr.write(String(msg) + '\n');
|
|
82
|
+
},
|
|
83
|
+
clearScreen: noop,
|
|
84
|
+
error(msg) {
|
|
85
|
+
hasError = true;
|
|
86
|
+
const s = String(msg ?? '');
|
|
87
|
+
if (s.startsWith('[round] Runtime error')) {
|
|
88
|
+
process.stderr.write(`${c(s, 'red')}\n`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (/^\s*at\s+/.test(s) || s.includes('http://localhost:')) {
|
|
92
|
+
process.stderr.write(`${c(s, 'gray')}\n`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
process.stderr.write(s + '\n');
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function printUrls(resolvedUrls, base = '/', ms = null) {
|
|
101
|
+
const local = resolvedUrls?.local?.[0];
|
|
102
|
+
const network = resolvedUrls?.network?.[0];
|
|
103
|
+
const label = c('ROUND', 'cyan');
|
|
104
|
+
const ready = c('ready', 'green');
|
|
105
|
+
const inMs = (typeof ms === 'number') ? `${c('in', 'gray')} ${c(`${ms} ms`, 'gray')}` : '';
|
|
106
|
+
process.stdout.write(` ${label} ${ready}${inMs ? ' ' + inMs : ''}\n\n`);
|
|
107
|
+
if (local) process.stdout.write(` ${c('➜', 'green')} ${c('Local:', 'green')} ${local}\n`);
|
|
108
|
+
if (network) process.stdout.write(` ${c('➜', 'green')} ${c('Network:', 'green')} ${network}\n`);
|
|
109
|
+
process.stdout.write(`\n`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveFrom(baseDir, p) {
|
|
113
|
+
if (!p) return null;
|
|
114
|
+
if (path.isAbsolute(p)) return p;
|
|
115
|
+
return path.resolve(baseDir, p);
|
|
116
|
+
}
|
|
117
|
+
|
|
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
|
+
|
|
145
|
+
function parseArgs(argv) {
|
|
146
|
+
const args = { _: [] };
|
|
147
|
+
for (let i = 0; i < argv.length; i++) {
|
|
148
|
+
const a = argv[i];
|
|
149
|
+
if (!a) continue;
|
|
150
|
+
if (a === '--help' || a === '-h') {
|
|
151
|
+
args.help = true;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (a.startsWith('--')) {
|
|
155
|
+
const eq = a.indexOf('=');
|
|
156
|
+
if (eq !== -1) {
|
|
157
|
+
const k = a.slice(2, eq);
|
|
158
|
+
const v = a.slice(eq + 1);
|
|
159
|
+
args[k] = v;
|
|
160
|
+
} else {
|
|
161
|
+
const k = a.slice(2);
|
|
162
|
+
const next = argv[i + 1];
|
|
163
|
+
if (next && !next.startsWith('-')) {
|
|
164
|
+
args[k] = next;
|
|
165
|
+
i++;
|
|
166
|
+
} else {
|
|
167
|
+
args[k] = true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
args._.push(a);
|
|
173
|
+
}
|
|
174
|
+
return args;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function printHelp() {
|
|
178
|
+
const header = `${c('round', 'cyan')} ${c('(CLI)', 'gray')}`;
|
|
179
|
+
process.stdout.write(
|
|
180
|
+
[
|
|
181
|
+
header,
|
|
182
|
+
'',
|
|
183
|
+
c('Usage', 'bold') + ':',
|
|
184
|
+
` ${c('round', 'cyan')} dev ${c('[--config <path>] [--root <path>]', 'gray')}`,
|
|
185
|
+
` ${c('round', 'cyan')} build ${c('[--config <path>] [--root <path>]', 'gray')}`,
|
|
186
|
+
` ${c('round', 'cyan')} preview ${c('[--config <path>] [--root <path>]', 'gray')}`,
|
|
187
|
+
` ${c('round', 'cyan')} init ${c('<name>', 'yellow')}`,
|
|
188
|
+
'',
|
|
189
|
+
c('Options', 'bold') + ':',
|
|
190
|
+
` ${c('--config', 'yellow')} ${c('Path to round.config.json', 'gray')} ${c('(default: ./round.config.json)', 'gray')}`,
|
|
191
|
+
` ${c('--root', 'yellow')} ${c('Project root', 'gray')} ${c('(default: process.cwd())', 'gray')}`,
|
|
192
|
+
` ${c('-h, --help', 'yellow')} ${c('Show this help', 'gray')}`,
|
|
193
|
+
''
|
|
194
|
+
].join('\n')
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function ensureDir(dir) {
|
|
199
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function writeFileIfMissing(filePath, content) {
|
|
203
|
+
if (fs.existsSync(filePath)) {
|
|
204
|
+
throw new Error(`File already exists: ${filePath}`);
|
|
205
|
+
}
|
|
206
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function runInit({ name }) {
|
|
210
|
+
if (!name || typeof name !== 'string') {
|
|
211
|
+
throw new CliError(
|
|
212
|
+
`Missing project name.\n\nUsage:\n round init <name>`,
|
|
213
|
+
{ code: 1 }
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const mode = 'spa';
|
|
218
|
+
|
|
219
|
+
const projectDir = path.resolve(process.cwd(), name);
|
|
220
|
+
const srcDir = path.join(projectDir, 'src');
|
|
221
|
+
|
|
222
|
+
ensureDir(srcDir);
|
|
223
|
+
|
|
224
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
225
|
+
const configPath = path.join(projectDir, 'round.config.json');
|
|
226
|
+
const viteConfigPath = path.join(projectDir, 'vite.config.js');
|
|
227
|
+
const indexHtmlPath = path.join(projectDir, 'index.html');
|
|
228
|
+
const appRoundPath = path.join(srcDir, 'app.round');
|
|
229
|
+
const counterRoundPath = path.join(srcDir, 'counter.round');
|
|
230
|
+
|
|
231
|
+
writeFileIfMissing(pkgPath, JSON.stringify({
|
|
232
|
+
name,
|
|
233
|
+
private: true,
|
|
234
|
+
version: '0.0.1',
|
|
235
|
+
type: 'module',
|
|
236
|
+
scripts: {
|
|
237
|
+
dev: 'round dev',
|
|
238
|
+
build: 'round build',
|
|
239
|
+
preview: 'round preview'
|
|
240
|
+
},
|
|
241
|
+
dependencies: {
|
|
242
|
+
'round-core': '^0.0.1'
|
|
243
|
+
},
|
|
244
|
+
devDependencies: {
|
|
245
|
+
vite: '^5.0.0'
|
|
246
|
+
}
|
|
247
|
+
}, null, 4) + '\n');
|
|
248
|
+
|
|
249
|
+
writeFileIfMissing(configPath, JSON.stringify({
|
|
250
|
+
mode,
|
|
251
|
+
entry: './src/app.round',
|
|
252
|
+
public: './public',
|
|
253
|
+
output: './dist',
|
|
254
|
+
include: ['./src'],
|
|
255
|
+
exclude: ['./node_modules', './dist'],
|
|
256
|
+
dev: {
|
|
257
|
+
port: 5173,
|
|
258
|
+
open: false,
|
|
259
|
+
hmr: true
|
|
260
|
+
},
|
|
261
|
+
build: {
|
|
262
|
+
minify: true,
|
|
263
|
+
sourcemap: false,
|
|
264
|
+
target: 'es2020',
|
|
265
|
+
splitting: true
|
|
266
|
+
},
|
|
267
|
+
routing: {
|
|
268
|
+
base: '/',
|
|
269
|
+
trailingSlash: true
|
|
270
|
+
}
|
|
271
|
+
}, null, 4) + '\n');
|
|
272
|
+
|
|
273
|
+
writeFileIfMissing(viteConfigPath, [
|
|
274
|
+
"import { defineConfig } from 'vite';",
|
|
275
|
+
"import RoundPlugin from 'round-core/src/compiler/vite-plugin.js';",
|
|
276
|
+
'',
|
|
277
|
+
'export default defineConfig({',
|
|
278
|
+
" plugins: [RoundPlugin({ configPath: './round.config.json' })],",
|
|
279
|
+
' server: {',
|
|
280
|
+
' port: 5173',
|
|
281
|
+
' }',
|
|
282
|
+
'});',
|
|
283
|
+
''
|
|
284
|
+
].join('\n'));
|
|
285
|
+
|
|
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
|
+
|
|
307
|
+
writeFileIfMissing(appRoundPath, [
|
|
308
|
+
"import { Route } from 'round-core';",
|
|
309
|
+
'import { Counter } from "./counter"',
|
|
310
|
+
'',
|
|
311
|
+
'export default function App() {',
|
|
312
|
+
' return (',
|
|
313
|
+
' <div style={{display: "flex", flexDirection: "column", alignItems: "center"}}>',
|
|
314
|
+
' <Route route="/" title="Home">',
|
|
315
|
+
' <Counter />',
|
|
316
|
+
' </Route>',
|
|
317
|
+
' </div>',
|
|
318
|
+
' )',
|
|
319
|
+
'}',
|
|
320
|
+
''
|
|
321
|
+
].join('\n'));
|
|
322
|
+
|
|
323
|
+
writeFileIfMissing(counterRoundPath, [
|
|
324
|
+
"import { signal } from 'round-core';",
|
|
325
|
+
'',
|
|
326
|
+
'export function Counter() {',
|
|
327
|
+
' const count = signal(0)',
|
|
328
|
+
'',
|
|
329
|
+
' return (',
|
|
330
|
+
" <div style={{ padding: '16px', fontFamily: 'system-ui' }}>",
|
|
331
|
+
" <h1 style={{ fontSize: '32px', fontWeight: '700', marginBottom: '12px' }}>",
|
|
332
|
+
' Counter: {count()}',
|
|
333
|
+
' </h1>',
|
|
334
|
+
'',
|
|
335
|
+
" <div style={{ display: 'flex', gap: '8px' }}>",
|
|
336
|
+
" <button onClick={() => count(count() + 1)} style={{ padding: '8px 12px', borderRadius: '8px' }}>",
|
|
337
|
+
' Increment',
|
|
338
|
+
' </button>',
|
|
339
|
+
'',
|
|
340
|
+
" <button onClick={() => count(count() - 1)} style={{ padding: '8px 12px', borderRadius: '8px' }}>",
|
|
341
|
+
' Decrement',
|
|
342
|
+
' </button>',
|
|
343
|
+
'',
|
|
344
|
+
" <button onClick={() => count(0)} style={{ padding: '8px 12px', borderRadius: '8px' }}>",
|
|
345
|
+
' Reset',
|
|
346
|
+
' </button>',
|
|
347
|
+
' </div>',
|
|
348
|
+
' </div>',
|
|
349
|
+
' )',
|
|
350
|
+
'}',
|
|
351
|
+
''
|
|
352
|
+
].join('\n'));
|
|
353
|
+
|
|
354
|
+
process.stdout.write(`\n${c('Project created:', 'green')} ${projectDir}\n`);
|
|
355
|
+
process.stdout.write(`${c('Mode:', 'cyan')} ${mode}\n\n`);
|
|
356
|
+
process.stdout.write(`${c('Next steps:', 'bold')}\n`);
|
|
357
|
+
process.stdout.write(`${c(' 1)', 'cyan')} cd ${name}\n`);
|
|
358
|
+
process.stdout.write(`${c(' 2)', 'cyan')} npm install ${c('(or bun install)', 'gray')}\n`);
|
|
359
|
+
process.stdout.write(`${c(' 3)', 'cyan')} npm run dev\n\n`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function loadRoundConfig(configPathAbs) {
|
|
363
|
+
const raw = fs.readFileSync(configPathAbs, 'utf8');
|
|
364
|
+
const json = JSON.parse(raw);
|
|
365
|
+
return json && typeof json === 'object' ? json : {};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function coerceNumber(v, fallback) {
|
|
369
|
+
const n = Number(v);
|
|
370
|
+
return Number.isFinite(n) ? n : fallback;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function runDev({ rootDir, configPathAbs, config }) {
|
|
374
|
+
const startedAt = Date.now();
|
|
375
|
+
const configDir = path.dirname(configPathAbs);
|
|
376
|
+
|
|
377
|
+
const entryAbs = config?.entry ? resolveFrom(configDir, config.entry) : null;
|
|
378
|
+
if (!entryAbs || !fs.existsSync(entryAbs)) {
|
|
379
|
+
throw new Error(`Entry not found: ${entryAbs ?? '(missing entry)'} (config: ${configPathAbs})`);
|
|
380
|
+
}
|
|
381
|
+
const entryRel = normalizePath(path.relative(rootDir, entryAbs));
|
|
382
|
+
ensureIndexHtml(rootDir, entryRel, config?.name ?? 'Round');
|
|
383
|
+
|
|
384
|
+
let viteServer = null;
|
|
385
|
+
let restarting = false;
|
|
386
|
+
let restartTimer = null;
|
|
387
|
+
|
|
388
|
+
const startServer = async (nextConfig, { showBanner, showReady } = { showBanner: true, showReady: true }) => {
|
|
389
|
+
const cfgDir = path.dirname(configPathAbs);
|
|
390
|
+
|
|
391
|
+
const entryAbs2 = nextConfig?.entry ? resolveFrom(cfgDir, nextConfig.entry) : null;
|
|
392
|
+
if (!entryAbs2 || !fs.existsSync(entryAbs2)) {
|
|
393
|
+
throw new Error(`Entry not found: ${entryAbs2 ?? '(missing entry)'} (config: ${configPathAbs})`);
|
|
394
|
+
}
|
|
395
|
+
const entryRel2 = normalizePath(path.relative(rootDir, entryAbs2));
|
|
396
|
+
ensureIndexHtml(rootDir, entryRel2, nextConfig?.name ?? 'Round');
|
|
397
|
+
|
|
398
|
+
const serverPort2 = coerceNumber(nextConfig?.dev?.port, 5173);
|
|
399
|
+
const open2 = Boolean(nextConfig?.dev?.open);
|
|
400
|
+
const base2 = nextConfig?.routing?.base ?? '/';
|
|
401
|
+
|
|
402
|
+
if (showBanner) {
|
|
403
|
+
banner('Dev Server');
|
|
404
|
+
process.stdout.write(`${c(' Config', 'gray')} ${configPathAbs}\n`);
|
|
405
|
+
process.stdout.write(`${c(' Base', 'gray')} ${base2}\n`);
|
|
406
|
+
process.stdout.write(`${c(' Port', 'gray')} ${serverPort2}\n\n`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const server = await createServer({
|
|
410
|
+
configFile: false,
|
|
411
|
+
root: rootDir,
|
|
412
|
+
base: base2,
|
|
413
|
+
logLevel: 'info',
|
|
414
|
+
customLogger: createViteLogger(),
|
|
415
|
+
plugins: [RoundPlugin({
|
|
416
|
+
configPath: normalizePath(path.relative(rootDir, configPathAbs)),
|
|
417
|
+
restartOnConfigChange: false
|
|
418
|
+
})],
|
|
419
|
+
server: {
|
|
420
|
+
port: serverPort2,
|
|
421
|
+
open: open2
|
|
422
|
+
},
|
|
423
|
+
publicDir: nextConfig?.public ? resolveFrom(cfgDir, nextConfig.public) : undefined
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
await server.listen();
|
|
427
|
+
|
|
428
|
+
if (showReady) {
|
|
429
|
+
const ms = Date.now() - startedAt;
|
|
430
|
+
printUrls(server.resolvedUrls, base2, ms);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return server;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
viteServer = await startServer(config, { showBanner: true, showReady: true });
|
|
437
|
+
|
|
438
|
+
if (typeof fs.watch === 'function') {
|
|
439
|
+
try {
|
|
440
|
+
fs.watch(configPathAbs, { persistent: true }, () => {
|
|
441
|
+
if (restartTimer) clearTimeout(restartTimer);
|
|
442
|
+
restartTimer = setTimeout(async () => {
|
|
443
|
+
if (restarting) return;
|
|
444
|
+
restarting = true;
|
|
445
|
+
try {
|
|
446
|
+
const next = loadRoundConfig(configPathAbs);
|
|
447
|
+
process.stdout.write(`\n${c('[round]', 'cyan')} ${c('config changed', 'gray')} ${c('restarting dev server...', 'gray')}\n`);
|
|
448
|
+
if (viteServer) await viteServer.close();
|
|
449
|
+
viteServer = await startServer(next, { showBanner: true, showReady: true });
|
|
450
|
+
} catch (e) {
|
|
451
|
+
process.stderr.write(String(e?.stack ?? e?.message ?? e) + '\n');
|
|
452
|
+
} finally {
|
|
453
|
+
restarting = false;
|
|
454
|
+
}
|
|
455
|
+
}, 150);
|
|
456
|
+
});
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function runBuild({ rootDir, configPathAbs, config }) {
|
|
463
|
+
const startedAt = Date.now();
|
|
464
|
+
const configDir = path.dirname(configPathAbs);
|
|
465
|
+
|
|
466
|
+
const entryAbs = config?.entry ? resolveFrom(configDir, config.entry) : null;
|
|
467
|
+
if (!entryAbs || !fs.existsSync(entryAbs)) {
|
|
468
|
+
throw new Error(`Entry not found: ${entryAbs ?? '(missing entry)'} (config: ${configPathAbs})`);
|
|
469
|
+
}
|
|
470
|
+
const entryRel = normalizePath(path.relative(rootDir, entryAbs));
|
|
471
|
+
ensureIndexHtml(rootDir, entryRel, config?.name ?? 'Round');
|
|
472
|
+
|
|
473
|
+
const outDir = config?.output ? resolveFrom(configDir, config.output) : resolveFrom(rootDir, './dist');
|
|
474
|
+
const base = config?.routing?.base ?? '/';
|
|
475
|
+
|
|
476
|
+
banner('Build');
|
|
477
|
+
process.stdout.write(`${c(' Config', 'gray')} ${configPathAbs}\n`);
|
|
478
|
+
process.stdout.write(`${c(' OutDir', 'gray')} ${outDir}\n`);
|
|
479
|
+
process.stdout.write(`${c(' Base', 'gray')} ${base}\n\n`);
|
|
480
|
+
|
|
481
|
+
await viteBuild({
|
|
482
|
+
configFile: false,
|
|
483
|
+
root: rootDir,
|
|
484
|
+
base,
|
|
485
|
+
logLevel: 'warn',
|
|
486
|
+
customLogger: createViteLogger(),
|
|
487
|
+
plugins: [RoundPlugin({ configPath: normalizePath(path.relative(rootDir, configPathAbs)) })],
|
|
488
|
+
publicDir: config?.public ? resolveFrom(configDir, config.public) : undefined,
|
|
489
|
+
build: {
|
|
490
|
+
outDir,
|
|
491
|
+
sourcemap: Boolean(config?.build?.sourcemap),
|
|
492
|
+
minify: config?.build?.minify !== undefined ? config.build.minify : true,
|
|
493
|
+
target: config?.build?.target ?? 'es2020'
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const ms = Date.now() - startedAt;
|
|
498
|
+
process.stdout.write(`\n ${c('ROUND', 'cyan')} ${c('built', 'green')} ${c('in', 'gray')} ${c(`${ms} ms`, 'gray')}\n\n`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function runPreview({ rootDir, configPathAbs, config }) {
|
|
502
|
+
const configDir = path.dirname(configPathAbs);
|
|
503
|
+
const outDir = config?.output ? resolveFrom(configDir, config.output) : resolveFrom(rootDir, './dist');
|
|
504
|
+
const base = config?.routing?.base ?? '/';
|
|
505
|
+
const previewPort = coerceNumber(config?.dev?.port, 5173);
|
|
506
|
+
|
|
507
|
+
const entryAbs = config?.entry ? resolveFrom(configDir, config.entry) : null;
|
|
508
|
+
if (entryAbs && fs.existsSync(entryAbs)) {
|
|
509
|
+
const entryRel = normalizePath(path.relative(rootDir, entryAbs));
|
|
510
|
+
ensureIndexHtml(rootDir, entryRel, config?.name ?? 'Round');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
banner('Preview');
|
|
514
|
+
process.stdout.write(`${c('Config:', 'cyan')} ${configPathAbs}\n`);
|
|
515
|
+
process.stdout.write(`${c('OutDir:', 'cyan')} ${outDir}\n`);
|
|
516
|
+
process.stdout.write(`${c('Base:', 'cyan')} ${base}\n`);
|
|
517
|
+
process.stdout.write(`${c('Port:', 'cyan')} ${previewPort}\n\n`);
|
|
518
|
+
|
|
519
|
+
if (!fs.existsSync(outDir)) {
|
|
520
|
+
process.stdout.write(`${c('Error:', 'red')} Build output not found: ${outDir}\n`);
|
|
521
|
+
process.stdout.write(`${c('Hint:', 'gray')} Run \"round build\" first.\n`);
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const server = await vitePreview({
|
|
526
|
+
configFile: false,
|
|
527
|
+
root: rootDir,
|
|
528
|
+
base,
|
|
529
|
+
logLevel: 'warn',
|
|
530
|
+
customLogger: createViteLogger(),
|
|
531
|
+
preview: {
|
|
532
|
+
port: previewPort
|
|
533
|
+
},
|
|
534
|
+
build: {
|
|
535
|
+
outDir
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
printUrls(server.resolvedUrls, base);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function main() {
|
|
543
|
+
const args = parseArgs(process.argv.slice(2));
|
|
544
|
+
const cmd = args._[0];
|
|
545
|
+
|
|
546
|
+
if (args.help || !cmd || (cmd !== 'dev' && cmd !== 'build' && cmd !== 'preview' && cmd !== 'init')) {
|
|
547
|
+
printHelp();
|
|
548
|
+
process.exit(cmd ? 1 : 0);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (cmd === 'init') {
|
|
552
|
+
const name = args._[1];
|
|
553
|
+
await runInit({ name, template: args.template });
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const initialRootDir = path.resolve(process.cwd(), args.root ?? '.');
|
|
558
|
+
const configPathAbs = resolveFrom(initialRootDir, args.config ?? './round.config.json');
|
|
559
|
+
|
|
560
|
+
const rootDir = args.root
|
|
561
|
+
? initialRootDir
|
|
562
|
+
: path.dirname(configPathAbs);
|
|
563
|
+
|
|
564
|
+
if (!fs.existsSync(configPathAbs)) {
|
|
565
|
+
throw new CliError(`Config not found: ${configPathAbs}`, { code: 1 });
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
let config;
|
|
569
|
+
try {
|
|
570
|
+
config = loadRoundConfig(configPathAbs);
|
|
571
|
+
} catch (e) {
|
|
572
|
+
throw new CliError(`Failed to read config: ${configPathAbs}\n${String(e?.message ?? e)}`, { code: 1 });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (cmd === 'dev') {
|
|
576
|
+
await runDev({ rootDir, configPathAbs, config });
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (cmd === 'build') {
|
|
581
|
+
await runBuild({ rootDir, configPathAbs, config });
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (cmd === 'preview') {
|
|
586
|
+
await runPreview({ rootDir, configPathAbs, config });
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
main().catch((e) => {
|
|
591
|
+
if (e && e.name === 'CliError') {
|
|
592
|
+
printError(e.message);
|
|
593
|
+
if (e.showHelp) printHelp();
|
|
594
|
+
process.exit(e.code ?? 1);
|
|
595
|
+
}
|
|
596
|
+
const msg = String(e?.stack ?? e?.message ?? e);
|
|
597
|
+
process.stderr.write(c(msg, 'red') + '\n');
|
|
598
|
+
process.exit(1);
|
|
599
|
+
});
|