LearnNewsExamplesServices
Frontmatter
id17868
titleThree dependencies resolved by filesystem path, not module resolution — the engine assumes its node_modules is never hoisted
stateClosed
labels
bugaiarchitecturebuild
assigneesneo-gpt-emmy
createdAtAug 30, 2026, 12:07 AM
updatedAtSep 8, 2026, 6:53 PM
githubUrlhttps://github.com/neomjs/neo/issues/17868
authortobiu
commentsCount3
parentIssuenull
subIssues
17961 Two build programs resolve webpack and parse5 from the consumer''s cwd, so a hoisted install cannot build
subIssuesCompleted1
subIssuesTotal1
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 8, 2026, 6:53 PM

Three dependencies resolved by filesystem path, not module resolution — the engine assumes its node_modules is never hoisted

Closed v13.2.0/archive-v13-2-0-chunk-17 bugaiarchitecturebuild
tobiu
tobiu commented on Aug 30, 2026, 12:07 AM

Current decision and implementation contract — 2026-09-08

All three sites still need delivery at dev@b537d0a3c8101c7d9fcb2a23317b6133d8d356ec. PR #17962 was closed without merging; its earlier implementation is prior art, not completed ACs. #17997 was closed back into this ticket, not resolved.

Decision: keep browser-relative imports and publish a generated dist/marked.mjs, following the existing parse5 artifact producer/release/package contract (#17943). Copy marked's already self-contained upstream ESM file and license without recompiling it, include it in initial/release builds and package checks, and carry it from the engine package into consumer ESM output. Consumers must not install the engine's dev dependencies or edit its source to use it.

The webpack binary resolves from the active project's package context, preserving the generated workspace's compiler choice even when the engine postinstall creates its own nested webpack. The original neoPath config selection remains unchanged. Parser producers resolve their inputs from their own module context. The webpack runner invokes the resolved JavaScript entry with Node and an argv array, preserving its existing env/thread selection without shell re-splitting.

Why this shape: native import maps do not reach worker graphs. A bare marked import would therefore break dev workers. Requiring an unhoisted runtime installation adds a consumer layout constraint without need; the existing engine-owned artifact pattern avoids it. The alternative bundledDependencies layout does not survive the current ESM rewrite: it puts marked inside the engine package, while the emitted node_modules specifier addresses the consumer root. The engine-owned artifact avoids adding another rewrite rule. This is not a new dependency-loading architecture. The published artifact must also survive the existing ESM copy/flattening path; an in-repo success is insufficient.

Current falsifiers: invoking the existing parse5 producer from /private/tmp exits 1 on node_modules/parse5/dist/index.js. A real npm pack --dry-run --json --ignore-scripts includes both marked-importing source modules but zero marked files; marked is only a devDependency (^18.0.11). The source import sweep finds only these two runtime node_modules imports (the Task module hit is a JSDoc example).

Contract Ledger

Surface Owner Delivered behavior Failure boundary Evidence
Thread-build CLI Existing buildThreads script and webpack package bin Project-relative compiler resolution; original engine/workspace config selection; correct argv under hoisting and spaces Failed launch/build exits nonzero Real runner with relocated/hoisted dependency fixture plus installed-engine thread builds
parse5 producer Existing buildScripts/build/parse5.mjs Resolves package entry independent of cwd Missing dependency/build failure remains an error Producer invoked from unrelated cwd
marked runtime imports Engine-owned generated bundle Both importers address a shipped relative ESM asset No import-map or nested node_modules requirement Packed-package negative/positive controls and parser execution
Initial/release build and tarball Existing build-all/package guard Generate and include required browser bundles, with license Missing artifact fails package check Actual pack contents, not ignore-rule inference
Consumer ESM carriage Existing esmodules artifact-copy stage Copy required bundles from the engine that supplied the source Unresolvable emitted imports remain errors Consumer output fixture, including marked source imports

Intake: created 2026-08-29; previous substantive update 2026-09-01. Pre-stale under the workflow's 90-day/14-day bands; no stale/exemption label. No parent or open blocker returned by GitHub. Current-source and closed-successor checks above establish currency. ADR successor-risk: no-adr-impact found for dependency classification/carriage; existing BuildArchitecture and parse5 packaging are the local authority. Recovered Grace's packing investigation (Memory Core 57814f10-66f8-43a2-a308-5ad3066c498d); other semantic queries were irrelevant. Native Graph prebrief did not resolve the numeric ticket, so the live issue conversation/source and that memory are the grounding, not an invented graph result.

The four original AC outcomes below remain binding. This decision resolves the previously deferred packaging choice; it does not count the implementation as completed.


Original report

Three places resolve a dependency by filesystem path instead of by module resolution. All three break for a consumer that installs the engine as a dependency, because npm hoists — and the failures are unrelated-looking, so they get diagnosed one at a time.

Found while getting dist builds running for a workspace app that consumes the engine via file:. All three still present at origin/dev @ 8a5fb9c0aa.

# site specifier
1 buildScripts/webpack/buildThreads.mjs:21 let webpack = './node_modules/.bin/webpack';
2 buildScripts/build/parse5.mjs:11 entryPoints: ['node_modules/parse5/dist/index.js']
3 src/component/Markdown.mjs:4, src/app/content/Component.mjs:2 import {marked} from '../../node_modules/marked/lib/marked.esm.js'

These are two different problems and only one of them is a defect

1 and 2 are Node-side and have no constraint forcing them. Both are resolved relative to the process cwd, not to the script. A consumer running the engine's build scripts has its own node_modules layout — webpack and parse5 are hoisted to the consumer root, so ./node_modules/… from the consumer's cwd may or may not hit them, and the failure is a bare ENOENT that says nothing about hoisting. Nothing here needs a path: import.meta.resolve / createRequire(import.meta.url) resolves both correctly from either layout.

3 is deliberate and must not be "fixed" by switching to a bare specifier. The engine loads ES modules directly in the browser in dev mode, and there is no import map anywhere in the repo (checked). A bare import {marked} from 'marked' would simply fail to resolve in the browser. The relative path is what makes dev mode work — it is load-bearing, not sloppiness.

What it silently assumes is that the engine's own node_modules is not hoisted. Inside this repo that holds. For a consumer it does not: the engine lands at <app>/node_modules/neo.mjs/, marked hoists to <app>/node_modules/marked/, and ../../node_modules/marked/… resolves to <app>/node_modules/neo.mjs/node_modules/marked/ — which is not there.

So #3 is a design tension needing a decision, not a line to patch:

  • an import map for dev mode, freeing src/ to use bare specifiers; or
  • vendoring the handful of browser-loaded deps under a path the engine owns; or
  • declaring the non-hoisted layout a supported requirement and having consumers pin it (nohoist / an install-time check that fails with a sentence naming the cause).

I have deliberately not picked one — it touches dev-mode loading, packaging, and consumer install, so it wants a maintainer decision before an implementation.

Why file it as one ticket

Because the shared root is what makes it expensive: each site fails differently (a missing binary, a bundler entry point that is not found, a module that will not resolve in the browser), so they read as three unrelated build problems. Naming the class once is worth more than three leaf fixes discovered separately.

Acceptance Criteria

  • buildScripts/webpack/buildThreads.mjs resolves the webpack binary through module resolution rather than a cwd-relative path, and works when invoked from a consumer whose node_modules is hoisted.
  • buildScripts/build/parse5.mjs resolves its entry point the same way.
  • A decision is recorded for the browser-loaded marked imports — import map, vendored copy, or a supported non-hoisted layout — with the reason, so the next reader does not "fix" a deliberate relative path into a broken bare one.
  • Whichever is chosen, a consumer installing the engine as a dependency can run the thread build without editing engine files.

Out of Scope

  • The engine's service-thread entry: a workspace build fails that thread for lack of an app-owned ServiceWorker.mjs, which is expected — the reference workspace has none either.
tobiu referenced in commit 772a6e6 - "fix(build): resolve webpack and parse5 by module resolution, not cwd (#17868) on Aug 31, 2026, 7:12 PM
tobiu referenced in commit dd78582 - "fix(build): resolve webpack and parse5 by module resolution, not cwd (#17961) on Aug 31, 2026, 7:13 PM
tobiu unassigned from @neo-opus-grace on Sep 1, 2026, 10:08 AM