Skip to content

Fix ar-button rendering unstyled under Lit 3, plus deploy pipeline - #10

Merged
pixlhero merged 6 commits into
developfrom
bugfix/lit3-styles-and-deploy-scripts
Jul 23, 2026
Merged

Fix ar-button rendering unstyled under Lit 3, plus deploy pipeline#10
pixlhero merged 6 commits into
developfrom
bugfix/lit3-styles-and-deploy-scripts

Conversation

@pixlhero

Copy link
Copy Markdown
Contributor

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 to display: inline. Reported against app.dev.yago.cloud, reproducible with the component standalone.

ArButton overrides static observedAttributes without calling super. That getter is the only thing that triggers Lit's finalize(), which builds elementStyles from static 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() called finalize() itself. Lit 3 replaced that with a lighter __prepare(), leaving observedAttributes as 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 unaffecteddist.yago.cloud still serves the Dec 2025 Lit 2 build. But main already contains the upgrade, so prod is protected only by not having been deployed. This makes that deploy safe.

Commits

commit what
6387d68 The fix: call super.observedAttributes
d8c941b Deploy scripts: correct target dir, drop --delete, pair build with deploy
fa045bb Point index.html at source so yarn serve works
3409340 CI: Node 12 → 20, actions v2 → v4, add smoke test
c7b13ee Rename harness to demo.html so it isn't a build entry
51ad97e Restore Cache-Control, ship both filenames, publish identifiable builds

Deploy scripts were destructive

rsync --delete dist/ …/dist.dev.yago.cloud/ targeted the docroot, one level above the ar-button/ folder actually served. Verified with --dry-run, it would have deleted the live ar-button/ directory and the .htaccess providing CORS. It also didn't build, so deploy:prod after a build:dev would have published a bundle pointing customers at dev.yago.cloudNODE_ENV bakes 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.4 requires node >= 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 is display: none, and the button is inline-flex. Validated both ways against the real pre-fix bundle:

bundle result exit
fixed elementStyles: 1, adopted: 1, popup: none, button: inline-flex 0
pre-fix elementStyles: 0, adopted: 0, popup: inline, button: inline 1

Publishing

  • Cache-Control was lost in the gsutil→rsync move (950b9e0) because .htaccess only ever existed on the server. Now versioned per environment and shipped with the bundle; merges with the docroot CORS file without collision.
  • mvcp: ar-button.min.js has shipped in customer embeds since the Vue CLI days and can never disappear, so both names are now published in both environments.
  • Identifiability: stable names can't be hashed (customers control those references), so each build also publishes an immutable content-addressed copy plus a version.json.

Verification

Deployed to dev and confirmed live:

ar-button.js                  no-store, no-cache, must-revalidate   CORS ✓
ar-button-2.0.0-3764c64b.js   public, max-age=31536000, immutable   CORS ✓
version.json  { "version": "2.0.0", "hash": "3764c64b", "commit": "3409340", "env": "dev" }

yarn build:dev, yarn build:prod, yarn serve and yarn test:smoke all pass; deploy dry-runs show 0 deletions.

Notes for review

  • Prod is not deployed. Worth doing: the latent bug is armed there.
  • Kebab-case attributes (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.
  • Source maps stay excluded from prod, matching what's deployed today.

🤖 Generated with Claude Code

pixlhero and others added 6 commits July 22, 2026 16:39
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>
@pixlhero
pixlhero requested a review from lucatescari July 23, 2026 06:58

@lucatescari lucatescari left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 served ar-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:dev then deploy:prod could have shipped a dev-backend bundle to customers (NODE_ENV bakes 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

  1. 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.
  2. Kebab attributes — agree with leaving qr-title/qr-text/project-color alone 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.
  3. No --delete = server accumulates old content-addressed builds forever — intended for immutability, just no cleanup story. Fine long-term.
  4. Couldn't independently verify the server-side path layout / Apache merging the two .htaccess files, 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

@pixlhero
pixlhero merged commit 6fddd96 into develop Jul 23, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants