Repository navigation
imports: import * as Ns from "./mod.js" produces non-object namespace; class members unreachable #574
Description
Repro
lib.ts:
export class A {}
export class B {}
export const NUM = 42;main.ts:
import * as Lib from "./lib.js";
console.log("typeof Lib:", typeof Lib);
console.log("typeof Lib.A:", typeof Lib.A);
console.log("typeof Lib.NUM:", typeof Lib.NUM);
console.log("Lib.NUM:", Lib.NUM);bun main.ts:
typeof Lib: object
typeof Lib.A: function
typeof Lib.NUM: number
Lib.NUM: 42
perry main.ts:
typeof Lib: boolean <- should be "object"
typeof Lib.A: undefined <- class is unreachable through namespace
typeof Lib.NUM: number <- primitive const works
Lib.NUM: 42 <- value reads correctly
So primitive consts via namespace import partially work (the actual value is correct, even though typeof Lib is "boolean"), but class exports are completely unreachable — Lib.A returns undefined.
Why this matters
Discovered while compiling @bradenmacdonald/s3-lite-client via #551. The package's mod.ts ends with:
export * as S3Errors from "./errors.js";Which the consumer uses as:
import { S3Errors } from "@bradenmacdonald/s3-lite-client";
catch (err) {
if (err instanceof S3Errors.ServerError && err.statusCode === 404) ...
}Every instanceof S3Errors.ServerError check fails because S3Errors.ServerError is undefined. Every error-bucket-namespace pattern silently breaks.
This pattern is widespread — import * as fs from "node:fs" (Node-style; Perry handles node: differently but the idiom carries over), import * as Sentry from "@sentry/node", every package that exposes a "namespace of related things" instead of flat exports.
Three observations
-
typeof Libis"boolean"— meaning the namespace itself is being represented as a NaN-boxed bool tag. Likely the import is initializingLibto TAG_TRUE or TAG_FALSE instead of building a namespace object. -
Primitive const access works despite the broken
typeof. Suggests there's a fallback path that resolvesLib.NUMdirectly to the underlying export's storage location — bypassing the (broken) namespace object lookup. -
Class access doesn't go through that same fallback. The
Lib.Aproperty read returns undefined. This may be related to how class-refs are NaN-boxed via INT32_TAG (per recent v0.5.622 wiring) — the namespace fallback path doesn't handle the INT32-tagged form.
Suggested implementation
Search where import * as Ns from "..." is lowered (likely crates/perry-hir/src/lower.rs or compile/resolve.rs) and verify it creates an actual namespace object whose own-properties enumerate every named export of the source module. The current behavior implies the lowering elides the namespace object entirely and special-cases primitive value resolution at usage sites — which is why classes leak.
Acceptance
The repro at the top prints object / function / number / 42 matching bun. Plus regressions:
Lib.B instanceof Lib.Aworks for class hierarchiesLib.helperFn()calls work for function exportsfor (const k in Lib)enumerates exported namesObject.keys(Lib)returns the exportsimport { S3Errors } from "..."whereS3Errorscame from a re-export * aschain
Refs #551
Activity
proggeramlug commentedon May 8, 2026
Fixed in v0.5.664 (320a162).
Two surgical fixes in
crates/perry-codegen/src/expr.rs:Namespace-member PropertyGet for class exports — when the property name is in
class_ids, emit the INT32-tagged class-id NaN-box (mirrorsExpr::ClassRef) instead of the function-getter call which silently returned undefined for class exports.Expr::TypeOfshort-circuits four shapes thatjs_value_typeofwould misclassify:"object"(was"boolean"via TAG_TRUE fallback)"function"(was"number"via INT32 path)class_ids→"function""function"Bonus:
b instanceof Lib.Anow works —Expr::InstanceOfstrips theNs.prefix whenNsis a namespace import and re-looks up the class id by bare name.Repro from the issue (
typeof Lib: object/typeof Lib.A: function/typeof Lib.NUM: number/Lib.NUM: 42) matches bun byte-for-byte.Lib.B instanceof Lib.Aworks for hierarchies.Lib.helperFn()calls work.new Lib.A()constructs correctly.Note:
for (const k in Lib)andObject.keys(Lib)are still NOT enumerable — namespace imports don't materialize a real object with own properties. Tracked separately if needed.fix(hir+codegen): closes #572 #573 #574 — class destructure params + …