Skip to content

refactor(workspace): resolve @mlx-node/* through each package's exports map - #151

Merged
Brooooooklyn merged 3 commits into
mainfrom
refactor/monorepo-paths-exports
Sep 15, 2026
Merged

Brooooooklyn merged 3 commits into
mainfrom
refactor/monorepo-paths-exports

Conversation

@Brooooooklyn

@Brooooooklyn Brooooooklyn commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

What changed

@mlx-node/* specifiers now resolve through each package's own exports map. The hand-maintained resolve.alias table in vite.config.ts is gone — no alias entry names a workspace package any more.

  • Every package that compiles TypeScript declares a source entry as an @mlx-node/source condition, first key of each exports entry, ahead of types/import. The subpath list lives next to the subpath it names, in the package that owns it.
  • The test resolver opts into that condition via ssr.resolve.conditions, with Vite's ssr defaults repeated because that key replaces them.
  • Published packages gained "src" in files, since the condition names files that must exist wherever the manifest does.
  • docs/monorepo-paths.md documents the arrangement, the trap described below, and the checklist for adding a subpath.

Why

The alias table was a second, unvalidated copy of every exports map, and it had already drifted:

Finding Evidence
@mlx-node/lm/model-discovery + ./model-detection declared in exports, imported by packages/agent/src/provider/models.ts and packages/server/src/host/discover.ts, no dist artifact tests stayed green through the alias; yarn mlx and any published install could not resolve them
@mlx-node/vlm had no alias entry resolved to dist while its siblings resolved to src
Alias matching is prefix-based, first-match-wins, and rewrites with a plain string replace a bare package key above its subpaths turns @mlx-node/server/host into .../src/index.ts/host — the table carried a comment about the ordering it had to keep

The sharp edge worth knowing

ssr.resolve.conditions is not just a bundler setting in this stack. Vitest reads it and re-emits it as real --conditions flags on its Node processes, so Node-level resolution inside the test process picks up the condition too — and new Worker() inherits those flags. That is how the dashboard's SQLite worker started resolving @mlx-node/* to TypeScript and dying at startup with "TypeScript parameter property is not supported in strip-only mode" (packages/server/src/host/index.ts has one). packages/dashboard/src/worker/client.ts now spawns with execArgv: [], mirroring the rule the desktop sidecar fork already followed in supervisor/child-node.ts. Any future nested Node spawn that loads a workspace package needs the same.

The condition cannot leak into the other two surfaces: Node ignores @mlx-node/source (it is not in its condition list, so import still resolves dist), and oxnode likewise.

Verification

Check Result
yarn typecheck (full tsc -b) clean
yarn vitest packages/dashboard/__test__ 710/710 — was 82 failures deep before the execArgv fix
80-file slice: agent, privacy, dashboard, server/host, models/doc-preprocess, core 1367 passed / 8 skipped, 0 failures
yarn mlx --help CLI starts from source through exportsdist, so the oxnode path is intact
Resolution direction, not just pass/fail import.meta.resolve inside Vitest reports packages/*/src/**; a child node process reports packages/*/dist/**

The model-dependent tests outside that slice were not run locally.

The decision worth your eyes

Shipping src in the tarball is the one product-level call here. It is what makes the declared condition a real target rather than a dangling entry, and it lets a consumer opt into source intentionally. Yarn 4 cannot strip the condition at pack time (publishConfig supports a fixed field list that excludes exports/types), so the alternative is a prepack rewrite of package.json. Easy to drop: remove "src" from the seven files arrays and the condition becomes repo-only.


Note

Medium Risk
Changes how all Vitest runs resolve workspace imports and expands published tarballs with src; misconfiguration could break tests or hide missing dist artifacts until runtime.

Overview
Replaces the root hand-maintained resolve.alias list for workspace packages with a workspaceSource() Vite plugin that reads each package's exports map and resolves @mlx-node/* imports to the @mlx-node/source TypeScript paths during Vitest/dev only. tsc, oxnode, and Node consumers still use dist because the custom condition is not wired into resolve.conditions / ssr.resolve.conditions (that path would leak into Vitest's Node execArgv and break worker/sidecar loads on non-stripable TS).

Every affected package package.json now declares @mlx-node/source on each export entry and includes "src" in files so published manifests stay consistent. The @mlx-node/core alias is removed (its exports already unify import/require); only the dashboard @/ test alias remains.

Adds docs/monorepo-paths.md (linked from CLAUDE.md) documenting resolution surfaces, why the Vite-plugin approach was chosen, and how to add subpaths. Updates a comment in core-exports.test.ts to match the new resolution story.

Reviewed by Cursor Bugbot for commit 849d638. Bugbot is set up for automated code reviews on this repo. Configure here.

…ts map

The root vite.config.ts kept one resolve.alias entry per `@mlx-node/*`
specifier, subpaths included, rewriting each to that package's TypeScript
source. Tests therefore ran against `src` while Node ran against `dist`, and
the table was a second copy of every package's exports map that nothing
validated. It had already drifted: `lm/model-discovery` and
`lm/model-detection` were declared in exports and imported by two source
files, but had no dist artifact, and `@mlx-node/vlm` had no alias at all, so
it silently resolved to dist while its siblings resolved to src.

Each package now declares its own source entry as an `@mlx-node/source`
condition ahead of `types`/`import` in its exports map, and the test resolver
opts into it through `ssr.resolve.conditions`. That key is the only one that
reaches the test resolver, and Vitest mirrors it onto its Node processes as
real `--conditions` flags — so anything Node loads as JavaScript must not
inherit them. The dashboard's SQLite worker thread now spawns with
`execArgv: []`, the same rule the desktop sidecar fork already followed;
without it the worker resolved workspace packages to TypeScript and died on
a parameter property Node cannot strip.

Deleting the table also deleted the alias-ordering hazard: matching is
prefix-based and first-match-wins, so a bare package key listed above its
subpaths rewrote `@mlx-node/server/host` into `.../src/index.ts/host`.

Published packages gained `src` in `files`, since the condition names files
that must exist wherever the manifest does.

Verified: `tsc -b` clean; `yarn mlx --help` starts the CLI from source through
exports -> dist; dashboard suite 710/710 (it was 82 failures deep before the
execArgv fix); an 80-file / 1375-test slice across agent, privacy, dashboard,
server/host, models and core at 1367 passed / 8 skipped.

docs/monorepo-paths.md documents the arrangement, the Vitest forwarding trap,
and the checklist for adding a subpath.
@coderabbitai

coderabbitai Bot commented Sep 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 26ee51ef-175a-47a7-89c1-fc996fd18fec

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-15T05:15:27.395517Z 849d638 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cursor

cursor Bot commented Sep 15, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_73308919-7e31-42e5-a1d8-fd984b2809d9)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9675933eaf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vite.config.ts Outdated
Resolving the source condition through `ssr.resolve.conditions` leaked into
Node: Vitest forwards that key to its processes as real `--conditions` flags,
so Node's own resolution of `@mlx-node/*` selected TypeScript, and every
Node-side load died on source Node cannot strip. The dashboard's SQLite worker
took 82 tests down with it (fixed here with `execArgv: []`, now reverted); the
desktop sidecar acceptance test's fork of the built entry was still broken and
took all four `sidecar-e2e.test.ts` cases with it, failing on the parameter
property in `packages/server/src/host/index.ts`.

`workspaceSource()` in the root config now performs the same mapping as a
`resolveId` hook (`enforce: 'pre'`) that reads each package's own
`@mlx-node/source` entry at config load. The subpath list still exists once, in
the package that owns it, and nothing is wired into resolver conditions — so
Node, `oxnode`, spawned workers and the published map stay exactly as they were.
The `execArgv: []` workaround is reverted; the doc records the measurements
behind the choice.

Verified: tsc -b clean; `yarn mlx --help` fine; 80-file slice 1367 passed /
8 skipped; 221-file slice 3651 passed / 40 skipped, no failures.
@Brooooooklyn

Copy link
Copy Markdown
Contributor Author

Adversarial review round 1: one real defect, fixed

The review returned needs-attention and it was right:

Source condition leaks into the desktop sidecar forks … sidecar-e2e.test.ts forks the built inference entry without overriding inherited flags, so its @mlx-node/server/host import selects src/host/index.ts, and the fork exits 1 with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX at the parameter property on line 67.

I reproduced it independently before touching anything: packages/desktop/__test__/sidecar-e2e.test.ts failed all four cases with exactly that stack, from the built sidecar entry loading source through --conditions @mlx-node/source.

Rather than patch each spawn, I removed the leak. The mechanism is now a workspaceSource() resolveId hook (enforce: 'pre') in the root config that reads each package's own @mlx-node/source entry at config load. Same single source of truth, same subpath-next-to-subpath declaration — but the mapping never enters resolver conditions, so Node, oxnode, spawned workers and the published map are untouched. The execArgv: [] workaround in the dashboard worker is reverted with it, and the doc now records both measurements (worker thread and sidecar fork) as the reason.

That is also the reviewer's own first recommendation ("Prefer Vite-only resolution derived from the exports maps"), and it matches where Vite is heading: resolve.alias[].customResolver is deprecated in favour of enforce: 'pre' resolveId plugins.

Verification after the fix

Check Result
packages/desktop/__test__/sidecar-e2e.test.ts green (was 4 failures)
221-file slice incl. all of desktop, cli, lm, asr, __test__ 3651 passed / 40 skipped, 0 failures
80-file slice across agent, privacy, dashboard, server/host, models, core 1367 passed / 8 skipped
yarn typecheck, yarn mlx --help clean
No --conditions @mlx-node/source on any Vitest process confirmed

Finding 2 (packed-consumer checks) is not addressed here: it is the standing gap this PR documents rather than closes, and it is a separate change.

@cursor

cursor Bot commented Sep 15, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_b5c97759-95be-4ef9-90bf-43ad190869e9)

`workspaceSource()` builds its map at config load, and a package.json is not a
Vite config file: editing one restarted nothing, so a watch session kept
resolving a subpath that had moved, missed one that was new, and fell back to
`dist` for it. The plugin now watches the workspace manifests and restarts the
server on add/change/unlink.

Verified by running the dev server and touching `packages/vlm/package.json`:
the log reports "server restarted.".
@Brooooooklyn

Copy link
Copy Markdown
Contributor Author

Adversarial review round 2: one fixed, one handed back

Verdict: needs-attention. Two findings, both worth stating plainly.

1. The manifest map never refreshed — fixed. The old alias table lived in vite.config.ts, so editing it restarted Vite; the new map is read from package.json files, which Vite does not watch. A watch session would have kept resolving a subpath that moved, missed a new one, and fallen back to dist for it — exactly the silent wrong-resolution this PR exists to remove.

The plugin now watches the workspace manifests and restarts the server on add/change/unlink. Verified by actually running the dev server and touching a manifest:

➜  Local:   http://localhost:5199/
1:09:46 PM [vite+] server restarted.

2. Coverage traded away — handed back, not fixed here. Correct observation: @mlx-node/vlm tests used to import dist by accident (it was the one package the alias table missed), and now nothing exercises its published entry. That loss is inherent to the intended design — every other package's tests already ran against src — and the mitigation is the publish-time gate the doc lists as future work: an exported-target/post-build check, publint for files coverage, and a plain-Node smoke import of each runtime entry. I am not adding it in this PR: it is a new gate with its own shape (and this PR deliberately adds no config assertions), so it belongs in its own change with the decision about how much packaging policy this repo wants.

The tsc half is not lost: types still resolves to dist, so a declaration target that goes missing fails type-checking. It is only the runtime (import) target that nothing checks today.

@cursor

cursor Bot commented Sep 15, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_d11602da-4f43-4e28-942b-33b7daf5a9b9)

@Brooooooklyn
Brooooooklyn merged commit 30d3372 into main Sep 15, 2026
8 checks passed
@Brooooooklyn
Brooooooklyn deleted the refactor/monorepo-paths-exports branch September 15, 2026 06:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant