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
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.
- 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.
- Drop the two
.brain entries from the head.
- Decide separately whether
dist/mermaid.mjs ships by default. Nothing above depends on that answer.
Acceptance Criteria
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"
Context
Raised by @tobiu while reviewing #18813:
.npmignoreis 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/*negationbuildScripts/release/prepare.mjs:105-128composes 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..gitignorecarries/dist(line 68) and no negations. Simulating the generator against the current tree:and the lines a release run would drop:
Two of those are statically imported by the engine, so their loss is not a degraded feature, it is an install that cannot boot:
dist/parse5.mjssrc/functional/util/HtmlTemplateProcessor.mjs:3dist/marked.mjssrc/app/content/Component.mjs:2,src/component/Markdown.mjs:4dist/mermaid.mjssrc/main/addon/Mermaid.mjs:101await import(), configurable pathThe negations survive today only because nobody has run
prepare.mjssince 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:
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.
dist/parse5.mjsparse5andesbuildare devDependenciesdist/marked.mjsmarkedandesbuildare devDependenciesdist/highlight/src/util/HighlightJs.mjsdist/mermaid.mjsmermaid,esbuildare devDependenciesSo 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.mjsis 5.0 MB โ 91% of the 5.5 MBdistpayload โ and it is the only one that is optional. It is loaded by an addon, lazily, throughNeo.config.basePath + 'dist/mermaid.mjs', andmain.addon.Mermaidalready exposes amermaidPathconfig, 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
Move theFALSIFIED โ this does not work, and it fails silently. Ignore files resolve last-match-wins, and the generator appends the!/dist/*and!/resources/*negations above the marker, into the hand-maintained head, where the generator preserves them..gitignorecopy after the head. That copy carries a bare/dist, so it re-excludes anything a head negation re-included. Measured withnpm pack --dry-runon a candidate built exactly that way:dist/parse5.mjs,dist/marked.mjs,dist/mermaid.mjsall MISSING,dist/highlight0 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.mjsgains a second preserved region (# npm-only rules that must OUTRANK the .gitignore copy above) appended after the.gitignorecontent, and the npm-only rules move there. Measured on that shape: all bundles SHIPPED, highlight 2, and a simulated release loses 0 rules.check-package-contents.mjsalready exists and knows this domain โ assert that every statically-importeddist/*bundle survives a simulated regeneration..brainentries from the head.dist/mermaid.mjsships by default. Nothing above depends on that answer.Acceptance Criteria
prepare.mjs's composition against the tree produces a.npmignorethat still carries every!/dist/*negation and still packs them. Both halves are required: a composition that keeps the rules but places them above the.gitignorecopy 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.xmland/apps/portal/llms.txt.dist/*bundle that the engine imports statically would not be published. Fixture-driven, not a hardcoded list โparse5andmarkedare the current members and the set will change./package.brain.jsonand/package-lock.brain.jsonare gone from the head, and no other head line names a path absent from the repository.npm pack --dry-runlistsdist/parse5.mjs,dist/marked.mjsand the highlight bundles before and after, so the repair is proven against npm's own file selection rather than against a simulation only.npx neo-apppath: the scaffolded workspace'spostinstallinstalls the engine's own devDependencies intonode_modules/neo.mjs, and the bundle scripts write back into that same tree. It holds only for a directnpm i neo.mjs.Out of Scope
dependencies.dist/esmbuild..gitignoreside:/distis correct there, and the negations were never in it.Avoided Traps
.gitignorecopy lands after the head and wins. This is the trap the ticket itself originally prescribed..gitignoreinstead. Git does not descend into an ignored directory, so!/dist/parse5.mjsunder/distis unreachable there โ the same shape#16600already documented for/docs/output/*. It would look like it worked and change nothing.mermaid.mjsas the same case asparse5.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.npm packdecides 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"