LearnNewsExamplesServices
Frontmatter
id18814
titleThe next release deletes every !/dist negation from .npmignore, including two bundles the engine imports statically
stateClosed
labels
bugaibuild
assigneesneo-opus-vega
createdAtSep 16, 2026, 9:35 PM
updatedAtSep 17, 2026, 3:06 PM
githubUrlhttps://github.com/neomjs/neo/issues/18814
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 17, 2026, 3:06 PM

The next release deletes every !/dist negation from .npmignore, including two bundles the engine imports statically

neo-opus-vega
neo-opus-vega commented on Sep 16, 2026, 9:35 PM

Context

Raised by @tobiu while reviewing #18813: .npmignore is generated by the release script except for a hand-maintained head, it still carries pre-split entries, and the !/dist/* negations look like they ship generated artifacts that could be rebuilt instead.

Exploring it turned up a latent release-breaking defect that inverts the third question.

๐Ÿ”ด The urgent one: the next release deletes every !/dist/* negation

buildScripts/release/prepare.mjs:105-128 composes the file as:

splitIndex  = npmIgnoreContent.indexOf('# Original content of the .gitignore file');
headerLines = npmIgnoreContent.slice(0, splitIndex + 1);
newNpmIgnoreContent = headerLines.join(os.EOL) + os.EOL + gitIgnoreContent;

Everything below the marker is replaced by a verbatim copy of .gitignore. The marker sits at index 5, so the hand-maintained head is 6 lines. Every !/dist/* negation sits below it, at lines 74-90 โ€” inside the regenerated region.

.gitignore carries /dist (line 68) and no negations. Simulating the generator against the current tree:

current .npmignore   173 lines
regenerated          178 lines
identical?           false

and the lines a release run would drop:

!/dist/parse5.mjs
!/dist/marked.mjs
!/dist/mermaid.mjs
!/dist/monaco/
!/dist/highlight/
!/dist/highlight/highlight.custom.js
!/dist/highlight/highlight.custom.min.js

Two of those are statically imported by the engine, so their loss is not a degraded feature, it is an install that cannot boot:

bundle imported by how
dist/parse5.mjs src/functional/util/HtmlTemplateProcessor.mjs:3 static, module scope
dist/marked.mjs src/app/content/Component.mjs:2, src/component/Markdown.mjs:4 static, module scope
dist/mermaid.mjs src/main/addon/Mermaid.mjs:101 dynamic await import(), configurable path

The negations survive today only because nobody has run prepare.mjs since they were added. They are one release away from vanishing, and the comment above them documents them as load-bearing โ€” so the reasoning is right and its location is wrong.

The second: stale pre-split entries in the head

The head is hand-maintained and never regenerated, so what lands there is permanent until someone removes it:

/package.brain.json          tracked=0  onDisk=no
/package-lock.brain.json     tracked=0  onDisk=no

Both are pre-split residue โ€” the Brain moved to neo-agent-brain. Harmless, and exactly the kind of thing that reads as a live rule to the next reader.

The third: should generated artifacts ship at all?

Not a single answer โ€” the three differ, and the measurement separates them.

bundle size loaded can a consumer build it?
dist/parse5.mjs 164 KB static import no โ€” parse5 and esbuild are devDependencies
dist/marked.mjs 48 KB static import no โ€” marked and esbuild are devDependencies
dist/highlight/ 220 KB via src/util/HighlightJs.mjs no
dist/mermaid.mjs 5.0 MB dynamic, on demand no โ€” mermaid, esbuild are devDependencies

So the comment's claim holds for all four: none can be produced on the consumer side, because every source package is a devDependency. Shipping them is not carelessness; it is the only way an installed engine works without moving four build-time toolchains into dependencies.

But mermaid.mjs is 5.0 MB โ€” 91% of the 5.5 MB dist payload โ€” and it is the only one that is optional. It is loaded by an addon, lazily, through Neo.config.basePath + 'dist/mermaid.mjs', and main.addon.Mermaid already exposes a mermaidPath config, so a consumer can redirect it today. That combination โ€” large, optional, already redirectable โ€” makes it the one genuine candidate for not shipping by default.

That is a product decision, not a cleanup: every consumer currently gets working Mermaid diagrams with no setup, and any alternative (a separate package, a CDN default, an opt-in postinstall) trades 5 MB of install for a support question. Recorded here with the numbers; not taken.

The Fix

  1. Move the !/dist/* and !/resources/* negations above the marker, into the hand-maintained head, where the generator preserves them. FALSIFIED โ€” this does not work, and it fails silently. Ignore files resolve last-match-wins, and the generator appends the .gitignore copy after the head. That copy carries a bare /dist, so it re-excludes anything a head negation re-included. Measured with npm pack --dry-run on a candidate built exactly that way: dist/parse5.mjs, dist/marked.mjs, dist/mermaid.mjs all MISSING, dist/highlight 0 files โ€” the same breakage, now with a file that reads as if it were repaired. The rules have to outrank the copy, so they must live BELOW it: prepare.mjs gains a second preserved region (# npm-only rules that must OUTRANK the .gitignore copy above) appended after the .gitignore content, and the npm-only rules move there. Measured on that shape: all bundles SHIPPED, highlight 2, and a simulated release loses 0 rules.
  2. Add a guard so this cannot recur: check-package-contents.mjs already exists and knows this domain โ€” assert that every statically-imported dist/* bundle survives a simulated regeneration.
  3. Drop the two .brain entries from the head.
  4. Decide separately whether dist/mermaid.mjs ships by default. Nothing above depends on that answer.

Acceptance Criteria

  • AC-1: simulating prepare.mjs's composition against the tree produces a .npmignore that still carries every !/dist/* negation and still packs them. Both halves are required: a composition that keeps the rules but places them above the .gitignore copy keeps every rule and ships nothing. Red control: the simulation on today's tree drops 12 rules โ€” the seven negations plus /dist/*, /dist/highlight/*, /docs/output, /apps/portal/sitemap.xml and /apps/portal/llms.txt.
  • AC-2: a guard fails when a dist/* bundle that the engine imports statically would not be published. Fixture-driven, not a hardcoded list โ€” parse5 and marked are the current members and the set will change.
  • AC-3: /package.brain.json and /package-lock.brain.json are gone from the head, and no other head line names a path absent from the repository.
  • AC-4: npm pack --dry-run lists dist/parse5.mjs, dist/marked.mjs and the highlight bundles before and after, so the repair is proven against npm's own file selection rather than against a simulation only.
  • AC-5: the mermaid question is not settled here; it carries its own ticket โ€” #18823, filed with the 5.0 MB recorded and the premise corrected. This ticket's reasoning that "none can be produced on the consumer side" is false for the npx neo-app path: the scaffolded workspace's postinstall installs the engine's own devDependencies into node_modules/neo.mjs, and the bundle scripts write back into that same tree. It holds only for a direct npm i neo.mjs.

Out of Scope

  • Moving any build-time toolchain into dependencies.
  • Changing what the bundles contain, or the dist/esm build.
  • The .gitignore side: /dist is correct there, and the negations were never in it.

Avoided Traps

  • โ›” Do not "fix" this by deleting the negations. They read like shipped build output and they are load-bearing: two are statically imported, so removing them publishes an engine that cannot boot.
  • โ›” Do not hoist the negations into the head. Measured: it keeps every rule and ships none of the bundles, because the .gitignore copy lands after the head and wins. This is the trap the ticket itself originally prescribed.
  • โ›” Do not add the negations to .gitignore instead. Git does not descend into an ignored directory, so !/dist/parse5.mjs under /dist is unreachable there โ€” the same shape #16600 already documented for /docs/output/*. It would look like it worked and change nothing.
  • โ›” Do not treat mermaid.mjs as the same case as parse5.mjs. One is 5 MB, optional, lazily loaded and already redirectable; the other is 164 KB and imported at module scope. A single verdict over both is wrong in one direction or the other.
  • โ›” Do not verify with the simulation alone. AC-4 exists because npm pack decides what ships, and a regenerated file is a prediction about it.

Related

#18823 (the mermaid decision this defers) ยท neomjs/create-app#33 / #34 (the scaffold's side of it) ยท #18813 (surfaced it) ยท buildScripts/release/prepare.mjs:105-128 ยท buildScripts/util/check-package-contents.mjs ยท #16600 (the same directory-negation trap in .gitignore)

Origin Session ID: 5bf0b816-b919-40fc-9c18-fee751fd1635

Retrieval Hint: "npmignore generated from gitignore below marker deletes dist negations parse5 marked statically imported release breaking mermaid 5MB"

tobiu referenced in commit f4fa3f0 - "fix(release): the npm-only header owns its paths, so the .gitignore copy cannot contradict it (#18814) (#18822) on Sep 17, 2026, 3:06 PM
tobiu closed this issue on Sep 17, 2026, 3:06 PM