@nowline/cli 0.2.0
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/LICENSE +190 -0
- package/README.md +372 -0
- package/dist/cli/args.d.ts +54 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +165 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/formats.d.ts +61 -0
- package/dist/cli/formats.d.ts.map +1 -0
- package/dist/cli/formats.js +153 -0
- package/dist/cli/formats.js.map +1 -0
- package/dist/cli/help.d.ts +3 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +90 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/output-path.d.ts +57 -0
- package/dist/cli/output-path.d.ts.map +1 -0
- package/dist/cli/output-path.js +70 -0
- package/dist/cli/output-path.js.map +1 -0
- package/dist/commands/init.d.ts +20 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +80 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/render.d.ts +15 -0
- package/dist/commands/render.d.ts.map +1 -0
- package/dist/commands/render.js +435 -0
- package/dist/commands/render.js.map +1 -0
- package/dist/commands/serve.d.ts +16 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +287 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/convert/parse-json.d.ts +7 -0
- package/dist/convert/parse-json.d.ts.map +1 -0
- package/dist/convert/parse-json.js +34 -0
- package/dist/convert/parse-json.js.map +1 -0
- package/dist/convert/printer.d.ts +6 -0
- package/dist/convert/printer.d.ts.map +1 -0
- package/dist/convert/printer.js +334 -0
- package/dist/convert/printer.js.map +1 -0
- package/dist/convert/schema.d.ts +33 -0
- package/dist/convert/schema.d.ts.map +1 -0
- package/dist/convert/schema.js +77 -0
- package/dist/convert/schema.js.map +1 -0
- package/dist/core/parse.d.ts +24 -0
- package/dist/core/parse.d.ts.map +1 -0
- package/dist/core/parse.js +58 -0
- package/dist/core/parse.js.map +1 -0
- package/dist/diagnostics/adapt.d.ts +46 -0
- package/dist/diagnostics/adapt.d.ts.map +1 -0
- package/dist/diagnostics/adapt.js +109 -0
- package/dist/diagnostics/adapt.js.map +1 -0
- package/dist/diagnostics/format.d.ts +18 -0
- package/dist/diagnostics/format.d.ts.map +1 -0
- package/dist/diagnostics/format.js +41 -0
- package/dist/diagnostics/format.js.map +1 -0
- package/dist/diagnostics/index.d.ts +5 -0
- package/dist/diagnostics/index.d.ts.map +1 -0
- package/dist/diagnostics/index.js +5 -0
- package/dist/diagnostics/index.js.map +1 -0
- package/dist/diagnostics/json.d.ts +8 -0
- package/dist/diagnostics/json.d.ts.map +1 -0
- package/dist/diagnostics/json.js +24 -0
- package/dist/diagnostics/json.js.map +1 -0
- package/dist/diagnostics/model.d.ts +44 -0
- package/dist/diagnostics/model.d.ts.map +1 -0
- package/dist/diagnostics/model.js +2 -0
- package/dist/diagnostics/model.js.map +1 -0
- package/dist/diagnostics/text.d.ts +6 -0
- package/dist/diagnostics/text.d.ts.map +1 -0
- package/dist/diagnostics/text.js +43 -0
- package/dist/diagnostics/text.js.map +1 -0
- package/dist/generated/templates.d.ts +4 -0
- package/dist/generated/templates.d.ts.map +1 -0
- package/dist/generated/templates.js +9 -0
- package/dist/generated/templates.js.map +1 -0
- package/dist/generated/version.d.ts +11 -0
- package/dist/generated/version.d.ts.map +1 -0
- package/dist/generated/version.js +8 -0
- package/dist/generated/version.js.map +1 -0
- package/dist/i18n/locale.d.ts +56 -0
- package/dist/i18n/locale.d.ts.map +1 -0
- package/dist/i18n/locale.js +107 -0
- package/dist/i18n/locale.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/io/config.d.ts +2 -0
- package/dist/io/config.d.ts.map +1 -0
- package/dist/io/config.js +5 -0
- package/dist/io/config.js.map +1 -0
- package/dist/io/exit-codes.d.ts +12 -0
- package/dist/io/exit-codes.d.ts.map +1 -0
- package/dist/io/exit-codes.js +15 -0
- package/dist/io/exit-codes.js.map +1 -0
- package/dist/io/read.d.ts +13 -0
- package/dist/io/read.d.ts.map +1 -0
- package/dist/io/read.js +53 -0
- package/dist/io/read.js.map +1 -0
- package/dist/io/write.d.ts +32 -0
- package/dist/io/write.d.ts.map +1 -0
- package/dist/io/write.js +61 -0
- package/dist/io/write.js.map +1 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +20 -0
- package/dist/version.js.map +1 -0
- package/man/fr/nowline.1 +424 -0
- package/man/fr/nowline.5 +1864 -0
- package/man/nowline.1 +517 -0
- package/man/nowline.5 +1784 -0
- package/package.json +66 -0
- package/scripts/bundle-templates.mjs +105 -0
- package/scripts/compile.mjs +131 -0
- package/src/cli/args.ts +252 -0
- package/src/cli/formats.ts +207 -0
- package/src/cli/help.ts +92 -0
- package/src/cli/output-path.ts +98 -0
- package/src/commands/init.ts +99 -0
- package/src/commands/render.ts +566 -0
- package/src/commands/serve.ts +322 -0
- package/src/convert/parse-json.ts +57 -0
- package/src/convert/printer.ts +376 -0
- package/src/convert/schema.ts +105 -0
- package/src/core/parse.ts +93 -0
- package/src/diagnostics/adapt.ts +148 -0
- package/src/diagnostics/format.ts +70 -0
- package/src/diagnostics/index.ts +4 -0
- package/src/diagnostics/json.ts +30 -0
- package/src/diagnostics/model.ts +48 -0
- package/src/diagnostics/text.ts +62 -0
- package/src/generated/templates.ts +12 -0
- package/src/generated/version.ts +18 -0
- package/src/i18n/locale.ts +133 -0
- package/src/index.ts +60 -0
- package/src/io/config.ts +11 -0
- package/src/io/exit-codes.ts +18 -0
- package/src/io/read.ts +70 -0
- package/src/io/write.ts +94 -0
- package/src/version.ts +21 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { type FSWatcher, promises as fs, watch as fsWatch } from 'node:fs';
|
|
2
|
+
import * as http from 'node:http';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { resolveIncludes } from '@nowline/core';
|
|
5
|
+
import { layoutRoadmap, type ThemeName } from '@nowline/layout';
|
|
6
|
+
import { renderSvg } from '@nowline/renderer';
|
|
7
|
+
import type { ParsedArgs } from '../cli/args.js';
|
|
8
|
+
import { getServices, parseSource } from '../core/parse.js';
|
|
9
|
+
import { type DiagnosticSource, formatDiagnostics } from '../diagnostics/index.js';
|
|
10
|
+
import {
|
|
11
|
+
describeContentLocaleSource,
|
|
12
|
+
operatorLocale,
|
|
13
|
+
readDirectiveLocale,
|
|
14
|
+
resolveLocaleOverride,
|
|
15
|
+
} from '../i18n/locale.js';
|
|
16
|
+
import { CliError, ExitCode } from '../io/exit-codes.js';
|
|
17
|
+
import { createAssetResolver } from './render.js';
|
|
18
|
+
|
|
19
|
+
export interface ServeHandlerOptions {
|
|
20
|
+
args: ParsedArgs;
|
|
21
|
+
/** Test seam: cwd override. Defaults to `process.cwd()`. */
|
|
22
|
+
cwd?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `--serve` mode handler. Starts a local HTTP server that watches the input
|
|
27
|
+
* file and live-reloads the rendered SVG via Server-Sent Events.
|
|
28
|
+
*
|
|
29
|
+
* If `-o <path>` is provided, also writes the rendered SVG to that path on
|
|
30
|
+
* each successful rebuild. `-o -` (stdout) is rejected with exit 2 — serving
|
|
31
|
+
* to stdout is meaningless in this mode.
|
|
32
|
+
*/
|
|
33
|
+
export async function serveHandler(options: ServeHandlerOptions): Promise<void> {
|
|
34
|
+
const { args } = options;
|
|
35
|
+
const cwd = options.cwd ?? process.cwd();
|
|
36
|
+
|
|
37
|
+
if (!args.positional) {
|
|
38
|
+
throw new CliError(
|
|
39
|
+
ExitCode.InputError,
|
|
40
|
+
'nowline: --serve requires an input file. Pass a path or run `nowline --help`.',
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (args.positional === '-') {
|
|
44
|
+
throw new CliError(
|
|
45
|
+
ExitCode.InputError,
|
|
46
|
+
'nowline: --serve cannot read from stdin. Pass a file path.',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (args.output === '-') {
|
|
50
|
+
throw new CliError(
|
|
51
|
+
ExitCode.InputError,
|
|
52
|
+
'nowline: --serve does not support -o - (stdout). Use a file path or omit -o.',
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const port = parsePort(args.port);
|
|
57
|
+
const host = args.host ?? '127.0.0.1';
|
|
58
|
+
const theme = parseTheme(args.theme);
|
|
59
|
+
const today = resolveNowArg(args);
|
|
60
|
+
const resolvedLocale = resolveLocaleOverride({ flag: args.locale, env: process.env });
|
|
61
|
+
const locale = resolvedLocale.tag;
|
|
62
|
+
const opLocale = operatorLocale(resolvedLocale);
|
|
63
|
+
|
|
64
|
+
const inputPath = path.resolve(cwd, args.positional);
|
|
65
|
+
try {
|
|
66
|
+
await fs.access(inputPath);
|
|
67
|
+
} catch {
|
|
68
|
+
throw new CliError(ExitCode.InputError, `nowline: file not found: ${args.positional}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const assetRoot = args.assetRoot ? path.resolve(cwd, args.assetRoot) : path.dirname(inputPath);
|
|
72
|
+
|
|
73
|
+
const fileSink = args.output ? path.resolve(cwd, args.output) : undefined;
|
|
74
|
+
|
|
75
|
+
const clients = new Set<http.ServerResponse>();
|
|
76
|
+
let lastPayload: { kind: 'svg'; body: string } | { kind: 'error'; body: string } = {
|
|
77
|
+
kind: 'svg',
|
|
78
|
+
body: '',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const rebuild = async (): Promise<void> => {
|
|
82
|
+
try {
|
|
83
|
+
const text = await fs.readFile(inputPath, 'utf-8');
|
|
84
|
+
const parse = await parseSource(text, inputPath, { validate: true });
|
|
85
|
+
if (parse.hasErrors) {
|
|
86
|
+
const sources = new Map<string, DiagnosticSource>([[inputPath, parse.source]]);
|
|
87
|
+
const rendered = formatDiagnostics(parse.diagnostics, 'text', sources, {
|
|
88
|
+
color: false,
|
|
89
|
+
operatorLocale: opLocale,
|
|
90
|
+
});
|
|
91
|
+
lastPayload = { kind: 'error', body: rendered };
|
|
92
|
+
process.stderr.write(`${rendered}\n`);
|
|
93
|
+
broadcast(clients, 'error', rendered);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (args.logLevel === 'verbose') {
|
|
97
|
+
const directive = readDirectiveLocale(parse.ast);
|
|
98
|
+
const { tag, source } = describeContentLocaleSource(directive, resolvedLocale);
|
|
99
|
+
process.stderr.write(`nowline: locale=${tag} (${source})\n`);
|
|
100
|
+
}
|
|
101
|
+
const resolved = await resolveIncludes(parse.ast, inputPath, {
|
|
102
|
+
services: getServices().Nowline,
|
|
103
|
+
});
|
|
104
|
+
if (resolved.diagnostics.some((d) => d.severity === 'error')) {
|
|
105
|
+
const msg = resolved.diagnostics
|
|
106
|
+
.filter((d) => d.severity === 'error')
|
|
107
|
+
.map((d) => `${d.sourcePath}: ${d.message}`)
|
|
108
|
+
.join('\n');
|
|
109
|
+
lastPayload = { kind: 'error', body: msg };
|
|
110
|
+
broadcast(clients, 'error', msg);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const model = layoutRoadmap(parse.ast, resolved, { theme, today, locale });
|
|
114
|
+
const svg = await renderSvg(model, {
|
|
115
|
+
assetResolver: createAssetResolver(assetRoot),
|
|
116
|
+
warn: (m) => process.stderr.write(`warning: ${m}\n`),
|
|
117
|
+
});
|
|
118
|
+
lastPayload = { kind: 'svg', body: svg };
|
|
119
|
+
if (fileSink) {
|
|
120
|
+
try {
|
|
121
|
+
await fs.writeFile(fileSink, svg);
|
|
122
|
+
if (args.logLevel === 'verbose') {
|
|
123
|
+
process.stderr.write(`nowline: wrote ${fileSink}\n`);
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
127
|
+
process.stderr.write(`nowline: failed to write ${fileSink}: ${msg}\n`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
broadcast(clients, 'update', svg);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
133
|
+
lastPayload = { kind: 'error', body: msg };
|
|
134
|
+
process.stderr.write(`error: ${msg}\n`);
|
|
135
|
+
broadcast(clients, 'error', msg);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
await rebuild();
|
|
140
|
+
|
|
141
|
+
const watcher = watchFile(inputPath, rebuild);
|
|
142
|
+
const server = http.createServer((req, res) => {
|
|
143
|
+
const url = req.url ?? '/';
|
|
144
|
+
if (url === '/' || url === '/index.html') {
|
|
145
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
|
146
|
+
res.end(shellHtml(theme));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (url === '/svg') {
|
|
150
|
+
res.writeHead(200, { 'content-type': 'image/svg+xml; charset=utf-8' });
|
|
151
|
+
res.end(lastPayload.kind === 'svg' ? lastPayload.body : emptySvg);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (url === '/events') {
|
|
155
|
+
res.writeHead(200, {
|
|
156
|
+
'content-type': 'text/event-stream',
|
|
157
|
+
'cache-control': 'no-cache, no-transform',
|
|
158
|
+
connection: 'keep-alive',
|
|
159
|
+
});
|
|
160
|
+
clients.add(res);
|
|
161
|
+
res.write(`event: hello\ndata: connected\n\n`);
|
|
162
|
+
const initialEvent = lastPayload.kind === 'svg' ? 'update' : 'error';
|
|
163
|
+
sendEvent(res, initialEvent, lastPayload.body);
|
|
164
|
+
req.on('close', () => {
|
|
165
|
+
clients.delete(res);
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
res.writeHead(404);
|
|
170
|
+
res.end('not found');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await new Promise<void>((resolve) => {
|
|
174
|
+
server.listen(port, host, () => resolve());
|
|
175
|
+
});
|
|
176
|
+
process.stdout.write(`nowline serve: http://${host}:${port}\n`);
|
|
177
|
+
|
|
178
|
+
if (args.open) {
|
|
179
|
+
await openBrowser(`http://${host}:${port}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const shutdown = async (): Promise<void> => {
|
|
183
|
+
watcher.close();
|
|
184
|
+
for (const c of clients) {
|
|
185
|
+
try {
|
|
186
|
+
c.end();
|
|
187
|
+
} catch {
|
|
188
|
+
/* ignore */
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
clients.clear();
|
|
192
|
+
server.close();
|
|
193
|
+
};
|
|
194
|
+
process.once('SIGINT', () => {
|
|
195
|
+
void shutdown().then(() => process.exit(0));
|
|
196
|
+
});
|
|
197
|
+
process.once('SIGTERM', () => {
|
|
198
|
+
void shutdown().then(() => process.exit(0));
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Keep the process alive until a signal.
|
|
202
|
+
await new Promise<void>(() => {});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parsePort(raw: string | undefined): number {
|
|
206
|
+
const port = parseInt(raw ?? '4318', 10);
|
|
207
|
+
if (!Number.isFinite(port) || port <= 0 || port >= 65536) {
|
|
208
|
+
throw new CliError(ExitCode.InputError, `nowline: invalid --port "${raw}".`);
|
|
209
|
+
}
|
|
210
|
+
return port;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function parseTheme(raw: string | undefined): ThemeName {
|
|
214
|
+
const lower = (raw ?? 'light').toLowerCase();
|
|
215
|
+
if (lower !== 'light' && lower !== 'dark') {
|
|
216
|
+
throw new CliError(ExitCode.InputError, `nowline: invalid --theme "${raw}".`);
|
|
217
|
+
}
|
|
218
|
+
return lower;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Mirrors render.ts: `--now -` disables the line, `--now <date>` overrides
|
|
222
|
+
// it, and the default is today's UTC calendar date.
|
|
223
|
+
function resolveNowArg(args: { now?: string }): Date | undefined {
|
|
224
|
+
if (args.now === '-') return undefined;
|
|
225
|
+
if (args.now) {
|
|
226
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(args.now);
|
|
227
|
+
if (!m) throw new CliError(ExitCode.InputError, `nowline: invalid --now "${args.now}".`);
|
|
228
|
+
return new Date(Date.UTC(parseInt(m[1], 10), parseInt(m[2], 10) - 1, parseInt(m[3], 10)));
|
|
229
|
+
}
|
|
230
|
+
const t = new Date();
|
|
231
|
+
return new Date(Date.UTC(t.getUTCFullYear(), t.getUTCMonth(), t.getUTCDate()));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function sendEvent(res: http.ServerResponse, event: string, data: string): void {
|
|
235
|
+
const lines = data
|
|
236
|
+
.split('\n')
|
|
237
|
+
.map((l) => `data: ${l}`)
|
|
238
|
+
.join('\n');
|
|
239
|
+
res.write(`event: ${event}\n${lines}\n\n`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function broadcast(clients: Set<http.ServerResponse>, event: string, data: string): void {
|
|
243
|
+
for (const c of clients) {
|
|
244
|
+
try {
|
|
245
|
+
sendEvent(c, event, data);
|
|
246
|
+
} catch {
|
|
247
|
+
clients.delete(c);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function watchFile(target: string, onChange: () => Promise<void>): FSWatcher {
|
|
253
|
+
let timer: NodeJS.Timeout | undefined;
|
|
254
|
+
const w = fsWatch(target, { persistent: true }, () => {
|
|
255
|
+
if (timer) clearTimeout(timer);
|
|
256
|
+
timer = setTimeout(() => {
|
|
257
|
+
void onChange();
|
|
258
|
+
}, 75);
|
|
259
|
+
});
|
|
260
|
+
return w;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function openBrowser(url: string): Promise<void> {
|
|
264
|
+
const { spawn } = await import('node:child_process');
|
|
265
|
+
const platform = process.platform;
|
|
266
|
+
const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
267
|
+
const argList = platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
268
|
+
try {
|
|
269
|
+
const child = spawn(command, argList, { stdio: 'ignore', detached: true });
|
|
270
|
+
child.unref();
|
|
271
|
+
} catch {
|
|
272
|
+
process.stderr.write(`nowline: warning — failed to open browser; visit ${url} manually.\n`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const emptySvg =
|
|
277
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="400" height="60"><text x="10" y="30" font-family="system-ui" font-size="14" fill="#999">no output yet</text></svg>';
|
|
278
|
+
|
|
279
|
+
function shellHtml(theme: ThemeName): string {
|
|
280
|
+
const bg = theme === 'dark' ? '#121212' : '#ffffff';
|
|
281
|
+
const fg = theme === 'dark' ? '#e0e0e0' : '#212121';
|
|
282
|
+
return `<!doctype html>
|
|
283
|
+
<html lang="en">
|
|
284
|
+
<head>
|
|
285
|
+
<meta charset="utf-8"/>
|
|
286
|
+
<title>nowline serve</title>
|
|
287
|
+
<style>
|
|
288
|
+
html, body { margin: 0; padding: 0; height: 100%; background: ${bg}; color: ${fg}; font-family: system-ui, -apple-system, sans-serif; }
|
|
289
|
+
#root { padding: 12px; }
|
|
290
|
+
#error { display: none; position: fixed; bottom: 0; left: 0; right: 0; padding: 12px; background: rgba(183,28,28,0.9); color: white; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; max-height: 40%; overflow: auto; }
|
|
291
|
+
#error.show { display: block; }
|
|
292
|
+
#root svg { max-width: 100%; height: auto; display: block; }
|
|
293
|
+
</style>
|
|
294
|
+
</head>
|
|
295
|
+
<body>
|
|
296
|
+
<div id="root"></div>
|
|
297
|
+
<pre id="error"></pre>
|
|
298
|
+
<script>
|
|
299
|
+
(function(){
|
|
300
|
+
var root = document.getElementById('root');
|
|
301
|
+
var errBox = document.getElementById('error');
|
|
302
|
+
function show(svg){
|
|
303
|
+
root.innerHTML = svg;
|
|
304
|
+
errBox.className = '';
|
|
305
|
+
errBox.textContent = '';
|
|
306
|
+
}
|
|
307
|
+
function err(msg){
|
|
308
|
+
errBox.textContent = msg;
|
|
309
|
+
errBox.className = 'show';
|
|
310
|
+
}
|
|
311
|
+
fetch('/svg').then(function(r){ return r.text(); }).then(show).catch(function(e){ err(String(e)); });
|
|
312
|
+
var ev = new EventSource('/events');
|
|
313
|
+
ev.addEventListener('update', function(e){ show(e.data); });
|
|
314
|
+
ev.addEventListener('error', function(e){
|
|
315
|
+
if (e.data) err(e.data);
|
|
316
|
+
});
|
|
317
|
+
})();
|
|
318
|
+
</script>
|
|
319
|
+
</body>
|
|
320
|
+
</html>
|
|
321
|
+
`;
|
|
322
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { CliError, ExitCode } from '../io/exit-codes.js';
|
|
2
|
+
import { type JsonAstNode, NOWLINE_SCHEMA_VERSION, type NowlineDocument } from './schema.js';
|
|
3
|
+
|
|
4
|
+
export interface ParseJsonResult {
|
|
5
|
+
document: NowlineDocument;
|
|
6
|
+
ast: JsonAstNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function parseNowlineJson(text: string, filePath: string): ParseJsonResult {
|
|
10
|
+
let parsed: unknown;
|
|
11
|
+
try {
|
|
12
|
+
parsed = JSON.parse(text);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
throw new CliError(
|
|
15
|
+
ExitCode.ValidationError,
|
|
16
|
+
`${filePath}: invalid JSON — ${err instanceof Error ? err.message : String(err)}`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
const doc = parsed;
|
|
20
|
+
if (!isRecord(doc)) {
|
|
21
|
+
throw new CliError(
|
|
22
|
+
ExitCode.ValidationError,
|
|
23
|
+
`${filePath}: JSON root must be an object with $nowlineSchema and ast.`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const schema = doc.$nowlineSchema;
|
|
27
|
+
if (typeof schema !== 'string') {
|
|
28
|
+
throw new CliError(
|
|
29
|
+
ExitCode.ValidationError,
|
|
30
|
+
`${filePath}: missing "$nowlineSchema" at document root.`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (schema !== NOWLINE_SCHEMA_VERSION) {
|
|
34
|
+
throw new CliError(
|
|
35
|
+
ExitCode.ValidationError,
|
|
36
|
+
`${filePath}: unsupported $nowlineSchema "${schema}" (this CLI supports "${NOWLINE_SCHEMA_VERSION}").`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const ast = doc.ast;
|
|
40
|
+
if (!isRecord(ast) || typeof ast.$type !== 'string') {
|
|
41
|
+
throw new CliError(
|
|
42
|
+
ExitCode.ValidationError,
|
|
43
|
+
`${filePath}: document.ast must be an object with a "$type" field.`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (ast.$type !== 'NowlineFile') {
|
|
47
|
+
throw new CliError(
|
|
48
|
+
ExitCode.ValidationError,
|
|
49
|
+
`${filePath}: document.ast.$type must be "NowlineFile" (got "${String(ast.$type)}").`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return { document: doc as unknown as NowlineDocument, ast: ast as unknown as JsonAstNode };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
56
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
57
|
+
}
|