Repository navigation
The packaged app ships the whole repository, because the files rule is an exclusion list #399
Description
What happens
The files rule in package.json is a list of exclusions:
"files": [
"!vendor{,/**/*}",
"!.codesigning{,/**/*}"
]Everything else in the project directory at build time is packed into app.asar. Listing the asar inside the published v1.0.1 macOS artifact, by top-level entry:
23704 593.3 MB node_modules
70 4.2 MB src
50 1.2 MB docs
85 0.8 MB tests
5 0.0 MB scripts
5 0.0 MB .buildkite
3 0.0 MB fastlane
… .agents .bundle .claude .nvmrc .ruby-version
AGENTS.md CLAUDE.md CONTRIBUTING.md README.md
STATS.md TESTING.md Gemfile Gemfile.lock
eslint.config.mjs playwright.config.js
This is not a size problem, and the issue should not be read as one. node_modules is production dependencies only — electron-builder already prunes the dev ones, and there is no playwright, eslint or electron-builder in the asar. Everything the repository itself contributes is 6 MB of 600, and the non-runtime part of that — docs/ and tests/ — is under 2 MB. Inverting the rule would save about a third of one percent. Nobody should merge this expecting a smaller app.
It is a problem about shape: which way the default points.
Why the direction matters
An exclusion list means a new top-level directory ships by default, and nobody reviews a directory that ships itself. Two things in this repository's own history show what that costs.
The signing key (#390, #396). The CI signing step wrote the App Store Connect API key to .codesigning/ in the project root, and the packer swept it into app.asar. Every macOS artifact from v0.1.0 to v1.0.0 shipped it. .gitignore listed the directory, which protected the repository and did nothing for the artifact.
That specific failure is closed and cannot recur: the key is now written to a temporary directory outside the project and removed on exit, a unit test compares real paths and fails if it lands inside the project, and the packaged smoke test walks the payload — inside the asar and outside it, on macOS and on Windows — asserting .codesigning is absent. The key itself was revoked and rotated on 20 August, and v1.0.1 carries the first clean macOS build.
What is not closed is the shape that allowed it. The canary looks for .codesigning by name. A future CI step that writes a credential under any other name is packaged, and nothing goes red.
The directory rename (#377). Unit tests moved from test/ to tests/. Any exclusion written against the old path would have silently stopped matching — no error, no failing test, just a directory that quietly began shipping. An exclusion list chases a moving target; an allow-list does not move.
How Studio avoids this, for reference
Automattic/studio ships a clean asar — dist/, node_modules, php-bin, package.json, nothing else — through four independent layers in apps/studio/forge.config.ts, any one of which would have prevented the key from shipping:
- The packaging root is
apps/studio/, not the repo root. Anything CI writes at the repo root physically cannot enter the asar. - The first
ignorerule excludes every dotfile and dot-directory (/^\/\..*/). - The notarization key never exists as a file in the project tree. Studio packages first and notarizes afterwards as a separate fastlane lane over the finished
.app/.dmg, with the key read from the environment. This repository is now equivalent by a different route — Fix macOS signing key leakage in packaged apps #390 moved the key out of the tree while keeping electron-builder's in-bandnotarize: true. - Only built artifacts ship.
src/is ignored,dist/goes in, out-of-asar resources are copied explicitly.
Proposed change
Invert files into a positive list naming what the app needs at runtime: src/ (minus the renderer sources the bundle replaces), package.json, local-playground-web, and the build/ resources. This repository's root carries far more non-runtime material than a monorepo app directory would, which is why an allow-list here is shorter than the exclusion list it would replace.
Then widen the canary. With an allow-list in place, tests/e2e/packaged/smoke.spec.js can assert that app.asar contains no top-level entry outside the list, rather than that one named directory is absent. That is the assertion that would have caught the key without anyone having predicted its name — and it keeps catching whatever appears next.
The two go together. The allow-list alone is a rule someone can widen in a hurry; the assertion is what makes widening it a visible decision.
Related
- Fix macOS signing key leakage in packaged apps #390, Require the safe setup for rotated macOS signing credentials #396 — the incident, closed
- Put every test under one tests/ directory #377 — the directory move an exclusion list would have had to track
- Stop shipping build-only deps in the app #23 — trims build-only dependencies; adjacent, and the one change that would actually move the size numbers
- The app ships the Git it runs on #364 — found while measuring the spike's artifact
Activity
iangmaia commentedon Aug 21, 2026
That makes sense and will be a nice improvement for the long run 👍
[Fix] Reject a run that never started, instead of streaming to nobody (…
[Fix] Ship only what the app needs: turn build.files into an allow-li…