Fix ar-button rendering unstyled under Lit 3, plus deploy pipeline - #10
Conversation
ArButton overrode the static observedAttributes getter without calling
super. That getter is the only thing that triggers ReactiveElement's
finalize(), which computes elementStyles from `static styles`. Without
it the shadow root received no stylesheet at all, so
`.hidden { display: none !important }` never applied: the QR and
browser-unsupported popups rendered permanently and every element fell
back to display:inline.
The override was always incorrect but harmless under Lit 2, where
@Property() -> createProperty() called finalize() itself. Lit 3 replaced
that with a lighter __prepare(), leaving observedAttributes as the sole
trigger, so the dependency upgrade in 9be1ae2 exposed it.
Verified: elementStyles 0 -> 1, both popups back to display:none, button
renders inline-flex again.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three bugs, each verified against the server with rsync --dry-run: - Wrong directory level: dist/ was synced to the docroot, one level above the ar-button/ folder that is actually served, so deployed files were never read. - --delete at the docroot removed the live ar-button/ directory and the .htaccess that sets the CORS headers. - deploy did not build, so deploy:prod run after build:dev would publish a dev-configured bundle (dev.yago.cloud backend, dev model slug) to production. NODE_ENV bakes those in at build time, so the build and deploy targets must not be mixed. Source maps stay excluded from prod, matching what is deployed there now. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
index.html referenced ./dist/ar-button.min.js, a build output. Parcel treats the page as the serve entry and resolves that script tag, so on a fresh clone `yarn install && yarn serve` (the README's instructions) failed outright: dist/ is gitignored. Even with a dist/ present it only resolves after build:prod, since build:dev emits ar-button.js. Pointing at src/ar-button.ts also restores rebuild-on-change, which is lost when serving a prebuilt bundle. build:dev/build:prod are unaffected, they use the "source" field rather than index.html. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parcel auto-detects index.html in the project root as an entry. That went unnoticed while the page pointed at ./dist/ar-button.min.js, since the reference did not resolve. Once fa045bb repointed it at ./src/ar-button.ts the page became buildable, so `yarn build:dev` started emitting index.html plus a ~326 kB page bundle into dist/ - which the deploy would have rsynced straight to the CDN. Passing the entry explicitly does not help, the detection is by filename, so the harness is renamed instead. dist/ now holds only the component bundle, and `yarn serve` still works. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three related gaps in how dist/ is published, all handled by a postbuild step so the layout is produced in one place. Cache-Control: the gsutil deploys used to set it explicitly (no-store on dev, max-age on prod). The move to rsync in 950b9e0 dropped that, and nothing noticed because .htaccess only ever existed on the server. It is now in the repo per environment and shipped with the bundle, into the ar-button/ directory. Apache merges it with the docroot .htaccess that sets CORS; the two touch different headers so they do not collide. Without it responses carry no Cache-Control at all and browsers fall back to heuristic caching, holding a stale bundle for weeks. Filenames: build:prod used to `mv` the bundle to ar-button.min.js, so each environment served one name and only one. That name has shipped in customer embeds since the Vue CLI days and can never disappear, so it is now a copy rather than a rename. Both environments publish both names, which lets both move to ar-button.js without breaking anything already embedded. Identifiability: the stable names cannot be hashed, since customers control those references. Instead each build also publishes an immutable content-addressed copy (cacheable for a year, and available to pin against) plus a version.json naming the version, hash and commit that is live. Answering "which build is deployed?" previously meant comparing CDN Last-Modified headers by hand. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lucatescari
left a comment
There was a problem hiding this comment.
Reviewed the diff and cross-checked the key claims against the code. Approving.
Root cause is correct and proven, not just plausible
The one-line fix ([...super.observedAttributes, ...]) is right. In Lit that getter is the hook that lazily triggers finalize(), which compiles static styles into elementStyles — so overriding it without super meant the shadow root got zero stylesheets. I confirmed the symptom lines up: styles.ts defines .hidden { display:none !important }, .ar-link { display:inline-flex }, .ar-button { display:inline-block }, so with no styles the popups stay permanently visible and everything falls back to inline — exactly what was reported.
The Lit 2→3 nuance (@property()/createProperty() used to call finalize(), Lit 3 doesn't) correctly explains why a latent-forever bug only surfaced after the dependency upgrade. And the smoke test measures both states against the real bundle (pre-fix elementStyles: 0, fixed elementStyles: 1) — that's the difference between "I think this is the cause" and proving it. Since the broken bundle compiles fine, rendering it in Chromium is the only thing that could have caught this; good call adding it to CI.
The deploy fixes are the sleeper value
The bug fix is one line; the deploy rewrite is what I looked at hardest and it's the bigger win:
rsync --delete dist/ .../dist.dev.yago.cloud/targeting the docroot (one level above the servedar-button/folder) would have wiped the live directory and the CORS.htaccess. Dropping--delete+ targeting.../ar-button/is a real safety fix.- Deploys didn't build first, so
build:devthendeploy:prodcould have shipped a dev-backend bundle to customers (NODE_ENVbakes the backend URL + model slug in at build time). Pairing build+deploy closes that. - CI red for 4 months (Node 12 vs. parcel needing ≥16) — fixed.
Non-blocking notes
- Scope breadth — bug fix + deploy + CI + publishing/cache scheme in one PR. It's cleanly commit-separated and the write-up is excellent, so it's reviewable; just noting it for anyone who prefers tighter PRs.
- Kebab attributes — agree with leaving
qr-title/qr-text/project-coloralone here, but the note that they may be silently inert (don't match Lit's derived names) means some documented config/styling attributes may not actually work. Worth its own follow-up ticket. - No
--delete= server accumulates old content-addressed builds forever — intended for immutability, just no cleanup story. Fine long-term. - Couldn't independently verify the server-side path layout / Apache merging the two
.htaccessfiles, but the--dry-run+ live dev deploy evidence is convincing.
Follow-up
Prod is still on the old Lit 2 build and undeployed — the bug is armed there. After merge, someone should actually run the (now-safe) prod deploy to close it out.
🤖 Reviewed with Claude Code
The bug
<ar-button>on dev renders with no styles at all: the QR and browser-unsupported popups are permanently visible and every element falls back todisplay: inline. Reported againstapp.dev.yago.cloud, reproducible with the component standalone.ArButtonoverridesstatic observedAttributeswithout callingsuper. That getter is the only thing that triggers Lit'sfinalize(), which buildselementStylesfromstatic styles— so the shadow root ends up with zero stylesheets and.hidden { display: none !important }never exists.The override has always been wrong, but was harmless under Lit 2, where
@property()→createProperty()calledfinalize()itself. Lit 3 replaced that with a lighter__prepare(), leavingobservedAttributesas the sole trigger — so the Lit 2→3 upgrade in 9be1ae2 didn't introduce the defect, it removed the accident that was masking it.Production is unaffected —
dist.yago.cloudstill serves the Dec 2025 Lit 2 build. Butmainalready contains the upgrade, so prod is protected only by not having been deployed. This makes that deploy safe.Commits
6387d68super.observedAttributesd8c941b--delete, pair build with deployfa045bbindex.htmlat source soyarn serveworks3409340c7b13eedemo.htmlso it isn't a build entry51ad97eCache-Control, ship both filenames, publish identifiable buildsDeploy scripts were destructive
rsync --delete dist/ …/dist.dev.yago.cloud/targeted the docroot, one level above thear-button/folder actually served. Verified with--dry-run, it would have deleted the livear-button/directory and the.htaccessproviding CORS. It also didn't build, sodeploy:prodafter abuild:devwould have published a bundle pointing customers atdev.yago.cloud—NODE_ENVbakes the backend URL and model slug in at build time.CI had been red for four months
Failing since 10 March, at
Install dependencies:parcel@2.16.4requiresnode >= 16, the workflow pinned Node 12. It never reached the build step, so nothing in the pipeline ever saw this bundle. The failure was unrelated to the bug — and a build-only pipeline couldn't have caught it anyway, since the broken bundle compiles fine.The new smoke test renders the built bundle in Chromium and asserts
elementStyles >= 1, a stylesheet is attached, the QR popup isdisplay: none, and the button isinline-flex. Validated both ways against the real pre-fix bundle:elementStyles: 1, adopted: 1, popup: none, button: inline-flexelementStyles: 0, adopted: 0, popup: inline, button: inlinePublishing
Cache-Controlwas lost in the gsutil→rsync move (950b9e0) because.htaccessonly ever existed on the server. Now versioned per environment and shipped with the bundle; merges with the docroot CORS file without collision.mv→cp:ar-button.min.jshas shipped in customer embeds since the Vue CLI days and can never disappear, so both names are now published in both environments.version.json.Verification
Deployed to dev and confirmed live:
yarn build:dev,yarn build:prod,yarn serveandyarn test:smokeall pass; deploy dry-runs show 0 deletions.Notes for review
qr-title,qr-text,project-color) don't match Lit's derived names (qrtitle, …) and may be silently inert. Deliberately left alone — the attribute list is byte-identical here. Needs its own investigation.🤖 Generated with Claude Code