Skip to content

The packaged app ships the whole repository, because the files rule is an exclusion list #399

Closed

Description

@juanmaguitar

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:

  1. The packaging root is apps/studio/, not the repo root. Anything CI writes at the repo root physically cannot enter the asar.
  2. The first ignore rule excludes every dotfile and dot-directory (/^\/\..*/).
  3. 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-band notarize: true.
  4. 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

Activity

  1. iangmaia commented on Aug 21, 2026

    @iangmaia
    Collaborator

    Invert files into a positive list naming what the app needs at runtime

    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 makes sense and will be a nice improvement for the long run 👍

  2. added this to the v1.1.0 milestone on Sep 11, 2026
  3. added a commit that references this issue on Sep 11, 2026
    b638e67
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: build-installnpm install, builds, the dev serverrefactorInternal restructuring; no user-visible change

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions