@malloydata/render 0.0.423 → 0.0.425
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/module/index.mjs +17074 -17021
- package/dist/module/index.umd.js +308 -283
- package/docs/plans/field-creation-analysis.md +178 -0
- package/docs/plugin-api-reference.md +238 -0
- package/docs/plugin-quick-start.md +103 -0
- package/docs/plugin-system.md +321 -0
- package/docs/renderer_tag_cheatsheet.md +72 -0
- package/docs/renderer_tags_overview.md +916 -0
- package/docs/testing.md +67 -0
- package/docs/validation.md +139 -0
- package/package.json +8 -8
package/docs/testing.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Testing the renderer
|
|
2
|
+
|
|
3
|
+
The renderer's behavior splits into layers with very different testing costs.
|
|
4
|
+
Test at the cheapest layer that can observe the behavior; escalate only when a
|
|
5
|
+
lower layer can't see it.
|
|
6
|
+
|
|
7
|
+
| Layer | What it answers | Harness | Needs |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| Dispatch & tag config | "Does this markup select the right renderer, with the right resolved settings?" | `src/render-field-metadata.spec.ts` (compile-only, see below) | schema only — no query run, no data, no DOM |
|
|
10
|
+
| Chart spec generation | "Does the generated Vega spec encode the data and tags correctly?" | `src/plugins/spec-test-support/harness.ts` (`runChartQuery`) | runs a real DuckDB query, no DOM |
|
|
11
|
+
| Tag validation contract | "Do bad tags produce the right diagnostics?" | `test/src/render/render-validator.spec.ts` | the **built UMD bundle** — run a full `npm run build` first, `npm run dev` is not enough |
|
|
12
|
+
| Pixels | "Does it look right?" | Storybook (`npm run storybook`), stories in `src/stories/*.stories.malloy` | a browser and your eyes |
|
|
13
|
+
|
|
14
|
+
## The compile-only harness (dispatch & tag config)
|
|
15
|
+
|
|
16
|
+
Everything the renderer decides at setup time — which renderer a field gets
|
|
17
|
+
(`renderAs()`), what the tag resolvers in `tag-configs.ts` produce, what chart
|
|
18
|
+
settings resolve to — is a function of the result **schema**, not the data.
|
|
19
|
+
That means these tests never need to execute a query: compile the model with
|
|
20
|
+
`getPreparedResult()` and construct `RenderFieldMetadata` directly on the
|
|
21
|
+
stable result. DuckDB is used only to describe an inline `duckdb.sql(...)`
|
|
22
|
+
table at compile time.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
async function metadataFor(malloySource: string): Promise<RenderFieldMetadata> {
|
|
26
|
+
const pr = await runtime
|
|
27
|
+
.loadModel(malloySource)
|
|
28
|
+
.loadQueryByName('q')
|
|
29
|
+
.getPreparedResult();
|
|
30
|
+
return new RenderFieldMetadata(pr.toStableResult());
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
From the metadata you can assert on `field.renderAs()` (dispatch),
|
|
35
|
+
`field.getTagConfig<T>()` (setup-time resolvers), and per-plugin settings
|
|
36
|
+
functions like `getBarChartSettings(field)`. These tests run in plain Node in
|
|
37
|
+
under a second — no Solid, no Vega runtime, no DOM stubs.
|
|
38
|
+
|
|
39
|
+
Prefer this harness for any new tag, any change to dispatch precedence, and
|
|
40
|
+
any resolver in `tag-configs.ts`. If a behavior can be pinned here, a
|
|
41
|
+
Storybook story for it is documentation, not the test.
|
|
42
|
+
|
|
43
|
+
## The chart-query harness (spec generation)
|
|
44
|
+
|
|
45
|
+
When the behavior lives in a Vega spec generator (axis titles, legend scales,
|
|
46
|
+
tooltip shapes), use `runChartQuery` from `src/plugins/spec-test-support/`.
|
|
47
|
+
It runs a real query, builds the full render metadata, and hands back what the
|
|
48
|
+
spec generators consume. Costs a query execution per test but still no DOM.
|
|
49
|
+
|
|
50
|
+
## Pinning known bugs
|
|
51
|
+
|
|
52
|
+
Some tests deliberately pin behavior that is known to be wrong (they are
|
|
53
|
+
labeled `KNOWN BUG` / `PINNED` in the test name, with a comment explaining
|
|
54
|
+
the actual behavior). Their job is to make a behavior change *loud*: if a
|
|
55
|
+
change flips one, update it knowingly — don't "fix" the test to keep it
|
|
56
|
+
green without understanding what changed.
|
|
57
|
+
|
|
58
|
+
## Running
|
|
59
|
+
|
|
60
|
+
Renderer unit tests live in the `malloy-render` jest project:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
npx jest --selectProjects malloy-render
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Do not run the repo's full test suite unrestricted; use targeted projects or
|
|
67
|
+
paths.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Renderer Validation
|
|
2
|
+
|
|
3
|
+
## What validation is
|
|
4
|
+
|
|
5
|
+
Renderer tags (`# bar_chart`, `# viz.x = ...`, `# currency=usd`, etc.) are a DSL embedded in Malloy annotations. The renderer interprets that DSL to decide what to draw and how. Validation is the layer that catches DSL mistakes — wrong tag on wrong field type, invalid enum values, bad field references, structural conflicts — and surfaces them as errors with source locations the user can act on.
|
|
6
|
+
|
|
7
|
+
Two things consume validation:
|
|
8
|
+
|
|
9
|
+
- **The VS Code Problems panel.** Validation runs during `setResult()` so logs are available synchronously after the result lands.
|
|
10
|
+
- **`@malloydata/render-validator`.** A headless wrapper that runs the validator in Node.js (no DOM, no Solid.js) so MCP pipelines and CI can validate a query's renderer tags without a browser.
|
|
11
|
+
|
|
12
|
+
Validation only needs the result **schema** (field names, types, annotations). It does not need data rows. Anything that requires inspecting rows (e.g., "transpose column limit exceeded") cannot be validated this way and must remain a render-time check.
|
|
13
|
+
|
|
14
|
+
## The design principle
|
|
15
|
+
|
|
16
|
+
> Push tag interpretation as far toward setup-time as possible. Anything resolved at setup-time can be validated at setup-time.
|
|
17
|
+
|
|
18
|
+
Concretely, this means:
|
|
19
|
+
|
|
20
|
+
- **Do tag reads in `create()` or in a `tag-configs.ts` resolver**, not in `renderComponent()`, `beforeRender()`, or any shared code they call into. Setup-time reads are automatically marked-as-read by the tag API, and their results are available for validation.
|
|
21
|
+
- **Store resolved values on the plugin instance or via `setTagConfig` / `setColumnConfig`**, and have render-time code consume the resolved values. Render-time code should never call `field.tag.*` directly.
|
|
22
|
+
- **When a render-time read is unavoidable** (e.g., a context-sensitive per-child tag like dashboard's `break`, where setup-time resolution would be materially worse than ownership), declare the path in `getValidationSpec().ownedPaths` or `childOwnedPaths` so unread-tag detection doesn't warn on valid uses.
|
|
23
|
+
|
|
24
|
+
If you internalize one thing from this document: **setup-time is the default; render-time reads are exceptions that must be declared**.
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
### Pipeline
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
setResult(result)
|
|
32
|
+
→ new RenderFieldMetadata(result, plugins)
|
|
33
|
+
→ registerFields(rootField) // recursive, depth-first
|
|
34
|
+
for each field:
|
|
35
|
+
instantiatePluginsForField(field) // factory.matches + factory.create — may throw
|
|
36
|
+
resolveBuiltInTags(field) // typed config resolvers (image, link, ...)
|
|
37
|
+
validateFieldTags(field) // semantic checks → logCollector
|
|
38
|
+
markOwnedTagPaths(field, ...) // mark renderer-owned tag paths as read
|
|
39
|
+
getLogs()
|
|
40
|
+
→ semantic logs (from validateFieldTags + resolvers + plugin-error wrappers)
|
|
41
|
+
→ unread-tag warnings (collected once, idempotent)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### The four producers of log entries
|
|
45
|
+
|
|
46
|
+
1. **Semantic validation in `validateFieldTags()`** (`src/render-field-metadata.ts`) — The central place for "this tag is wrong" checks. Calls `log.error(msg, tagRef)` / `log.warn(msg, tagRef)`. The `tagRef` carries a source location so VS Code can underline the offending tag.
|
|
47
|
+
|
|
48
|
+
2. **Built-in tag config resolvers in `tag-configs.ts`** — Read tags at setup time, return typed configs, optionally log issues with the same `tagRef` pattern. Components consume the typed config rather than re-reading the tag.
|
|
49
|
+
|
|
50
|
+
3. **Plugin throws from `matches()` / `create()`** — Caught in `instantiatePluginsForField` and routed to `ErrorPlugin`, which renders a red error tile in place of the visualization with the thrown message. This is the correct UX when the user asked for a specific renderer (e.g., `# bar_chart`) and the renderer literally cannot produce output (wrong field shape, missing required structure). The thrown message gets wrapped as `"Plugin <name> failed for field '<key>': <msg>"` so the source location of the offending tag is **not** preserved — see "Throw vs log" below for when this trade-off is right.
|
|
51
|
+
|
|
52
|
+
4. **Unread-tag detection** — After validation, any tag property the user wrote that no resolver, validator, or ownership spec touched becomes a warning. This is the safety net for misspellings and unknown tags.
|
|
53
|
+
|
|
54
|
+
**Render-time surfacing.** A producer-#1 error on a field that then ends up with no matching renderer (it would otherwise stringify to `[object Object]` in the result panel) is also shown inline at render time, using the same `ErrorMessage` UI as producer #3's red tile (`apply-renderer`'s `default` branch reads `field.getValidationErrors()`). This is the decline-side counterpart to a plugin throw: when a renderer *declines* a field a validator has flagged, the user sees the error in the render, not only in the Problems panel. Fields that render fine (scalars, the `none` chart-child case, built-in renderers) are untouched.
|
|
55
|
+
|
|
56
|
+
### Tag ownership
|
|
57
|
+
|
|
58
|
+
A renderer "owns" a tag when removing the renderer would make the tag meaningless (e.g., `viz.x` only matters when a chart is active). Ownership is declared on the factory:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
getValidationSpec: (): RendererValidationSpec => ({
|
|
62
|
+
renderer: 'bar',
|
|
63
|
+
ownedPaths: [['viz', 'x'], ['viz', 'y'], ['bar_chart'], ...],
|
|
64
|
+
childOwnedPaths: [['tooltip']], // claimed on direct children
|
|
65
|
+
}),
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`markOwnedTagPaths` walks these declarations and calls `tag.find(path)` on each one, marking the path as read so unread-tag detection won't false-warn. Built-in renderers use the same mechanism via `renderer-validation-specs.ts`.
|
|
69
|
+
|
|
70
|
+
Ownership is **bookkeeping**, not validation. It tells the framework "these tag paths are legitimate when this renderer is active." It does not check whether the values are sane — that is `validateFieldTags`'s job.
|
|
71
|
+
|
|
72
|
+
Ownership is a manual list with no compile-time check. Adding a new render-time tag read without updating `getValidationSpec()` produces false unread-tag warnings on valid input. Keeping the list in sync with actual render-time reads is the author's responsibility.
|
|
73
|
+
|
|
74
|
+
### Why a tag path needs ownership
|
|
75
|
+
|
|
76
|
+
A tag path needs to be in an ownership spec **only if it is read after `setResult()` returns** (i.e., at render time or interaction time) and is not consumed by a setup-time resolver. Setup-time reads already mark the tag as read; ownership specs cover the gap for tags that are still render-time-only.
|
|
77
|
+
|
|
78
|
+
When a tag migrates from render-time to a setup-time typed config, its entry in `ownedPaths` becomes redundant and should be removed.
|
|
79
|
+
|
|
80
|
+
## How to validate well
|
|
81
|
+
|
|
82
|
+
### Throw vs log — when you actually want each
|
|
83
|
+
|
|
84
|
+
This is the rule that matters most and is easiest to get wrong.
|
|
85
|
+
|
|
86
|
+
**Throw** from `matches()` / `create()` **when the user asked for a specific renderer and that renderer cannot produce output for this field.** The thrown message replaces the visualization with a red error tile (via `ErrorPlugin`). **That red tile is the feature, not a workaround** — it tells the author, visibly and unmissably: "you asked for X, here's why X can't happen." Examples:
|
|
87
|
+
|
|
88
|
+
- `# bar_chart` on a scalar field → throw. Bar chart was requested; can't deliver.
|
|
89
|
+
- `# big_value` on a non-record → throw. Big value was requested; can't deliver.
|
|
90
|
+
- `# bar_chart` on a nested query with zero dimensions → throw. Bar chart needs an x axis; can't deliver.
|
|
91
|
+
|
|
92
|
+
**Log** via `validateFieldTags()` with `log.error(msg, tagRef)` **when a renderer is still going to render, but one tag or setting is wrong.** The render proceeds with defaults; the log tells the author their setting was ignored. Examples:
|
|
93
|
+
|
|
94
|
+
- `# currency=yen` on a number → log. Table still renders, currency tag ignored.
|
|
95
|
+
- `# big_value { comparison_format=yuh }` → log. Big value still renders, bad format defaults to `pct`.
|
|
96
|
+
- `# viz.x = nonexistent_field` on a bar chart → log. Chart still renders, chooses x differently.
|
|
97
|
+
- `# y` on a non-numeric dimension → log. Chart still renders, chooses y from measures instead.
|
|
98
|
+
|
|
99
|
+
**The hinge:** *is the plugin unable to produce output at all, or just unable to apply this one setting?*
|
|
100
|
+
|
|
101
|
+
- Unable at all → red box. Throw.
|
|
102
|
+
- One setting wrong, main render still works → log + default.
|
|
103
|
+
|
|
104
|
+
A second way to check: **would the author prefer a red box, or a silently-defaulted render with a line in the Problems panel?** A whole failed viz deserves a red box. One ignored setting deserves a log.
|
|
105
|
+
|
|
106
|
+
### The source-location caveat
|
|
107
|
+
|
|
108
|
+
Thrown errors do not carry their `Tag` reference through `instantiatePluginsForField`, so red-box messages do not get a clickable source location. The red-box UX is still the right choice when the plugin can't produce output — do not route a whole-renderer failure through `validateFieldTags()` just to get a source location, since that produces a warning in the Problems panel while rendering the broken chart, which is worse UX than the red box.
|
|
109
|
+
|
|
110
|
+
### The ownership test
|
|
111
|
+
|
|
112
|
+
Separately from throw-vs-log: **if a tag gets read after `setResult()` returns**, its path must be in `getValidationSpec().ownedPaths` (or `childOwnedPaths` for direct children). Otherwise the unread-tag detector will warn about it on every valid use. Better still: move the read into a setup-time resolver (see `tag-configs.ts`) and drop the ownership entry.
|
|
113
|
+
|
|
114
|
+
### What the validator can't catch
|
|
115
|
+
|
|
116
|
+
- Anything that needs actual data rows (transpose limit exceeded, empty result handling).
|
|
117
|
+
- Anything that depends on layout measurements (sizing fallbacks).
|
|
118
|
+
- Logic errors in renderer code that produce a wrong-but-rendered chart.
|
|
119
|
+
|
|
120
|
+
For these, a Storybook case is the regression net. If you are adding a validation rule, also add a story exercising the bad input — the story exercises the validator path, and visual regressions cover what validation can't.
|
|
121
|
+
|
|
122
|
+
### Pre-PR checklist
|
|
123
|
+
|
|
124
|
+
- [ ] New tag or property a renderer reads → it is consumed by a setup-time resolver in `tag-configs.ts` **or** declared in the relevant plugin's `getValidationSpec().ownedPaths` / `childOwnedPaths`.
|
|
125
|
+
- [ ] New constraint on a tag value (enum members, type compatibility, structural rule) → encoded in `validateFieldTags()` with `log.error(msg, tagRef)` so the user sees a source-located error.
|
|
126
|
+
- [ ] New throw in `matches()` / `create()` → the failure is "this plugin cannot produce output for this field," and a red-box tile is the UX you want. If the main renderer would still work fine and only one tag-setting is wrong, use `validateFieldTags()` with a logged error instead.
|
|
127
|
+
- [ ] New plugin factory → defines `getValidationSpec()` on the factory (not on the instance).
|
|
128
|
+
- [ ] Storybook case added or updated to exercise the new tag/validation path.
|
|
129
|
+
|
|
130
|
+
## Key files
|
|
131
|
+
|
|
132
|
+
| File | Role |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `src/render-field-metadata.ts` | Validation engine — `validateFieldTags`, `markOwnedTagPaths`, plugin instantiation |
|
|
135
|
+
| `src/component/render-log-collector.ts` | Log collection, unread-tag walking |
|
|
136
|
+
| `src/component/tag-configs.ts` | Setup-time resolvers for built-in renderers (image, link, list, cell format, table nest, dashboard) |
|
|
137
|
+
| `src/component/renderer-validation-specs.ts` | Built-in renderer ownership specs |
|
|
138
|
+
| `src/api/plugin-types.ts` | `RenderPluginFactory`, `RendererValidationSpec` interfaces |
|
|
139
|
+
| `packages/malloy-render-validator/src/index.ts` | Headless wrapper — `validateRenderTags(result)` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloydata/render",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.425",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/module/index.umd.js",
|
|
6
6
|
"types": "dist/module/index.d.ts",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
"generate-flow": "ts-node ../../scripts/gen-flow.ts"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@malloydata/malloy-interfaces": "0.0.
|
|
35
|
-
"@malloydata/malloy-tag": "0.0.
|
|
36
|
-
"@tanstack/solid-virtual": "^3.13.
|
|
34
|
+
"@malloydata/malloy-interfaces": "0.0.425",
|
|
35
|
+
"@malloydata/malloy-tag": "0.0.425",
|
|
36
|
+
"@tanstack/solid-virtual": "^3.13.33",
|
|
37
37
|
"lodash": "^4.18.1",
|
|
38
38
|
"luxon": "^3.7.2",
|
|
39
|
-
"solid-js": "^1.9.
|
|
39
|
+
"solid-js": "^1.9.14",
|
|
40
40
|
"ssf": "^0.11.2",
|
|
41
41
|
"us-atlas": "^3.0.1",
|
|
42
42
|
"vega": "^6.2.0",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"vega-lite": "^5.2.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@malloydata/db-duckdb": "0.0.
|
|
48
|
-
"@malloydata/malloy": "0.0.
|
|
47
|
+
"@malloydata/db-duckdb": "0.0.425",
|
|
48
|
+
"@malloydata/malloy": "0.0.425",
|
|
49
49
|
"@storybook/addon-essentials": "^8.6.18",
|
|
50
50
|
"@storybook/addon-interactions": "^8.6.18",
|
|
51
51
|
"@storybook/addon-links": "^8.6.15",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"esbuild": "0.28.1",
|
|
62
62
|
"storybook": "^8.6.15",
|
|
63
63
|
"vite": "^6.4.2",
|
|
64
|
-
"vite-plugin-dts": "^5.0.
|
|
64
|
+
"vite-plugin-dts": "^5.0.3",
|
|
65
65
|
"vite-plugin-solid": "^2.11.12",
|
|
66
66
|
"vite-tsconfig-paths": "^5.1.4"
|
|
67
67
|
}
|