Skip to content

feat(dynamic-import): resolve variable/computed specifiers via type-narrowing + directory glob #1674

Description

@proggeramlug

Summary

A dynamic import() whose specifier is a runtime variable or computed valueimport(path) where path is a function parameter or non-const local — is a hard compile error today. Because Perry is AOT and closed-world (the full module universe is known at collect_modules time), we can resolve a finite candidate set in many cases instead of erroring.

Real-world drivers are tooling-side: config-file loading (await import(resolvedConfigPath) — Vite, ESLint flat config, Jest, Drizzle Kit, tsx) and plugin/adapter-by-name from user config (await import(userConfig.adapter), import(`@scope/${name}`) — Vite/Rollup/PostCSS/unplugin). Lower priority than the literal bare-specifier / node:-builtin tier, but needed to run modern tooling natively.

Background

crates/perry-hir/src/dynamic_import.rs:560 (resolve_import_path_with_consts) currently resolves: string literals, ternaries, template literals over const interpolations, and module-level const locals. Anything else returns Resolution::Unresolved, which the driver turns into a compile error. The downstream multi-target dispatch (dyn_extern_i18n.rs) already consumes a Vec<String> and emits an N-way js_string_equals chain, so widening the resolver requires no new runtime or codegen for the type-narrowing path.

Proposed approach (two sub-parts, can ship independently)

A. Type/flow narrowing. Resolve a finite candidate set from static info already available:

  • String-literal union types: path: './a.ts' | './b.ts' → enumerate the union.
  • Finite reaching-definitions: a non-const local assigned only literals across the function → union those. (scan_mutations_expr, dynamic_import.rs:517, already tracks which locals are mutated; invert it to collect assigned values.)

B. Glob / context-module scoping (Vite import.meta.glob style). When a template literal has a non-const interpolation but fixed prefix/suffix, treat ${var} as a wildcard and glob <prefix>*<suffix> against the discovered module universe at collect_modules time. The existing DYNAMIC_IMPORT_PATH_CAP (64) bounds the candidate set.

Cross-cutting concerns

  • Path canonicalization. Once candidates come from globbing or runtime variables, the runtime arg string must match a compile-time key exactly. Apply one normalization function to both the keys (build time) and the runtime arg (dispatch): extension resolution (./foo./foo.ts), index resolution, ./ vs absolute. This is the most likely source of "compiled fine but silently rejects" bugs.
  • Miss semantics. No-match → rejected promise (current fallthrough), which matches Node. Optionally offer a compile-time exhaustiveness mode for provably-covered union-typed args that skips the reject branch.
  • Retention. Every candidate module must survive dead-stripping (cf. #[used] / auto-optimize interaction). The is_dynamic edge handles graph inclusion; verify the namespace global isn't stripped when its only reference is a runtime string compare.

Acceptance criteria

  • let p = cond ? './a.ts' : './b.ts'; await import(p) (non-const local) resolves both targets — type/flow narrowing.
  • await import(`./plugins/${name}.ts`) with non-const name resolves to all matching files under ./plugins/ — glob.
  • Unresolvable-and-unbounded specifiers still produce a clear compile error (no silent whole-program retention).
  • Canonicalization covered by tests for ./foo vs ./foo.ts vs absolute.

Files

  • crates/perry-hir/src/dynamic_import.rs:560 (resolver)
  • crates/perry/src/commands/compile/collect_modules.rs:465-548 (glob cross-ref against module universe; cap enforcement)

Part of the dynamic-import Node.js-compatibility series (variable/computed-specifier tier — lower priority than the literal 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