@definite-app/data-apps 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +686 -0
- package/LICENSE +201 -0
- package/README.md +643 -0
- package/build.mjs +459 -0
- package/examples/_refined_demo/app.json +15 -0
- package/examples/_refined_demo/data/sample.parquet +0 -0
- package/examples/_refined_demo/gen_preview_data.py +59 -0
- package/examples/_refined_demo/preview-data.json +13 -0
- package/examples/_refined_demo/src/App.tsx +188 -0
- package/examples/_refined_demo/src/main.tsx +12 -0
- package/examples/loan-portfolio/app.json +31 -0
- package/examples/loan-portfolio/data/loan_book.parquet +0 -0
- package/examples/loan-portfolio/gen_preview_data.py +454 -0
- package/examples/loan-portfolio/preview-data.json +84 -0
- package/examples/loan-portfolio/src/App.tsx +1103 -0
- package/examples/loan-portfolio/src/main.tsx +12 -0
- package/examples/revenue-explorer/app.json +23 -0
- package/examples/revenue-explorer/data/transactions.parquet +0 -0
- package/examples/revenue-explorer/gen_preview_data.py +129 -0
- package/examples/revenue-explorer/preview-data.json +49 -0
- package/examples/revenue-explorer/src/App.tsx +527 -0
- package/examples/revenue-explorer/src/main.tsx +12 -0
- package/package.json +55 -0
- package/preview.mjs +35 -0
- package/runtime/definite-runtime.tsx +5934 -0
- package/scripts/headless-smoke.mjs +196 -0
- package/templates/blank/app.json +15 -0
- package/templates/blank/src/App.tsx +41 -0
- package/templates/blank/src/main.tsx +12 -0
- package/templates/refined/app.json +15 -0
- package/templates/refined/src/App.tsx +198 -0
- package/templates/refined/src/main.tsx +12 -0
package/build.mjs
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { build } from "esbuild";
|
|
7
|
+
|
|
8
|
+
const buildMjsDir = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const toolkitNodeModules = path.join(buildMjsDir, "node_modules");
|
|
10
|
+
const canonicalRuntimePath = path.join(buildMjsDir, "runtime", "definite-runtime.tsx");
|
|
11
|
+
|
|
12
|
+
const definiteRuntimeAliasPlugin = {
|
|
13
|
+
name: "definite-runtime-alias",
|
|
14
|
+
setup(build) {
|
|
15
|
+
build.onResolve({ filter: /^@definite\/runtime$/ }, () => ({
|
|
16
|
+
path: canonicalRuntimePath,
|
|
17
|
+
}));
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function usage() {
|
|
22
|
+
console.error("Usage: node build.mjs <app-dir> [--preview-data /path/to/preview-data.json]");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function escapeInlineScript(value) {
|
|
27
|
+
return value.replace(/<\/script/gi, "<\\/script");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function collectSourceFiles(dir) {
|
|
31
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
32
|
+
const files = [];
|
|
33
|
+
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const fullPath = path.join(dir, entry.name);
|
|
36
|
+
if (entry.isDirectory()) {
|
|
37
|
+
if (entry.name === "dist" || entry.name === "node_modules" || entry.name.startsWith(".")) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
files.push(...await collectSourceFiles(fullPath));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!/\.(jsx?|tsx?)$/.test(entry.name)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
files.push(fullPath);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return files;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getLineNumber(source, index) {
|
|
55
|
+
return source.slice(0, index).split("\n").length;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseLiteralString(argumentSource) {
|
|
59
|
+
const trimmed = argumentSource.trim();
|
|
60
|
+
const match = /^(["'`])(?<value>[\s\S]*?)\1$/u.exec(trimmed);
|
|
61
|
+
if (!match?.groups) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return match.groups.value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function findHookCalls(source) {
|
|
68
|
+
const calls = [];
|
|
69
|
+
const pattern = /\b(useDataset|useJsonResource)\s*\(([\s\S]*?)\)/g;
|
|
70
|
+
|
|
71
|
+
for (const match of source.matchAll(pattern)) {
|
|
72
|
+
const fullMatch = match[0];
|
|
73
|
+
const hookName = match[1];
|
|
74
|
+
const args = match[2] ?? "";
|
|
75
|
+
|
|
76
|
+
// Skip the hook function definitions in the runtime file.
|
|
77
|
+
const definitionPrefix = source.slice(Math.max(0, match.index - 24), match.index);
|
|
78
|
+
if (/\b(?:export\s+)?function\s*$/.test(definitionPrefix)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const firstArg = args.split(",")[0] ?? "";
|
|
83
|
+
calls.push({
|
|
84
|
+
hookName,
|
|
85
|
+
raw: fullMatch,
|
|
86
|
+
firstArg,
|
|
87
|
+
index: match.index ?? 0,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return calls;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const REQUIRED_FILTERS_MARKER_RE = /\{\{\s*required_filters\s*\}\}/;
|
|
95
|
+
|
|
96
|
+
function validateManifest(manifest, manifestPath) {
|
|
97
|
+
if (manifest.version !== 2) {
|
|
98
|
+
throw new Error(`Expected ${manifestPath} to contain {"version": 2}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
102
|
+
throw new Error(`Expected ${manifestPath} to contain a JSON object`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (typeof manifest.entry !== "string" || manifest.entry.length === 0) {
|
|
106
|
+
throw new Error(`Expected ${manifestPath} to define a non-empty "entry"`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!manifest.resources || typeof manifest.resources !== "object" || Array.isArray(manifest.resources)) {
|
|
110
|
+
throw new Error(`Expected ${manifestPath} to define a "resources" object`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const validKinds = new Set(["dataset", "json"]);
|
|
114
|
+
for (const [key, resource] of Object.entries(manifest.resources)) {
|
|
115
|
+
if (!key || typeof key !== "string") {
|
|
116
|
+
throw new Error(`Expected all resource keys in ${manifestPath} to be non-empty strings`);
|
|
117
|
+
}
|
|
118
|
+
if (!resource || typeof resource !== "object" || Array.isArray(resource)) {
|
|
119
|
+
throw new Error(`Resource ${key} in ${manifestPath} must be an object`);
|
|
120
|
+
}
|
|
121
|
+
if (!validKinds.has(resource.kind)) {
|
|
122
|
+
throw new Error(`Resource ${key} in ${manifestPath} has unsupported kind "${resource.kind}"`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const bindings = resource.requiredFilters;
|
|
126
|
+
if (bindings === undefined || bindings === null) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (typeof bindings !== "object" || Array.isArray(bindings)) {
|
|
130
|
+
throw new Error(`Resource ${key} in ${manifestPath}: requiredFilters must be an object`);
|
|
131
|
+
}
|
|
132
|
+
for (const [member, binding] of Object.entries(bindings)) {
|
|
133
|
+
if (!binding || typeof binding !== "object" || Array.isArray(binding)) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Resource ${key} in ${manifestPath}: requiredFilters[${member}] must be an object with a "column" field`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (typeof binding.column !== "string" || binding.column.length === 0) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Resource ${key} in ${manifestPath}: requiredFilters[${member}].column must be a non-empty string`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// SQL resources with requiredFilters MUST contain the {{ required_filters }} marker.
|
|
146
|
+
// Cube resources don't need the marker — Cube filter members are dimension names and
|
|
147
|
+
// get merged server-side by the query handler.
|
|
148
|
+
const source = resource.source;
|
|
149
|
+
if (source && typeof source === "object" && source.type === "sql") {
|
|
150
|
+
if (typeof source.sql === "string" && !REQUIRED_FILTERS_MARKER_RE.test(source.sql)) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Resource ${key} in ${manifestPath} declares requiredFilters but its SQL is missing the ` +
|
|
153
|
+
`{{ required_filters }} marker. Add it to a WHERE/AND clause where the tenant filter should land.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function validateResourceHookUsage(appDir, manifest) {
|
|
161
|
+
const srcDir = path.join(appDir, "src");
|
|
162
|
+
const sourceFiles = await collectSourceFiles(srcDir);
|
|
163
|
+
const errors = [];
|
|
164
|
+
const resources = manifest.resources ?? {};
|
|
165
|
+
|
|
166
|
+
for (const filePath of sourceFiles) {
|
|
167
|
+
const source = await readFile(filePath, "utf8");
|
|
168
|
+
const calls = findHookCalls(source);
|
|
169
|
+
|
|
170
|
+
for (const call of calls) {
|
|
171
|
+
const line = getLineNumber(source, call.index);
|
|
172
|
+
const literalKey = parseLiteralString(call.firstArg);
|
|
173
|
+
const relativePath = path.relative(appDir, filePath);
|
|
174
|
+
const expectedKind = call.hookName === "useDataset" ? "dataset" : "json";
|
|
175
|
+
|
|
176
|
+
if (!literalKey) {
|
|
177
|
+
errors.push(
|
|
178
|
+
`${relativePath}:${line} ${call.hookName}() must use a literal manifest key string as its first argument.`,
|
|
179
|
+
);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const resource = resources[literalKey];
|
|
184
|
+
if (!resource) {
|
|
185
|
+
errors.push(
|
|
186
|
+
`${relativePath}:${line} ${call.hookName}("${literalKey}") does not match any key in app.json resources.`,
|
|
187
|
+
);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (resource.kind !== expectedKind) {
|
|
192
|
+
errors.push(
|
|
193
|
+
`${relativePath}:${line} ${call.hookName}("${literalKey}") expects a ${expectedKind} resource, but app.json declares "${literalKey}" as ${resource.kind}.`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (errors.length > 0) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
[
|
|
202
|
+
"Invalid data-apps-v2 app. Resource hooks must use literal keys that exist in app.json.",
|
|
203
|
+
...errors,
|
|
204
|
+
].join("\n"),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const args = process.argv.slice(2);
|
|
210
|
+
const appDir = args[0] ? path.resolve(args[0]) : "";
|
|
211
|
+
if (!appDir) {
|
|
212
|
+
usage();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let previewDataPath = null;
|
|
216
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
217
|
+
if (args[index] === "--preview-data") {
|
|
218
|
+
previewDataPath = args[index + 1] ? path.resolve(args[index + 1]) : null;
|
|
219
|
+
index += 1;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const manifestPath = path.join(appDir, "app.json");
|
|
224
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
225
|
+
validateManifest(manifest, manifestPath);
|
|
226
|
+
await validateResourceHookUsage(appDir, manifest);
|
|
227
|
+
const previewData = previewDataPath ? JSON.parse(await readFile(previewDataPath, "utf8")) : null;
|
|
228
|
+
if (previewData?.datasets) {
|
|
229
|
+
const previewBaseDir = path.dirname(previewDataPath);
|
|
230
|
+
for (const [key, value] of Object.entries(previewData.datasets)) {
|
|
231
|
+
if (value && typeof value === "object" && !Array.isArray(value) && typeof value.file === "string") {
|
|
232
|
+
const filePath = path.resolve(previewBaseDir, value.file);
|
|
233
|
+
let bytes;
|
|
234
|
+
try {
|
|
235
|
+
bytes = await readFile(filePath);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Preview dataset "${key}" references file "${value.file}" (resolved: ${filePath}) which could not be read: ${
|
|
240
|
+
error instanceof Error ? error.message : String(error)
|
|
241
|
+
}`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
const format = value.format ?? (filePath.endsWith(".duckdb") ? "duckdb" : "parquet");
|
|
245
|
+
previewData.datasets[key] = {
|
|
246
|
+
format,
|
|
247
|
+
base64: bytes.toString("base64"),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const entry = path.join(appDir, manifest.entry ?? "src/main.tsx");
|
|
254
|
+
const outDir = path.join(appDir, "dist");
|
|
255
|
+
await mkdir(outDir, { recursive: true });
|
|
256
|
+
|
|
257
|
+
const result = await build({
|
|
258
|
+
absWorkingDir: appDir,
|
|
259
|
+
entryPoints: [entry],
|
|
260
|
+
bundle: true,
|
|
261
|
+
write: false,
|
|
262
|
+
format: "esm",
|
|
263
|
+
platform: "browser",
|
|
264
|
+
target: ["chrome123", "safari17", "firefox124"],
|
|
265
|
+
jsx: "automatic",
|
|
266
|
+
legalComments: "none",
|
|
267
|
+
sourcemap: "inline",
|
|
268
|
+
// nodePaths lets esbuild resolve react/react-dom from the toolkit's own
|
|
269
|
+
// node_modules even when appDir lives outside this repo (e.g., a user
|
|
270
|
+
// scaffolds an app anywhere on disk and runs `node <toolkit>/build.mjs <app>`).
|
|
271
|
+
nodePaths: [toolkitNodeModules],
|
|
272
|
+
plugins: [definiteRuntimeAliasPlugin],
|
|
273
|
+
define: {
|
|
274
|
+
"process.env.NODE_ENV": JSON.stringify("production"),
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const jsFile = result.outputFiles.find((file) => file.path.endsWith(".js")) ?? result.outputFiles[0];
|
|
279
|
+
if (!jsFile) {
|
|
280
|
+
throw new Error("esbuild did not emit a JavaScript bundle");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const title = typeof manifest.name === "string" && manifest.name.length > 0
|
|
284
|
+
? manifest.name
|
|
285
|
+
: "Definite Data App";
|
|
286
|
+
const importMap = {
|
|
287
|
+
imports: {
|
|
288
|
+
"apache-arrow": "https://storage.googleapis.com/definite-public/libs/apache-arrow@17.0.0/apache-arrow.esm.js",
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const html = `<!DOCTYPE html>
|
|
293
|
+
<html lang="en">
|
|
294
|
+
<head>
|
|
295
|
+
<meta charset="UTF-8" />
|
|
296
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
297
|
+
<meta name="definite-app-version" content="2" />
|
|
298
|
+
<link rel="icon" href="data:," />
|
|
299
|
+
<title>${title}</title>
|
|
300
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
301
|
+
<script src="https://cdn.jsdelivr.net/npm/echarts@5.6.0/dist/echarts.min.js"></script>
|
|
302
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
303
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
304
|
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
305
|
+
<link rel="preload" href="https://cdn.jsdelivr.net/npm/@perspective-dev/server@4.3.0/dist/wasm/perspective-server.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
|
306
|
+
<link rel="preload" href="https://cdn.jsdelivr.net/npm/@perspective-dev/viewer@4.3.0/dist/wasm/perspective-viewer.wasm" as="fetch" type="application/wasm" crossorigin="anonymous" />
|
|
307
|
+
<link rel="stylesheet" crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/@perspective-dev/viewer@4.3.0/dist/css/themes.css" />
|
|
308
|
+
<script type="importmap">${escapeInlineScript(JSON.stringify(importMap))}</script>
|
|
309
|
+
<style>
|
|
310
|
+
:root {
|
|
311
|
+
color-scheme: dark;
|
|
312
|
+
--bg-primary: #09090b;
|
|
313
|
+
--bg-card: #0f0f12;
|
|
314
|
+
--bg-elevated: #16161a;
|
|
315
|
+
--bg-hover: #1c1c22;
|
|
316
|
+
--border: #1e1e26;
|
|
317
|
+
--border-hover: #2e2e38;
|
|
318
|
+
--text-primary: #ececef;
|
|
319
|
+
--text-secondary: #9898a0;
|
|
320
|
+
--text-muted: #5c5c66;
|
|
321
|
+
--accent: #0A99FF;
|
|
322
|
+
--accent-muted: rgba(10, 153, 255, 0.15);
|
|
323
|
+
--accent-subtle: rgba(10, 153, 255, 0.06);
|
|
324
|
+
--accent-strong: #38bdf8;
|
|
325
|
+
--ring: rgba(255,255,255,0.06);
|
|
326
|
+
--shadow-card: 0 1px 2px rgba(0,0,0,0.4), 0 0 0 1px var(--border);
|
|
327
|
+
--shadow-card-hover: 0 4px 16px rgba(0,0,0,0.5), 0 0 0 1px var(--border-hover);
|
|
328
|
+
}
|
|
329
|
+
html.light {
|
|
330
|
+
color-scheme: light;
|
|
331
|
+
--bg-primary: #fafafa;
|
|
332
|
+
--bg-card: #ffffff;
|
|
333
|
+
--bg-elevated: #f4f4f5;
|
|
334
|
+
--bg-hover: #ebebed;
|
|
335
|
+
--border: #e4e4e7;
|
|
336
|
+
--border-hover: #c8c8ce;
|
|
337
|
+
--text-primary: #09090b;
|
|
338
|
+
--text-secondary: #52525b;
|
|
339
|
+
--text-muted: #a1a1aa;
|
|
340
|
+
--accent: #0A84D0;
|
|
341
|
+
--accent-muted: rgba(10, 132, 208, 0.12);
|
|
342
|
+
--accent-subtle: rgba(10, 132, 208, 0.04);
|
|
343
|
+
--accent-strong: #0284c7;
|
|
344
|
+
--ring: rgba(0,0,0,0.05);
|
|
345
|
+
--shadow-card: 0 1px 2px rgba(0,0,0,0.05), 0 0 0 1px var(--border);
|
|
346
|
+
--shadow-card-hover: 0 4px 12px rgba(0,0,0,0.08), 0 0 0 1px var(--border-hover);
|
|
347
|
+
}
|
|
348
|
+
* {
|
|
349
|
+
box-sizing: border-box;
|
|
350
|
+
font-family: "Inter", system-ui, sans-serif;
|
|
351
|
+
}
|
|
352
|
+
h1, h2, h3 {
|
|
353
|
+
font-family: "DM Sans", system-ui, sans-serif;
|
|
354
|
+
}
|
|
355
|
+
html, body, #root {
|
|
356
|
+
min-height: 100%;
|
|
357
|
+
margin: 0;
|
|
358
|
+
background: var(--bg-primary);
|
|
359
|
+
color: var(--text-primary);
|
|
360
|
+
}
|
|
361
|
+
#root { position: relative; z-index: 1; }
|
|
362
|
+
body::before {
|
|
363
|
+
content: "";
|
|
364
|
+
position: fixed;
|
|
365
|
+
top: -200px;
|
|
366
|
+
left: 50%;
|
|
367
|
+
transform: translateX(-50%);
|
|
368
|
+
width: 900px;
|
|
369
|
+
height: 500px;
|
|
370
|
+
background: radial-gradient(ellipse at center, var(--accent-subtle) 0%, transparent 70%);
|
|
371
|
+
pointer-events: none;
|
|
372
|
+
z-index: 0;
|
|
373
|
+
}
|
|
374
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
375
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
376
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
377
|
+
::-webkit-scrollbar-thumb:hover { background: var(--border-hover); }
|
|
378
|
+
@keyframes fade-up {
|
|
379
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
380
|
+
to { opacity: 1; transform: translateY(0); }
|
|
381
|
+
}
|
|
382
|
+
@keyframes pulse-dot {
|
|
383
|
+
0%, 100% { opacity: 0.3; }
|
|
384
|
+
50% { opacity: 1; }
|
|
385
|
+
}
|
|
386
|
+
@keyframes shimmer {
|
|
387
|
+
0% { background-position: -200% 0; }
|
|
388
|
+
100% { background-position: 200% 0; }
|
|
389
|
+
}
|
|
390
|
+
perspective-viewer, perspective-viewer * {
|
|
391
|
+
transition: none !important;
|
|
392
|
+
}
|
|
393
|
+
perspective-viewer {
|
|
394
|
+
width: 100%;
|
|
395
|
+
height: 100%;
|
|
396
|
+
--plugin--font-family: "Inter", system-ui, sans-serif;
|
|
397
|
+
}
|
|
398
|
+
</style>
|
|
399
|
+
<script id="definite-app-manifest" type="application/json">${escapeInlineScript(JSON.stringify(manifest))}</script>
|
|
400
|
+
${previewData ? `<script id="definite-app-preview-data" type="application/json">${escapeInlineScript(JSON.stringify(previewData))}</script>` : ""}
|
|
401
|
+
</head>
|
|
402
|
+
<body>
|
|
403
|
+
<div id="root"></div>
|
|
404
|
+
<script type="module">${escapeInlineScript(jsFile.text)}</script>
|
|
405
|
+
</body>
|
|
406
|
+
</html>
|
|
407
|
+
`;
|
|
408
|
+
|
|
409
|
+
await writeFile(path.join(outDir, "index.html"), html, "utf8");
|
|
410
|
+
console.log(`Built ${path.join(outDir, "index.html")}`);
|
|
411
|
+
|
|
412
|
+
// =============================================================================
|
|
413
|
+
// Embedded variant: strip query declarations from the manifest and inject a
|
|
414
|
+
// placeholder script tag that the Definite backend swaps for the real token
|
|
415
|
+
// at serve time when the app is fetched via the embed route.
|
|
416
|
+
// =============================================================================
|
|
417
|
+
|
|
418
|
+
function buildEmbeddedManifest(src) {
|
|
419
|
+
const strippedResources = {};
|
|
420
|
+
for (const [key, resource] of Object.entries(src.resources ?? {})) {
|
|
421
|
+
if (!resource || typeof resource !== "object") {
|
|
422
|
+
strippedResources[key] = resource;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const source = resource.source ?? null;
|
|
426
|
+
if (source && typeof source === "object" && (source.type === "sql" || source.type === "cube")) {
|
|
427
|
+
// Replace the source with just its type + embedded:true so the runtime
|
|
428
|
+
// knows which backend branch to call, but doesn't see the raw SQL or
|
|
429
|
+
// cube query JSON.
|
|
430
|
+
strippedResources[key] = {
|
|
431
|
+
...resource,
|
|
432
|
+
source: { type: source.type, embedded: true },
|
|
433
|
+
};
|
|
434
|
+
} else {
|
|
435
|
+
strippedResources[key] = resource;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return { ...src, resources: strippedResources };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const embeddedManifest = buildEmbeddedManifest(manifest);
|
|
442
|
+
const EMBED_TOKEN_PLACEHOLDER =
|
|
443
|
+
'<script id="__definite_embed_token">window.__DEFINITE_EMBED=null;</script>';
|
|
444
|
+
|
|
445
|
+
const embeddedHtml = html
|
|
446
|
+
.replace(
|
|
447
|
+
`<script id="definite-app-manifest" type="application/json">${escapeInlineScript(JSON.stringify(manifest))}</script>`,
|
|
448
|
+
`<script id="definite-app-manifest" type="application/json">${escapeInlineScript(JSON.stringify(embeddedManifest))}</script>\n ${EMBED_TOKEN_PLACEHOLDER}`,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
if (!embeddedHtml.includes(EMBED_TOKEN_PLACEHOLDER)) {
|
|
452
|
+
throw new Error(
|
|
453
|
+
"build.mjs failed to emit the __definite_embed_token placeholder in the embedded HTML. " +
|
|
454
|
+
"The placeholder script tag format must match what the Definite embed route expects.",
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
await writeFile(path.join(outDir, "index.embedded.html"), embeddedHtml, "utf8");
|
|
459
|
+
console.log(`Built ${path.join(outDir, "index.embedded.html")}`);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 2,
|
|
3
|
+
"name": "My App",
|
|
4
|
+
"entry": "src/main.tsx",
|
|
5
|
+
"resources": {
|
|
6
|
+
"main": {
|
|
7
|
+
"kind": "dataset",
|
|
8
|
+
"source": {
|
|
9
|
+
"type": "sql",
|
|
10
|
+
"sql": "SELECT id, originated, name FROM LAKE.SCHEMA.sample_events LIMIT 10000"
|
|
11
|
+
},
|
|
12
|
+
"public": false
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = ["pyarrow"]
|
|
5
|
+
# ///
|
|
6
|
+
"""Generate a tiny synthetic dataset for _refined_demo.
|
|
7
|
+
|
|
8
|
+
Writes data/sample.parquet (100 rows) so CI (and local previews) can verify
|
|
9
|
+
the refined template + runtime actually start up against real preview data.
|
|
10
|
+
|
|
11
|
+
Schema matches what examples/_refined_demo/src/App.tsx queries:
|
|
12
|
+
- id (int) row identifier
|
|
13
|
+
- originated (string) ISO date used as DATE_COLUMN in App.tsx
|
|
14
|
+
- name (string) free-form label for detail views
|
|
15
|
+
|
|
16
|
+
Dates are spread from 2020-01 through 2029-12 so the default "Last 12 months"
|
|
17
|
+
date filter has rows to pick up regardless of when the test runs.
|
|
18
|
+
|
|
19
|
+
Run: uv run examples/_refined_demo/gen_preview_data.py
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import random
|
|
23
|
+
from datetime import date, timedelta
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import pyarrow as pa
|
|
27
|
+
import pyarrow.parquet as pq
|
|
28
|
+
|
|
29
|
+
HERE = Path(__file__).parent
|
|
30
|
+
DATA_DIR = HERE / "data"
|
|
31
|
+
DATA_DIR.mkdir(exist_ok=True)
|
|
32
|
+
PARQUET_PATH = DATA_DIR / "sample.parquet"
|
|
33
|
+
|
|
34
|
+
random.seed(42)
|
|
35
|
+
START = date(2020, 1, 1)
|
|
36
|
+
END = date(2029, 12, 31)
|
|
37
|
+
SPAN_DAYS = (END - START).days
|
|
38
|
+
|
|
39
|
+
NAMES = [
|
|
40
|
+
"Acme",
|
|
41
|
+
"Globex",
|
|
42
|
+
"Initech",
|
|
43
|
+
"Umbrella",
|
|
44
|
+
"Soylent",
|
|
45
|
+
"Hooli",
|
|
46
|
+
"Vandelay",
|
|
47
|
+
"Pied Piper",
|
|
48
|
+
"Stark",
|
|
49
|
+
"Wayne",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
ROWS = 100
|
|
53
|
+
ids = list(range(1, ROWS + 1))
|
|
54
|
+
originated = [(START + timedelta(days=random.randint(0, SPAN_DAYS))).isoformat() for _ in range(ROWS)]
|
|
55
|
+
names = [random.choice(NAMES) for _ in range(ROWS)]
|
|
56
|
+
|
|
57
|
+
table = pa.table({"id": ids, "originated": originated, "name": names})
|
|
58
|
+
pq.write_table(table, PARQUET_PATH)
|
|
59
|
+
print(f"Wrote {PARQUET_PATH} ({ROWS} rows)")
|