Skip to content

fix(environments): keep local custom/ media out of every production build - #44

Merged
rosspeili merged 2 commits into
ARPAHLS:mainfrom
AUDOSt0ck1ng:fix/build-omits-custom-envs
Aug 8, 2026
Merged

fix(environments): keep local custom/ media out of every production build#44
rosspeili merged 2 commits into
ARPAHLS:mainfrom
AUDOSt0ck1ng:fix/build-omits-custom-envs

Conversation

@AUDOSt0ck1ng

Copy link
Copy Markdown
Collaborator

Closes #13.

What

npm run build and npm run desktop bundled anything sitting in avatar/src/assets/environments/custom/. Only dist:win did not. Now no build does, unless asked.

Why

stripCustomEnvsForShip rewrites the eager import.meta.glob in src/config/environments.js to {} before Vite's import-analysis expands it, so custom media never enters the module graph. It gated on process.env.AVATAR_SHIP === '1', and dist:win was the only script setting it — the other two fell through the first line of transform.

Inverted per the issue's preferred direction:

const includeCustom = command === 'serve' || process.env.AVATAR_INCLUDE_CUSTOM === '1';

Keyed on command rather than mode, so --mode can't flip the embargo. AVATAR_SHIP is removed rather than left as a dead flag. Opt in with npx cross-env AVATAR_INCLUDE_CUSTOM=1 npm run build (cross-env because bare VAR=1 cmd doesn't work in PowerShell or cmd).

Two things beyond the straight inversion:

  • The rewrite no longer fails silently. String.replace returns its input when the pattern misses, and the plugin returned that as if it had worked — so a rename or reformat of the glob would have quietly restored the leak. It now calls this.error() and fails the build.
  • npm test covers the embargo. A clean checkout has an empty custom/, so npm run build in CI stays green even with the plugin deleted outright. The build is not a regression test for this on its own — which is also why the bug survived and why a casual recheck makes it look already fixed.

How tested

End to end, by hand (Windows), planting a 2 MB PROBE_TEST.gif in custom/ and running the real npm run build:

build dist/assets
npm run build (no match)
npx cross-env AVATAR_INCLUDE_CUSTOM=1 npm run build PROBE_TEST-ZgW6haIg.gif, 2,000,000 bytes

The opt-in row is the control. Without it, "found nothing" and "wasn't looking" are indistinguishable — which is the trap this bug sits in.

Bundle comparison. Built twice, once with custom/ empty and once holding 5.3 MB across three files, then diffed every emitted file by md5:

A: 23 files, 89,585,246 bytes
B: 23 files, 89,585,246 bytes
identical — every path and content hash matches

So it is not just that the probes are absent; nothing in custom/ influences chunking or content hashes either. That comparison is now scripts/custom-envs.test.mjs's is byte-identical whatever custom/ holds, using two differently-sized probe sets so it works without assuming custom/ starts empty (a contributor's local trial media would break that assumption).

Also verified: vite build --mode development excludes; the dev server still expands the glob (GET /src/config/environments.js returns the environments/custom/… paths); renaming the glob fails the build with a named error.

Mutation-checked, to confirm the suite isn't vacuous:

mutation result
plugin unregistered from plugins 6 fail
includeCustom = true (this bug reintroduced) 3 fail
a second import.meta.glob for custom/ added in src/main.jsx 1 fail

The third is why the file isn't only transform-level assertions. Five tests check that the one glob in environments.js gets rewritten, and all five stay green when media reaches the bundle by another route; only the ones that plant real files and run real builds catch it.

CI is green on my fork (Ubuntu, Node 22, npm ci): lint, 35 tests, production build. npm run lint, npm test, npm run build also pass locally in avatar/.

Behavior change

npm run desktop no longer shows custom/ media, since it runs a production build. Use npm run dev:desktop, or the opt-in. Documented in README.md, CONTRIBUTING.md, docs/environments.md, docs/development/project-layout.md.

Not verified

  • Installer not built end to end (dist:win is Windows-only and not in CI). electron-builder packs dist/** verbatim per the build.files field, so a clean dist/ implies a clean installer — inferred, not run.
  • UI not smoke-tested for this specifically: with the asset absent from dist/, the Custom expander has nothing to render. Note it also lists runtime entries from Settings → Directories, which this change does not touch.
  • The suite goes from ~0.5s to ~15s because three tests-worth of assertions share three real builds. Noted in docs/development/project-layout.md; happy to trim if that's unwelcome in npm test.

🤖 Generated with Claude Code

AUDOSt0ck1ng and others added 2 commits August 7, 2026 23:46
…uild

The strip plugin gated on AVATAR_SHIP=1, which only dist:win set, so
`npm run build` and `npm run desktop` still expanded the eager glob over
src/assets/environments/custom/ and hashed contributor trial media into
dist/. Invert the default: anything that emits a bundle drops custom/,
keyed on command === 'serve' so --mode cannot flip it, with
AVATAR_INCLUDE_CUSTOM=1 as the deliberate opt-in. AVATAR_SHIP is removed.

The rewrite also failed silently — String.replace returns its input when
the pattern misses, and the plugin returned that as if it had worked, so
renaming or reformatting the glob would have quietly restored the leak.
It now errors and fails the build.

Add scripts/custom-envs.test.mjs. A clean checkout has an empty custom/,
so CI's production build stays green even with the plugin deleted; the
build is not a regression test for this on its own. Five tests drive the
real plugin over the real environments.js, and two plant a file and run
an actual build, which is what catches media reaching the bundle by a
route other than that one glob.

Closes ARPAHLS#13

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…olds

The build tests asserted only that the planted probes were absent, which
leaves room for custom/ influencing the bundle without its files being
copied — different chunking, different content hashes. Compare two builds
populated with different probe sets instead, over a manifest of every
emitted file and its md5, so the promise being checked is the one the
docs make: what sits in custom/ makes no difference to a production
bundle.

Three builds shared across four assertions rather than one build each, so
the stronger check costs one extra build. Probe stems now depend only on
the set label: the opt-in build reuses the plain build's filenames, which
leaves the flag as the only variable between them and makes the control
fail when the embargo is broken (it did not before).

Surface vite's stderr when a build fails, since stdio: 'pipe' otherwise
leaves a CI failure here with nothing to go on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rosspeili

Copy link
Copy Markdown
Contributor

Thanks @AUDOSt0ck1ng, this looks solid.

For tests I think it's ok, still not much time, but good point, maybe worth exploring follow up with node --test --test-name-pattern / separate test:build if CI time becomes an issue.

Merging this as-is. Appreciate the thorough write-up and the mutation checks. <3

@rosspeili
rosspeili merged commit e7f205c into ARPAHLS:main Aug 8, 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.

[Bug]: \npm run build\ still bundles local custom/ environment media

2 participants