@graphenedata/cli 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.ts +7 -43
- package/dist/cli/cli.js +509 -277
- package/dist/docs/graphene.md +924 -63
- package/dist/ui/component-utilities/echarts.js +3 -1
- package/dist/ui/component-utilities/themeStores.ts +35 -7
- package/dist/ui/components/AreaChart.svelte +2 -1
- package/dist/ui/components/BarChart.svelte +2 -1
- package/dist/ui/components/BigValue.svelte +1 -1
- package/dist/ui/components/Chart.svelte +10 -1
- package/dist/ui/components/ECharts.svelte +2 -0
- package/dist/ui/components/LineChart.svelte +2 -1
- package/dist/ui/components/PieChart.svelte +1 -1
- package/dist/ui/components/QueryLoad.svelte +5 -6
- package/dist/ui/components/TableRow.svelte +1 -1
- package/dist/ui/components/_Table.svelte +2 -0
- package/dist/ui/internal/queryEngine.ts +16 -13
- package/dist/ui/internal/telemetry.ts +5 -3
- package/dist/ui/web.js +26 -11
- package/package.json +2 -1
- package/dist/docs/data_apps/components/charts/annotations.md +0 -673
- package/dist/docs/data_apps/components/charts/area-chart.md +0 -202
- package/dist/docs/data_apps/components/charts/bar-chart.md +0 -317
- package/dist/docs/data_apps/components/charts/box-plot.md +0 -190
- package/dist/docs/data_apps/components/charts/bubble-chart.md +0 -151
- package/dist/docs/data_apps/components/charts/calendar-heatmap.md +0 -112
- package/dist/docs/data_apps/components/charts/custom-echarts.md +0 -308
- package/dist/docs/data_apps/components/charts/echarts-options.md +0 -217
- package/dist/docs/data_apps/components/charts/funnel-chart.md +0 -106
- package/dist/docs/data_apps/components/charts/heatmap.md +0 -180
- package/dist/docs/data_apps/components/charts/histogram.md +0 -107
- package/dist/docs/data_apps/components/charts/line-chart.md +0 -265
- package/dist/docs/data_apps/components/charts/mixed-type-charts.md +0 -240
- package/dist/docs/data_apps/components/charts/sankey-diagram.md +0 -301
- package/dist/docs/data_apps/components/charts/scatter-plot.md +0 -134
- package/dist/docs/data_apps/components/charts/sparkline.md +0 -68
- package/dist/docs/data_apps/components/data/big-value.md +0 -153
- package/dist/docs/data_apps/components/data/delta.md +0 -89
- package/dist/docs/data_apps/components/data/table.md +0 -470
- package/dist/docs/data_apps/components/data/value.md +0 -97
- package/dist/docs/data_apps/components/inputs/button-group.md +0 -154
- package/dist/docs/data_apps/components/inputs/checkbox.md +0 -52
- package/dist/docs/data_apps/components/inputs/date-input.md +0 -131
- package/dist/docs/data_apps/components/inputs/date-range.md +0 -124
- package/dist/docs/data_apps/components/inputs/dimension-grid.md +0 -67
- package/dist/docs/data_apps/components/inputs/dropdown.md +0 -199
- package/dist/docs/data_apps/components/inputs/index.md +0 -3
- package/dist/docs/data_apps/components/inputs/slider.md +0 -126
- package/dist/docs/data_apps/components/inputs/text-input.md +0 -86
- package/dist/docs/data_apps/components/maps/area-map.md +0 -397
- package/dist/docs/data_apps/components/maps/base-map.md +0 -269
- package/dist/docs/data_apps/components/maps/bubble-map.md +0 -361
- package/dist/docs/data_apps/components/maps/point-map.md +0 -326
- package/dist/docs/data_apps/components/maps/us-map.md +0 -167
- package/dist/docs/data_apps/components/ui/accordion.md +0 -116
- package/dist/docs/data_apps/components/ui/alert.md +0 -37
- package/dist/docs/data_apps/components/ui/big-link.md +0 -19
- package/dist/docs/data_apps/components/ui/details.md +0 -58
- package/dist/docs/data_apps/components/ui/download-data.md +0 -41
- package/dist/docs/data_apps/components/ui/embed.md +0 -47
- package/dist/docs/data_apps/components/ui/grid.md +0 -45
- package/dist/docs/data_apps/components/ui/image.md +0 -61
- package/dist/docs/data_apps/components/ui/info.md +0 -47
- package/dist/docs/data_apps/components/ui/last-refreshed.md +0 -28
- package/dist/docs/data_apps/components/ui/link-button.md +0 -20
- package/dist/docs/data_apps/components/ui/link.md +0 -40
- package/dist/docs/data_apps/components/ui/modal.md +0 -57
- package/dist/docs/data_apps/components/ui/note.md +0 -32
- package/dist/docs/data_apps/components/ui/print-format-components.md +0 -85
- package/dist/docs/data_apps/components/ui/tabs.md +0 -122
package/dist/cli/cli.js
CHANGED
|
@@ -58,6 +58,15 @@ function walkExpression(root, fn, parent = null) {
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
async function pollFor(fn, timeoutMs, interval) {
|
|
62
|
+
let end = Date.now() + timeoutMs;
|
|
63
|
+
while (Date.now() < end) {
|
|
64
|
+
let res = fn();
|
|
65
|
+
if (res) return res;
|
|
66
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
61
70
|
var init_util = __esm({
|
|
62
71
|
"../lang/util.ts"() {
|
|
63
72
|
}
|
|
@@ -147,17 +156,15 @@ function loadConfig(dir) {
|
|
|
147
156
|
} catch {
|
|
148
157
|
console.warn("No package.json found in current directory");
|
|
149
158
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
root: packageJsonObject.root || process.cwd()
|
|
155
|
-
});
|
|
159
|
+
let dialect = "duckdb";
|
|
160
|
+
if (packageJsonObject.bigquery) dialect = "bigquery";
|
|
161
|
+
else if (packageJsonObject.snowflake) dialect = "snowflake";
|
|
162
|
+
setConfig({ ...packageJsonObject, dialect, root: packageJsonObject.root || process.cwd() });
|
|
156
163
|
}
|
|
157
164
|
var config;
|
|
158
165
|
var init_config = __esm({
|
|
159
166
|
"../lang/config.ts"() {
|
|
160
|
-
config = { dialect: "duckdb" };
|
|
167
|
+
config = { dialect: "duckdb", root: "" };
|
|
161
168
|
}
|
|
162
169
|
});
|
|
163
170
|
|
|
@@ -521,10 +528,10 @@ function analyzeQueryTable(table2) {
|
|
|
521
528
|
if (table2.query) return;
|
|
522
529
|
let node = TABLE_NODE_MAP.get(table2);
|
|
523
530
|
let query = analyzeQuery(node.getChild("QueryStatement"));
|
|
524
|
-
if (!query)
|
|
531
|
+
if (!query) return;
|
|
525
532
|
table2.fields = query.fields.map((f) => ({ type: f.type, name: f.name, metadata: f.metadata }));
|
|
526
533
|
table2.query = query.malloyQuery;
|
|
527
|
-
if (typeof table2.query.structRef == "string") {
|
|
534
|
+
if (table2.query && typeof table2.query.structRef == "string") {
|
|
528
535
|
table2.query.structRef = lookupTable(table2.query.structRef, node);
|
|
529
536
|
}
|
|
530
537
|
}
|
|
@@ -534,9 +541,10 @@ function analyzeQuery(queryNode) {
|
|
|
534
541
|
let isAgg = false;
|
|
535
542
|
let subQuerySources = [];
|
|
536
543
|
if (!txt(queryNode)) return;
|
|
544
|
+
if (txt(queryNode).trim().toLowerCase() == "select 1") return { fields: [{ name: "col_0", type: "number", metadata: {}, e: { node: "numberLiteral", literal: "1", type: "number" } }], subQuerySources, rawSql: "select 1" };
|
|
537
545
|
let froms = queryNode.getChild("FromClause")?.getChildren("TablePrimary") || [];
|
|
538
546
|
if (froms.find((f) => f.name == "JoinClause")) diag(froms[0], "Query joins not yet supported");
|
|
539
|
-
if (froms.length == 0) diag(queryNode, "No tables in FROM clause");
|
|
547
|
+
if (froms.length == 0) return diag(queryNode, "No tables in FROM clause");
|
|
540
548
|
if (froms.length > 1) diag(froms[0], "Multiple tables/joins in FROM clause not yet supported");
|
|
541
549
|
if (froms[0].name == "Subquery") {
|
|
542
550
|
structRef = txt(froms[0].getChild("Alias")) || "subquery";
|
|
@@ -862,6 +870,10 @@ function convertDataType(dataType) {
|
|
|
862
870
|
return "number";
|
|
863
871
|
case "INT64":
|
|
864
872
|
return "number";
|
|
873
|
+
case "NUMBER":
|
|
874
|
+
return "number";
|
|
875
|
+
case "VARIANT":
|
|
876
|
+
return "string";
|
|
865
877
|
case "TEXT":
|
|
866
878
|
return "string";
|
|
867
879
|
case "STRING":
|
|
@@ -884,6 +896,8 @@ function convertDataType(dataType) {
|
|
|
884
896
|
return "timestamp";
|
|
885
897
|
case "TIMESTAMP":
|
|
886
898
|
return "timestamp";
|
|
899
|
+
case "TIMESTAMP_NTZ":
|
|
900
|
+
return "timestamp";
|
|
887
901
|
case "DECIMAL":
|
|
888
902
|
return "number";
|
|
889
903
|
case "DOUBLE":
|
|
@@ -1243,11 +1257,13 @@ function analyze(contents, type) {
|
|
|
1243
1257
|
}
|
|
1244
1258
|
}
|
|
1245
1259
|
function toSql(query, params = {}) {
|
|
1260
|
+
if (query.rawSql) return query.rawSql;
|
|
1246
1261
|
let contents = {};
|
|
1247
1262
|
let gsqlTables = Object.values(FILE_MAP).filter((f) => f.path !== "input").flatMap((f) => f.tables);
|
|
1248
1263
|
gsqlTables.forEach((t) => contents[t.name] = t);
|
|
1249
1264
|
let inputTables = [...FILE_MAP["input"]?.tables || [], ...query.subQuerySources];
|
|
1250
1265
|
inputTables.forEach((t) => contents[t.name] = { ...t, query: t.query && fillInParams(t.query, params) });
|
|
1266
|
+
if (!query.malloyQuery) throw new Error("Cannot compile query without Malloy query");
|
|
1251
1267
|
let malloyQuery = fillInParams(query.malloyQuery, params);
|
|
1252
1268
|
let qm = new QueryModel({
|
|
1253
1269
|
name: "generated_model",
|
|
@@ -1281,13 +1297,177 @@ var init_core = __esm({
|
|
|
1281
1297
|
}
|
|
1282
1298
|
});
|
|
1283
1299
|
|
|
1300
|
+
// printer.ts
|
|
1301
|
+
import { styleText as nodeStyleText } from "node:util";
|
|
1302
|
+
import Table from "cli-table3";
|
|
1303
|
+
import chalk from "chalk";
|
|
1304
|
+
function offsetToLineCol(src, offset2) {
|
|
1305
|
+
let lines = src.split(/\r?\n/);
|
|
1306
|
+
let acc = 0;
|
|
1307
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1308
|
+
let lineText = lines[i];
|
|
1309
|
+
let nextAcc = acc + lineText.length + 1;
|
|
1310
|
+
if (offset2 < nextAcc || i === lines.length - 1) {
|
|
1311
|
+
let col = Math.max(0, offset2 - acc);
|
|
1312
|
+
return { line: i + 1, col, lineStart: acc, lineText };
|
|
1313
|
+
}
|
|
1314
|
+
acc = nextAcc;
|
|
1315
|
+
}
|
|
1316
|
+
return { line: 1, col: 0, lineStart: 0, lineText: lines[0] || "" };
|
|
1317
|
+
}
|
|
1318
|
+
function printDiagnostics(diags, log) {
|
|
1319
|
+
log ||= console.log;
|
|
1320
|
+
let parts = [];
|
|
1321
|
+
for (let d of diags) {
|
|
1322
|
+
let src = getFile2(d.file)?.contents || "";
|
|
1323
|
+
let { line, col, lineStart, lineText } = offsetToLineCol(src, d.from.offset);
|
|
1324
|
+
let endCol = Math.max(col + 1, Math.min(lineText.length, d.to.offset - lineStart));
|
|
1325
|
+
let caretLen = Math.max(1, endCol - col);
|
|
1326
|
+
let sev = d.severity === "error" ? "red" : "yellow";
|
|
1327
|
+
let header = `${styleText(sev, d.severity.toUpperCase())}: ${d.file} line ${line}: ${d.message}`;
|
|
1328
|
+
let gutter = " | ";
|
|
1329
|
+
let caretLine = `${" ".repeat(col)}${styleText(sev, "^".repeat(caretLen))}`;
|
|
1330
|
+
parts.push([header, `${gutter}${lineText}`, `${gutter}${caretLine}`].join("\n"));
|
|
1331
|
+
}
|
|
1332
|
+
if (parts.length) log(parts.join("\n"));
|
|
1333
|
+
}
|
|
1334
|
+
function printTable(rows) {
|
|
1335
|
+
if (!rows || rows.length === 0) {
|
|
1336
|
+
console.log(chalk.yellow("No results returned"));
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
let headers = Object.keys(rows[0]);
|
|
1340
|
+
let table2 = new Table({ head: headers.map((h) => chalk.blue(h)) });
|
|
1341
|
+
let MAX_DISPLAY_ROWS = 200;
|
|
1342
|
+
let displayRows = rows.slice(0, MAX_DISPLAY_ROWS);
|
|
1343
|
+
displayRows.forEach((row) => table2.push(headers.map((h) => row[h]?.toString() || "")));
|
|
1344
|
+
console.log(table2.toString());
|
|
1345
|
+
if (rows.length > MAX_DISPLAY_ROWS) {
|
|
1346
|
+
console.log(chalk.yellow(`Displayed first ${MAX_DISPLAY_ROWS} rows (of ${rows.length} total).`));
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
var styleText;
|
|
1350
|
+
var init_printer = __esm({
|
|
1351
|
+
"printer.ts"() {
|
|
1352
|
+
init_core();
|
|
1353
|
+
styleText = (style, text) => {
|
|
1354
|
+
try {
|
|
1355
|
+
return nodeStyleText ? nodeStyleText(style, text) : text;
|
|
1356
|
+
} catch {
|
|
1357
|
+
return text;
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
// background.ts
|
|
1364
|
+
import { spawn } from "child_process";
|
|
1365
|
+
import { fileURLToPath } from "url";
|
|
1366
|
+
import fs2 from "fs-extra";
|
|
1367
|
+
import path3 from "path";
|
|
1368
|
+
async function runServeInBackground() {
|
|
1369
|
+
let root = process.cwd();
|
|
1370
|
+
let grapheneCache = getGrapheneCache(root);
|
|
1371
|
+
let logFile = path3.join(grapheneCache, "serve.log");
|
|
1372
|
+
await fs2.ensureDir(grapheneCache);
|
|
1373
|
+
await stopGrapheneIfRunning(root);
|
|
1374
|
+
let log = fs2.openSync(logFile, "w");
|
|
1375
|
+
let entryPoint = process.argv[1] || fileURLToPath(import.meta.url);
|
|
1376
|
+
let childArgs = [...process.execArgv, entryPoint, "serve", "--fg", ...process.argv.slice(3)];
|
|
1377
|
+
let child = spawn(process.execPath, childArgs, {
|
|
1378
|
+
cwd: root,
|
|
1379
|
+
detached: true,
|
|
1380
|
+
env: { ...process.env },
|
|
1381
|
+
stdio: ["ignore", log, log]
|
|
1382
|
+
});
|
|
1383
|
+
if (!child.pid) throw new Error("Failed to start server process");
|
|
1384
|
+
await new Promise((resolve, reject) => {
|
|
1385
|
+
let buffer = "";
|
|
1386
|
+
fs2.watchFile(logFile, { interval: 200 }, (curr, prev) => {
|
|
1387
|
+
if (curr.size > prev.size) {
|
|
1388
|
+
let stream = fs2.createReadStream(logFile, { start: 0, end: curr.size - 1 });
|
|
1389
|
+
stream.on("data", (d) => {
|
|
1390
|
+
process.stdout.write(d);
|
|
1391
|
+
buffer = (buffer + d.toString()).slice(-200);
|
|
1392
|
+
if (buffer.includes("Server running at http://localhost:")) resolve();
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
child.once("exit", () => {
|
|
1397
|
+
process.stdout.write(fs2.readFileSync(logFile));
|
|
1398
|
+
reject(new Error("Exited before server started"));
|
|
1399
|
+
});
|
|
1400
|
+
child.once("error", (e) => reject(e));
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
function getGrapheneCache(root) {
|
|
1404
|
+
return path3.join(root, "node_modules", ".graphene");
|
|
1405
|
+
}
|
|
1406
|
+
function getPidFilePath(root) {
|
|
1407
|
+
return path3.join(getGrapheneCache(root), process.env.NODE_ENV == "test" ? "test.pid" : "serve.pid");
|
|
1408
|
+
}
|
|
1409
|
+
function targetPids(pid) {
|
|
1410
|
+
if (process.platform === "win32") return [pid];
|
|
1411
|
+
return [pid, -pid];
|
|
1412
|
+
}
|
|
1413
|
+
function sendSignal(pid, signal) {
|
|
1414
|
+
for (let target of targetPids(pid)) {
|
|
1415
|
+
try {
|
|
1416
|
+
process.kill(target, signal);
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
let code = err.code;
|
|
1419
|
+
if (code === "ESRCH" || code === "EINVAL") continue;
|
|
1420
|
+
return false;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
return true;
|
|
1424
|
+
}
|
|
1425
|
+
async function stopGrapheneIfRunning(root) {
|
|
1426
|
+
if (!await isServerRunning()) return;
|
|
1427
|
+
let pidFile = getPidFilePath(root);
|
|
1428
|
+
let pid = await readPid(pidFile);
|
|
1429
|
+
if (!pid) return;
|
|
1430
|
+
console.log(`Stopping server (${pid})`);
|
|
1431
|
+
sendSignal(pid, "SIGTERM");
|
|
1432
|
+
let end = Date.now() + 5e3;
|
|
1433
|
+
while (Date.now() < end && isServerRunning()) {
|
|
1434
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1435
|
+
}
|
|
1436
|
+
if (!isServerRunning()) return;
|
|
1437
|
+
sendSignal(pid, "SIGKILL");
|
|
1438
|
+
await fs2.remove(pidFile);
|
|
1439
|
+
}
|
|
1440
|
+
async function readPid(pidFile) {
|
|
1441
|
+
if (!await fs2.pathExists(pidFile)) return void 0;
|
|
1442
|
+
let contents = (await fs2.readFile(pidFile, "utf8")).trim();
|
|
1443
|
+
if (!contents) return void 0;
|
|
1444
|
+
let pid = Number.parseInt(contents, 10);
|
|
1445
|
+
if (Number.isNaN(pid)) return void 0;
|
|
1446
|
+
return pid;
|
|
1447
|
+
}
|
|
1448
|
+
async function isServerRunning() {
|
|
1449
|
+
let pidFile = getPidFilePath(config.root);
|
|
1450
|
+
let pid = await readPid(pidFile);
|
|
1451
|
+
if (!pid) return false;
|
|
1452
|
+
try {
|
|
1453
|
+
process.kill(pid, 0);
|
|
1454
|
+
return true;
|
|
1455
|
+
} catch {
|
|
1456
|
+
fs2.removeSync(pidFile);
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
var init_background = __esm({
|
|
1461
|
+
"background.ts"() {
|
|
1462
|
+
init_config();
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1284
1466
|
// connections/bigQuery.ts
|
|
1285
1467
|
var bigQuery_exports = {};
|
|
1286
1468
|
__export(bigQuery_exports, {
|
|
1287
1469
|
BigQueryConnection: () => BigQueryConnection
|
|
1288
1470
|
});
|
|
1289
|
-
import fs3 from "fs";
|
|
1290
|
-
import path4 from "path";
|
|
1291
1471
|
import { BigQuery, BigQueryDate, BigQueryTimestamp } from "@google-cloud/bigquery";
|
|
1292
1472
|
var BigQueryConnection;
|
|
1293
1473
|
var init_bigQuery = __esm({
|
|
@@ -1296,18 +1476,14 @@ var init_bigQuery = __esm({
|
|
|
1296
1476
|
BigQueryConnection = class {
|
|
1297
1477
|
client;
|
|
1298
1478
|
constructor(options = {}) {
|
|
1479
|
+
options.projectId ||= config.bigquery?.projectId;
|
|
1299
1480
|
if (process.env.GOOGLE_CREDENTIALS_CONTENT) {
|
|
1300
1481
|
let parsed = JSON.parse(process.env.GOOGLE_CREDENTIALS_CONTENT);
|
|
1301
|
-
let credPath = path4.resolve("./bq.json");
|
|
1302
|
-
fs3.writeFileSync("./bq.json", process.env.GOOGLE_CREDENTIALS_CONTENT.replace(" ", "\n "));
|
|
1303
|
-
process.env.GOOGLE_APPLICATION_CREDENTIALS = credPath;
|
|
1304
1482
|
options.projectId = parsed.project_id;
|
|
1483
|
+
options.credentials = parsed;
|
|
1305
1484
|
}
|
|
1306
|
-
options.projectId
|
|
1307
|
-
|
|
1308
|
-
options.userAgent ||= "Graphene";
|
|
1309
|
-
if (!options.projectId) throw new Error("googleProjectId must be set in config or provided in service account credentials");
|
|
1310
|
-
this.client = new BigQuery(options);
|
|
1485
|
+
if (!options.projectId) throw new Error("projectId must be set in config or provided in service account credentials");
|
|
1486
|
+
this.client = new BigQuery({ ...options, userAgent: "Graphene" });
|
|
1311
1487
|
}
|
|
1312
1488
|
async runQuery(sql) {
|
|
1313
1489
|
let [job] = await this.client.createQueryJob({ query: sql, useLegacySql: false });
|
|
@@ -1331,27 +1507,32 @@ var duckdb_exports = {};
|
|
|
1331
1507
|
__export(duckdb_exports, {
|
|
1332
1508
|
DuckDBConnection: () => DuckDBConnection
|
|
1333
1509
|
});
|
|
1334
|
-
import { promises as
|
|
1335
|
-
import
|
|
1510
|
+
import { promises as fs3 } from "fs";
|
|
1511
|
+
import path4 from "path";
|
|
1336
1512
|
import { DuckDBTimestampValue, DuckDBInstance, DuckDBDateValue } from "@duckdb/node-api";
|
|
1337
1513
|
var DuckDBConnection;
|
|
1338
1514
|
var init_duckdb = __esm({
|
|
1339
1515
|
"connections/duckdb.ts"() {
|
|
1340
1516
|
init_config();
|
|
1341
1517
|
DuckDBConnection = class {
|
|
1518
|
+
options;
|
|
1342
1519
|
ready;
|
|
1343
1520
|
connection = null;
|
|
1344
|
-
constructor() {
|
|
1521
|
+
constructor(options) {
|
|
1522
|
+
this.options = options || {};
|
|
1345
1523
|
this.ready = this.initialize();
|
|
1346
1524
|
}
|
|
1347
1525
|
async initialize() {
|
|
1348
|
-
let
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1526
|
+
let dbPath = this.options.path;
|
|
1527
|
+
if (!dbPath) {
|
|
1528
|
+
let files = await fs3.readdir(config.root);
|
|
1529
|
+
dbPath = files.find((f) => f.endsWith(".duckdb"));
|
|
1530
|
+
if (!dbPath) throw new Error("No .duckdb file found in current directory");
|
|
1531
|
+
dbPath = path4.resolve(config.root, dbPath);
|
|
1532
|
+
}
|
|
1352
1533
|
let db = await DuckDBInstance.create(":memory:");
|
|
1353
1534
|
this.connection = await db.connect();
|
|
1354
|
-
let escapedPath =
|
|
1535
|
+
let escapedPath = dbPath.replace(/'/g, "''");
|
|
1355
1536
|
await this.connection.run(`attach '${escapedPath}' as graphene_cli (READ_ONLY);`);
|
|
1356
1537
|
await this.connection.run("use graphene_cli;");
|
|
1357
1538
|
}
|
|
@@ -1376,6 +1557,76 @@ var init_duckdb = __esm({
|
|
|
1376
1557
|
}
|
|
1377
1558
|
});
|
|
1378
1559
|
|
|
1560
|
+
// connections/snowflake.ts
|
|
1561
|
+
var snowflake_exports = {};
|
|
1562
|
+
__export(snowflake_exports, {
|
|
1563
|
+
SnowflakeConnection: () => SnowflakeConnection
|
|
1564
|
+
});
|
|
1565
|
+
import { createPrivateKey } from "node:crypto";
|
|
1566
|
+
import snowflake from "snowflake-sdk";
|
|
1567
|
+
var SnowflakeConnection;
|
|
1568
|
+
var init_snowflake = __esm({
|
|
1569
|
+
"connections/snowflake.ts"() {
|
|
1570
|
+
init_config();
|
|
1571
|
+
SnowflakeConnection = class {
|
|
1572
|
+
ready;
|
|
1573
|
+
connection;
|
|
1574
|
+
constructor(opts) {
|
|
1575
|
+
this.ready = this.initialize(opts || {});
|
|
1576
|
+
}
|
|
1577
|
+
async initialize(opts) {
|
|
1578
|
+
let privateKeyPath = process.env.SNOWFLAKE_PRI_KEY_PATH || config.snowflake?.privateKeyPath;
|
|
1579
|
+
let privateKeyPass = process.env.SNOWFLAKE_PRI_PASSPHRASE;
|
|
1580
|
+
let authOptions = {};
|
|
1581
|
+
if (privateKeyPath) {
|
|
1582
|
+
authOptions = { privateKeyPath, privateKeyPass };
|
|
1583
|
+
} else if (opts.privateKey) {
|
|
1584
|
+
let privateKey = createPrivateKey({ key: opts.privateKey, format: "pem", passphrase: privateKeyPass });
|
|
1585
|
+
authOptions = { privateKey: privateKey.export({ format: "pem", type: "pkcs8" }) };
|
|
1586
|
+
}
|
|
1587
|
+
snowflake.configure({ logLevel: process.env.SNOWFLAKE_LOG_LEVEL || "WARN", logFilePath: "/dev/null" });
|
|
1588
|
+
this.connection = snowflake.createConnection({
|
|
1589
|
+
...opts,
|
|
1590
|
+
...config.snowflake || {},
|
|
1591
|
+
...authOptions,
|
|
1592
|
+
authenticator: "SNOWFLAKE_JWT",
|
|
1593
|
+
application: "Graphene"
|
|
1594
|
+
});
|
|
1595
|
+
await new Promise((resolve, reject) => {
|
|
1596
|
+
this.connection.connect((err, conn) => err ? reject(err) : resolve(conn));
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
async runQuery(sql) {
|
|
1600
|
+
await this.ready;
|
|
1601
|
+
return await new Promise((resolve, reject) => {
|
|
1602
|
+
let rows = [];
|
|
1603
|
+
this.connection.execute({
|
|
1604
|
+
sqlText: sql,
|
|
1605
|
+
streamResult: true,
|
|
1606
|
+
complete: (error, statement) => {
|
|
1607
|
+
if (error) {
|
|
1608
|
+
reject(new Error(`Snowflake query failed: ${error.message || error}`));
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
let stream = statement.streamRows();
|
|
1612
|
+
stream.on("error", (err) => reject(err));
|
|
1613
|
+
stream.on("readable", function(row) {
|
|
1614
|
+
while ((row = this.read()) !== null) {
|
|
1615
|
+
rows.push(row);
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
stream.on("end", () => {
|
|
1619
|
+
let totalRows = Number(statement.getNumRows());
|
|
1620
|
+
resolve({ rows, totalRows });
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1379
1630
|
// connections/index.ts
|
|
1380
1631
|
async function getConnection() {
|
|
1381
1632
|
if (config.dialect === "bigquery") {
|
|
@@ -1383,7 +1634,10 @@ async function getConnection() {
|
|
|
1383
1634
|
return new mod.BigQueryConnection();
|
|
1384
1635
|
} else if (config.dialect === "duckdb") {
|
|
1385
1636
|
let mod = await Promise.resolve().then(() => (init_duckdb(), duckdb_exports));
|
|
1386
|
-
return new mod.DuckDBConnection();
|
|
1637
|
+
return new mod.DuckDBConnection({});
|
|
1638
|
+
} else if (config.dialect === "snowflake") {
|
|
1639
|
+
let mod = await Promise.resolve().then(() => (init_snowflake(), snowflake_exports));
|
|
1640
|
+
return new mod.SnowflakeConnection({});
|
|
1387
1641
|
} else {
|
|
1388
1642
|
throw new Error(`Unsupported dialect: ${config.dialect}`);
|
|
1389
1643
|
}
|
|
@@ -1394,6 +1648,201 @@ var init_connections = __esm({
|
|
|
1394
1648
|
}
|
|
1395
1649
|
});
|
|
1396
1650
|
|
|
1651
|
+
// mockFiles.ts
|
|
1652
|
+
var mockFileMap;
|
|
1653
|
+
var init_mockFiles = __esm({
|
|
1654
|
+
"mockFiles.ts"() {
|
|
1655
|
+
mockFileMap = {};
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
// check.ts
|
|
1660
|
+
import fs4 from "fs-extra";
|
|
1661
|
+
import os from "os";
|
|
1662
|
+
import path5 from "path";
|
|
1663
|
+
import { spawn as spawn2 } from "child_process";
|
|
1664
|
+
import { WebSocketServer } from "ws";
|
|
1665
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1666
|
+
import { styleText as styleText2 } from "node:util";
|
|
1667
|
+
async function check(options) {
|
|
1668
|
+
let log = options.log || console.log;
|
|
1669
|
+
let mdFile = options.mdArg && normalizeMdFile(options.mdArg);
|
|
1670
|
+
if (options.mdArg && !mdFile) {
|
|
1671
|
+
log(`Couldn't find ${options.mdArg}`);
|
|
1672
|
+
return false;
|
|
1673
|
+
}
|
|
1674
|
+
await loadWorkspace(config.root, !mdFile);
|
|
1675
|
+
if (mdFile) {
|
|
1676
|
+
if (process.env.NODE_ENV == "test" && mockFileMap[mdFile]) {
|
|
1677
|
+
updateFile(mockFileMap[mdFile], mdFile);
|
|
1678
|
+
} else {
|
|
1679
|
+
let content = readFileSync2(path5.resolve(config.root, mdFile), "utf-8");
|
|
1680
|
+
updateFile(content, mdFile);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
analyze();
|
|
1684
|
+
if (getDiagnostics().length > 0) {
|
|
1685
|
+
printDiagnostics(getDiagnostics(), log);
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
if (!mdFile) {
|
|
1689
|
+
log("No errors found \u{1F48E}");
|
|
1690
|
+
return true;
|
|
1691
|
+
}
|
|
1692
|
+
let host = `http://localhost:${config.port || Number(process.env.GRAPHENE_PORT) || 4e3}`;
|
|
1693
|
+
let pageUrl = "/" + mdFile.replace(/\.md$/, "").replace(/^\//, "").replace(/\\/g, "/");
|
|
1694
|
+
if (pageUrl === "/index") pageUrl = "/";
|
|
1695
|
+
if (process.env.NODE_ENV !== "test" && !await isServerRunning()) {
|
|
1696
|
+
log("Starting Graphene server...");
|
|
1697
|
+
await runServeInBackground();
|
|
1698
|
+
}
|
|
1699
|
+
let resp = await sendCheckRequest({ host, pageUrl, chart: options.chart });
|
|
1700
|
+
if (resp.checkError == "no_server") {
|
|
1701
|
+
log("Failed to start Graphene server");
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
if (resp.checkError == "no_tab" && process.env.NODE_ENV !== "test") {
|
|
1705
|
+
log(`Opening page ${host}${pageUrl}`);
|
|
1706
|
+
spawn2("open", [host + pageUrl]);
|
|
1707
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1708
|
+
resp = await sendCheckRequest({ host, pageUrl, chart: options.chart });
|
|
1709
|
+
}
|
|
1710
|
+
if (resp.checkError == "no_tab") {
|
|
1711
|
+
log("Failed to open a new tab");
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
if (resp.checkError) {
|
|
1715
|
+
log("Failed to run check: " + resp.checkError);
|
|
1716
|
+
return false;
|
|
1717
|
+
}
|
|
1718
|
+
let errors = Array.from(resp.errors || []);
|
|
1719
|
+
if (errors.length) {
|
|
1720
|
+
log(styleText2("red", "Runtime errors") + ` in ${mdFile}:`);
|
|
1721
|
+
} else {
|
|
1722
|
+
log("No errors found \u{1F48E}");
|
|
1723
|
+
}
|
|
1724
|
+
errors.forEach((e) => {
|
|
1725
|
+
if (e.file && e.line) printDiagnostics([e], log);
|
|
1726
|
+
else if (e.id) log(`${e.id}: ${e.message}`);
|
|
1727
|
+
else log(e.message);
|
|
1728
|
+
});
|
|
1729
|
+
if (resp?.stillLoading) {
|
|
1730
|
+
log("Warning: Queries were still loading when the screenshot was taken");
|
|
1731
|
+
}
|
|
1732
|
+
if (resp?.screenshot) {
|
|
1733
|
+
let filename = `graphene-screenshot-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.png`;
|
|
1734
|
+
let screenshotPath = path5.join(os.tmpdir(), filename);
|
|
1735
|
+
let base64Data = resp.screenshot.replace(/^data:image\/png;base64,/, "");
|
|
1736
|
+
await fs4.writeFile(screenshotPath, base64Data, "base64");
|
|
1737
|
+
log("Screenshot saved to", screenshotPath);
|
|
1738
|
+
}
|
|
1739
|
+
return !!errors.length;
|
|
1740
|
+
}
|
|
1741
|
+
async function sendCheckRequest({ host, pageUrl, chart }) {
|
|
1742
|
+
let abort = new AbortController();
|
|
1743
|
+
let timeout = setTimeout(() => abort.abort(), 3e4);
|
|
1744
|
+
try {
|
|
1745
|
+
let response = await fetch(`${host}/_api/check`, {
|
|
1746
|
+
method: "POST",
|
|
1747
|
+
headers: { "Content-Type": "application/json" },
|
|
1748
|
+
body: JSON.stringify({ pageUrl: host + pageUrl, chart }),
|
|
1749
|
+
signal: abort.signal
|
|
1750
|
+
});
|
|
1751
|
+
clearTimeout(timeout);
|
|
1752
|
+
let body = response.headers.get("content-type") == "application/json" ? await response.json() : { error: await response.text() };
|
|
1753
|
+
if (!response.ok) {
|
|
1754
|
+
if (body.error) return { checkError: body.error };
|
|
1755
|
+
console.error(`Unexpected response: ${JSON.stringify(body)}`);
|
|
1756
|
+
return { checkError: "Unexpected response from Graphene server" };
|
|
1757
|
+
}
|
|
1758
|
+
return body;
|
|
1759
|
+
} catch (err) {
|
|
1760
|
+
clearTimeout(timeout);
|
|
1761
|
+
if (err.name === "AbortError") return { checkError: "timeout" };
|
|
1762
|
+
return { checkError: "no_server" };
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
function normalizeMdFile(mdFile) {
|
|
1766
|
+
let clean = mdFile.trim();
|
|
1767
|
+
if (!clean) return null;
|
|
1768
|
+
if (!clean.endsWith(".md")) clean = clean + ".md";
|
|
1769
|
+
if (process.env.NODE_ENV == "test" && mockFileMap[clean]) {
|
|
1770
|
+
return clean;
|
|
1771
|
+
}
|
|
1772
|
+
let absolute = [
|
|
1773
|
+
path5.resolve(process.cwd(), clean),
|
|
1774
|
+
path5.resolve(config.root, clean)
|
|
1775
|
+
].find((p) => fs4.existsSync(p)) || null;
|
|
1776
|
+
if (!absolute) return null;
|
|
1777
|
+
let relative = path5.relative(config.root, absolute);
|
|
1778
|
+
return relative;
|
|
1779
|
+
}
|
|
1780
|
+
async function proxyCheckRequest(req, res) {
|
|
1781
|
+
let chunks = [];
|
|
1782
|
+
for await (let chunk of req) chunks.push(chunk);
|
|
1783
|
+
let { pageUrl, chart } = JSON.parse(Buffer.concat(chunks).toString());
|
|
1784
|
+
let id = Math.random().toString(36).slice(2);
|
|
1785
|
+
res.setHeader("Content-Type", "application/json");
|
|
1786
|
+
let normalizedPageUrl = pageUrl.replace(/\/$/, "");
|
|
1787
|
+
let conn = await pollFor(() => browserConnections.find((conn2) => conn2.url === normalizedPageUrl), 5e3, 100);
|
|
1788
|
+
if (!conn) {
|
|
1789
|
+
res.statusCode = 400;
|
|
1790
|
+
res.end(JSON.stringify({ error: "no_tab" }));
|
|
1791
|
+
return;
|
|
1792
|
+
} else {
|
|
1793
|
+
conn.socket.send(JSON.stringify({ type: "check", chart, requestId: id }));
|
|
1794
|
+
pendingRequests[id] = { response: res };
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
function checkVitePlugin() {
|
|
1798
|
+
return {
|
|
1799
|
+
name: "graphene-check-plugin",
|
|
1800
|
+
configureServer(server) {
|
|
1801
|
+
let wss = new WebSocketServer({ noServer: true });
|
|
1802
|
+
server.httpServer?.on("upgrade", (req, socket, head) => {
|
|
1803
|
+
if (!req.url || !req.url.includes("/_api/ws") && !req.url.includes("graphene-ws")) return;
|
|
1804
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
1805
|
+
wss.emit("connection", ws, req);
|
|
1806
|
+
});
|
|
1807
|
+
});
|
|
1808
|
+
wss.on("connection", (socket) => {
|
|
1809
|
+
socket.on("message", (data) => {
|
|
1810
|
+
let message = JSON.parse(data.toString());
|
|
1811
|
+
if (message.type === "register") {
|
|
1812
|
+
let normalizedUrl = message.url.replace(/\/$/, "");
|
|
1813
|
+
browserConnections.push({ url: normalizedUrl, socket });
|
|
1814
|
+
}
|
|
1815
|
+
if (message.type === "checkResponse") {
|
|
1816
|
+
pendingRequests[message.requestId].response.end(JSON.stringify(message));
|
|
1817
|
+
delete pendingRequests[message.requestId];
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
socket.on("close", () => {
|
|
1821
|
+
browserConnections = browserConnections.filter((conn) => conn.socket !== socket);
|
|
1822
|
+
});
|
|
1823
|
+
});
|
|
1824
|
+
server.httpServer?.on("close", () => wss.close());
|
|
1825
|
+
server.middlewares.use(async (req, res, next) => {
|
|
1826
|
+
let [pathName] = (req.url || "").split("?");
|
|
1827
|
+
if (pathName === "/_api/check") await proxyCheckRequest(req, res);
|
|
1828
|
+
else next();
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
var browserConnections, pendingRequests;
|
|
1834
|
+
var init_check = __esm({
|
|
1835
|
+
"check.ts"() {
|
|
1836
|
+
init_core();
|
|
1837
|
+
init_printer();
|
|
1838
|
+
init_mockFiles();
|
|
1839
|
+
init_background();
|
|
1840
|
+
init_util();
|
|
1841
|
+
browserConnections = [];
|
|
1842
|
+
pendingRequests = {};
|
|
1843
|
+
}
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1397
1846
|
// mdCompile.ts
|
|
1398
1847
|
import fs5 from "fs";
|
|
1399
1848
|
import path6 from "path";
|
|
@@ -1487,7 +1936,6 @@ var init_mdCompile = __esm({
|
|
|
1487
1936
|
// serve2.ts
|
|
1488
1937
|
var serve2_exports = {};
|
|
1489
1938
|
__export(serve2_exports, {
|
|
1490
|
-
mockFileMap: () => mockFileMap,
|
|
1491
1939
|
serve2: () => serve2
|
|
1492
1940
|
});
|
|
1493
1941
|
import { createServer, optimizeDeps } from "vite";
|
|
@@ -1497,13 +1945,11 @@ import crypto from "crypto";
|
|
|
1497
1945
|
import { mdsvex } from "mdsvex";
|
|
1498
1946
|
import path7 from "path";
|
|
1499
1947
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1500
|
-
import { WebSocketServer } from "ws";
|
|
1501
|
-
import { spawn as spawn2 } from "child_process";
|
|
1502
1948
|
async function serve2() {
|
|
1503
|
-
grapheneRoot = config.root;
|
|
1504
1949
|
uiRoot = path7.join(fileURLToPath2(import.meta.url), "../../ui");
|
|
1505
|
-
|
|
1506
|
-
await fs6.
|
|
1950
|
+
let port = Number(process.env.GRAPHENE_PORT) || 4e3;
|
|
1951
|
+
await fs6.ensureDir(path7.resolve(config.root, "node_modules/.graphene"));
|
|
1952
|
+
await fs6.writeFile(path7.resolve(config.root, `node_modules/.graphene/${process.env.NODE_ENV == "test" ? "test" : "serve"}.pid`), String(process.pid));
|
|
1507
1953
|
let server = await createServer({
|
|
1508
1954
|
root: config.root,
|
|
1509
1955
|
plugins: [
|
|
@@ -1519,12 +1965,14 @@ async function serve2() {
|
|
|
1519
1965
|
injectComponentImports()
|
|
1520
1966
|
]
|
|
1521
1967
|
}),
|
|
1968
|
+
checkVitePlugin(),
|
|
1522
1969
|
handleRequestPlugin,
|
|
1523
1970
|
updateWorkspacePlugin,
|
|
1524
1971
|
mockFilesForTests()
|
|
1525
1972
|
],
|
|
1973
|
+
publicDir: path7.resolve(uiRoot),
|
|
1526
1974
|
server: {
|
|
1527
|
-
port
|
|
1975
|
+
port,
|
|
1528
1976
|
fs: { strict: false },
|
|
1529
1977
|
strictPort: true
|
|
1530
1978
|
},
|
|
@@ -1533,10 +1981,16 @@ async function serve2() {
|
|
|
1533
1981
|
graphene: path7.resolve(uiRoot, "web.js")
|
|
1534
1982
|
}
|
|
1535
1983
|
}
|
|
1984
|
+
// optimizeDeps: { // this seems prudent in tests, but currently breaks because ssf needs to be optimized, even in tests
|
|
1985
|
+
// noDiscovery: process.env.NODE_ENV == 'test',
|
|
1986
|
+
// include: process.env.NODE_ENV == 'test' ? [] : undefined,
|
|
1987
|
+
// },
|
|
1536
1988
|
});
|
|
1537
1989
|
await optimizeDeps(server.config);
|
|
1538
1990
|
await server.listen();
|
|
1539
|
-
|
|
1991
|
+
if (process.env.NODE_ENV !== "test") {
|
|
1992
|
+
console.log(`Server running at http://localhost:${port}`);
|
|
1993
|
+
}
|
|
1540
1994
|
return server;
|
|
1541
1995
|
}
|
|
1542
1996
|
async function handleQuery(req, res) {
|
|
@@ -1566,31 +2020,6 @@ async function handleQuery(req, res) {
|
|
|
1566
2020
|
let fields = queries[0].fields.map((f) => ({ name: f.name, type: f.type }));
|
|
1567
2021
|
res.end(JSON.stringify({ rows: queryResults.rows, hash, fields, sql }));
|
|
1568
2022
|
}
|
|
1569
|
-
async function handleView(req, res) {
|
|
1570
|
-
let chunks = [];
|
|
1571
|
-
for await (let chunk of req) chunks.push(chunk);
|
|
1572
|
-
let { mdFile, chart } = JSON.parse(Buffer.concat(chunks).toString());
|
|
1573
|
-
let id = Math.random().toString(36).slice(2);
|
|
1574
|
-
res.setHeader("Content-Type", "application/json");
|
|
1575
|
-
viewRequests[id] = { response: res };
|
|
1576
|
-
let pageUrl = "/" + mdFile.replace(/\.md$/, "").replace(/^\//, "");
|
|
1577
|
-
if (pageUrl === "/index") pageUrl = "/";
|
|
1578
|
-
pageUrl = `http://localhost:${config.port || 4e3}${pageUrl}`;
|
|
1579
|
-
let conn = browserConnections.find((conn2) => conn2.url === pageUrl);
|
|
1580
|
-
if (!conn) {
|
|
1581
|
-
spawn2("open", [pageUrl]);
|
|
1582
|
-
let end = Date.now() + 5e3;
|
|
1583
|
-
while (Date.now() < end && !conn) {
|
|
1584
|
-
conn = browserConnections.find((conn2) => conn2.url === pageUrl);
|
|
1585
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1586
|
-
}
|
|
1587
|
-
if (!conn) {
|
|
1588
|
-
res.statusCode = 500;
|
|
1589
|
-
return res.end(JSON.stringify({ error: "No browser tab available and failed to open one" }));
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
conn.socket.send(JSON.stringify({ type: "view", chart, requestId: id }));
|
|
1593
|
-
}
|
|
1594
2023
|
async function handlePage(server, res, filePath, mount) {
|
|
1595
2024
|
res.setHeader("Content-Type", "text/html");
|
|
1596
2025
|
let mdMount = mount ? `
|
|
@@ -1603,7 +2032,7 @@ async function handlePage(server, res, filePath, mount) {
|
|
|
1603
2032
|
<meta charset="UTF-8" />
|
|
1604
2033
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1605
2034
|
<title>Graphene</title>
|
|
1606
|
-
<link rel="icon" href="
|
|
2035
|
+
<link rel="icon" href="/assets/favicon.ico" />
|
|
1607
2036
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1608
2037
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
1609
2038
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
|
|
@@ -1613,10 +2042,7 @@ async function handlePage(server, res, filePath, mount) {
|
|
|
1613
2042
|
<div id="content"></div>
|
|
1614
2043
|
</main>
|
|
1615
2044
|
<script type="module">
|
|
1616
|
-
// do this first so we can track errors caused by importing the md file
|
|
1617
|
-
import 'graphene'
|
|
1618
|
-
</script>
|
|
1619
|
-
<script type="module">
|
|
2045
|
+
import 'graphene' // do this first so we can track errors caused by importing the md file
|
|
1620
2046
|
${mdMount}
|
|
1621
2047
|
</script>
|
|
1622
2048
|
</body>
|
|
@@ -1629,62 +2055,43 @@ function mockFilesForTests() {
|
|
|
1629
2055
|
name: "mock-files-for-tests",
|
|
1630
2056
|
enforce: "pre",
|
|
1631
2057
|
resolveId(id) {
|
|
1632
|
-
if (mockFileMap[id.replace(
|
|
2058
|
+
if (mockFileMap[id.replace(config.root + "/", "")]) return id + "?mock";
|
|
1633
2059
|
},
|
|
1634
2060
|
load(id) {
|
|
1635
2061
|
if (!id.endsWith("?mock")) return null;
|
|
1636
|
-
return mockFileMap[id.replace(
|
|
2062
|
+
return mockFileMap[id.replace(config.root + "/", "").replace(/\?mock$/, "")];
|
|
1637
2063
|
}
|
|
1638
2064
|
};
|
|
1639
2065
|
}
|
|
1640
|
-
var
|
|
2066
|
+
var uiRoot, workspaceLoadPromise, updateWorkspacePlugin, handleRequestPlugin;
|
|
1641
2067
|
var init_serve2 = __esm({
|
|
1642
2068
|
"serve2.ts"() {
|
|
1643
2069
|
init_core();
|
|
1644
2070
|
init_connections();
|
|
1645
2071
|
init_mdCompile();
|
|
2072
|
+
init_check();
|
|
2073
|
+
init_mockFiles();
|
|
1646
2074
|
updateWorkspacePlugin = {
|
|
1647
2075
|
name: "updateWorkspace",
|
|
1648
2076
|
configureServer: (s) => {
|
|
1649
2077
|
s.watcher.add("**/*.gsql");
|
|
1650
2078
|
s.watcher.on("change", () => {
|
|
1651
2079
|
clearWorkspace();
|
|
1652
|
-
workspaceLoadPromise = loadWorkspace(
|
|
2080
|
+
workspaceLoadPromise = loadWorkspace(config.root, false);
|
|
1653
2081
|
});
|
|
1654
|
-
workspaceLoadPromise = loadWorkspace(
|
|
2082
|
+
workspaceLoadPromise = loadWorkspace(config.root, false);
|
|
1655
2083
|
}
|
|
1656
2084
|
};
|
|
1657
2085
|
handleRequestPlugin = {
|
|
1658
2086
|
name: "handleRequest",
|
|
1659
2087
|
configureServer: (s) => {
|
|
1660
|
-
let wss = new WebSocketServer({ noServer: true });
|
|
1661
|
-
s.httpServer.on("upgrade", (req, socket, head) => {
|
|
1662
|
-
if (!req.url?.endsWith("/graphene-ws")) return;
|
|
1663
|
-
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
1664
|
-
wss.emit("connection", ws, req);
|
|
1665
|
-
});
|
|
1666
|
-
});
|
|
1667
|
-
wss.on("connection", (socket) => {
|
|
1668
|
-
socket.on("message", (data) => {
|
|
1669
|
-
let message = JSON.parse(data.toString());
|
|
1670
|
-
if (message.type === "register") {
|
|
1671
|
-
browserConnections.push({ url: message.url, socket });
|
|
1672
|
-
}
|
|
1673
|
-
if (message.type === "viewResponse") {
|
|
1674
|
-
viewRequests[message.requestId].response.end(JSON.stringify(message));
|
|
1675
|
-
delete viewRequests[message.requestId];
|
|
1676
|
-
}
|
|
1677
|
-
});
|
|
1678
|
-
socket.on("close", () => browserConnections = browserConnections.filter((conn) => conn.socket !== socket));
|
|
1679
|
-
});
|
|
1680
2088
|
s.middlewares.use(async function handleRequest(req, res, next) {
|
|
1681
2089
|
try {
|
|
1682
2090
|
let [pathName] = (req.url || "").split("?");
|
|
1683
2091
|
if (pathName == "/_api/query") return await handleQuery(req, res);
|
|
1684
|
-
if (pathName == "/graphene/view") return await handleView(req, res);
|
|
1685
2092
|
if (pathName == "/__ct") return await handlePage(s, res, "__ct", false);
|
|
1686
2093
|
if (!pathName || pathName == "/") pathName = "index";
|
|
1687
|
-
let mdPath = path7.join(
|
|
2094
|
+
let mdPath = path7.join(config.root, pathName + ".md");
|
|
1688
2095
|
if (await fs6.exists(mdPath)) {
|
|
1689
2096
|
await handlePage(s, res, mdPath, true);
|
|
1690
2097
|
} else {
|
|
@@ -1698,166 +2105,19 @@ var init_serve2 = __esm({
|
|
|
1698
2105
|
});
|
|
1699
2106
|
}
|
|
1700
2107
|
};
|
|
1701
|
-
browserConnections = [];
|
|
1702
|
-
viewRequests = {};
|
|
1703
|
-
mockFileMap = {};
|
|
1704
2108
|
}
|
|
1705
2109
|
});
|
|
1706
2110
|
|
|
1707
2111
|
// cli.ts
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
// printer.ts
|
|
1711
|
-
init_core();
|
|
1712
|
-
import { styleText as nodeStyleText } from "node:util";
|
|
1713
|
-
import Table from "cli-table3";
|
|
1714
|
-
import chalk from "chalk";
|
|
1715
|
-
var styleText = (style, text) => {
|
|
1716
|
-
try {
|
|
1717
|
-
return nodeStyleText ? nodeStyleText(style, text) : text;
|
|
1718
|
-
} catch {
|
|
1719
|
-
return text;
|
|
1720
|
-
}
|
|
1721
|
-
};
|
|
1722
|
-
function offsetToLineCol(src, offset2) {
|
|
1723
|
-
let lines = src.split(/\r?\n/);
|
|
1724
|
-
let acc = 0;
|
|
1725
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1726
|
-
let lineText = lines[i];
|
|
1727
|
-
let nextAcc = acc + lineText.length + 1;
|
|
1728
|
-
if (offset2 < nextAcc || i === lines.length - 1) {
|
|
1729
|
-
let col = Math.max(0, offset2 - acc);
|
|
1730
|
-
return { line: i + 1, col, lineStart: acc, lineText };
|
|
1731
|
-
}
|
|
1732
|
-
acc = nextAcc;
|
|
1733
|
-
}
|
|
1734
|
-
return { line: 1, col: 0, lineStart: 0, lineText: lines[0] || "" };
|
|
1735
|
-
}
|
|
1736
|
-
function printDiagnostics(diags) {
|
|
1737
|
-
let parts = [];
|
|
1738
|
-
for (let d of diags) {
|
|
1739
|
-
let src = getFile2(d.file)?.contents || "";
|
|
1740
|
-
let { line, col, lineStart, lineText } = offsetToLineCol(src, d.from.offset);
|
|
1741
|
-
let endCol = Math.max(col + 1, Math.min(lineText.length, d.to.offset - lineStart));
|
|
1742
|
-
let caretLen = Math.max(1, endCol - col);
|
|
1743
|
-
let sev = d.severity === "error" ? "red" : "yellow";
|
|
1744
|
-
let header = `${styleText(sev, d.severity.toUpperCase())}: ${d.file} line ${line}: ${d.message}`;
|
|
1745
|
-
let gutter = " | ";
|
|
1746
|
-
let caretLine = `${" ".repeat(col)}${styleText(sev, "^".repeat(caretLen))}`;
|
|
1747
|
-
parts.push([header, `${gutter}${lineText}`, `${gutter}${caretLine}`].join("\n"));
|
|
1748
|
-
}
|
|
1749
|
-
if (parts.length) console.error(parts.join("\n"));
|
|
1750
|
-
}
|
|
1751
|
-
function printTable(rows) {
|
|
1752
|
-
if (!rows || rows.length === 0) {
|
|
1753
|
-
console.log(chalk.yellow("No results returned"));
|
|
1754
|
-
return;
|
|
1755
|
-
}
|
|
1756
|
-
let headers = Object.keys(rows[0]);
|
|
1757
|
-
let table2 = new Table({ head: headers.map((h) => chalk.blue(h)) });
|
|
1758
|
-
let MAX_DISPLAY_ROWS = 200;
|
|
1759
|
-
let displayRows = rows.slice(0, MAX_DISPLAY_ROWS);
|
|
1760
|
-
displayRows.forEach((row) => table2.push(headers.map((h) => row[h]?.toString() || "")));
|
|
1761
|
-
console.log(table2.toString());
|
|
1762
|
-
if (rows.length > MAX_DISPLAY_ROWS) {
|
|
1763
|
-
console.log(chalk.yellow(`Displayed first ${MAX_DISPLAY_ROWS} rows (of ${rows.length} total).`));
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
// cli.ts
|
|
2112
|
+
init_printer();
|
|
1768
2113
|
init_core();
|
|
1769
2114
|
init_config();
|
|
2115
|
+
init_background();
|
|
2116
|
+
init_connections();
|
|
2117
|
+
init_check();
|
|
2118
|
+
import { Command } from "commander";
|
|
1770
2119
|
import fs7 from "fs-extra";
|
|
1771
2120
|
import path8 from "path";
|
|
1772
|
-
import os from "os";
|
|
1773
|
-
|
|
1774
|
-
// background.ts
|
|
1775
|
-
import { spawn } from "child_process";
|
|
1776
|
-
import { fileURLToPath } from "url";
|
|
1777
|
-
import fs2 from "fs-extra";
|
|
1778
|
-
import path3 from "path";
|
|
1779
|
-
async function runServeInBackground() {
|
|
1780
|
-
let root = process.cwd();
|
|
1781
|
-
let grapheneCache = getGrapheneCache(root);
|
|
1782
|
-
let logFile = path3.join(grapheneCache, "serve.log");
|
|
1783
|
-
await fs2.ensureDir(grapheneCache);
|
|
1784
|
-
await stopGrapheneIfRunning(root);
|
|
1785
|
-
let log = fs2.openSync(logFile, "w");
|
|
1786
|
-
let entryPoint = process.argv[1] || fileURLToPath(import.meta.url);
|
|
1787
|
-
let childArgs = [...process.execArgv, entryPoint, "serve", "--fg", ...process.argv.slice(3)];
|
|
1788
|
-
let child = spawn(process.execPath, childArgs, {
|
|
1789
|
-
cwd: root,
|
|
1790
|
-
detached: true,
|
|
1791
|
-
env: { ...process.env },
|
|
1792
|
-
stdio: ["ignore", log, log]
|
|
1793
|
-
});
|
|
1794
|
-
if (!child.pid) throw new Error("Failed to start server process");
|
|
1795
|
-
await new Promise((resolve, reject) => {
|
|
1796
|
-
let buffer = "";
|
|
1797
|
-
fs2.watchFile(logFile, { interval: 200 }, (curr, prev) => {
|
|
1798
|
-
if (curr.size > prev.size) {
|
|
1799
|
-
let stream = fs2.createReadStream(logFile, { start: 0, end: curr.size - 1 });
|
|
1800
|
-
stream.on("data", (d) => {
|
|
1801
|
-
process.stdout.write(d);
|
|
1802
|
-
buffer = (buffer + d.toString()).slice(-200);
|
|
1803
|
-
if (buffer.includes("Server running at http://localhost:")) resolve();
|
|
1804
|
-
});
|
|
1805
|
-
}
|
|
1806
|
-
});
|
|
1807
|
-
child.once("exit", () => {
|
|
1808
|
-
process.stdout.write(fs2.readFileSync(logFile));
|
|
1809
|
-
reject(new Error("Exited before server started"));
|
|
1810
|
-
});
|
|
1811
|
-
child.once("error", (e) => reject(e));
|
|
1812
|
-
});
|
|
1813
|
-
}
|
|
1814
|
-
function getGrapheneCache(root) {
|
|
1815
|
-
return path3.join(root, "node_modules", ".graphene");
|
|
1816
|
-
}
|
|
1817
|
-
function getPidFilePath(root) {
|
|
1818
|
-
return path3.join(getGrapheneCache(root), process.env.NODE_ENV == "test" ? "test.pid" : "serve.pid");
|
|
1819
|
-
}
|
|
1820
|
-
async function stopGrapheneIfRunning(root) {
|
|
1821
|
-
let pidFile = getPidFilePath(root);
|
|
1822
|
-
let pid = await readPid(pidFile);
|
|
1823
|
-
if (!pid) return true;
|
|
1824
|
-
if (!isProcessRunning(pid)) {
|
|
1825
|
-
await fs2.remove(pidFile);
|
|
1826
|
-
return true;
|
|
1827
|
-
}
|
|
1828
|
-
try {
|
|
1829
|
-
console.log(`Stopping server (${pid})`);
|
|
1830
|
-
process.kill(pid, "SIGTERM");
|
|
1831
|
-
} catch (err) {
|
|
1832
|
-
if (err.code === "ESRCH") return true;
|
|
1833
|
-
return false;
|
|
1834
|
-
}
|
|
1835
|
-
let end = Date.now() + 5e3;
|
|
1836
|
-
while (Date.now() < end && isProcessRunning(pid)) {
|
|
1837
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1838
|
-
}
|
|
1839
|
-
await fs2.remove(pidFile);
|
|
1840
|
-
return !isProcessRunning(pid);
|
|
1841
|
-
}
|
|
1842
|
-
async function readPid(pidFile) {
|
|
1843
|
-
if (!await fs2.pathExists(pidFile)) return void 0;
|
|
1844
|
-
let contents = (await fs2.readFile(pidFile, "utf8")).trim();
|
|
1845
|
-
if (!contents) return void 0;
|
|
1846
|
-
let pid = Number.parseInt(contents, 10);
|
|
1847
|
-
if (Number.isNaN(pid)) return void 0;
|
|
1848
|
-
return pid;
|
|
1849
|
-
}
|
|
1850
|
-
function isProcessRunning(pid) {
|
|
1851
|
-
try {
|
|
1852
|
-
process.kill(pid, 0);
|
|
1853
|
-
return true;
|
|
1854
|
-
} catch {
|
|
1855
|
-
return false;
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
// cli.ts
|
|
1860
|
-
init_connections();
|
|
1861
2121
|
var program = new Command();
|
|
1862
2122
|
program.name("graphene").description("Graphene CLI").version("1.0.0");
|
|
1863
2123
|
program.hook("preAction", async () => {
|
|
@@ -1895,37 +2155,9 @@ program.command("serve").description("Run the local server").option("--fg", "Run
|
|
|
1895
2155
|
program.command("stop").description("Stop the local server").action(async () => {
|
|
1896
2156
|
await stopGrapheneIfRunning(process.cwd());
|
|
1897
2157
|
});
|
|
1898
|
-
program.command("check").description("Check the project for errors").action(async () => {
|
|
1899
|
-
await
|
|
1900
|
-
|
|
1901
|
-
if (getDiagnostics().length) {
|
|
1902
|
-
printDiagnostics(getDiagnostics());
|
|
1903
|
-
process.exit(1);
|
|
1904
|
-
}
|
|
1905
|
-
console.log("No errors found \u{1F48E}");
|
|
1906
|
-
});
|
|
1907
|
-
program.command("view").description("Capture a screenshot of a rendered markdown file").argument("<mdFile>", "Markdown file to view (e.g., index.md)").option("-c, --chart <chartName>", "Name of specific chart to capture").action(async (mdFile, options) => {
|
|
1908
|
-
let response = await fetch("http://localhost:4000/graphene/view", {
|
|
1909
|
-
method: "POST",
|
|
1910
|
-
headers: { "Content-Type": "application/json" },
|
|
1911
|
-
body: JSON.stringify({ mdFile, chart: options.chart })
|
|
1912
|
-
});
|
|
1913
|
-
if (!response.ok) throw new Error(`View request failed: ${await response.text()}`);
|
|
1914
|
-
let result = await response.json();
|
|
1915
|
-
if (result.errors && result.errors.length > 0) {
|
|
1916
|
-
console.error("Errors found:");
|
|
1917
|
-
result.errors.forEach((error) => console.error(JSON.stringify(error)));
|
|
1918
|
-
}
|
|
1919
|
-
if (result.stillLoading) {
|
|
1920
|
-
console.error("Warning: Queries were still loading when the screenshot was taken");
|
|
1921
|
-
}
|
|
1922
|
-
if (result.screenshot) {
|
|
1923
|
-
let filename = `graphene-screenshot-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.png`;
|
|
1924
|
-
let screenshotPath = path8.join(os.tmpdir(), filename);
|
|
1925
|
-
let base64Data = result.screenshot.replace(/^data:image\/png;base64,/, "");
|
|
1926
|
-
await fs7.writeFile(screenshotPath, base64Data, "base64");
|
|
1927
|
-
console.log("Screenshot saved to", screenshotPath);
|
|
1928
|
-
}
|
|
2158
|
+
program.command("check").description("Check the project for errors, optionally capturing a page screenshot").argument("[mdFile]", "Markdown file to check (e.g., index.md)").option("-c, --chart <chartTitle>", "Title of a specific chart to capture").action(async (mdArg, options) => {
|
|
2159
|
+
let res = await check({ mdArg, chart: options.chart });
|
|
2160
|
+
process.exit(res ? 0 : 1);
|
|
1929
2161
|
});
|
|
1930
2162
|
program.parse(process.argv);
|
|
1931
2163
|
async function readInput(arg) {
|