Repository navigation
Conversation
@yao-pkg/pkg drops a pkg.scripts entry that matches no file silently, so the module is missing from the packaged binary and the service crashes at runtime with MODULE_NOT_FOUND (the class of failure in DocumentServer#267). Add tools/check-pkg-scripts.js (Node built-ins only), an npm script, a jest fixture test, and a CI gate in e2e.yml that runs after install and before pkg. Complements the module-specific packaged smoke test in #46 with a generic check over every pkg.scripts entry in all four components. Signed-off-by: j-base64 <jcentenero@arsys.es> Assisted-by: ClaudeCode:claude-opus-4-8
…after self-review) Address review findings on the initial guard: - Narrow glob detection so a literal path containing a bare @, + or ! (e.g. a scoped-package path like node_modules/@scope/pkg/index.js) is checked as a file instead of being wrongly refused as a glob. Real globs and extglobs are still caught via their glob syntax. - Discover components that declare pkg.scripts instead of hardcoding the list, so a newly added component cannot silently escape the guard. - Run the guard in the production image build (server.bake.Dockerfile) before pkg, not only in the e2e CI mirror. - Add tests for the scoped-literal, non-string, no-scripts and discovery cases. Signed-off-by: j-base64 <jcentenero@arsys.es> Assisted-by: ClaudeCode:claude-opus-4-8
…f-review) Second-review fix. discoverComponents scanned only depth 1-2, so a component nested deeper would be silently skipped, the same silent-omission class the discovery was meant to remove. Recurse the full tree instead, pruning node_modules/.git/tests and not following symlinks (a real tree is acyclic, so the walk cannot loop). Add a nested fixture + test to lock it in, and note that manifest JSON validity is out of scope (npm install, which precedes the guard, catches a malformed package.json first). Signed-off-by: j-base64 <jcentenero@arsys.es> Assisted-by: ClaudeCode:claude-opus-4-8
|
I checked it against the real toolchain rather than only reading it: I built binaries with Here is what I found, most important first. 1. Merge timing: this breaks the DocumentServer builds, not only e2eThe guard runs in So please don't merge this on its own while it is red. Either merge it with or right after #46, or drop the three stale 2. A string-form
|
|
Thanks for the review! On "1. Merge timing": this is the point I raised in the PR description under "Timing to merge". I don't have a single fixed answer and saw it as an open question: whether this PR should depend on the timing of the related PRs like #44 or #46. a. Either we wait and land this with/after the one that adds the currently missing file, I lean toward b via a small dedicated PR, as it could streamline getting this landed. Happy to discuss which way we prefer. I will go through your other points and address them asap. Thanks again! |
pkg accepts scripts as a single string and wraps it into an array before globbing. The guard only accepted arrays, so a string-form component was never discovered and, if checked directly, iterated the string per character into bogus failures. Normalise scripts (string -> one entry, falsy -> none) the same way pkg does, in both discovery and the per-component check. Signed-off-by: j-base64 <jcentenero@arsys.es> Assisted-by: ClaudeCode:claude-opus-4-8 Assisted-by: ClaudeCode:claude-sonnet-5
pkg passes all entries to one glob call, so a negation ("!x") removes any positive
entry it matches and the module is dropped from the binary. The guard skipped all "!"
entries, so an entry cancelled by a negation passed. Record what literal negations
exclude and fail a positive entry whose file is cancelled; refuse glob negations, as
positive globs already are.
Signed-off-by: j-base64 <jcentenero@arsys.es>
Assisted-by: ClaudeCode:claude-opus-4-8
Assisted-by: ClaudeCode:claude-sonnet-5
pkg globs path.join(componentDir, entry), i.e. the whole absolute path, so a repo checked out under a path like "a (b)/" makes every entry resolve to nothing while the guard, checking only the entry, passed. Detect glob syntax in the component path and report it once. Also corrects the header comment that claimed the guard could never diverge from pkg. Signed-off-by: j-base64 <jcentenero@arsys.es> Assisted-by: ClaudeCode:claude-opus-4-8 Assisted-by: ClaudeCode:claude-sonnet-5
The CLI printed a fixed "the following entries match no file" header and a blanket "pkg would drop these silently" footer, which is wrong for a glob refusal (pkg bundles a glob fine) and a non-string entry (pkg fails loudly). Use a neutral header and carry the consequence in each failure's own reason. Also accept an optional repo-root argument so the CLI can be exercised against a fixture tree in tests. Signed-off-by: j-base64 <jcentenero@arsys.es> Assisted-by: ClaudeCode:claude-opus-4-8 Assisted-by: ClaudeCode:claude-sonnet-5
The glob-refusal test asserted /glob/i, which also matches the fixture folder name "globEntry" in a "matches no file" message, so it passed even with the refusal branch removed; assert the refusal reason specifically. Add discovery tests that fail if the node_modules/tests/.git pruning is dropped or if symlinked directories are followed (verified by mutation: each bites). Signed-off-by: j-base64 <jcentenero@arsys.es> Assisted-by: ClaudeCode:claude-opus-4-8 Assisted-by: ClaudeCode:claude-sonnet-5
Note in the header that the guard's resolution matches pkg's tinyglobby ^0.2.11 matcher, which every pkg version in play depends on (6.14.x on Node 20 through 6.23.x on Node 22), so a future reader knows to re-check if pkg changes its engine. Signed-off-by: j-base64 <jcentenero@arsys.es> Assisted-by: ClaudeCode:claude-opus-4-8 Assisted-by: ClaudeCode:claude-sonnet-5
Each failure now carries a machine-readable `kind` (missing-file, negation-cancelled, glob-unsupported, path-glob, non-string, unreadable-manifest) alongside the human `reason`. Tests branch on KIND symbols instead of matching message substrings, so a reworded message can no longer silently break a test or falsely pass one (the tautology class is gone), and consumers get a stable contract. Verified by mutation: rewording a message keeps tests green; routing to the wrong kind fails the test. Signed-off-by: j-base64 <jcentenero@arsys.es> Assisted-by: ClaudeCode:claude-opus-4-8 Assisted-by: ClaudeCode:claude-sonnet-5
…ssages Address the review's documentation findings without changing behavior: - Header now states the assurance boundary (checks pkg.scripts resolvability for discovered components; does not model the pkg invocation list or check pkg.assets) and the install-context (e2e installs 4 components and checks all discovered, so it over-checks AdminPanel/server, harmless today but a latent e2e false-FAIL if it ever lists its own node_modules file). - discoverComponents documents that it fails open on unreadable/dot dirs (scanning the whole repo, failing closed would break on any unrelated unreadable dir). - Clarify normalizeScripts handling of non-string/array values; make the five failure reason strings consistent (present tense, path in parens). - Correct the e2e.yml scope comment: three binaries are overlaid, not four; AdminPanel is not overlaid, and the guard still checks it. Signed-off-by: j-base64 <jcentenero@arsys.es> Assisted-by: ClaudeCode:claude-opus-4-8 Assisted-by: ClaudeCode:claude-sonnet-5
pkg expands a directory entry to its files recursively (a positive bundles the subtree; a negation drops it). The guard models only literal files and exact-path negations, so a positive directory looked like a missing file and a directory negation silently cancelled a subtree without the guard noticing (a false pass). Refuse any entry, positive or negation, that resolves to a directory, with a clear message pointing at tinyglobby as the upgrade path. Verified against real pkg: both directions were confirmed, and the refusal keeps the guard faithful-or-loud with no new dependency. New DIRECTORY_UNSUPPORTED kind + fixtures/tests (mutation-checked). Signed-off-by: j-base64 <jcentenero@arsys.es> Assisted-by: ClaudeCode:claude-opus-4-8 Assisted-by: ClaudeCode:claude-sonnet-5
|
Pushed a revision that addresses the points you raised @MonaAghili and hardened the approach a bit further. Cool if you can take a look 🙌. ✅ Fixed (your points):
💪 Solidified:
📌 Follow-up (agreeing with your steer):
❓ Two open questions:
|
MonaAghili
left a comment
There was a problem hiding this comment.
Re-checked at f02e70e. Only the merge order is left from my side; answers to your two questions below.
1. Merge order: I'd go with (b), merge right after #46
I tried (a) and it fails silently. With the three editorDataRedis.js entries removed on main, #46 (at f62c462) merges with no conflict, because it never touches those lines. The result has DocService/sources/editorDataRedis.js on disk but no pkg.scripts entry for it in any of the three manifests.
The backend is only loaded through dynamic requires: Common/sources/notificationService.js:36 and DocService/sources/DocsCoServer.js (line 100 on main). Nothing requires editorDataRedis.js or editorDataRedis/index.js statically, so pkg doesn't bundle it. I built DocService from that tree with pkg 6.14.2 (node20-linux-x64) and ran it with editorDataStorage: "editorDataRedis". It dies at startup with Cannot find module './../../DocService/sources/editorDataRedis' (MODULE_NOT_FOUND, from notificationService.js via server.js). On the same tree, with deps installed, this guard prints "every entry resolves to a file" and exits 0, because it catches stale entries, not missing ones.
So (a) only works if someone remembers to re-add all three entries when the file lands, and nothing flags it if they don't. With (b), there's nothing to remember: merging this branch with #46 at f62c462 clears all three failures without touching any manifest, the guard passes, and the same binary starts normally in Redis mode.
2. Pinning pkg: fine in a separate PR
Not blocking for me. Pinning @yao-pkg/pkg here wouldn't fix the guard's reference anyway. It's a global install, and pkg ships no shrinkwrap, so the matcher resolves tinyglobby@^0.2.11 fresh on every build (0.2.17 today with pkg 6.14.2 on Node 20). The header already names the range it was checked against, which is the accurate statement either way.
TL;DR: A
pkg.scriptsentry that matches no file is silently dropped from the packaged binary,and only crashes with
MODULE_NOT_FOUNDat runtime when a specific code path is exercised. Withoutexercising that path, it's easy to assume a feature is in the build when it's not (it was the case for DocumentServer#267).
This PR adds a small build guard that fails when any component's
pkg.scriptsentry resolves to nofile, and runs it before the
pkgstep in both the production image build and the e2e CI job.🔥Ready for review; it stays a draft only because e2e is correctly red until #46 adds the editorDataRedis module or we decide to remove the stale entries (+details below)
Details
A component's
package.jsonpkg.scriptsblock reads like the list of modules that ship in thebuild, so an entry there implies the module is present. It may not be:
@yao-pkg/pkgbundles thefiles listed under
pkg.scripts, and if an entry matches no file it is dropped silently (nowarning, exit 0). The module is simply absent from the binary, and the only symptom is a
MODULE_NOT_FOUNDcrash the first time that code path runs, never a build failure. This is thesame class of silent-packaging gap that issues like DocumentServer#267 surfaced, and the same class of gap the Redis work in #44 and #46
runs into.
Each of the four components (
DocService,FileConverter,Metrics,AdminPanel/server)declares
pkg.scriptsentries, and a typo or a deleted/renamed source file would reintroduce thesame silent gap. This guard turns that class of silent omission into a loud build failure. It
complements a module-specific packaged smoke test like the one in #46: that proves one specific module is
bundled; this generic check protects every
pkg.scriptsentry in every component, at build time.How it resolves globs (faithful to pkg)
@yao-pkg/pkgresolves eachpkg.scriptsentry aspath.join(componentDir, entry)and thenglobs it (via
tinyglobby,{absolute:true, dot:true}), bundling a result only if it is afile. Every current
pkg.scriptsentry is a literal path, and for a literal pattern thatresolution is equivalent to a plain
fs.statSync(resolved).isFile()check. The guard thereforeuses that check and needs no new dependency. It also:
!entry as a pkg exclusion (not a file requirement); and@,+or!inside a path as literal (so a scoped-package path such asnode_modules/@scope/pkg/index.jsis checked, not mistaken for a pattern), while refusing anentry that contains real glob syntax rather than guessing at it (none exist today).
It must run after
npm install, since some entries point intonode_modules(e.g.axios,statsd) that only exist post-install, exactly aspkgrequires. In both the production imagebuild and the e2e CI job it is wired after the component installs and before the
pkgstep.No dependency choice
I considered using pkg's own matcher (
tinyglobby) so the guard could resolve globs exactly aspkg does. I did not, for two reasons: every current
pkg.scriptsentry is a literal path, forwhich the built-in
fscheck is already identical to what pkg does; and a real glob is refusedrather than resolved, so no glob engine is needed. Staying dependency-free also means the guard
runs anywhere
nodeis available, including build and CI steps that do not install the repo'sroot dev-dependencies. If a glob is ever added to
pkg.scripts,tinyglobby(pkg's own matcher)is the documented upgrade path.
Discover components choice (instead of a static list)
The guard finds what to check by scanning for
package.jsonfiles that declare apkg.scriptsblock, rather than hardcoding the list of components. A fixed list is one more thing to keep in
sync by hand, and its failure mode is quiet: add a component (or introduce
pkg.scriptsin anexisting one) and forget to update the list, and that component's entries silently escape the
guard, the very silent-omission this change exists to prevent. Discovery keeps coverage
automatic.
Possible objections
We could reconsider, going back to a static component list to address specific parts only, or even dropping this guard
altogether, if it turns out other developers deliberately rely on pkg's permissiveness (a missing
file dropping silently) to keep a module optional. But that is a very fragile mechanism: it is exactly
what produced the issues this PR addresses, which is why we want to catch the problem at build
time.
Changes
tools/check-pkg-scripts.js: the guard (Node built-ins only, no new dependency).package.json:npm run check:pkg-scripts..docker/server.bake.Dockerfile: run the guard before thepkgbuild (guards the realproduction packaging step).
.github/workflows/e2e.yml: run the guard after install, beforepkg(fast PR feedback).tests/unit/checkPkgScripts.tests.js+tests/fixtures/pkgScripts/*: regression tests(valid, zero-match, scoped-literal, glob-refusal,
!-exclusion, non-string, no-scripts,missing package.json, and component discovery).
Try it
Timing to merge
The guard only passes once the files that
pkg.scriptspoints at actually exist. Onmaintodaya few entries reference a module that has not been added yet (for example the
editorDataRedismodule that #46 introduces), so the guard will correctly fail and CI stays red until that module
lands. Two options for merge timing:
editorDataRedis);the entries already exist, so the guard goes green once the file is there.
module must re-add its
pkg.scriptsentry (otherwisepkgwould drop it again).Hope this helps close this class of silent-omission gap..
Review and feedback appreciated,
thanks!