@malloydata/malloyyo 0.2.20 → 0.2.22
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/dist/frame-wasm-entry.tsx +21 -15
- package/dist/index.js +107 -13
- package/package.json +1 -1
|
@@ -46,24 +46,30 @@ class StaticWasmConnection extends DuckDBWASMConnection {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Whole-file fetch, not ranged reads.
|
|
50
|
-
// every query touches, ranged reads trade one bulk download for dozens of WAN
|
|
51
|
-
// round trips and lose badly.
|
|
49
|
+
// Whole-file fetch, not ranged reads.
|
|
52
50
|
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
// __TABLE_FILES__ maps each table reference the model makes to the URL this
|
|
52
|
+
// page fetches for it. The KEY is what the model wrote and what DuckDB will
|
|
53
|
+
// look up, so registering the bytes under that exact name means the model needs
|
|
54
|
+
// no rewriting between `dashboard dev` (reads the file off disk) and here.
|
|
55
|
+
//
|
|
56
|
+
// Project-relative refs like `data/x.parquet` resolve against the page, so they
|
|
57
|
+
// are SAME-ORIGIN — no CORS involved at all. Absolute URLs are fetched
|
|
58
|
+
// cross-origin and do need CORS on that host.
|
|
59
|
+
//
|
|
60
|
+
// This cannot go through db-duckdb's registerRemoteTableCallback: findTables
|
|
61
|
+
// skips anything matching ^https?:// ("handled by duckdb-wasm"), so the callback
|
|
62
|
+
// never fires for a remote URL. Pre-registering is also strictly better — one
|
|
63
|
+
// bulk GET instead of dozens of ranged round trips.
|
|
64
|
+
const TABLE_FILES: Record<string, string> = (window as any).__TABLE_FILES__ || {};
|
|
65
|
+
|
|
66
|
+
async function preloadTables(db: any) {
|
|
62
67
|
await Promise.all(
|
|
63
|
-
|
|
68
|
+
Object.entries(TABLE_FILES).map(async ([name, href]) => {
|
|
69
|
+
const url = new URL(href, document.baseURI).href;
|
|
64
70
|
const r = await fetch(url);
|
|
65
71
|
if (!r.ok) throw new Error(`fetch ${url} failed: ${r.status} ${r.statusText}`);
|
|
66
|
-
await db.registerFileBuffer(
|
|
72
|
+
await db.registerFileBuffer(name, new Uint8Array(await r.arrayBuffer()));
|
|
67
73
|
}),
|
|
68
74
|
);
|
|
69
75
|
}
|
|
@@ -89,7 +95,7 @@ function getRuntime() {
|
|
|
89
95
|
runtimeP = (async () => {
|
|
90
96
|
const connection = new StaticWasmConnection({ name: "duckdb" });
|
|
91
97
|
await connection.connecting;
|
|
92
|
-
await
|
|
98
|
+
await preloadTables((connection as any).database);
|
|
93
99
|
return new SingleConnectionRuntime({ connection, urlReader });
|
|
94
100
|
})();
|
|
95
101
|
}
|
package/dist/index.js
CHANGED
|
@@ -3202,13 +3202,54 @@ function inlineModelFiles(root) {
|
|
|
3202
3202
|
walk(root);
|
|
3203
3203
|
return files;
|
|
3204
3204
|
}
|
|
3205
|
-
function
|
|
3206
|
-
const
|
|
3207
|
-
const
|
|
3205
|
+
function reachableModelFiles(modelFiles, entries) {
|
|
3206
|
+
const IMPORT = /\bimport\s+(?:\{[^}]*\}\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
3207
|
+
const out = {};
|
|
3208
|
+
const queue = entries.map((e) => `file:///${e.replace(/^\.?\//, "")}`);
|
|
3209
|
+
while (queue.length) {
|
|
3210
|
+
const key = queue.shift();
|
|
3211
|
+
if (key in out) continue;
|
|
3212
|
+
const src = modelFiles[key];
|
|
3213
|
+
if (src == null) continue;
|
|
3214
|
+
out[key] = src;
|
|
3215
|
+
const dir = key.slice(0, key.lastIndexOf("/"));
|
|
3216
|
+
for (const m of src.matchAll(IMPORT)) {
|
|
3217
|
+
queue.push(new URL(m[1], dir + "/").href);
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
return out;
|
|
3221
|
+
}
|
|
3222
|
+
function findTableRefs(modelFiles) {
|
|
3223
|
+
const refs = /* @__PURE__ */ new Set();
|
|
3224
|
+
const re = /\btable\(\s*(['"])([^'"]+)\1/g;
|
|
3208
3225
|
for (const src of Object.values(modelFiles)) {
|
|
3209
|
-
for (const m of src.matchAll(re))
|
|
3226
|
+
for (const m of src.matchAll(re)) refs.add(m[2]);
|
|
3227
|
+
}
|
|
3228
|
+
return [...refs].sort();
|
|
3229
|
+
}
|
|
3230
|
+
function isDataFile(ref) {
|
|
3231
|
+
if (/^https?:\/\//i.test(ref)) return true;
|
|
3232
|
+
return /\.(parquet|csv|tsv|json|ndjson)$/i.test(ref);
|
|
3233
|
+
}
|
|
3234
|
+
function tableFilePlan(modelFiles, outRel) {
|
|
3235
|
+
const map = {};
|
|
3236
|
+
const copies = [];
|
|
3237
|
+
const out = outRel.replace(/^\.?\//, "").replace(/\/$/, "");
|
|
3238
|
+
for (const ref of findTableRefs(modelFiles)) {
|
|
3239
|
+
if (!isDataFile(ref)) continue;
|
|
3240
|
+
if (/^https?:\/\//i.test(ref)) {
|
|
3241
|
+
map[ref] = ref;
|
|
3242
|
+
continue;
|
|
3243
|
+
}
|
|
3244
|
+
const rel = ref.replace(/^\.?\//, "");
|
|
3245
|
+
if (out && (rel === out || rel.startsWith(out + "/"))) {
|
|
3246
|
+
map[ref] = `./${rel.slice(out.length + 1)}`;
|
|
3247
|
+
} else {
|
|
3248
|
+
map[ref] = `./${rel}`;
|
|
3249
|
+
copies.push(rel);
|
|
3250
|
+
}
|
|
3210
3251
|
}
|
|
3211
|
-
return
|
|
3252
|
+
return { map, copies };
|
|
3212
3253
|
}
|
|
3213
3254
|
function copyDuckDBAssets(outDir) {
|
|
3214
3255
|
const names = [
|
|
@@ -3234,7 +3275,14 @@ function navFor(dash, all, cleanUrls) {
|
|
|
3234
3275
|
(n) => cleanUrls ? `./${encodeURIComponent(n)}` : `./${encodeURIComponent(n)}.html`
|
|
3235
3276
|
);
|
|
3236
3277
|
}
|
|
3237
|
-
function
|
|
3278
|
+
function analyticsSnippet(id) {
|
|
3279
|
+
if (!id) return "";
|
|
3280
|
+
const j = JSON.stringify(id);
|
|
3281
|
+
return `<script async src="https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}"></script>
|
|
3282
|
+
<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}
|
|
3283
|
+
gtag('js',new Date());gtag('config',${j});</script>`;
|
|
3284
|
+
}
|
|
3285
|
+
function page(dash, all, title, givenSpecs, tileSpecs, cleanUrls, analytics) {
|
|
3238
3286
|
const info = {
|
|
3239
3287
|
name: dash.name,
|
|
3240
3288
|
query: dash.query,
|
|
@@ -3254,6 +3302,7 @@ function page(dash, all, title, givenSpecs, tileSpecs, cleanUrls) {
|
|
|
3254
3302
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3255
3303
|
<title>${esc3(dash.title || dash.name)}${title ? ` \xB7 ${esc3(title)}` : ""}</title>
|
|
3256
3304
|
<link rel="stylesheet" href="./assets/site.css">
|
|
3305
|
+
${analyticsSnippet(analytics)}
|
|
3257
3306
|
</head>
|
|
3258
3307
|
<body>
|
|
3259
3308
|
${navFor(dash, all, cleanUrls)}
|
|
@@ -3276,7 +3325,7 @@ window.__GIVENS__ = ${JSON.stringify(givenSpecs)};
|
|
|
3276
3325
|
</html>
|
|
3277
3326
|
`;
|
|
3278
3327
|
}
|
|
3279
|
-
function indexPage(dashboards, title, custom, cleanUrls) {
|
|
3328
|
+
function indexPage(dashboards, title, custom, cleanUrls, analytics) {
|
|
3280
3329
|
const link = (n) => cleanUrls ? `./${encodeURIComponent(n)}` : `./${encodeURIComponent(n)}.html`;
|
|
3281
3330
|
const body = custom ? `<div id="root"></div>
|
|
3282
3331
|
<script>window.__DASHBOARDS__ = ${JSON.stringify(
|
|
@@ -3292,6 +3341,7 @@ function indexPage(dashboards, title, custom, cleanUrls) {
|
|
|
3292
3341
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3293
3342
|
<title>${esc3(title)}</title>
|
|
3294
3343
|
<link rel="stylesheet" href="./assets/site.css">
|
|
3344
|
+
${analyticsSnippet(analytics)}
|
|
3295
3345
|
</head>
|
|
3296
3346
|
<body>
|
|
3297
3347
|
${navHtml("", dashboards, link)}
|
|
@@ -3321,6 +3371,12 @@ async function bundleDashboards(opts = {}) {
|
|
|
3321
3371
|
const runner = await makeRunner(root);
|
|
3322
3372
|
const dashboards = await discoverDashboards(root, runner);
|
|
3323
3373
|
if (dashboards.length === 0) throw new Error(`no dashboards found in ${path7.join(root, "dashboards")}`);
|
|
3374
|
+
const manifestPath = path7.join(outDir, ".bundle-manifest.json");
|
|
3375
|
+
let priorData = [];
|
|
3376
|
+
try {
|
|
3377
|
+
priorData = JSON.parse(fs6.readFileSync(manifestPath, "utf8")).dataFiles ?? [];
|
|
3378
|
+
} catch {
|
|
3379
|
+
}
|
|
3324
3380
|
for (const sub of ["assets", "duckdb"]) {
|
|
3325
3381
|
fs6.rmSync(path7.join(outDir, sub), { recursive: true, force: true });
|
|
3326
3382
|
}
|
|
@@ -3357,11 +3413,39 @@ async function bundleDashboards(opts = {}) {
|
|
|
3357
3413
|
);
|
|
3358
3414
|
}
|
|
3359
3415
|
const modelFiles = inlineModelFiles(root);
|
|
3360
|
-
const
|
|
3416
|
+
const outRel = path7.relative(root, outDir).split(path7.sep).join("/");
|
|
3417
|
+
const usedFiles = reachableModelFiles(
|
|
3418
|
+
modelFiles,
|
|
3419
|
+
dashboards.map((d) => d.entryFile).filter((f) => !!f)
|
|
3420
|
+
);
|
|
3421
|
+
const { map: tableFiles, copies } = tableFilePlan(usedFiles, outRel);
|
|
3422
|
+
for (const rel of copies) {
|
|
3423
|
+
const from = path7.join(root, rel);
|
|
3424
|
+
if (!fs6.existsSync(from)) {
|
|
3425
|
+
throw new Error(
|
|
3426
|
+
`model reads '${rel}' but ${from} does not exist.
|
|
3427
|
+
Data files are referenced by a path relative to the project root.`
|
|
3428
|
+
);
|
|
3429
|
+
}
|
|
3430
|
+
const to = path7.join(outDir, rel);
|
|
3431
|
+
fs6.mkdirSync(path7.dirname(to), { recursive: true });
|
|
3432
|
+
fs6.copyFileSync(from, to);
|
|
3433
|
+
}
|
|
3434
|
+
const copiedData = copies;
|
|
3435
|
+
for (const stale of priorData) {
|
|
3436
|
+
if (copies.includes(stale)) continue;
|
|
3437
|
+
fs6.rmSync(path7.join(outDir, stale), { force: true });
|
|
3438
|
+
try {
|
|
3439
|
+
fs6.rmdirSync(path7.dirname(path7.join(outDir, stale)));
|
|
3440
|
+
} catch {
|
|
3441
|
+
}
|
|
3442
|
+
console.log(` removed stale ${stale}`);
|
|
3443
|
+
}
|
|
3444
|
+
fs6.writeFileSync(manifestPath, JSON.stringify({ dataFiles: copies }, null, 2) + "\n");
|
|
3361
3445
|
fs6.writeFileSync(
|
|
3362
3446
|
path7.join(outDir, "assets", "model-files.js"),
|
|
3363
3447
|
`window.__MODEL_FILES__ = ${JSON.stringify(modelFiles)};
|
|
3364
|
-
window.
|
|
3448
|
+
window.__TABLE_FILES__ = ${JSON.stringify(tableFiles)};
|
|
3365
3449
|
` + (selfHostDuckdb ? `window.__DUCKDB_BASE__ = "./duckdb/";
|
|
3366
3450
|
` : "")
|
|
3367
3451
|
);
|
|
@@ -3418,9 +3502,9 @@ boot(Dashboard);
|
|
|
3418
3502
|
if (!got.ok) throw new Error(`dashboard ${d.name}: ${got.error}`);
|
|
3419
3503
|
specs = got.givens;
|
|
3420
3504
|
}
|
|
3421
|
-
fs6.writeFileSync(path7.join(outDir, `${d.name}.html`), page(d, dashboards, title, specs, tileSpecs, cleanUrls));
|
|
3505
|
+
fs6.writeFileSync(path7.join(outDir, `${d.name}.html`), page(d, dashboards, title, specs, tileSpecs, cleanUrls, opts.analytics));
|
|
3422
3506
|
}
|
|
3423
|
-
fs6.writeFileSync(path7.join(outDir, "index.html"), indexPage(dashboards, title, !!landing, cleanUrls));
|
|
3507
|
+
fs6.writeFileSync(path7.join(outDir, "index.html"), indexPage(dashboards, title, !!landing, cleanUrls, opts.analytics));
|
|
3424
3508
|
if (landing) {
|
|
3425
3509
|
await esbuild3.build({
|
|
3426
3510
|
stdin: {
|
|
@@ -3458,6 +3542,10 @@ bundled ${dashboards.length} dashboard(s) \u2192 ${path7.relative(process.cwd(),
|
|
|
3458
3542
|
selfHostDuckdb ? ` duckdb ${duck.length} assets (self-hosted)` : ` duckdb jsDelivr CDN (nothing copied)`
|
|
3459
3543
|
);
|
|
3460
3544
|
console.log(` model ${Object.keys(modelFiles).length} .malloy files inlined`);
|
|
3545
|
+
if (copiedData.length) {
|
|
3546
|
+
const mb = copiedData.reduce((a, r) => a + fs6.statSync(path7.join(root, r)).size, 0) / 1048576;
|
|
3547
|
+
console.log(` data ${copiedData.length} file(s) copied, ${mb.toFixed(1)} MB (same-origin)`);
|
|
3548
|
+
}
|
|
3461
3549
|
console.log(
|
|
3462
3550
|
target === "pages" ? `
|
|
3463
3551
|
Publish: commit ${path7.basename(outDir)}/ and point GitHub Pages at it.` : `
|
|
@@ -3589,7 +3677,7 @@ async function launchCmd(mode, opts) {
|
|
|
3589
3677
|
}
|
|
3590
3678
|
|
|
3591
3679
|
// package.json
|
|
3592
|
-
var version = "0.2.
|
|
3680
|
+
var version = "0.2.22";
|
|
3593
3681
|
|
|
3594
3682
|
// src/index.ts
|
|
3595
3683
|
function shortSha(sha) {
|
|
@@ -3696,7 +3784,7 @@ program.command("author").option("-C, --root <dir>", "project root (default: cur
|
|
|
3696
3784
|
program.command("test").option("-C, --root <dir>", "project root (default: current directory)").description("launch Claude wired ONLY to the explore surface \u2014 the claude.ai web preview").action(async (opts) => {
|
|
3697
3785
|
await launchCmd("test", opts);
|
|
3698
3786
|
});
|
|
3699
|
-
program.command("dashboard").argument("<action>", "action to run (dev | bundle)").option("-C, --root <dir>", "project root (default: current directory)").option("-p, --port <port>", "port to serve on (dev)", "4173").option("-o, --out <dir>", "output directory (bundle)", "docs").option("--title <title>", "site title (bundle; default: project directory name)").option("--target <target>", "deploy target: pages | vercel (bundle)", "pages").option("--duckdb <source>", "DuckDB binaries: cdn | bundled (bundle)", "cdn").option("--no-serve", "bundle only; don't serve the result (bundle)").description("preview dashboards locally (dev), or build a static site from them (bundle)").action(
|
|
3787
|
+
program.command("dashboard").argument("<action>", "action to run (dev | bundle)").option("-C, --root <dir>", "project root (default: current directory)").option("-p, --port <port>", "port to serve on (dev)", "4173").option("-o, --out <dir>", "output directory (bundle)", "docs").option("--title <title>", "site title (bundle; default: project directory name)").option("--target <target>", "deploy target: pages | vercel (bundle)", "pages").option("--duckdb <source>", "DuckDB binaries: cdn | bundled (bundle)", "cdn").option("--analytics <id>", "Google Analytics 4 Measurement ID, e.g. G-XXXXXXXXXX (bundle)").option("--no-serve", "bundle only; don't serve the result (bundle)").description("preview dashboards locally (dev), or build a static site from them (bundle)").action(
|
|
3700
3788
|
async (action, opts) => {
|
|
3701
3789
|
if (action === "dev") {
|
|
3702
3790
|
await serveDashboard({ root: opts.root, port: Number(opts.port) });
|
|
@@ -3706,6 +3794,11 @@ program.command("dashboard").argument("<action>", "action to run (dev | bundle)"
|
|
|
3706
3794
|
if (opts.target !== "pages" && opts.target !== "vercel") {
|
|
3707
3795
|
throw new Error(`unknown --target '${opts.target}' (expected: pages | vercel)`);
|
|
3708
3796
|
}
|
|
3797
|
+
if (opts.analytics && !/^G-[A-Z0-9]+$/i.test(opts.analytics)) {
|
|
3798
|
+
throw new Error(
|
|
3799
|
+
`--analytics expects a GA4 Measurement ID like G-XXXXXXXXXX, got '${opts.analytics}'`
|
|
3800
|
+
);
|
|
3801
|
+
}
|
|
3709
3802
|
if (opts.duckdb !== "cdn" && opts.duckdb !== "bundled") {
|
|
3710
3803
|
throw new Error(`unknown --duckdb '${opts.duckdb}' (expected: cdn | bundled)`);
|
|
3711
3804
|
}
|
|
@@ -3716,6 +3809,7 @@ program.command("dashboard").argument("<action>", "action to run (dev | bundle)"
|
|
|
3716
3809
|
serve: opts.serve,
|
|
3717
3810
|
target: opts.target,
|
|
3718
3811
|
duckdb: opts.duckdb,
|
|
3812
|
+
analytics: opts.analytics,
|
|
3719
3813
|
// `dashboard dev` owns 4173/4174; default the bundle preview clear of
|
|
3720
3814
|
// both so you can run the two side by side.
|
|
3721
3815
|
port: opts.port === "4173" ? 4180 : Number(opts.port)
|