Skip to content

feat(dynamic-import): resolve literal bare-specifier import('pkg') via compilePackages #1672

Description

@proggeramlug

Summary

A dynamic import() of a literal bare npm specifier — e.g. await import('chalk') — returns a rejected promise at runtime even when the package is in perry.compilePackages. Dynamic-import resolution does not route the specifier through the same resolve_import() machinery that static imports use, so a bare package name is never resolved to its compiled-module namespace.

This is the single most common modern dynamic-import pattern: packages that load ESM-only dependencies (chalk v5, node-fetch v3, nanoid, execa, ora) or lazily load optional/peer dependencies (DB drivers pg / mysql2 / better-sqlite3, Pino transports, mailer providers), almost always with a literal specifier inside a conditional or try/catch.

Current behavior

  • crates/perry/src/commands/compile/collect_modules.rs:465-548 — when const-folding resolves a dynamic-import path string, it classifies it (is_native_module) and pushes an import edge, but with resolved_path: None. It treats the bare string as-is and never runs the file/package resolution that static imports undergo.
  • crates/perry-codegen/src/expr/dyn_extern_i18n.rs:85 — the dispatch unconditionally looks up ctx.dynamic_import_path_to_prefix[path] expecting a compiled-module prefix with a @__perry_ns_<prefix> global. A bare specifier isn't in that map → fall through to js_promise_rejected(undefined).

Proposed fix

Route the resolved dynamic-import path string through the same resolver static imports use (crates/perry/src/commands/compile/resolve.rs:1344, resolve_import()), so 'chalk' finds its compilePackages copy and gets registered as a real compiled module. Compiled packages already materialize @__perry_ns_<prefix> in their __init, so once the bare name maps to the compiled module prefix in dynamic_import_path_to_prefix, the existing dispatch loads the namespace correctly.

(node: builtins are split out into the sibling issue, since they need native-namespace materialization rather than file resolution.)

Acceptance criteria

  • const { default: chalk } = await import('chalk') resolves to the package namespace (package listed in perry.compilePackages).
  • The optional-dependency idiom behaves per Node: try { await import('pg') } catch {} resolves when the package is compiled-in, rejects (→ catch) when it is not. The reject-on-miss fallthrough already gives the correct failure semantics; this issue is about fixing the success path.
  • Deferred-init semantics preserved: a package reached only via dynamic import still inits lazily on first dispatch.
  • New parity/test-files coverage for a literal bare-specifier dynamic import.

Files

  • crates/perry/src/commands/compile/collect_modules.rs:465-548
  • crates/perry/src/commands/compile/resolve.rs:1344 (resolve_import)
  • crates/perry-codegen/src/expr/dyn_extern_i18n.rs

Part of the dynamic-import Node.js-compatibility series (literal-resolution tier).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions