@nachoggodino/cello 0.1.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.
Files changed (43) hide show
  1. package/BYLAWS.md +446 -0
  2. package/CHANGELOG.md +11 -0
  3. package/LICENSE +12 -0
  4. package/README.md +211 -0
  5. package/dist/cli/cli.d.ts +16 -0
  6. package/dist/cli/cli.js +360 -0
  7. package/dist/cli/serve.d.ts +15 -0
  8. package/dist/cli/serve.js +226 -0
  9. package/dist/evaluator/evaluate.d.ts +2 -0
  10. package/dist/evaluator/evaluate.js +129 -0
  11. package/dist/evaluator/formula.d.ts +13 -0
  12. package/dist/evaluator/formula.js +141 -0
  13. package/dist/formatter/format.d.ts +1 -0
  14. package/dist/formatter/format.js +112 -0
  15. package/dist/index.d.ts +8 -0
  16. package/dist/index.js +6 -0
  17. package/dist/parser/parse.d.ts +2 -0
  18. package/dist/parser/parse.js +552 -0
  19. package/dist/renderer/render.d.ts +2 -0
  20. package/dist/renderer/render.js +295 -0
  21. package/dist/serializer/serialize.d.ts +2 -0
  22. package/dist/serializer/serialize.js +104 -0
  23. package/dist/shared/types.d.ts +88 -0
  24. package/dist/shared/types.js +1 -0
  25. package/dist/shared/utils.d.ts +16 -0
  26. package/dist/shared/utils.js +142 -0
  27. package/dist/validator/validate.d.ts +8 -0
  28. package/dist/validator/validate.js +10 -0
  29. package/dist/version.d.ts +1 -0
  30. package/dist/version.js +1 -0
  31. package/docs/ARCHITECTURE.md +43 -0
  32. package/docs/CLI.md +58 -0
  33. package/docs/COMPLIANCE.md +82 -0
  34. package/docs/ERROR_MODEL.md +25 -0
  35. package/docs/FORMULA_SUPPORT.md +33 -0
  36. package/docs/SPEC.md +723 -0
  37. package/docs/SYNTAX_HIGHLIGHTING.md +91 -0
  38. package/examples/advanced_kpi.cel +42 -0
  39. package/examples/basic.cel +8 -0
  40. package/examples/feature_showcase.cel +37 -0
  41. package/package.json +96 -0
  42. package/syntaxes/cel.language-configuration.json +31 -0
  43. package/syntaxes/cel.tmLanguage.json +250 -0
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from "node:fs";
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import { dirname, resolve } from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { evaluate } from "../evaluator/evaluate.js";
7
+ import { format as formatCello } from "../formatter/format.js";
8
+ import { parse } from "../parser/parse.js";
9
+ import { render } from "../renderer/render.js";
10
+ import { serialize } from "../serializer/serialize.js";
11
+ import { validate } from "../validator/validate.js";
12
+ import { VERSION } from "../version.js";
13
+ import { startServe } from "./serve.js";
14
+ export function createCliDeps(overrides = {}) {
15
+ return {
16
+ cwd: overrides.cwd ?? process.cwd(),
17
+ readFileFn: overrides.readFileFn ?? readFile,
18
+ writeFileFn: overrides.writeFileFn ?? writeFile,
19
+ startServeFn: overrides.startServeFn ?? startServe,
20
+ stayOpenFn: overrides.stayOpenFn ?? (() => new Promise(() => undefined)),
21
+ stdoutWrite: overrides.stdoutWrite ?? ((text) => process.stdout.write(text)),
22
+ stderrWrite: overrides.stderrWrite ?? ((text) => process.stderr.write(text))
23
+ };
24
+ }
25
+ export async function runCli(argv, deps = createCliDeps()) {
26
+ const request = parseCliRequest(argv, deps.cwd);
27
+ if (request && "error" in request) {
28
+ deps.stderrWrite(`${request.error}\n`);
29
+ return 1;
30
+ }
31
+ if (!request) {
32
+ printUsage(deps.stdoutWrite);
33
+ return 1;
34
+ }
35
+ return runCliRequest(request, deps);
36
+ }
37
+ export async function runMain(argv, deps = createCliDeps(), exitFn = (code) => process.exit(code), runCliFn = runCli, stderrWrite = (text) => process.stderr.write(text)) {
38
+ try {
39
+ const code = await runCliFn(argv, deps);
40
+ exitFn(code);
41
+ }
42
+ catch (err) {
43
+ const message = err instanceof Error ? err.stack ?? err.message : String(err);
44
+ stderrWrite(`${message}\n`);
45
+ exitFn(1);
46
+ }
47
+ }
48
+ function printUsage(write) {
49
+ write([
50
+ "Usage:",
51
+ " cello help [command]",
52
+ " cello version",
53
+ " cello parse <file.cel>",
54
+ " cello evaluate <file.cel>",
55
+ " cello format <file.cel> [--check] [-o out.cel]",
56
+ " cello validate <file.cel>",
57
+ " cello render <file.cel> [-o out.html] [--no-eval] [--format document|fragment]",
58
+ " cello serialize <file.cel> [-o out.cel]",
59
+ " cello serve <file.cel> [--port 4321] [--host 127.0.0.1] [--open] [--no-eval]"
60
+ ].join("\n"));
61
+ write("\n");
62
+ }
63
+ const HELP_TEXT = {
64
+ help: "Usage: cello help [command]\n\nPrint general help or details for one command.\n",
65
+ version: "Usage: cello --version\n\nPrint the cello CLI version.\n",
66
+ parse: "Usage: cello parse <file.cel>\n\nParse a workbook and print the AST as JSON.\n",
67
+ evaluate: "Usage: cello evaluate <file.cel>\n\nParse and evaluate formulas, then print the evaluated AST as JSON.\n",
68
+ format: "Usage: cello format <file.cel> [--check] [-o out.cel]\n\nPretty-print native Cello pipe tables. Writes in place by default, supports -o/--out for an alternate destination, and --check to report whether formatting changes are needed.\n",
69
+ validate: "Usage: cello validate <file.cel>\n\nParse and evaluate diagnostics. Prints JSON with valid and diagnostics fields. Exits 0 when valid, 1 when diagnostics exist.\n",
70
+ render: "Usage: cello render <file.cel> [-o out.html] [--no-eval] [--format document|fragment]\n\nRender a workbook to HTML. The default format is document. Use fragment for an embeddable chunk without html/head/body wrappers. Use --no-eval to leave formula cells unevaluated.\n",
71
+ serialize: "Usage: cello serialize <file.cel> [-o out.cel]\n\nParse and serialize a workbook back to .cel text.\n",
72
+ serve: "Usage: cello serve <file.cel> [--port 4321] [--host 127.0.0.1] [--open] [--no-eval]\n\nServe a live HTML preview. The server keeps the process warm for faster repeated renders. Use --open to open the URL in a browser.\n"
73
+ };
74
+ function parseCliRequest(argv, cwd) {
75
+ const [, , command, inputArg, ...rest] = argv;
76
+ if (command === "--version" || command === "-v" || command === "version") {
77
+ if (inputArg) {
78
+ return { error: `Unexpected argument for version: ${inputArg}` };
79
+ }
80
+ return { command: "version" };
81
+ }
82
+ if (command === "help" || command === "--help" || command === "-h") {
83
+ if (rest.length > 0) {
84
+ return { error: `Unexpected argument for help: ${rest[0]}` };
85
+ }
86
+ return { command: "help", topic: inputArg ?? "" };
87
+ }
88
+ if (inputArg === "--help" || inputArg === "-h") {
89
+ if (rest.length > 0) {
90
+ return { error: `Unexpected argument for help: ${rest[0]}` };
91
+ }
92
+ return { command: "help", topic: command ?? "" };
93
+ }
94
+ if (!command) {
95
+ return null;
96
+ }
97
+ if (!isCliCommand(command)) {
98
+ return { error: `Unknown command: ${command}` };
99
+ }
100
+ if (!inputArg) {
101
+ return { error: `Missing input file for ${command}.` };
102
+ }
103
+ if (inputArg.startsWith("-")) {
104
+ return { error: `Missing input file for ${command}; received option ${inputArg}.` };
105
+ }
106
+ const serveOptionError = validateServeOptionScope(command, rest);
107
+ if (serveOptionError) {
108
+ return serveOptionError;
109
+ }
110
+ const argumentError = validateArguments(command, rest);
111
+ if (argumentError) {
112
+ return argumentError;
113
+ }
114
+ const resolvedOutPath = resolveOutPath(cwd, rest);
115
+ if (typeof resolvedOutPath !== "string") {
116
+ return resolvedOutPath;
117
+ }
118
+ const resolvedRenderFormat = command === "render" ? resolveRenderFormat(rest) : undefined;
119
+ if (resolvedRenderFormat && "error" in resolvedRenderFormat) {
120
+ return resolvedRenderFormat;
121
+ }
122
+ const request = {
123
+ command,
124
+ inputPath: resolve(cwd, inputArg),
125
+ outPath: resolvedOutPath,
126
+ evaluate: !rest.includes("--no-eval")
127
+ };
128
+ if (command === "format") {
129
+ request.check = rest.includes("--check");
130
+ }
131
+ if (resolvedRenderFormat) {
132
+ request.renderFormat = resolvedRenderFormat.format;
133
+ }
134
+ if (command === "serve") {
135
+ const resolvedServeOptions = resolveServeOptions(rest);
136
+ if ("error" in resolvedServeOptions) {
137
+ return resolvedServeOptions;
138
+ }
139
+ request.host = resolvedServeOptions.host;
140
+ request.port = resolvedServeOptions.port;
141
+ request.open = resolvedServeOptions.open;
142
+ }
143
+ return request;
144
+ }
145
+ function isCliCommand(command) {
146
+ return (command === "parse" ||
147
+ command === "evaluate" ||
148
+ command === "format" ||
149
+ command === "validate" ||
150
+ command === "render" ||
151
+ command === "serialize" ||
152
+ command === "serve");
153
+ }
154
+ function validateServeOptionScope(command, rest) {
155
+ if (command !== "serve" && (rest.includes("--host") || rest.includes("--port") || rest.includes("--open"))) {
156
+ return { error: "--host, --port, and --open are only supported by serve." };
157
+ }
158
+ if (command !== "serve" && command !== "render" && rest.includes("--no-eval")) {
159
+ return { error: "--no-eval is only supported by render and serve." };
160
+ }
161
+ if (command !== "format" && rest.includes("--check")) {
162
+ return { error: "--check is only supported by format." };
163
+ }
164
+ return undefined;
165
+ }
166
+ function validateArguments(command, rest) {
167
+ const optionsWithValues = new Set(command === "serve" ? ["--host", "--port"] : ["--out", "-o", "--format"]);
168
+ const allowedOptions = new Set();
169
+ if (command === "render" || command === "serialize" || command === "format") {
170
+ allowedOptions.add("--out");
171
+ allowedOptions.add("-o");
172
+ }
173
+ if (command === "render" || command === "serve") {
174
+ allowedOptions.add("--no-eval");
175
+ }
176
+ if (command === "render") {
177
+ allowedOptions.add("--format");
178
+ }
179
+ if (command === "format") {
180
+ allowedOptions.add("--check");
181
+ }
182
+ if (command === "serve") {
183
+ allowedOptions.add("--host");
184
+ allowedOptions.add("--port");
185
+ allowedOptions.add("--open");
186
+ }
187
+ for (let index = 0; index < rest.length; index += 1) {
188
+ const arg = rest[index];
189
+ if (arg === undefined) {
190
+ continue;
191
+ }
192
+ if (!arg.startsWith("-")) {
193
+ return { error: `Unexpected argument: ${arg}` };
194
+ }
195
+ if (!allowedOptions.has(arg)) {
196
+ return { error: `Unsupported option for ${command}: ${arg}` };
197
+ }
198
+ if (optionsWithValues.has(arg)) {
199
+ index += 1;
200
+ continue;
201
+ }
202
+ }
203
+ return undefined;
204
+ }
205
+ function resolveServeOptions(rest) {
206
+ const hostValue = readOption(rest, "--host");
207
+ if (hostValue && "error" in hostValue) {
208
+ return hostValue;
209
+ }
210
+ const portValue = readOption(rest, "--port");
211
+ if (portValue && "error" in portValue) {
212
+ return portValue;
213
+ }
214
+ const host = hostValue?.value ?? "127.0.0.1";
215
+ const rawPort = portValue?.value;
216
+ const port = rawPort === undefined ? 4321 : Number(rawPort);
217
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
218
+ return { error: "Invalid --port value." };
219
+ }
220
+ return { host, port, open: rest.includes("--open") };
221
+ }
222
+ function readOption(rest, name) {
223
+ const index = rest.findIndex((arg) => arg === name);
224
+ if (index < 0) {
225
+ return undefined;
226
+ }
227
+ const value = rest[index + 1];
228
+ if (!value || value.startsWith("-")) {
229
+ return { error: `Missing value after ${name}.` };
230
+ }
231
+ return { value };
232
+ }
233
+ function resolveOutPath(cwd, rest) {
234
+ const outIndex = rest.findIndex((arg) => arg === "--out" || arg === "-o");
235
+ if (outIndex < 0) {
236
+ return "";
237
+ }
238
+ const outArg = rest[outIndex + 1];
239
+ if (!outArg || outArg.startsWith("-")) {
240
+ return { error: "Missing output file after -o/--out." };
241
+ }
242
+ return resolve(cwd, outArg);
243
+ }
244
+ function resolveRenderFormat(rest) {
245
+ const raw = readOption(rest, "--format");
246
+ if (!raw) {
247
+ return undefined;
248
+ }
249
+ if ("error" in raw) {
250
+ return raw;
251
+ }
252
+ if (raw.value !== "document" && raw.value !== "fragment") {
253
+ return { error: "Invalid --format value. Expected document or fragment." };
254
+ }
255
+ return { format: raw.value };
256
+ }
257
+ async function runCliRequest(request, deps) {
258
+ if (request.command === "version") {
259
+ deps.stdoutWrite(`${VERSION}\n`);
260
+ return 0;
261
+ }
262
+ if (request.command === "help") {
263
+ return writeHelp(request.topic, deps.stdoutWrite, deps.stderrWrite);
264
+ }
265
+ if (request.command === "serve") {
266
+ const serveOptions = { evaluate: request.evaluate };
267
+ if (request.host !== undefined) {
268
+ serveOptions.host = request.host;
269
+ }
270
+ if (request.port !== undefined) {
271
+ serveOptions.port = request.port;
272
+ }
273
+ if (request.open !== undefined) {
274
+ serveOptions.open = request.open;
275
+ }
276
+ const handle = await deps.startServeFn(request.inputPath, serveOptions);
277
+ deps.stdoutWrite(`Serving ${request.inputPath}\n${formatTerminalLink(handle.url)}\n`);
278
+ return deps.stayOpenFn();
279
+ }
280
+ const text = await deps.readFileFn(request.inputPath, "utf8");
281
+ let ast;
282
+ const getAst = () => {
283
+ ast ??= parse(text);
284
+ return ast;
285
+ };
286
+ const handlers = {
287
+ parse: async () => writeStdoutJson(getAst(), deps.stdoutWrite),
288
+ evaluate: async () => writeStdoutJson(await evaluate(getAst()), deps.stdoutWrite),
289
+ format: async () => writeFormattedOutput(formatCello(text), text, request, deps),
290
+ validate: async () => writeValidationResult(await validate(text, { baseDir: dirname(request.inputPath) }), deps.stdoutWrite),
291
+ render: async () => writeCliOutput(await render(text, {
292
+ baseDir: dirname(request.inputPath),
293
+ evaluate: request.evaluate,
294
+ ...(request.renderFormat ? { format: request.renderFormat } : {})
295
+ }), request.outPath, deps, false),
296
+ serialize: async () => writeCliOutput(serialize(getAst()), request.outPath, deps, true)
297
+ };
298
+ return handlers[request.command]();
299
+ }
300
+ async function writeFormattedOutput(formatted, original, request, deps) {
301
+ const changed = formatted !== original;
302
+ if (request.check) {
303
+ deps.stdoutWrite(`${changed ? "Needs formatting" : "Already formatted"}: ${request.inputPath}\n`);
304
+ return changed ? 1 : 0;
305
+ }
306
+ const destination = request.outPath || request.inputPath;
307
+ if (!changed && destination === request.inputPath) {
308
+ deps.stdoutWrite(`Already formatted: ${request.inputPath}\n`);
309
+ return 0;
310
+ }
311
+ await deps.writeFileFn(destination, formatted, "utf8");
312
+ deps.stdoutWrite(`Wrote ${destination}\n`);
313
+ return 0;
314
+ }
315
+ function writeHelp(topic, stdoutWrite, stderrWrite) {
316
+ if (topic.length === 0) {
317
+ printUsage(stdoutWrite);
318
+ return 0;
319
+ }
320
+ const text = HELP_TEXT[topic];
321
+ if (!text) {
322
+ stderrWrite(`Unknown help topic: ${topic}\n`);
323
+ return 1;
324
+ }
325
+ stdoutWrite(text);
326
+ return 0;
327
+ }
328
+ function formatTerminalLink(url) {
329
+ return `\u001B]8;;${url}\u0007${url}\u001B]8;;\u0007`;
330
+ }
331
+ function writeValidationResult(value, stdoutWrite) {
332
+ writeStdoutJson(value, stdoutWrite);
333
+ return value.valid ? 0 : 1;
334
+ }
335
+ function writeStdoutJson(value, stdoutWrite) {
336
+ stdoutWrite(`${JSON.stringify(value, null, 2)}\n`);
337
+ return 0;
338
+ }
339
+ async function writeCliOutput(output, outPath, deps, appendTrailingNewline) {
340
+ if (outPath) {
341
+ await deps.writeFileFn(outPath, output, "utf8");
342
+ deps.stdoutWrite(`Wrote ${outPath}\n`);
343
+ return 0;
344
+ }
345
+ deps.stdoutWrite(appendTrailingNewline ? `${output}\n` : output);
346
+ return 0;
347
+ }
348
+ const isDirectExecution = process.argv[1] !== undefined && isDirectCliExecution(process.argv[1], import.meta.url);
349
+ export function isDirectCliExecution(argvPath, moduleUrl) {
350
+ try {
351
+ return realpathSync(argvPath) === realpathSync(fileURLToPath(moduleUrl));
352
+ }
353
+ catch {
354
+ return pathToFileURL(argvPath).href === moduleUrl;
355
+ }
356
+ }
357
+ /* c8 ignore next 3 */
358
+ if (isDirectExecution) {
359
+ void runMain(process.argv);
360
+ }
@@ -0,0 +1,15 @@
1
+ import { type FSWatcher } from "node:fs";
2
+ import { type Server } from "node:http";
3
+ export interface ServeOptions {
4
+ host?: string;
5
+ port?: number;
6
+ open?: boolean;
7
+ evaluate?: boolean;
8
+ }
9
+ export interface ServeHandle {
10
+ url: string;
11
+ server: Server;
12
+ watcher: FSWatcher;
13
+ close: () => Promise<void>;
14
+ }
15
+ export declare function startServe(filePath: string, options?: ServeOptions): Promise<ServeHandle>;
@@ -0,0 +1,226 @@
1
+ import { watch } from "node:fs";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { createServer } from "node:http";
4
+ import { basename, dirname, resolve } from "node:path";
5
+ import { spawn } from "node:child_process";
6
+ import { render } from "../renderer/render.js";
7
+ const LIVE_VERSION_PATH = "/__cello/version";
8
+ export async function startServe(filePath, options = {}) {
9
+ const inputPath = resolve(filePath);
10
+ const host = options.host ?? "127.0.0.1";
11
+ const port = options.port ?? 4321;
12
+ const servedPath = `/${encodeURIComponent(basename(inputPath))}`;
13
+ const renderer = createCachedRenderer(inputPath, options.evaluate !== false);
14
+ await renderer.refresh();
15
+ const server = createServer((request, response) => {
16
+ void handleRequest(request, response, renderer, servedPath);
17
+ });
18
+ const watcher = watch(inputPath, () => {
19
+ renderer.markDirty();
20
+ });
21
+ try {
22
+ await listen(server, port, host);
23
+ const address = server.address();
24
+ const boundPort = typeof address === "object" && address ? address.port : port;
25
+ const url = `http://${host}:${boundPort}${servedPath}`;
26
+ if (options.open) {
27
+ openBrowser(url);
28
+ }
29
+ return {
30
+ url,
31
+ server,
32
+ watcher,
33
+ close: async () => {
34
+ watcher.close();
35
+ await closeServer(server);
36
+ }
37
+ };
38
+ }
39
+ catch (err) {
40
+ if (server.listening) {
41
+ await closeServer(server);
42
+ }
43
+ throw err;
44
+ }
45
+ finally {
46
+ if (!server.listening) {
47
+ watcher.close();
48
+ }
49
+ }
50
+ }
51
+ function createCachedRenderer(inputPath, evaluateFormulas) {
52
+ let dirty = true;
53
+ let cached = "";
54
+ let version = 0;
55
+ let lastMtimeMs;
56
+ return {
57
+ markDirty: () => {
58
+ markDirty();
59
+ },
60
+ refresh: async () => {
61
+ await detectFileChange();
62
+ if (!dirty) {
63
+ return cached;
64
+ }
65
+ const text = await readFile(inputPath, "utf8");
66
+ cached = await render(text, {
67
+ baseDir: dirname(inputPath),
68
+ evaluate: evaluateFormulas,
69
+ title: inputPath
70
+ });
71
+ cached = injectLiveReload(cached);
72
+ lastMtimeMs = (await stat(inputPath)).mtimeMs;
73
+ dirty = false;
74
+ return cached;
75
+ },
76
+ version: async () => {
77
+ await detectFileChange();
78
+ return version;
79
+ }
80
+ };
81
+ function markDirty() {
82
+ if (!dirty) {
83
+ version += 1;
84
+ }
85
+ dirty = true;
86
+ }
87
+ async function detectFileChange() {
88
+ const currentMtimeMs = (await stat(inputPath)).mtimeMs;
89
+ if (lastMtimeMs !== undefined && currentMtimeMs !== lastMtimeMs) {
90
+ markDirty();
91
+ }
92
+ }
93
+ }
94
+ async function handleRequest(request, response, renderer, servedPath) {
95
+ const pathname = getPathname(request.url ?? "/");
96
+ if (request.method !== "GET" || !isKnownRoute(pathname, servedPath)) {
97
+ response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
98
+ response.end("Not found\n");
99
+ return;
100
+ }
101
+ if (pathname === LIVE_VERSION_PATH) {
102
+ response.writeHead(200, {
103
+ "content-type": "application/json; charset=utf-8",
104
+ "cache-control": "no-store"
105
+ });
106
+ response.end(`${JSON.stringify({ version: await renderer.version() })}\n`);
107
+ return;
108
+ }
109
+ try {
110
+ const html = await renderer.refresh();
111
+ response.writeHead(200, {
112
+ "content-type": "text/html; charset=utf-8",
113
+ "cache-control": "no-store"
114
+ });
115
+ response.end(html);
116
+ }
117
+ catch (err) {
118
+ const message = err instanceof Error ? err.message : String(err);
119
+ response.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
120
+ response.end(`${message}\n`);
121
+ }
122
+ }
123
+ function getPathname(url) {
124
+ try {
125
+ return new URL(url, "http://localhost").pathname;
126
+ }
127
+ catch {
128
+ return "/";
129
+ }
130
+ }
131
+ function isKnownRoute(pathname, servedPath) {
132
+ return pathname === "/" || pathname === servedPath || pathname === LIVE_VERSION_PATH;
133
+ }
134
+ function injectLiveReload(html) {
135
+ const script = `<script>
136
+ (() => {
137
+ let currentVersion;
138
+ async function checkForCelloChanges() {
139
+ try {
140
+ const response = await fetch("${LIVE_VERSION_PATH}", { cache: "no-store" });
141
+ const data = await response.json();
142
+ if (currentVersion === undefined) {
143
+ currentVersion = data.version;
144
+ return;
145
+ }
146
+ if (data.version !== currentVersion) {
147
+ window.location.reload();
148
+ }
149
+ } catch {
150
+ // Keep the last rendered workbook visible if the dev server is stopped.
151
+ }
152
+ }
153
+ window.setInterval(checkForCelloChanges, 500);
154
+ void checkForCelloChanges();
155
+ })();
156
+ </script>`;
157
+ return html.replace("</body>", `${script}\n</body>`);
158
+ }
159
+ function listen(server, port, host) {
160
+ return new Promise((resolveListen, rejectListen) => {
161
+ const onError = (err) => {
162
+ server.off("listening", onListening);
163
+ rejectListen(err);
164
+ };
165
+ const onListening = () => {
166
+ server.off("error", onError);
167
+ resolveListen();
168
+ };
169
+ server.once("error", onError);
170
+ server.once("listening", onListening);
171
+ server.listen(port, host);
172
+ });
173
+ }
174
+ function closeServer(server) {
175
+ return new Promise((resolveClose, rejectClose) => {
176
+ server.close((err) => {
177
+ if (err) {
178
+ rejectClose(err);
179
+ return;
180
+ }
181
+ resolveClose();
182
+ });
183
+ });
184
+ }
185
+ function openBrowser(url) {
186
+ const commands = getOpenBrowserCommands(url);
187
+ let index = 0;
188
+ const tryOpen = () => {
189
+ const next = commands[index];
190
+ if (!next) {
191
+ return;
192
+ }
193
+ index += 1;
194
+ const child = spawn(next.command, next.args, {
195
+ detached: true,
196
+ stdio: "ignore"
197
+ });
198
+ child.once("error", tryOpen);
199
+ child.once("exit", (code) => {
200
+ if (code && code !== 0) {
201
+ tryOpen();
202
+ }
203
+ });
204
+ child.unref();
205
+ };
206
+ tryOpen();
207
+ }
208
+ function getOpenBrowserCommands(url) {
209
+ if (process.platform === "win32") {
210
+ return [{ command: "cmd.exe", args: ["/c", "start", "", url] }];
211
+ }
212
+ if (process.platform === "darwin") {
213
+ return [{ command: "open", args: [url] }];
214
+ }
215
+ if (isWsl()) {
216
+ return [
217
+ { command: "cmd.exe", args: ["/c", "start", "", url] },
218
+ { command: "wslview", args: [url] },
219
+ { command: "xdg-open", args: [url] }
220
+ ];
221
+ }
222
+ return [{ command: "xdg-open", args: [url] }];
223
+ }
224
+ function isWsl() {
225
+ return Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP);
226
+ }
@@ -0,0 +1,2 @@
1
+ import type { EvaluateOptions, WorkbookAst } from "../shared/types.js";
2
+ export declare function evaluate(ast: WorkbookAst, options?: EvaluateOptions): Promise<WorkbookAst>;