LearnNewsExamplesServices
Frontmatter
id17976
titledist/esm apps resolve basePath to the repo root, so the built tree never runs
stateClosed
labels
bugaiarchitecturebuild
assigneesneo-opus-grace
createdAtAug 31, 2026, 10:05 PM
updatedAtSep 1, 2026, 7:43 AM
githubUrlhttps://github.com/neomjs/neo/issues/17976
authorneo-opus-grace
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 1, 2026, 7:43 AM

dist/esm apps resolve basePath to the repo root, so the built tree never runs

Closed Backlog/active-chunk-19 bugaiarchitecturebuild
neo-opus-grace
neo-opus-grace commented on Aug 31, 2026, 10:05 PM

Context

Reported by @tobiu, 2026-08-31: opening http://localhost:63342/neo/dist/esm/apps/portal/index.html#/home renders the portal home fully — including offscreen canvases — while the app worker emits the same rejection thousands of times.

Base.mjs:411  Uncaught (in promise) undefined
(anonymous)         @ Base.mjs:411
promiseMessage      @ Base.mjs:405
(anonymous)         @ RemoteMethodAccess.mjs:164
(anonymous)         @ Body.mjs:1271
Promise.then
onStoreLoad         @ Body.mjs:1270
fire                @ Observable.mjs:284
onCollectionMutate  @ Store.mjs:1082
fire                @ Observable.mjs:284
splice              @ Base.mjs:1608
appendFeedBatch     @ Workspace.mjs:620
(anonymous)         @ Workspace.mjs:567

The rendering is genuinely correct. The defect this ticket files is what that trace proves about which tree is executing.

The Problem — measured

1. The built artifacts exist and are minified.

file newlines
dist/esm/src/worker/Base.mjs 0 (4850 bytes, single line)
dist/esm/apps/portal/app.mjs 0
apps/portal/app.mjs (source) 7

2. There are no source maps. find dist/esm -name '*.map' → 0 files; grep sourceMappingURL dist/esm/src/worker/Base.mjs → 0.

3. Therefore the reported line numbers cannot come from the built tree. A stack against a single-line file can only ever read Base.mjs:1. Base.mjs:411, Body.mjs:1271, Workspace.mjs:620 are source line numbers — and src/worker/Base.mjs:405 is return new Promise(function(resolve, reject) {, exactly the promiseMessage frame the trace names.

4. The emitted config points away from the emitted tree. dist/esm/apps/portal/neo-config.json:

{"appPath": "apps/portal/app.mjs", "basePath": "../../../../", "workerBasePath": "../../src/worker/"}

Resolved from dist/esm/apps/portal/ on disk:

value resolves to
basePath: "../../../../" <repo> — the repository root
workerBasePath: "../../src/worker/" <repo>/dist/esm/src/worker/

The source config carries basePath: "../../", which from apps/portal/ is also the repository root. esmDistTransforms#rewriteNeoConfig prefixes ../../ because the config moves two directories deeper — arithmetic that preserves the original destination. Its JSDoc states that intent: "basePath gains ../../ because the config moves two directories deeper in the output tree … it compensates for where the config now sits." Preserving the destination is the defect: a built app must resolve against the tree holding its own copied engine, so the value should remain ../../, which from the new location means dist/esm.

So dist/esm/apps/portal/index.html does not exercise the dist/esm build. It is a correct, complete, emitted tree that the runtime is configured to walk past. That is why it "fully works": the source tree works. Every defect the emitted tree could carry is invisible through this entry point.

What is NOT established

Stated explicitly so the next reader does not inherit a guess:

  • The rejection's root cause. worker/Base.mjs:311 rejects with promise.reject(data.data), so an undefined reason means a reply arrived with reject set and no payload. Which remote call refuses, and why, is not diagnosed here. Neo.main.DomAccess.scrollTo itself cannot throw — it is this.getElement(id)?.scrollTo(...), a no-op on a missing node.
  • The exact resolution path by which appPath becomes a URL. The basePath arithmetic above is measured on disk; the runtime's consumption of it was not traced to a single line.
  • Whether the same re-anchoring affects dist/production / dist/development.

The Fix

  1. rewriteNeoConfig must re-anchor basePath to the emitted tree rather than preserve its original destination — the built value should resolve to dist/esm, not to the repository root. isFixedBasePath (absolute / protocol-relative) keeps its existing pass-through.
  2. A regression arm that asserts the resolved target, not the string: from dist/esm/apps/<app>/, basePath must resolve inside dist/esm. A string comparison would have passed every version of this bug.
  3. The amplifier is separable and worth its own line: grid/Body.mjs:1270 dispatches Neo.main.DomAccess.scrollTo as .then() with no .catch(), so any refusal becomes an unhandled rejection; apps/workstation/view/Workspace.mjs:566 runs a setInterval feed that mutates the store every tick, re-entering onStoreLoad. One refusal per tick is what turns a single fault into thousands of console lines. Fixing the resolution may silence this instance without fixing the pattern.

Acceptance Criteria

  • dist/esm/apps/<app>/neo-config.json's basePath resolves inside dist/esm, asserted by resolution rather than string equality.
  • A built portal loaded from dist/esm/apps/portal/index.html executes the built modules — provable because a deliberate edit to a dist/esm/src/** file changes observed behaviour, and the equivalent source edit does not.
  • isFixedBasePath values (absolute, protocol-relative, fully-qualified) still pass through unchanged, with an arm each.
  • The remote-call refusal behind the reported trace is either fixed or filed with its own root cause; this ticket does not claim to have diagnosed it.

Out of Scope

  • The .then()-without-.catch() pattern at grid/Body.mjs:1270 and the workstation feed interval — the amplifier, not the fault.
  • #17972's WS/ addon dynamic import: same class (runtime reaching unbuilt source) via a different mechanism — a dynamic import inside Main.mjs rather than a static config value, and in a consumer workspace rather than the in-repo portal.
  • Typing the reply-path rejection at worker/Base.mjs:311. #17894 typed the dead-port path today; this one still rejects with whatever the reply carries.

Related

  • #17931 — the dist/esm boot arm. This is the defect that arm exists to catch, and could not be seen by a resolution-only fixture: every path it checks genuinely exists.
  • #17972 — sibling in the same class, different mechanism.
  • #17921 — the three original dist/esm load defects; this is a fourth on the same surface.

Live latest-open sweep: checked latest 20 open issues at 2026-08-31T20:03:41Z; #17972 and #17971 reviewed and distinguished above.

Origin Session ID: 56bc214a-5b55-41a2-848a-cdfa372abbcd