Struggling to understand how to compile the JavaScript runtime for Swift WebAssembly

a couple months ago, i taught myself how to build the JavaScript runtime for Swift WebAssembly by studying the source code of the carton tool. this was not easy, but i at least understood what a “resource” in that context was and what strings needed to be substituted into those resources to make it load the compiled WebAssembly correctly.

it seems in recent weeks, the JavaScript runtime has undergone major changes, and the maintainer of the repo has written their own TypeScript build system in Swift which i am really struggling to understand the workings of, as it’s much more complex than what was living in the carton repo previously.

i don’t want to use a SwiftPM Package Plugin to build my application, i just want to learn how to compile these TypeScript files into a minified, standalone JavaScript file, with a magic string that i can substitute with the relative location of the wasm file. is there a guide for how to do this?

(the only existing guide i could find is using the Package Plugin, and i don’t want to be using SwiftPM to compile TypeScript.)

Thanks for the feedback :slight_smile:

I’m curious about the motivation behind not wanting to use the plugin. The new plugin was designed to address several long-standing issues we had with carton, such as long build times of the tool itself and the fact that the output wasn’t very friendly to JS ecosystem bundlers like Vite.

To help with that, I’ve also added a section to the documentation explaining how to build your application with Vite.

That said, if you still prefer to handle the build manually, the files in Templates are processed by a simple preprocessor of fewer than 500 lines of Swift code. You can refer to this as a guide for how the substitution logic works.

We need this kind of preprocessing to emit the optimal code not just for basic WASI targets, but also for threads-enabled targets and Embedded Swift.

1 Like

in general, i find SwiftPM plugins less productive to use as installable developer tools because the lifetimes of the actual compiled binaries are tied to the state of the scratch build directory, which means every time you clear .build, you force the plugins to go back through Dependency Resolution (which is slow, and might additionally involve cloning a git repo depending on SwiftPM cache settings), as well as compiling the plugin itself from source.

another issue with Plugins as Tools is that you are forced to match the version of the Swift Toolchain used to invoke the plugin (what ought to just be a command line tool) with the version of the Swift Toolchain used to build the project. so if you are using or testing a nightly Swift Toolchain with your project, you also have to remember to use the path to that same toolchain to invoke the plugin, or it will invalidate your incremental build and rebuild the plugin from scratch. it’s way easier to think about Swift Toolchain + SDK version when building WebAssembly, and then only think about JavaScript related things when building JavaScript, as a separate step.

but more specifically to WebAssembly, i couldn’t figure out how to get the plugin to minify or consolidate the runtime JavaScript files, or get it to work without the --use-cdn option. the only way i could get it to work was under the default release configuration, which performs five HTTP requests for unminified JavaScript files, including one request to an external domain (jsdelivr.net).

it sounds like Vite might help with the last issue, but i wasn’t aware of that article as it was not present in the documentation for the most recent release version of JavaScriptKit.

1 Like

This is actually good and bad because we need to keep the tool and SwiftPM package versions in sync. Also, the new JavaScriptKit's package plugin is written with an awareness of its build time. If you give it a try, you will see significant build overhead reduction compared to carton.

With SwiftPM package plugin, they will never be different toolchains. I guess you are mixing up carton's problems.

1 Like

yes, i did notice it’s a speedy build :)

no, the problem is a bit more literal than that.

ideally, a tool at this layer should be able to do several tasks individually. such as:

  • Build the WebAssembly
  • Build the TypeScript
  • Browse the Help

right now, i don’t think js can Build the TypeScript only, but it still stands that you should be able to fuzz the tool without passing parameters for WebAssembly compilation, including Browse the Help.

this means that there is a noticeable difference between running

/swift/swift-DEVELOPMENT-SNAPSHOT-2025-03-25-a-ubuntu24.04/usr/bin/swift package js --help

and

swift package js --help

if you mindlessly type in the latter, it will invalidate what was previously in .build, which really shouldn’t be coupled to just launching the tool without actually compiling any WebAssembly.

Ok, I understand your concern. I don’t have a good idea to mitigate the situation, but I’m open to any concrete suggestions you might have.

2 Likes

I don't understand the part where you want to use SPM for TS/JS, just use the normal build tools for TS/JS projects.

Some examples I have:
Vendoring css/js files into a docker image: index.htmx/Dockerfile at master · Cyberbeni/index.htmx · GitHub
Building a minified js file from multiple ts sources with the @vercel/ncc npm package: install-swift-tool/package.json at 7c9856955bf5135f4d953c141fd3fb7573c04396 · Cyberbeni/install-swift-tool · GitHub

I don't know what's the best way to have a a magic string that i can substitute with the relative location of the wasm file, having a gitignored .ts file containing this constant that you can switch to the appropriate version before build should be a decent solution for this. (You would probably have debug/release/etc. templates in a folder and your npm scripts would start with copying over the appropriate one to the src folder)

Ah, seems like you want to create a .js wrapper and a .d.ts type definition file from your Swift code or wasm binary.

You could try using a prebuilt version of the bridge-js cli, the spm plugin seems to be a wrapper around that: JavaScriptKit/Plugins/BridgeJS at main · swiftwasm/JavaScriptKit · GitHub

If you know what you are doing you could also just use Sourcery and write your own templates.

You can specify --cache-path and --build-path to SPM commands, so you can use multiple versions of Swift in parallel.