Skip to content

BridgeJS: support importing from external ECMAScript modules - #795

Draft
kateinoigakukun wants to merge 2 commits into
mainfrom
yt/bridge-js-bare-module-specifiers
Draft

BridgeJS: support importing from external ECMAScript modules#795
kateinoigakukun wants to merge 2 commits into
mainfrom
yt/bridge-js-bare-module-specifiers

Conversation

@kateinoigakukun

@kateinoigakukun kateinoigakukun commented Jul 30, 2026

Copy link
Copy Markdown
Member

from: .module(...) currently only reaches JavaScript files that live inside your own Swift target. This extends it to modules you did not write: Node builtins and installed npm packages.

@JSFunction(jsName: "basename", from: .module("node:path"))
func basename(_ path: String) throws(JSException) -> String

@JSClass(jsName: "File", from: .module("@bjorn3/browser_wasi_shim"))
struct WasiFile {
    @JSFunction init(_ data: JSObject) throws(JSException)
    @JSGetter var size: Int64
}

A leading / still means "a file rooted at this Swift target". Anything else goes to the JavaScript module resolver verbatim.

Two smaller additions ride along. jsName: .default names a module's default export, which is how many npm packages expose their main value. Reaching it used to require a local re-export file.

Generated glue also switches from a namespace object plus property lookup to named ECMAScript imports. A misspelled export name is now reported by the engine at module-link time, rather than surfacing as undefined is not a function at call time. Existing local-file users get that improvement too.

Design notes

No build-time resolution check. Resolution is host-defined. A bundler alias, an import map, or Deno will resolve specifiers that Node rejects, so any check we wrote would reject working setups. Validation is limited to rejecting the empty string and relative specifiers (use the /-prefixed form for your own files). URL and #imports specifiers pass through untouched.

Static imports, which is already the precedent here. Plugins/PackageToJS/Templates/platforms/node.js statically imports node:url, node:worker_threads, and an npm package today.

Skeleton format churn is minimal. "from" stays a plain "global" or a plain path string; the tagged object {"kind": "module", "specifier": "..."} appears only for a bare specifier. No existing checked-in BridgeJS.json or JSON snapshot changed shape. The tagged form is needed because global is a real npm package name and would otherwise collide with the sentinel.

Named imports fall back per origin. If any export name for one specifier is not a valid JavaScript identifier, that specifier reverts to import * as. This avoids depending on ES2022 string-literal import names, which matters because TS2Swift emits names like jsName: "property-with-dashes".

Extend `from: .module(...)` to accept bare specifiers like `node:path` or an npm package,
add `jsName: .default` for default exports, and emit named imports so a wrong export name
now fails at module-link time instead of at call time.
Do not require a module export for a wrapper-only `@JSClass`, since a named import is a
link-time requirement and nothing looks that name up; accept `jsName: nil` and the explicit
`.name(...)` spelling; and validate the tagged `from` form like the plain-string form.
@sliemeobn

Copy link
Copy Markdown
Contributor

I like the idea, but I am not sure I like "losing" the diagnostic guidance to use "/my-file.js" in there - now "anything goes" and there is no way to find out until things don't work at runtime.

how about we use separate cases? eg: from: .import("node:path") vs from: .module("/my-file.js")
or, since we have not tagged a version with .module we could still find another name for it maybe?

@kateinoigakukun

Copy link
Copy Markdown
Member Author

@sliemeobn Yeah, I agree that it makes more sense to have different from: specifiers for snippet and bare ES module import. Given that both use ESM import statement, from: .import("node:path") sounds a little bit weird to me.

How about from: .module("node:path") and from: .snippet("/my-file.js")?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants