ReviewOS

stacks/stx

stx doctor reports "all layers aligned" while buddy serve renders through a nested, 58-version-old stx

#2052
Closed glennmichael123 opened this 9 days ago ·
9 days ago

colorMode.respectExisting landed in 0.2.401 (thanks, this is #2050 item 1) and works at the generator level, but it does not reach the boot script that buddy serve emits. The option is accepted silently and has no effect, so the forced-theme case #2050 was opened about still regresses in production.

What I see

config/ui.ts:

app: {
  colorMode: {
    storageKey: 'uptimestatus-theme',
    attribute: 'data-theme',
    darkClass: null,
    initialMode: 'auto',
    respectExisting: true,
  },
},

curl a served page, installed @stacksjs/stx and bun-plugin-stx both at 0.2.401:

<script data-stx-color-mode-boot>(function(){"use strict";var c={"storageKey":"uptimestatus-theme","initialMode":"auto","darkClass":null,"attribute":"data-theme","autoValue":null}; ...

No respectExisting key, and the emitted body contains neither forced nor any getAttribute call, so it has no way to consult what the server stamped.

Calling the generator directly on the same installed build gives the opposite:

import { generateColorModeBootScript } from '@stacksjs/stx/dist/color-mode-boot.js'

generateColorModeBootScript({
  storageKey: 'uptimestatus-theme', initialMode: 'auto', darkClass: null,
  attribute: 'data-theme', respectExisting: true,
})
// embedded config: {"storageKey":"...","initialMode":"auto","darkClass":null,
//                  "attribute":"data-theme","autoValue":null,"respectExisting":true}
// includes `forced`: true    includes getAttribute: true

Note the generator embeds the key even when the option is false, so a served config with no respectExisting key at all cannot have come from this generator.

Behaviour

Running the actual served bytes against a forced page:

owner forced light, visitor stored dark   -> data-theme = dark    REGRESSION
owner forced dark,  visitor stored light  -> data-theme = light   REGRESSION

Running the generator's output with respectExisting: true against the same cases, all nine combinations of stamped value / stored value / OS preference are correct:

forced light, stored dark  -> theme=light forced=light pref=dark
forced dark,  stored light -> theme=dark  forced=dark  pref=light
"system",     stored dark  -> theme=dark  forced=-     pref=dark
"system",     nothing, OS dark  -> theme=dark  forced=- pref=auto
no attribute, nothing, OS light -> theme=light forced=- pref=auto

So the fix itself is right. Only the plumbing to the serve path is missing.

What I ruled out

  • Not a stale process. Killed every buddy serve, restarted after the config edit, same output.
  • Not a stale dist/. dist/index.html contains no boot script at all, so the served page is not that artifact.
  • Not a second copy of the generator. Every file under @stacksjs/stx/dist that mentions autoValue also mentions respectExisting, bun-plugin-stx/dist contains no copy, and nothing else in the app does either.
  • Not key-picking in the obvious places. process.js does const colorModeConfig = options.app?.colorMode and production-builder.js does the same, both wholesale, so neither drops the key on its own. Whatever assembles options.app for the serve render is where I would start looking: the other four keys arrive intact, so app.colorMode is being forwarded, just not that field.

Worth checking whether the SSG path differs from serve here, since production-builder.js and process.js take the config by different routes and only the serve path is production for us.

Why it matters

This is silent. The option type-checks, the config loads, no warning is emitted, and the page looks correct in every case except the one the option exists for. Someone adopting respectExisting on the strength of #2050 would ship the forced-theme regression and only find out from a customer whose status page shows the wrong theme.

A cheap guard would be a one-grep check: if a served boot script mentioning forced is the signature of the fix being live, then a test asserting the injected script contains forced whenever respectExisting is set would have caught this, in the same spirit as the #2050 note about driving tests through the public entry points rather than the generators.

For now I have reverted the config block and kept the three hand-written per-page resolvers, with the finding recorded in config/ui.ts so nobody on my side retries it until a release emits forced.

9 days ago

Correcting this: my diagnosis was wrong. There is no dropped key, and nothing in 0.2.401 is at fault. The serve path forwards respectExisting correctly. What I measured was an older copy of stx executing.

Thanks to @stacksjs for pushing back on the conclusion rather than accepting it. The argument that settled it: a key dropped during config assembly would not reproduce the resolver's exact field order for the other five keys while landing values that all match. That shape can only be a generator from before the field existed.

What was actually happening

buddy serve was rendering through a nested copy of stx inside buddy's own node_modules, not the app's top-level one:

node_modules/@stacksjs/stx                              0.2.401   respectExisting present (5x)
node_modules/@stacksjs/buddy/node_modules/@stacksjs/stx 0.2.343   respectExisting absent

Proof, rather than inference: I generated the boot script from each copy with my exact options and compared against the bytes the page actually served.

top-level 0.2.401      byte-identical to served: false   (983 chars vs served 794)
buddy nested 0.2.343   byte-identical to served: true

So the served script came from 0.2.343, which predates #2050 by 58 versions. Everything I reported follows from that and says nothing about this release.

Worth keeping from the report

Two things are still true and may be worth acting on:

stx doctor reports this situation as healthy. Run in the app directory it says:

✓ @stacksjs/stx resolves to: <app>/node_modules/@stacksjs/stx/dist/index.js
✓ bun-plugin-stx resolves to: <app>/node_modules/bun-plugin-stx/dist/index.js
· pantry: no pantry/@stacksjs/stx found — standard install (nothing to rebuild)
✓ All framework-runtime layers are aligned.

Both ticks are correct from the app root, and both are the wrong question, because buddy serve executes from inside @stacksjs/buddy and Bun resolves from the importing file, so it finds buddy's nested copy first. Doctor exists to answer "is a layer stale" and it answered "aligned" while a 58-version-old stx was rendering every page. Resolving from the serve entrypoint's perspective, or simply reporting every @stacksjs/stx and bun-plugin-stx under node_modules with its version when they disagree, would have caught this in one command instead of the afternoon it cost me.

The new tests through processDirectives are the right call and I agree they would not have caught mine, since they prove the generator and injector agree and this was neither. The literal-ordering assertion is the useful half for anyone diffing a served page: a five-key literal now reads as a stale build rather than a lost key, which is exactly the inference I failed to make.

Closing this as invalid. Happy for it to stay open instead if the doctor gap is worth tracking here rather than separately.

9 days ago

Fixed in 02b78716ed. stx doctor now reports every copy, not just the one at the app root.

The retitled finding was right: both existing checks were correct and both answered the wrong question. They resolve @stacksjs/stx and bun-plugin-stx from the app root, and Bun resolves from the importing file — so a CLI executing inside node_modules/@stacksjs/buddy gets that package's own nested copy, and an app can serve every page through an stx 58 versions behind the one it reports.

What doctor does now:

  • walks every node_modules beneath the root, including nested ones, and reports each @stacksjs/stx and bun-plugin-stx with its version and path
  • errors when versions disagree, so report.ok is false and the command exits non-zero
  • follows symlinks, tracking realpaths so a cycle cannot hang the walk
  • suggests a verification command that uses find -L

Both of those last two are because grep -r does not follow symlinks, which was the second blind spot in the original investigation: the layer is present and the first search anyone reaches for cannot see it.

Disagreement is the finding, not nesting. A nested copy at the same version is harmless duplication and reports as info. Flagging that would train people to skip the check, which is how a real one gets missed.

Covers bun-plugin-stx as well as @stacksjs/stx, because the survey on this thread found two apps serving through a stale renderer rather than a stale framework.

10 tests, including the reported layout reproduced exactly — top level 0.2.401 with a nested 0.2.343 under @stacksjs/buddy — one through a symlink, and one asserting that matching versions stay info rather than erroring.

Not addressed here: resolving the skew itself. The overrides approach reported no changes while rewriting bun.lock with 532 deletions, so that remains open on the app side. doctor's job was to make the skew visible in one command, and that part is done.

Sign in to comment on this issue.