Stop bundling luma.gl into @spatialdata/layers; cover labels colour-by in CI - #108
Conversation
…y in CI `packages/layers` externalized the six specifiers its source imports and nothing else, so `@luma.gl/core`, `/engine` and `/shadertools` (plus `@probe.gl/*`) were pulled in transitively by the layers that build their own `Model` and shipped inside `dist/index.js`. 238 kB down to 92 kB now that they are not. Size was the least of it. deck.gl, Viv and this package must share ONE luma runtime. A consumer that also loaded deck.gl got two `ShaderAssembler` classes, and `ShaderAssembler.getDefaultShaderAssembler()` is a static — so "the default shader assembler" meant different objects to deck and to Viv. Viv's `VivShaderAssembler` builds itself by COPYING that default's modules and hook functions once, at construction, and never re-syncs; it could therefore copy from an assembler deck had never registered anything on and lose deck's `DECKGL_FILTER_*` hooks entirely. Every Viv-derived layer — labels included — then failed to compile its vertex shader, reporting only `Module layer not found` and `Link error: Vertex shader is not compiled`. The externals are now whole families by regex, matching what `@spatialdata/vis` has done all along. The consumer half of the same requirement: the browser harness aliased `@deck.gl/core` to `dist/index.cjs`, which split `@luma.gl/shadertools` a second way — CJS through deck, ESM through `@vivjs/extensions`. It now resolves the ESM entry. Both halves are load-bearing; restoring either alone leaves labels unable to draw. Which is how this was found. `labels-color-by` is a new scenario in the existing production-browser job: it hands `LabelsLayer` a synthetic two-label raster and a `featureState`, at full fill opacity and zero stroke width so the drawn pixel IS the feature colour, then samples the drawing buffer inside `onAfterRender` (the only point it is readable without `preserveDrawingBuffer`). It asserts the two bands come back as the LUT's colours, and fails explicitly on the case where both come back in the CHANNEL colour — which is what "colour-by does nothing" looks like. Verified red by forcing `useFeatureColors = 0` in the bitmask layer. Synthetic rather than fixture-backed on purpose: what is under test is featureState -> LUT -> texture -> shader, and a real store puts loading, tiling and transforms in front of all of it. The load path and the associated-table column projection are still uncovered; that wants an obs column in the v0.7.2 fixture, since the blobs table carries only `instance_id` and `region`. The harness serves several scenarios from one bundle via `?scenario=`, rather than one HTML entry each: the build keeps code splitting off to dodge a Rolldown panic in apache-arrow, and multiple entries into a single chunk is what turns it back on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe PR externalizes dependency families from the layers bundle and aligns browser builds with a single luma.gl runtime. It adds selectable polygon and label rendering scenarios, plus a Playwright regression test for label color-by rendering. ChangesRuntime externalization and browser validation
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
tests/production/browser/polygonShapesScenario.tsx (1)
32-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winValidate the NGFF fixture metadata with a schema instead of an unchecked type annotation.
response.json()returnsany. The parameter annotation onmetadataimposes a shape without validating it at runtime. If the fixture's JSON structure changes, this silently type-checks while failing at a different point (or not failing at all ifattributesis missing a different way than expected).Use the same strict-schema approach already established for NGFF-shaped data in this codebase, rather than an ad hoc typed literal.
Based on coding guidelines, "Prefer a strict schema aligned with specs (e.g. NGFF) over parsing 'whatever might show up.'"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/production/browser/polygonShapesScenario.tsx` around lines 32 - 37, Replace the unchecked metadata annotation in the response.json() handling within the fixture readiness chain with the established strict NGFF schema parser used elsewhere in the codebase. Parse and validate the JSON before checking attributes['encoding-type'], while preserving the existing ngff:shapes validation error and active/setFixtureReady behavior.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/production/browser/labelsColorByScenario.tsx`:
- Around line 103-120: Update buildLayer’s channelColors construction to avoid
the inline tuple assertion, preferably by adding a small typed RGB-tuple helper
that destructures CHANNEL_COLOR and returns [r, g, b]. If retaining the
assertion instead, add a concise local comment explaining why spreading
CHANNEL_COLOR widens the tuple type.
In `@tests/production/browser/src.tsx`:
- Around line 19-21: Update isScenarioName to validate scenario membership using
an own-property check on scenarios, while preserving the null guard and type
predicate so inherited keys such as constructor are rejected before
scenarios[requested] is indexed.
---
Nitpick comments:
In `@tests/production/browser/polygonShapesScenario.tsx`:
- Around line 32-37: Replace the unchecked metadata annotation in the
response.json() handling within the fixture readiness chain with the established
strict NGFF schema parser used elsewhere in the codebase. Parse and validate the
JSON before checking attributes['encoding-type'], while preserving the existing
ngff:shapes validation error and active/setFixtureReady behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: cfdafb57-0649-4d4d-b8f0-8ad9e3295cf5
📒 Files selected for processing (8)
.changeset/layers-external-luma.mdpackages/layers/vite.config.tstests/production/browser/labels-color-by.spec.tstests/production/browser/labelsColorByContract.tstests/production/browser/labelsColorByScenario.tsxtests/production/browser/polygonShapesScenario.tsxtests/production/browser/src.tsxtests/production/browser/vite.config.ts
Started as a hunt for a labels colour-by regression that turned out not to exist — colour-by works on the deployed docs demo, both categorical and numeric, and survives hovering. What the hunt did find is a packaging defect in the published
@spatialdata/layersartifact.The bug
packages/layersexternalized the six specifiers its source imports and nothing else, so@luma.gl/core,/engineand/shadertools(plus@probe.gl/*) were pulled in transitively by the layers that build their ownModeland shipped insidedist/index.js. 238 kB down to 92 kB now that they are not.Size was the least of it. deck.gl, Viv and this package must share ONE luma runtime. A consumer that also loaded deck.gl got two
ShaderAssemblerclasses, andShaderAssembler.getDefaultShaderAssembler()is a static — so "the default shader assembler" meant different objects to deck and to Viv.That matters because Viv's
VivShaderAssemblerbuilds itself by copying that default's modules and hook functions once, at construction, and never re-syncs. It could therefore copy from an assembler deck had never registered anything on, and lose deck'sDECKGL_FILTER_*hooks entirely. Every Viv-derived layer — labels included — then failed to compile its vertex shader, reporting only:Nothing surfaces through deck's
onError. The externals are now whole families by regex, matching what@spatialdata/vishas done all along.The consumer half of the same requirement: the browser harness aliased
@deck.gl/coretodist/index.cjs, which split@luma.gl/shadertoolsa second way — CJS through deck, ESM through@vivjs/extensions. It now resolves the ESM entry. Both halves are load-bearing; restoring either alone leaves labels unable to draw, which I verified.The test that found it
labels-color-byis a new scenario in the existingproduction-browserjob. It handsLabelsLayera synthetic two-label raster and afeatureState, at full fill opacity and zero stroke width so the drawn pixel is the feature colour, then samples the drawing buffer insideonAfterRender— the only point it is readable withoutpreserveDrawingBuffer. It asserts the two bands come back as the LUT's colours, and fails explicitly on the case where both come back in the channel colour, which is what "colour-by does nothing" looks like.Verified red by forcing
useFeatureColors = 0inLabelsBitmaskTileLayer.draw:Synthetic rather than fixture-backed on purpose: what is under test is
featureState→ LUT → texture → shader, and a real store puts loading, tiling and transforms in front of all of it.The harness now serves several scenarios from one bundle via
?scenario=, rather than one HTML entry each — the build keeps code splitting off to dodge a Rolldown panic in apache-arrow, and multiple entries into a single chunk is what turns it back on.Not covered
The load path and the associated-table column projection. That wants an obs column added in
python/v0.7.2, since the blobs table carries onlyinstance_idandregion— neither eligible — so labels colour-by isn't reachable through the UI on that fixture at all.Follow-up
The duplicated luma runtime was our bug and is fixed here, but
VivShaderAssemblersnapshotting mutable global state once is what turns any such duplication into a silent, total render failure — and it has two further failure modes that need no duplication at all (deck clears and re-adds_hookFunctionson everyLayerManagerconstruction; the GLSL/WGSL hook choice is baked into the snapshot). I have an upstream issue drafted for hms-dbmi/viv, not yet filed.Verification
pnpm test:browser:production— both specs passpnpm test:unit— 795 passingpnpm buildclean;lint:biomeshows only the pre-existingnoExplicitAnywarnings🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests
Chores