Skip to content

ReferenceError when resource has no functions #221

Description

@tomasol

Having an empty resource:

interface execution {

    resource join-set-id {
        // id: func() -> string;
    }

  new-join-set: func() -> join-set-id;
}

Calling new-join-set from a JavaScript guest fails with an error: ReferenceError: import_execution_1_0_0$JoinSetId is not defined.
This is because the generated initializer.jsdoes not contain class import_execution_1_0_0$JoinSetId {...}, but tries to use it in the generated new-join-set function: var rsc2 = new.target === import_execution_1_0_0$JoinSetId ? this : Object.create(import_execution_1_0_0$JoinSetId.prototype);

The workaround is to add a function, which triggers the class generation.
Tested with componentize-js version 0.18.2 .

Activity

  1. MendyBerger commented on Jun 12, 2025

    @MendyBerger

    Running into the same issue with wasi:graphics-context/graphics-context#abstract-buffer.

    It either gives a similar error to the one posted by the OP or sometimes even just hangs indefinitely without any errors.

    Adding a simple foo: func(); to the resource gets it to work correctly.

  2. ericgregory commented on Mar 19, 2026

    @ericgregory

    Confirming this bug in 0.19.4-rc.1 with a different WASI interface: wasi:sockets/network (wasi:sockets@0.2.3).

    wasi:sockets/network defines a single network resource with no methods. It is referenced by wasi:sockets/instance-network (instanceNetwork() returns a network) and by wasi:sockets/tcp (socket methods take borrow<network>). Neither of those interfaces defines the class — they just use it — so the class is never emitted and any guest code that calls instanceNetwork() or binds/connects a TCP socket throws at runtime:

    ReferenceError: import_network_0_2_3$Network is not defined
    

    Minimal reproducer

    WIT world:

    package example:sockets-demo@0.1.0;
    
    world sockets-demo {
      import wasi:sockets/instance-network@0.2.3;
      import wasi:sockets/tcp@0.2.3;
      import wasi:sockets/tcp-create-socket@0.2.3;
      import wasi:io/poll@0.2.0;
      export wasi:cli/run@0.2.0;
    }

    TypeScript guest:

    import { instanceNetwork } from 'wasi:sockets/instance-network@0.2.3';
    import { createTcpSocket } from 'wasi:sockets/tcp-create-socket@0.2.3';
    
    export const run = {
      run(): void {
        const network = instanceNetwork(); // throws: import_network_0_2_3$Network is not defined
        const socket = createTcpSocket('ipv4');
        // ...
      },
    };

    The generated bindings emit finalizationRegistry_import$network_0_2_3$network = new FinalizationRegistry(...) but never class import_network_0_2_3$Network { ... }. The class is only generated when the owning interface (wasi:sockets/network) has at least one callable function — since it has none, the class is skipped entirely.

    Root cause

    The issue is in spidermonkey-embedding-splicer. The class-generation code path is gated on the interface having callable functions; resource types in function-less interfaces fall through without a class definition. The class should be emitted for every imported resource, regardless of whether the owning interface has any callable functions.

    JS-side workaround patch

    While the right fix is in the splicer, here is a patch applied to componentize.js that works around the issue. It scans the generated bindings for finalization registries with no matching class and injects minimal class definitions after symbolDispose is declared:

    // After spliceBindings() — inject missing resource classes
    {
      const registryRe = /finalizationRegistry_import\$([^$]+)\$([^\s=]+) = new FinalizationRegistry/g;
      let m;
      const missingClasses = [];
      while ((m = registryRe.exec(jsBindings)) !== null) {
        const iface = m[1];
        const rsc = m[2];
        const className = `import_${iface}$${rsc.charAt(0).toUpperCase()}${rsc.slice(1)}`;
        if (!jsBindings.includes(`class ${className}`)) {
          missingClasses.push({ iface, rsc, className });
        }
      }
      if (missingClasses.length > 0) {
        const injected = missingClasses.map(({ iface, rsc, className }) =>
          `class ${className} {\n` +
          `  [symbolDispose]() {\n` +
          `    finalizationRegistry_import$${iface}$${rsc}.unregister(this);\n` +
          `    $resource_import$${iface}$drop$${rsc}(this[symbolRscHandle]);\n` +
          `    this[symbolRscHandle] = undefined;\n` +
          `  }\n` +
          `}\n`
        ).join('');
        jsBindings = jsBindings.replace(
          '\nconst symbolDispose = Symbol.dispose || Symbol.for(\'dispose\');',
          '\nconst symbolDispose = Symbol.dispose || Symbol.for(\'dispose\');\n' + injected,
        );
      }
    }

    This was developed while building a TypeScript wasmCloud service-and-component template that uses wasi:sockets for intra-workload TCP communication: wasmCloud/typescript#583

  3. carsonfarmer commented on Sep 26, 2026

    @carsonfarmer

    I reproduced this on 0.23.0 and 4d812f5. #357 fixes the missing class generation and adds a regression for returning, borrowing and disposing of a resource with no methods.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions