@malloydata/malloyyo 0.2.20 → 0.2.21
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 +87 -8
- 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 = [
|
|
@@ -3321,6 +3362,12 @@ async function bundleDashboards(opts = {}) {
|
|
|
3321
3362
|
const runner = await makeRunner(root);
|
|
3322
3363
|
const dashboards = await discoverDashboards(root, runner);
|
|
3323
3364
|
if (dashboards.length === 0) throw new Error(`no dashboards found in ${path7.join(root, "dashboards")}`);
|
|
3365
|
+
const manifestPath = path7.join(outDir, ".bundle-manifest.json");
|
|
3366
|
+
let priorData = [];
|
|
3367
|
+
try {
|
|
3368
|
+
priorData = JSON.parse(fs6.readFileSync(manifestPath, "utf8")).dataFiles ?? [];
|
|
3369
|
+
} catch {
|
|
3370
|
+
}
|
|
3324
3371
|
for (const sub of ["assets", "duckdb"]) {
|
|
3325
3372
|
fs6.rmSync(path7.join(outDir, sub), { recursive: true, force: true });
|
|
3326
3373
|
}
|
|
@@ -3357,11 +3404,39 @@ async function bundleDashboards(opts = {}) {
|
|
|
3357
3404
|
);
|
|
3358
3405
|
}
|
|
3359
3406
|
const modelFiles = inlineModelFiles(root);
|
|
3360
|
-
const
|
|
3407
|
+
const outRel = path7.relative(root, outDir).split(path7.sep).join("/");
|
|
3408
|
+
const usedFiles = reachableModelFiles(
|
|
3409
|
+
modelFiles,
|
|
3410
|
+
dashboards.map((d) => d.entryFile).filter((f) => !!f)
|
|
3411
|
+
);
|
|
3412
|
+
const { map: tableFiles, copies } = tableFilePlan(usedFiles, outRel);
|
|
3413
|
+
for (const rel of copies) {
|
|
3414
|
+
const from = path7.join(root, rel);
|
|
3415
|
+
if (!fs6.existsSync(from)) {
|
|
3416
|
+
throw new Error(
|
|
3417
|
+
`model reads '${rel}' but ${from} does not exist.
|
|
3418
|
+
Data files are referenced by a path relative to the project root.`
|
|
3419
|
+
);
|
|
3420
|
+
}
|
|
3421
|
+
const to = path7.join(outDir, rel);
|
|
3422
|
+
fs6.mkdirSync(path7.dirname(to), { recursive: true });
|
|
3423
|
+
fs6.copyFileSync(from, to);
|
|
3424
|
+
}
|
|
3425
|
+
const copiedData = copies;
|
|
3426
|
+
for (const stale of priorData) {
|
|
3427
|
+
if (copies.includes(stale)) continue;
|
|
3428
|
+
fs6.rmSync(path7.join(outDir, stale), { force: true });
|
|
3429
|
+
try {
|
|
3430
|
+
fs6.rmdirSync(path7.dirname(path7.join(outDir, stale)));
|
|
3431
|
+
} catch {
|
|
3432
|
+
}
|
|
3433
|
+
console.log(` removed stale ${stale}`);
|
|
3434
|
+
}
|
|
3435
|
+
fs6.writeFileSync(manifestPath, JSON.stringify({ dataFiles: copies }, null, 2) + "\n");
|
|
3361
3436
|
fs6.writeFileSync(
|
|
3362
3437
|
path7.join(outDir, "assets", "model-files.js"),
|
|
3363
3438
|
`window.__MODEL_FILES__ = ${JSON.stringify(modelFiles)};
|
|
3364
|
-
window.
|
|
3439
|
+
window.__TABLE_FILES__ = ${JSON.stringify(tableFiles)};
|
|
3365
3440
|
` + (selfHostDuckdb ? `window.__DUCKDB_BASE__ = "./duckdb/";
|
|
3366
3441
|
` : "")
|
|
3367
3442
|
);
|
|
@@ -3458,6 +3533,10 @@ bundled ${dashboards.length} dashboard(s) \u2192 ${path7.relative(process.cwd(),
|
|
|
3458
3533
|
selfHostDuckdb ? ` duckdb ${duck.length} assets (self-hosted)` : ` duckdb jsDelivr CDN (nothing copied)`
|
|
3459
3534
|
);
|
|
3460
3535
|
console.log(` model ${Object.keys(modelFiles).length} .malloy files inlined`);
|
|
3536
|
+
if (copiedData.length) {
|
|
3537
|
+
const mb = copiedData.reduce((a, r) => a + fs6.statSync(path7.join(root, r)).size, 0) / 1048576;
|
|
3538
|
+
console.log(` data ${copiedData.length} file(s) copied, ${mb.toFixed(1)} MB (same-origin)`);
|
|
3539
|
+
}
|
|
3461
3540
|
console.log(
|
|
3462
3541
|
target === "pages" ? `
|
|
3463
3542
|
Publish: commit ${path7.basename(outDir)}/ and point GitHub Pages at it.` : `
|
|
@@ -3589,7 +3668,7 @@ async function launchCmd(mode, opts) {
|
|
|
3589
3668
|
}
|
|
3590
3669
|
|
|
3591
3670
|
// package.json
|
|
3592
|
-
var version = "0.2.
|
|
3671
|
+
var version = "0.2.21";
|
|
3593
3672
|
|
|
3594
3673
|
// src/index.ts
|
|
3595
3674
|
function shortSha(sha) {
|