Skip to content

Class declarations in .js files inside compilePackages drop entirely from HIR #485

Closed

Description

@proggeramlug

Background

While shaking out #421 (compile hono end-to-end), we hit a blocker: `new Hono()` constructs without crashing as of v0.5.585 (post #482 + #484 fixes), but the resulting instance has only `router` as an own property. All of HonoBase's arrow-class-field methods (`request`, `fetch`, `get`, `post`, `put`, `delete`, `options`, `patch`, `all`, `on`, `use`, `notFound`) are missing → `app.fetch is not a function`.

Repro

```bash
mkdir test && cd test
echo '{ "type": "module", "perry": { "compilePackages": ["hono"] } }' > package.json
bun add hono
cat > app.ts <<EOJ
import { Hono } from 'hono';
const app = new Hono();
console.log('keys:', Object.keys(app).join(',')); // 'router' only
console.log('typeof fetch:', typeof app.fetch); // undefined
console.log('typeof request:', typeof app.request); // undefined
EOJ
perry app.ts -o app && ./app
```

Why

Hono's bundle has `class Hono extends HonoBase`. `HonoBase` lives in another file in node_modules/hono/. Its class fields are arrow initializers:

```js
var HonoBase = class {
router;
request = (input, ...) => { /* ... / };
fetch = (request, ...) => { /
... */ };
// ... etc
};
```

Perry's `apply_field_initializers_recursive` (perry-codegen/src/lower_call.rs) walks the class chain and applies parent's field initializers in the subclass's constructor. But for imported classes, the stub created in `imported_class_stubs` (perry-codegen/src/codegen.rs:407+) carries `field_names` without `init` expressions — so when Hono's constructor walks the chain into HonoBase (the stub), HonoBase.fields all have `init: None` and nothing gets applied to `this`.

Same-file inheritance works (verified locally: `class Base { request = (p) => ... } class Sub extends Base {}` produces a Sub instance with a working `.request`).

Likely fix

Imported class metadata (the `ImportedClass` struct in `perry-codegen/src/codegen.rs`) needs to carry field initializers, or perry-codegen needs to look up the parent class via the cross-module import resolver and walk the parent's HIR. Today `ImportedClass` only has `field_names: Vec` and `field_types: Vec` — no init expressions. Adding a third parallel `field_inits: Vec<Option>` field is the mechanical move; serializing the init expressions across module boundaries needs a stable encoding (HIR Expr is not stable; either snapshot the Function pointer for arrow inits or emit a synthetic per-(class, field) constructor-fragment helper).

Why it matters

Blocks #421 (hono compile). Probably also blocks #420 (drizzle) and any other npm package that uses class inheritance with arrow class fields across module boundaries.

Activity

  1. proggeramlug commented on May 6, 2026

    @proggeramlug
    ContributorAuthor

    Update after deeper investigation:

    The bug is broader than I initially described. Class declarations in .js files inside compilePackages packages aren't being lowered into the HIR's classes list at all. Inheritance is incidental — even the simplest case fails:

    // node_modules/handcl/index.js
    class Foo {
      constructor() { this.name = "Foo"; }
      add(a, b) { return a + b; }
    }
    export { Foo };
    // app.ts
    import { Foo } from 'handcl';
    const f = new Foo();
    console.log('typeof f.add:', typeof f.add);   // undefined
    console.log('result:', f.add(2, 3));            // [object Object]
    console.log('name:', f.name);                   // undefined

    Same exact code as a .ts file (index.ts) works perfectly: function, 5, Foo. The HIR --print-hir confirms:

    • handcl/index.ts → has the class in HIR's classes list (HIR --print-hir doesn't show it but codegen finds it).
    • handcl/index.js → Imports: 0, Exports: 1, Functions: 0, Init statements: 0 — class is DROPPED.

    Same shape, different extension. perry-parser uses Syntax::Typescript regardless of extension, so SWC parses both correctly. The drop happens in HIR lowering or somewhere between parser and codegen.

    Likely root cause is in how perry distinguishes JS from TS during lowering — possibly the cjs_wrap path (used for CommonJS) is hitting a heuristic and re-shaping the file's AST in a way that strips top-level class declarations, OR the is_in_compile_package + is_js_file resolution path skips class lowering. Needs perry-parser/perry-hir investigation.

    The hono failure mode is one consequence: HonoBase is in a .js bundle, so its class declaration gets dropped, so all its arrow class fields and methods are missing. Same applies to anything else in compilePackages whose files are .js.

    This bug is likely the load-bearing blocker for most compilePackages use cases — anything published as bundled JS (which is the npm norm) hits it. Renaming the issue to reflect the broader scope is appropriate.

  2. changed the title [-]Hono compile blocker: class-field arrow methods on inherited classes lost across compilePackages[/-] [+]Class declarations in .js files inside compilePackages drop entirely from HIR[/+] on May 6, 2026
  3. proggeramlug commented on May 6, 2026

    @proggeramlug
    ContributorAuthor

    v0.5.586 actually delivers the fix. The fix landed three coordinated changes: 1) Expr::SuperCall falls through to imported_class_ctors when parent is imported (so super() actually invokes parent ctor body); 2) exported_classes indexes export { Local as Exported } renames; 3) packed-keys / total_field_count walks include parent stubs across module boundaries, so subclass instances reserve enough inline slots for parent + child fields. End-to-end: a 4-file repro (handcl/base.js + handcl/index.js subclass + main.ts new Sub()) now correctly observes all parent fields + arrow methods. With hono itself, new Hono() instances now expose get/post/fetch/use/on/routes/_basePath correctly. Parity: 204 pass / 8 known fails — no regressions. Routes registration (the next blocker for hono — app.get('/', cb) still keeps app.routes.length at 0) tracked separately.

  4. added a commit that references this issue on May 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions