Skip to content

feat(plugin-js): oxc-based import-graph resolver, phantom-dep detection - #362

Closed
raphaelvigee wants to merge 3 commits into
raphaelvigee/feat-plugin-js-m0-m1-installfrom
raphaelvigee/feat-plugin-js-m2-import-graph
Closed

feat(plugin-js): oxc-based import-graph resolver, phantom-dep detection#362
raphaelvigee wants to merge 3 commits into
raphaelvigee/feat-plugin-js-m0-m1-installfrom
raphaelvigee/feat-plugin-js-m2-import-graph

Conversation

@raphaelvigee

@raphaelvigee raphaelvigee commented Aug 5, 2026

Copy link
Copy Markdown
Member

M2 of the JS/TS heph plugin plan (part 2/6 of the stack).

Fixes the deferred optionalDependencies hard-fail (platform-restricted optional deps now silently skip instead of failing Provider::get), then adds a real import-graph resolver on top of M1's package.json-declaration wiring.

  • oxc_parser + oxc_resolver extract and resolve import/require/dynamic-import specifiers per real Node condition-set semantics (separate ESM/CJS/types resolvers), building two distinct graphs (runtime vs type-only edges).
  • Phantom-dependency detection: an import resolving into a package not in the declared-dependency closure (deps + devDeps + peerDeps) is a hard error naming the file/specifier/package, hermetic against a fresh checkout (no ambient node_modules required) via a bare-specifier name check.
  • Conformance corpus covering exports-map condition ordering, wildcard specificity, array fallbacks, null-blocked subpaths, self-referencing imports, and the "imports" field — cross-checked live against a real Node binary when present, self-gated otherwise.

Reviewed by feature-quality/code-quality/hermeticity. Four BLOCKERs found and fixed: the phantom-dep check being a no-op without ambient node_modules, peerDependencies never counted as declared, an oxc_resolver default reading the ambient NODE_PATH env var, and a resolved-but-unclassifiable edge silently passing instead of failing closed. Each has a regression test.

Explicitly deferred: import-equals require() and require.resolve() extraction, per-specifier type modifier detection, provider-lifetime resolver caching (fixed later in the stack), pinning a hermetic Node toolchain for the conformance corpus's live cross-check.

Test plan

  • cargo build -p plugin-js -p plugin-js-cdylib
  • cargo test -p plugin-js
  • cargo clippy -p plugin-js -p plugin-js-cdylib --all-targets -- -D warnings
  • cargo fmt --check -p plugin-js -p plugin-js-cdylib

Stack created with GitHub Stacks CLIGive Feedback 💬

@raphaelvigee
raphaelvigee force-pushed the raphaelvigee/feat-plugin-js-m2-import-graph branch from e33420f to 58c133d Compare August 7, 2026 11:21
@raphaelvigee
raphaelvigee marked this pull request as ready for review August 7, 2026 12:57
@raphaelvigee
raphaelvigee force-pushed the raphaelvigee/feat-plugin-js-m2-import-graph branch from 58c133d to 4ecae51 Compare August 7, 2026 12:58
M2 of the JS/TS plugin: fixes the deferred optionalDependencies hard-fail
(platform-restricted optional deps now silently skip instead of failing
Provider::get), then adds a real import-graph resolver on top of M1's
package.json-declaration wiring.

- oxc_parser + oxc_resolver extract and resolve import/require/dynamic-import
  specifiers per real Node condition-set semantics (separate ESM/CJS/types
  resolvers), building two distinct graphs (runtime vs type-only edges).
- Phantom-dependency detection: an import resolving into a package not in the
  declared-dependency closure (deps + devDeps + peerDeps) is a hard error
  naming the file/specifier/package, hermetic against a fresh checkout (no
  ambient node_modules required) via a bare-specifier name check.
- Conformance corpus (crates/plugin-js/src/pluginjs/conformance.rs) covering
  exports-map condition ordering, wildcard specificity, array fallbacks,
  null-blocked subpaths, self-referencing imports, and the "imports" field —
  cross-checked live against a real Node binary when present, self-gated
  otherwise.

Reviewed by feature-quality/code-quality/hermeticity. Four BLOCKERs found and
fixed: the phantom-dep check being a no-op without ambient node_modules,
peerDependencies never counted as declared, an oxc_resolver default reading
the ambient NODE_PATH env var, and a resolved-but-unclassifiable edge
silently passing instead of failing closed. Each has a regression test.

Explicitly deferred (named, not silent): import-equals require() and
require.resolve() extraction, per-specifier `type` modifier detection,
provider-lifetime resolver caching (currently rebuilt per Provider::get
call — a stated perf follow-up), and pinning a hermetic Node toolchain so
the conformance corpus's live cross-check is guaranteed to run in CI rather
than opportunistically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M3wZfyPsG8stfRQuybLRjN
@raphaelvigee
raphaelvigee force-pushed the raphaelvigee/feat-plugin-js-m2-import-graph branch from 4ecae51 to 5b006c1 Compare August 7, 2026 16:00
…rde_json's Map ordering

oxc_resolver (a new plugin-js dependency) requires serde_json's
preserve_order feature unconditionally, which is a workspace-wide Cargo
choice. That silently flipped serde_json::Map from an always-sorted
BTreeMap to an insertion-order IndexMap everywhere, including in
plugin-oci's manifest/config json! literals, which relied on that
implicit sort for canonical, cache-key-stable output.

Sort explicitly at encode time instead of depending on which Map
backing happens to be active.
…uirk that changed after v18

array_exports_matched_entry_missing_on_disk_hard_fails_no_fallback's live
cross-check was authored against v18.12.1's --experimental-import-meta-resolve.
Later Node no longer checks file existence for array-form exports entries
during resolution, deferring it to module load — so any newer Node found on
PATH now resolves where the hard-coded expectation says it should throw.

plugin-js's own resolver (the assertion that actually matters here) still
implements the documented algorithm correctly and is unaffected; only the
live Node cross-check is dropped for this one fixture.
@raphaelvigee

Copy link
Copy Markdown
Member Author

Superseded by #370 — the whole stack was squash-collapsed into a single PR rebased on master, per request.

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