@graphenedata/cli 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/cli/cli.js +8591 -1214
  2. package/dist/docs/base.md +98 -0
  3. package/dist/docs/cli.md +22 -0
  4. package/dist/docs/graphene.md +10 -10
  5. package/dist/ui/component-utilities/echarts.js +2 -3
  6. package/dist/ui/component-utilities/formatting.js +3 -11
  7. package/dist/ui/component-utilities/getSeriesConfig.js +2 -1
  8. package/dist/ui/components/Area.svelte +188 -151
  9. package/dist/ui/components/AreaChart.svelte +43 -79
  10. package/dist/ui/components/Bar.svelte +273 -255
  11. package/dist/ui/components/BarChart.svelte +58 -112
  12. package/dist/ui/components/BigValue.svelte +13 -7
  13. package/dist/ui/components/Chart.svelte +280 -317
  14. package/dist/ui/components/Column.svelte +102 -113
  15. package/dist/ui/components/DateRange.svelte +37 -27
  16. package/dist/ui/components/Dropdown.svelte +77 -57
  17. package/dist/ui/components/DropdownOption.svelte +10 -7
  18. package/dist/ui/components/ECharts.svelte +23 -16
  19. package/dist/ui/components/ErrorChart.svelte +85 -21
  20. package/dist/ui/components/GrapheneQuery.svelte +7 -3
  21. package/dist/ui/components/InlineDelta.svelte +53 -34
  22. package/dist/ui/components/Line.svelte +192 -178
  23. package/dist/ui/components/LineChart.svelte +53 -96
  24. package/dist/ui/components/PieChart.svelte +26 -15
  25. package/dist/ui/components/QueryLoad.svelte +15 -10
  26. package/dist/ui/components/SortIcon.svelte +5 -1
  27. package/dist/ui/components/Table.svelte +15 -9
  28. package/dist/ui/components/TableCell.svelte +30 -17
  29. package/dist/ui/components/TableGroupRow.svelte +26 -19
  30. package/dist/ui/components/TableGroupToggle.svelte +9 -6
  31. package/dist/ui/components/TableHeader.svelte +37 -27
  32. package/dist/ui/components/TableRow.svelte +30 -20
  33. package/dist/ui/components/TableSubtotalRow.svelte +16 -9
  34. package/dist/ui/components/TableTotalRow.svelte +18 -11
  35. package/dist/ui/components/TextInput.svelte +23 -20
  36. package/dist/ui/components/_Table.svelte +303 -260
  37. package/dist/ui/internal/LocalApp.svelte +40 -0
  38. package/dist/ui/internal/NavSidebar.svelte +27 -30
  39. package/dist/ui/internal/PageError.svelte +23 -0
  40. package/dist/ui/internal/checkSocket.ts +48 -0
  41. package/dist/ui/internal/queryEngine.ts +9 -2
  42. package/dist/ui/internal/telemetry.ts +1 -0
  43. package/dist/ui/web.js +5 -55
  44. package/package.json +8 -9
  45. package/dist/ui/internal/NavSidebarHMR.svelte +0 -8
@@ -0,0 +1,40 @@
1
+ <script>
2
+ import {errorProvider} from './telemetry.ts'
3
+ import navFiles from 'virtual:nav'
4
+ import NavSidebar from './NavSidebar.svelte'
5
+ import PageError from './PageError.svelte'
6
+
7
+ // Nav sidebar with HMR support for the virtual file list
8
+ let navData = $state(navFiles)
9
+ import.meta.hot?.accept('virtual:nav', mod => navData = mod.default)
10
+
11
+ // Track compile errors from both initial load and subsequent HMR failures.
12
+ // Uses errorProvider so `check` can report compilation errors.
13
+ let compileError = $state(null)
14
+ errorProvider('compile', () => compileError ? [compileError] : [])
15
+ import.meta.hot?.on('vite:error', (payload) => {
16
+ compileError = payload.err
17
+ compileError.type = 'compile'
18
+ compileError.file = payload.err.id
19
+ Page = null
20
+ })
21
+
22
+ // The md file is dynamically imported, so even if there's a compile error, we'll still load LocalApp and can show the user the issue
23
+ let Page = $state(null)
24
+ let pathName = window.location.pathname.replace(/^\//, '') || 'index'
25
+ if (pathName !== '__ct') {
26
+ import(/* @vite-ignore */ '/' + pathName + '.md').then(mod => {
27
+ Page = mod.default
28
+ compileError = null
29
+ }).catch(() => {})
30
+ }
31
+ </script>
32
+
33
+ <nav id="nav"><NavSidebar files={navData} /></nav>
34
+ <main id="content">
35
+ {#if compileError}
36
+ <PageError error={compileError} />
37
+ {:else if Page}
38
+ <Page />
39
+ {/if}
40
+ </main>
@@ -1,25 +1,22 @@
1
1
  <script>
2
+ import {SvelteSet, SvelteMap} from 'svelte/reactivity'
3
+
2
4
  /** @type {string} */
3
- export let currentFile = ''
4
- /** @type {string[]} */
5
- export let files = []
6
- /** @type {((href: string) => void) | undefined} */
7
- export let onNavigate = undefined
8
- /** @type {string} */
9
- export let baseRoute = ''
5
+ let {currentFile = '', files = [], onNavigate = undefined, baseRoute = ''} = $props()
10
6
 
11
- let tree = []
12
- let flatNodes = []
13
- let openFolders = new Set()
14
- let treeSignature = ''
15
- let lastCurrent = ''
7
+ let tree = $state([])
8
+ let flatNodes = $state([])
9
+ // eslint-disable-next-line svelte/no-unnecessary-state-wrap -- openFolders is reassigned, needs $state
10
+ let openFolders = $state(new SvelteSet())
11
+ let treeSignature = $state('')
12
+ let lastCurrent = $state('')
16
13
 
17
- $: normalizedFiles = (files || [])
18
- .map((file) => file.replace(/^\.\//, '').replace(/\\/g, '/'))
14
+ let normalizedFiles = $derived((files || [])
15
+ .map((file) => file.replace(/^\.\//, '').replace(/\\/g, '/')))
19
16
 
20
17
 
21
- $: normalizedCurrent = deriveCurrentFile(currentFile, normalizedFiles, baseRoute)
22
- $: currentRoute = normalizedCurrent ? pathToRoute(normalizedCurrent) : '/'
18
+ let normalizedCurrent = $derived(deriveCurrentFile(currentFile, normalizedFiles, baseRoute))
19
+ let currentRoute = $derived(normalizedCurrent ? pathToRoute(normalizedCurrent) : '/')
23
20
 
24
21
  function deriveCurrentFile (_currentFile, _normalizedFiles, _baseRoute) {
25
22
  let fromProp = normalizeFilePath(currentFile)
@@ -42,7 +39,7 @@
42
39
  return route
43
40
  }
44
41
 
45
- $: {
42
+ $effect(() => {
46
43
  let nextSignature = normalizedFiles.join('|')
47
44
  if (nextSignature !== treeSignature) {
48
45
  treeSignature = nextSignature
@@ -50,18 +47,18 @@
50
47
  flatNodes = flattenTree(tree)
51
48
  openFolders = createDefaultOpenFolders(tree, normalizedCurrent)
52
49
  }
53
- }
50
+ })
54
51
 
55
- $: {
52
+ $effect(() => {
56
53
  if (normalizedCurrent !== lastCurrent) {
57
54
  openFolders = mergeAncestorFolders(openFolders, normalizedCurrent)
58
55
  lastCurrent = normalizedCurrent
59
56
  }
60
- }
57
+ })
61
58
 
62
59
  function toggleFolder (path) {
63
60
  if (!path) return
64
- let next = new Set(openFolders)
61
+ let next = new SvelteSet(openFolders)
65
62
  if (next.has(path)) next.delete(path)
66
63
  else next.add(path)
67
64
  openFolders = next
@@ -84,7 +81,7 @@
84
81
 
85
82
  function buildTree (paths) {
86
83
  let root = []
87
- let folderMap = new Map()
84
+ let folderMap = new SvelteMap()
88
85
 
89
86
  for (let filePath of paths) {
90
87
  let cleanPath = filePath.replace(/^\.\//, '').replace(/^\//, '')
@@ -167,16 +164,16 @@
167
164
  }
168
165
 
169
166
  function createDefaultOpenFolders (_treeNodes, currentPath) {
170
- let next = new Set()
167
+ let next = new SvelteSet()
171
168
  return mergeAncestorFolders(next, currentPath)
172
169
  }
173
170
 
174
171
  function mergeAncestorFolders (openSet, filePath) {
175
- if (!filePath) return new Set(openSet)
172
+ if (!filePath) return new SvelteSet(openSet)
176
173
  let parts = filePath.split('/')
177
174
  parts.pop()
178
175
  let aggregate = []
179
- let next = new Set(openSet)
176
+ let next = new SvelteSet(openSet)
180
177
  for (let part of parts) {
181
178
  aggregate.push(part)
182
179
  next.add(aggregate.join('/'))
@@ -217,15 +214,15 @@
217
214
  class={node.route ? 'folder-row' : 'folder-row clickable'}
218
215
  role={node.route ? undefined : 'button'}
219
216
  aria-expanded={node.route ? undefined : String(isOpen(node.path, openFolders))}
220
- on:click={node.route ? undefined : () => toggleFolder(node.path)}
221
- on:keydown={node.route ? undefined : (event) => handleFolderRowKey(event, node.path)}
217
+ onclick={node.route ? undefined : () => toggleFolder(node.path)}
218
+ onkeydown={node.route ? undefined : (event) => handleFolderRowKey(event, node.path)}
222
219
  >
223
220
  <button
224
221
  class="toggle"
225
222
  type="button"
226
223
  data-folder-toggle={node.path}
227
224
  aria-expanded={isOpen(node.path, openFolders)}
228
- on:click={(event) => { event.stopPropagation(); toggleFolder(node.path) }}
225
+ onclick={(event) => { event.stopPropagation(); toggleFolder(node.path) }}
229
226
  aria-label={(isOpen(node.path, openFolders) ? 'Collapse' : 'Expand') + ' ' + node.label}
230
227
  >
231
228
  <span class={isOpen(node.path, openFolders) ? 'chevron open' : 'chevron'}>▸</span>
@@ -235,7 +232,7 @@
235
232
  href={node.route}
236
233
  class={node.route === currentRoute ? 'active' : ''}
237
234
  aria-current={node.route === currentRoute ? 'page' : undefined}
238
- on:click={(e) => handleLinkClick(e, node.route)}
235
+ onclick={(e) => handleLinkClick(e, node.route)}
239
236
  >
240
237
  {node.label}
241
238
  </a>
@@ -250,7 +247,7 @@
250
247
  href={node.route}
251
248
  class={node.path === normalizedCurrent ? 'active' : ''}
252
249
  aria-current={node.path === normalizedCurrent ? 'page' : undefined}
253
- on:click={(e) => handleLinkClick(e, node.route)}
250
+ onclick={(e) => handleLinkClick(e, node.route)}
254
251
  >
255
252
  <span>{node.label}</span>
256
253
  </a>
@@ -0,0 +1,23 @@
1
+ <script>
2
+ let {error} = $props()
3
+ </script>
4
+
5
+ <h1>Error loading page</h1>
6
+ <p class="message">{error.message}</p>
7
+ {#if error.frame}<pre>{error.frame}</pre>{/if}
8
+ {#if error.file}<p class="file">{error.file}</p>{/if}
9
+
10
+ <style>
11
+ h1 { margin-top: 0; }
12
+ .message { color: var(--red-700); }
13
+ pre {
14
+ background: var(--grey-100);
15
+ border: 1px solid var(--grey-200);
16
+ border-radius: 4px;
17
+ padding: 1rem;
18
+ overflow-x: auto;
19
+ font-size: 0.875rem;
20
+ line-height: 1.6;
21
+ }
22
+ .file { color: var(--grey-500); font-size: 0.875rem; }
23
+ </style>
@@ -0,0 +1,48 @@
1
+ // WebSocket connection for the `graphene check` command.
2
+ // Listens for check requests, waits for queries to finish, captures screenshots, and reports errors.
3
+
4
+ import {getErrors} from './telemetry.ts'
5
+ import {isLoading} from './queryEngine.ts'
6
+
7
+ let socket: WebSocket | null = null
8
+ connect()
9
+
10
+ function captureChart (chartTitle: string) {
11
+ let escaped = window.CSS.escape(chartTitle)
12
+ let canvas = document.querySelector(`[data-chart-title="${escaped}"] canvas`) as HTMLCanvasElement | null
13
+ return canvas?.toDataURL('image/png')
14
+ }
15
+
16
+ async function takeScreenshot () {
17
+ if (!(window as any).html2canvas) {
18
+ let html2canvas = await import('@graphenedata/html2canvas')
19
+ ;(window as any).html2canvas = html2canvas.default
20
+ }
21
+ let canvas = await (window as any).html2canvas(document.body, {useCORS: true, allowTaint: true, scale: 1, liveDOM: true})
22
+ return canvas?.toDataURL('image/png')
23
+ }
24
+
25
+ async function waitForQueriesToFinish () {
26
+ let startTime = Date.now()
27
+ while (isLoading() && Date.now() - startTime < 20_000) {
28
+ await new Promise(resolve => setTimeout(resolve, 100))
29
+ }
30
+ }
31
+
32
+ function connect () {
33
+ let wsUrl = `ws://${window.location.host}/_api/ws`
34
+ socket = new WebSocket(wsUrl)
35
+ socket.onclose = () => setTimeout(connect, 2000)
36
+ socket.onopen = () => socket!.send(JSON.stringify({type: 'register', url: window.location.href}))
37
+
38
+ socket.onmessage = async (event) => {
39
+ let {type, requestId, chart} = JSON.parse(event.data)
40
+ if (type !== 'check') return
41
+
42
+ await waitForQueriesToFinish()
43
+ let errors = getErrors().map((e: any) => ({type: e.type, message: e.message, id: e.id, file: e.file, line: e.loc?.line, frame: e.frame, from: e.from, to: e.to}))
44
+ let stillLoading = isLoading()
45
+ let screenshot = chart ? captureChart(chart) : await takeScreenshot()
46
+ socket!.send(JSON.stringify({type: 'checkResponse', requestId, errors, stillLoading, screenshot}))
47
+ }
48
+ }
@@ -30,6 +30,7 @@ interface QueryNode {
30
30
  let runPending: Promise<void> | null = null
31
31
  let params = {} as Record<string, any>
32
32
  let queries = [] as QueryNode[]
33
+ let queryResults = {} as Record<string, {rows: any[], fields?: Field[]}>
33
34
 
34
35
  function registerQuery (name: string, contents: string) {
35
36
  queries = queries.filter(q => q.name !== name)
@@ -87,11 +88,16 @@ async function runNode (n: QueryNode) {
87
88
 
88
89
  if (response.status == 304) { // cache hit. Read it out and use that
89
90
  let body = await cacheRead(hash)
90
- n.callback(translateData(body, n))
91
+ let result = translateData(body, n)
92
+ if (n.source) queryResults[n.source] = {rows: result.rows, fields: body.fields}
93
+ n.callback(result)
91
94
  } else if (response.ok) { // cache miss. write it to the cache, and return the data
92
95
  cacheWrite(hash, response.clone()) // clone allows us to write the raw response into the cache
93
96
  let body = await response.json()
94
- n.callback(translateData(body, n)) // nb that translateData modifies in place for performance
97
+ let fields = body.fields // grab before translateData mutates
98
+ let result = translateData(body, n) // nb that translateData modifies in place for performance
99
+ if (n.source) queryResults[n.source] = {rows: result.rows, fields}
100
+ n.callback(result)
95
101
  } else { // request failed. Record it
96
102
  let isJson = response.headers.get('Content-Type') === 'application/json'
97
103
  let body = isJson ? await response.json() : await response.text()
@@ -192,4 +198,5 @@ Object.assign(window.$GRAPHENE, {
192
198
  query,
193
199
  unsubscribe,
194
200
  waitForQueries,
201
+ queryResults,
195
202
  })
@@ -7,6 +7,7 @@ let staticErrors: Error[] = []
7
7
  let errorProviders: Record<string, ErrorProvider> = {}
8
8
 
9
9
  window.addEventListener('error', (event) => {
10
+ if ((event.error?.message || '').match(/Failed to fetch dynamically imported module.*\.md\?import/)) return
10
11
  staticErrors.push(event.error)
11
12
  })
12
13
  window.addEventListener('unhandledrejection', (event) => {
package/dist/ui/web.js CHANGED
@@ -1,7 +1,8 @@
1
- import {getErrors} from './internal/telemetry.ts'
1
+ import './internal/telemetry.ts'
2
+ import './internal/checkSocket.ts'
2
3
  import './app.css'
3
- import {isLoading} from './internal/queryEngine.ts'
4
- import NavSidebar from './internal/NavSidebarHMR.svelte'
4
+ import {mount} from 'svelte'
5
+ import LocalApp from './internal/LocalApp.svelte'
5
6
 
6
7
  import Area from './components/Area.svelte'
7
8
  import AreaChart from './components/AreaChart.svelte'
@@ -67,55 +68,4 @@ window.$GRAPHENE.components = {
67
68
  TextInput,
68
69
  }
69
70
 
70
- let socket = null
71
-
72
- if (document.getElementById('nav')) {
73
- new NavSidebar({target: document.getElementById('nav')})
74
- }
75
-
76
- connectWebSocket()
77
-
78
- function captureChart (chartTitle) {
79
- let escaped = window.CSS.escape(chartTitle)
80
- let canvas = document.querySelector(`[data-chart-title="${escaped}"] canvas`)
81
- return canvas?.toDataURL('image/png')
82
- }
83
-
84
- async function takeScreenshot () {
85
- if (!window.html2canvas) {
86
- let html2canvas = await import('@graphenedata/html2canvas')
87
- window.html2canvas = html2canvas.default
88
- }
89
-
90
- let canvas = await window.html2canvas(document.body, {useCORS: true, allowTaint: true, scale: 1, liveDOM: true})
91
- return canvas?.toDataURL('image/png')
92
- }
93
-
94
- async function waitForQueriesToFinish () {
95
- let startTime = Date.now()
96
- while (isLoading() && Date.now() - startTime < 20_000) {
97
- await new Promise(resolve => setTimeout(resolve, 100))
98
- }
99
- }
100
-
101
- function connectWebSocket () {
102
- let wsUrl = `ws://${window.location.host}/_api/ws`
103
- socket = new WebSocket(wsUrl)
104
- socket.onclose = () => setTimeout(connectWebSocket, 2000)
105
-
106
- socket.onopen = () => {
107
- socket.send(JSON.stringify({type: 'register', url: window.location.href}))
108
- }
109
-
110
- socket.onmessage = async (event) => {
111
- let {type, requestId, chart} = JSON.parse(event.data)
112
-
113
- if (type === 'check') {
114
- await waitForQueriesToFinish()
115
- let errors = getErrors().map(e => ({message: e.message, id: e.id}))
116
- let stillLoading = isLoading()
117
- let screenshot = chart ? captureChart(chart) : await takeScreenshot()
118
- socket.send(JSON.stringify({type: 'checkResponse', requestId, errors, stillLoading, screenshot}))
119
- }
120
- }
121
- }
71
+ mount(LocalApp, {target: document.body})
package/package.json CHANGED
@@ -3,10 +3,10 @@
3
3
  "main": "bin.js",
4
4
  "type": "module",
5
5
  "author": "Graphene Systems Inc",
6
- "version": "0.0.13",
6
+ "version": "0.0.14",
7
7
  "license": "Elastic-2.0",
8
8
  "engines": {
9
- "node": ">=16"
9
+ "node": ">=20"
10
10
  },
11
11
  "files": [
12
12
  "dist",
@@ -27,10 +27,9 @@
27
27
  "@duckdb/node-api": "1.3.2-alpha.26",
28
28
  "@google-cloud/bigquery": "^8.1.1",
29
29
  "@graphenedata/html2canvas": "^1.4.1",
30
- "@graphenedata/malloy": "0.0.304",
31
30
  "@lezer/common": "^1.2.3",
32
31
  "@lezer/lr": "^1.4.2",
33
- "@sveltejs/vite-plugin-svelte": "3.1.2",
32
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
34
33
  "@tidyjs/tidy": "^2.5.2",
35
34
  "chalk": "^5.3.0",
36
35
  "chokidar": "3.6.0",
@@ -41,16 +40,16 @@
41
40
  "dotenv": "^17.2.3",
42
41
  "echarts": "^5.5.0",
43
42
  "fs-extra": "11.2.0",
44
- "glob": "^11.0.3",
43
+ "glob": "^13.0.1",
45
44
  "marked": "^16.3.0",
46
45
  "mdsvex": "^0.12.6",
47
46
  "nanoid": "3.3.8",
48
47
  "sanitize-html": "^2.17.0",
49
- "snowflake-sdk": "^2.3.1",
48
+ "snowflake-sdk": "^2.3.4",
50
49
  "ssf": "^0.11.2",
51
- "svelte": "4.2.19",
50
+ "svelte": "5.48.0",
52
51
  "unist-util-visit": "4.1.2",
53
- "vite": "5.4.14",
52
+ "vite": "7.3.1",
54
53
  "ws": "^8.18.0"
55
54
  },
56
55
  "devDependencies": {
@@ -58,7 +57,7 @@
58
57
  "@types/node": "^20.0.0",
59
58
  "@types/sanitize-html": "^2.16.0",
60
59
  "@types/ws": "^8.18.1",
61
- "esbuild": "^0.21.5",
60
+ "esbuild": "^0.27.2",
62
61
  "vitest": "4.0.15",
63
62
  "vscode-languageserver-types": "^3.17.0"
64
63
  },
@@ -1,8 +0,0 @@
1
- <script>
2
- import navData from 'virtual:nav'
3
- import NavSidebar from './NavSidebar.svelte'
4
-
5
- export let currentFile = ''
6
- </script>
7
-
8
- <NavSidebar files={navData} {currentFile} />