{"id":41644834,"url":"https://github.com/skoppe/spasm","last_synced_at":"2026-01-24T15:17:25.437Z","repository":{"id":89718599,"uuid":"152768534","full_name":"skoppe/spasm","owner":"skoppe","description":"Write single page applications in D that compile to webassembly","archived":false,"fork":false,"pushed_at":"2020-09-12T08:27:24.000Z","size":1492,"stargazers_count":217,"open_issues_count":20,"forks_count":18,"subscribers_count":14,"default_branch":"master","last_synced_at":"2023-10-20T23:09:25.801Z","etag":null,"topics":["dlang","framework","spa","wasm"],"latest_commit_sha":null,"homepage":"","language":"D","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/skoppe.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2018-10-12T15:08:02.000Z","updated_at":"2023-10-20T23:09:27.082Z","dependencies_parsed_at":"2023-10-22T09:45:35.118Z","dependency_job_id":null,"html_url":"https://github.com/skoppe/spasm","commit_stats":null,"previous_names":[],"tags_count":32,"template":null,"template_full_name":null,"purl":"pkg:github/skoppe/spasm","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skoppe%2Fspasm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skoppe%2Fspasm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skoppe%2Fspasm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skoppe%2Fspasm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/skoppe","download_url":"https://codeload.github.com/skoppe/spasm/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skoppe%2Fspasm/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28730317,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-24T10:24:43.181Z","status":"ssl_error","status_checked_at":"2026-01-24T10:24:36.112Z","response_time":89,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dlang","framework","spa","wasm"],"created_at":"2026-01-24T15:17:24.824Z","updated_at":"2026-01-24T15:17:25.418Z","avatar_url":"https://github.com/skoppe.png","language":"D","readme":"# Spasm\n\n\u003cimg src=\"https://github.com/skoppe/spasm/workflows/build/badge.svg\"/\u003e\u0026nbsp;\u003cimg src=\"https://img.shields.io/badge/ldc%201.17%20--%201.20%20-supported-brightgreen\"/\u003e\n\nSpasm is a library to develop single page applications in D that compile to webassembly.\n\nIt contains bindings to the most commonly used web apis, including the dom, fetch, audio, and webgl.\n\nAs well as a small but powerful SPA framework, which includes CSS. Yes. CSS-in-wasm.\n\n\u003cdetails\u003e\u003csummary\u003eTable Of Contents\u003c/summary\u003e\n\n- [Spasm](#Spasm)\n  - [Web Bindings](#Web-Bindings)\n  - [SPA framework](#SPA-framework)\n  - [Examples](#Examples)\n  - [How to start](#How-to-start)\n  - [Using the web bindings](#Using-the-web-bindings)\n  - [How to compile your application](#How-to-compile-your-application)\n  - [Migrating from an older version](#migrating-from-an-older-version)\n  - [Writing your own js bindings](#Writing-your-own-js-bindings)\n  - [Optimizing for size](#Optimizing-for-size)\n  - [Limitations](#Limitations)\n  - [Hot module reloading](#Hot-module-reloading)\n    - [Enabling hmr for new projects](#Enabling-hmr-for-new-projects)\n    - [Enabling hmr for existing projects](#Enabling-hmr-for-existing-projects)\n    - [How it works](#How-it-works)\n  - [How the SPA framework works](#How-the-SPA-framework-works)\n\n\u003c/details\u003e\n\n## Web Bindings\n\nD bindings are generated from webidl files. The bindings try to mimick as much as possible the javascript api's you are already familiar with.\n\nUntil Webassembly gets host bindings it is still necessary to generate JS glue code. A small bindgen utility is included to generate exactly the glue code you need.\n\n## SPA framework\n\nIt uses D's compile time feature to generate optimized rendering code specific for your application.\n\nNot only are your applications fast, they are also small. The [todo-mvc example](https://skoppe.github.io/spasm/examples/todo-mvc/) project is only 5797 (wasm) + 2199 (html+js) bytes when gzipped.\n\n## Examples\n\n- [fetch](https://github.com/skoppe/spasm/tree/master/examples/fetch). Shows how to use the fetch api and access returned json. [Demo](https://skoppe.github.io/spasm/examples/fetch/index.html).\n- [dom](https://github.com/skoppe/spasm/tree/master/examples/dom). Shows how to manipulate the DOM. [Demo](https://skoppe.github.io/spasm/examples/dom/index.html).\n- [canvas](https://github.com/skoppe/spasm/tree/master/examples/canvas). Shows how to draw text on the Canvas. [Demo](https://skoppe.github.io/spasm/examples/canvas/index.html).\n- todo-mvc. Uses the SPA framework to implement the famous [todo mvc application](http://todomvc.com).\n- underun. A D port of a js13k competition game written by Dominic Szablewski. You can play the D version [here](https://skoppe.github.io/spasm/examples/underrun/).\n\n## How to start\n\nMake sure to have at least ldc 1.17.0 installed.\n\n- run `dub init \u003cmy-project\u003e spasm`, this will create a folder named `\u003cmy-project\u003e` with a dub file and the latest spasm added as dependency\n- add `dflags \"-betterC\"` to your dub.sdl or add `\"dflags\": [\"-betterC\"]` to your dub.json\n- run `dub upgrade \u0026\u0026 dub run spasm:bootstrap-webpack` to generate the webpack/dev-server boilerplate\n- start writing!\n\nYou can add any extra css/js you'll need to the `index.template.html`, or you can use any of the myriad features of webpack to include what you need.\n\n## Using the web bindings\n\nThe `spasm.bindings` module defines most web apis. You probably need to import `spasm.dom` and `spasm.types` too as well.\n\nMake sure to run `dub run spasm:webidl -- --bindgen` after compiling to ensure all required js glue code is generated.\n\n## How to compile your application\n\nMake sure to have at least ldc 1.17.0 installed. Also, make sure that `ldc2 --version` returns the `wasm32` among its target types. If not, you may need to install ldc from official sources or run one in docker (e.g. `dlang2/ldc-ubuntu:1.20.0`).\n\nRun `dub build --compiler=ldc2 --build=release` to compile your application, then run `npx webpack` to generate the `index.html`.\n\nYou can also `npm run start` to start a webpack development server that serves your application on localhost:3000.\n\n- Note: I could not get it to build on my aged mac (el capitan). Instead I use the `dlang2/ldc-ubuntu:1.20.0` docker image to run ldc.\n\n- Note: if you have some issues please read the [BUILDING.md](BUILDING.md) file before opening an issue.\n\n## Migrating from an older version\n\nThis project is still in it's beta phase (0.x.x):\n\n- any minor upgrade WILL contain breaking changes. There will be at least one beta release (0.x.0-beta.1)\n- any patch upgrade SHOULD NOT contain breaking changes.\n\nPlease read the [CHANGELOG.md](CHANGELOG.md) for breaking changes, as well as [BUILDING.md](BUILDING.md) for supported compilers and open issues.\n\n## Writing your own js bindings\n\nIn case you want to write a custom js function, the first step is writing a function definition in D.\n\n```d\nextern(C) export int myFunc(uint index);\n```\n\nAfter that you write a spasm module in javascript. Simply put a file in the `./spasm/modules/` folder and export a jsExports object.\n\n```js\nexport let jsExports = {\n  myFunc: index =\u003e {\n    return 42;\n  }\n};\n```\n\nManually put the file in the `./spasm/modules/index.js` or just run `dub run spasm:webidl -- --bindgen` to automatically include it.\n\nThe `./spasm/entry.js` and the `./spasm/modules/spasm.js` file will combine all exports and use them during the WebAssembly initialization.\n\nWorking with strings (arrays) and aggregates requires a bit more work. You can study the generated `bindings.js` file in the examples to see how it works.\n\n## Optimizing for size\n\nSince ldc 1.13.0 there is the `-fvisibility=hidden` flag that hides all functions that aren't explicitly prefixed with the `export` keyword. This flag reduces binary size considerably and has reduced the need for manual stripping almost completely.\n\nBy default symbol names aren't stripped, which means the full mangled name is in the binary, this is convenient for debugging but adds to the binary's size. Add `-strip-all` to the lflags in your `dub.(sdl|json)` to strip all internal function names.\n\nFor yet unknown reasons a pointer to each struct's init section gets exported as a global. These globals are completely unused and add some additional bloat. The binaryen project has several tools to (dis)assemble a wasm to text representation and back, which allows manual removing of those exported symbols. (note: this section needs an update, as this no longer applies)\n\nAlso, llvm doesn't skip consecutive zeros in the data segment. Running wasm-opt (from binaryen project) removes them and reduces code size further.\n\nUsing the [Binaryen](https://github.com/WebAssembly/binaryen) toolkit we can optimize even further than LLVM's WebAssembly backend does.\n\n```bash\n# Optimize for size.\nwasm-opt -Os -o main-optimized.wasm main.wasm\n# Optimize aggressively for size.\nwasm-opt -Oz -o main-optimized.wasm main.wasm\n# Optimize for speed.\nwasm-opt -O -o main-optimized.wasm main.wasm\n# Optimize aggressively for speed.\nwasm-opt -O3 -o main-optimized.wasm main.wasm\n```\n\n## Limitations\n\nThis project uses betterC, which means there is no D runtime. This also means that most phobos functions don't work, as well any D features that rely on the D runtime. If you get any weird errors, this is probably the reason why.\n\n## Hot module reloading\n\nThe spa framework in Spasm has basic support for hot module reloading. Style changes are reloaded correctly as well as basic attributes (`@prop`, `@attr`, `@visible`, etc.) Anything more complex (like lists/arrays) will just revert to their init state.\n\n### Enabling hmr for new projects\n\nMake sure you use spasm `v0.2.0-beta.6` and add the following to your `dub.sdl`:\n\n```\nconfiguration \"hmr\" {\n  targetType \"executable\"\n  versions \"hmr\"\n  lflags \"--export=dumpApp\" \"--export=loadApp\"\n}\n```\n\nOr to your `dub.json`:\n\n```\n\"configurations\": [{\n  \"name\": \"hmr\",\n  \"targetType\": \"executable\",\n  \"versions\": [\"hmr\"]\n}]\n```\n\nAnd compile with `dub build --build=release --config=hmr`\n\n### Enabling hmr for existing projects\n\nUpdate to `\u003e=0.2.x` and add the same configuration mentioned above but also rerun `dub run spasm:bootstrap-webpack` in your projects root folder. This will update your dev-server.js and your spa.js and spasm.js modules.\n\n### How it works\n\nThe server running with `npm run start` starts up a websocket on port 3001 and notifies connected clients whenever the webassembly binary changes.\n\nThe js glue code connects to the websocket (dev-only) and does the following for each notification:\n\n- calls `dumpApp` which will serializes the aggregate in the `mixin Spa!(App, Theme)` to string\n- removes all created dom elements and styles\n- reload the wasm binary (which renders a fresh application)\n- calls `loadApp` which will deserializes the string and triggers dom updates\n\n## How the SPA framework works\n\nEach html element is mapped to a D struct. Each attribute, property, eventlistener and any children nodes are (annotated) members of that struct.\n\nHere is an example of rendering a div node.\n\n```d\nstruct App {\n  mixin Node!\"div\";\n}\nmixin Spa!App;\n```\n\nThe mixin ensures the app is rendered and integrates with the js runtime code.\n\nThe following example shows how to set properties on the rendered node.\n\n```d\nstruct App {\n  mixin Node!\"div\";\n  @prop innerText = \"Hello World!\";\n}\nmixin Spa!App;\n```\n\nProperties can also be a result of a function.\n\n```d\nstruct App {\n  mixin Node!\"div\";\n  @prop string innerText() {\n    return \"Hello World!\";\n  };\n}\nmixin Spa!App;\n```\n\nHere we add a button child component.\n\n```d\nstruct Button {\n  mixin Node!\"button\";\n  @prop innerText = \"Click me!\";\n}\nstruct App {\n  mixin Node!\"div\";\n  @child Button button;\n}\nmixin Spa!App;\n```\n\nNow we add a event listener to the button.\n\n```d\nstruct Button {\n  mixin Node!\"button\";\n  mixin Slot!\"click\";\n  @prop innerText = \"Click me!\";\n  @callback void onClick(MouseEvent event) {\n    this.emit(click);\n  }\n}\nstruct App {\n  mixin Node!\"div\";\n  @child Button button;\n}\nmixin Spa!App;\n```\n\nThe `onClick` function is called whenever an onclick event is generated on the dom node.\n\nIn order to propagate events between structs - often you have a parent component that has logic - a `Slot!click` is mixed into the struct. The separation between the slot and the callback function is on purpose. It provides isolation from dom events and it simplifies event listeners on arrays (doesn't require keying).\n\nHere we connect the slot from the App.\n\n```d\nstruct Button {\n  mixin Node!\"button\";\n  mixin Slot!\"click\";\n  @prop innerText = \"Click me!\";\n  @callback void onClick(MouseEvent event) {\n    this.emit(click);\n  }\n}\nstruct App {\n  mixin Node!\"div\";\n  @child Button button;\n  @connect!\"button.click\" void click() {\n  }\n}\nmixin Spa!App;\n```\n\nThe `@connect` annotation ensures the `click` function is called whenever there is an `this.emit(click)` call in Button.\n\nIn the next example we show how to propagate properties from one component down into another.\n\n```d\nstruct Button {\n  mixin Node!\"button\";\n  mixin Slot!\"click\"\n  @prop string* innerText;\n  @callback void onClick(MouseEvent event) {\n    this.emit(click);\n  }\n}\nstruct App {\n  mixin Node!\"div\";\n  @child Button button;\n  string innerText = \"Click Me!\";\n  @connect!\"button.click\" void click() {\n    this.update.innerText = \"Clicked!\";\n  }\n}\nmixin Spa!App;\n```\n\nThe result is when the button is clicked the text is changed into \"Clicked!\".\n\nWe have inserted a `string innerText` field into App, and made the one in Button a pointer. When a struct is rendered for the first time, spasm will assign any pointers to the equivalent member of their parent. This approach is chosen due to its low performance impact (just a extra pointer to store) and simplicity (no need to pass prop structs between components).\n\nThe second piece is the `update` template function, this function uses static introspection to determine exactly what to update. This is almost always inlined in the resulting wasm code. Here we deviate the most from traditional virtual-dom approaches. Instead of completely rendering the App component and diffing the result, the `update` template function knows exactly what to update.\n\nHere we show how lists are implemented.\n\n```d\nstruct Item {\n  mixin Node!\"li\";\n  @prop string innerText;\n}\nstruct Button {\n  mixin Node!\"button\";\n  mixin Slot!\"click\";\n  @prop string innerText = \"Add\";\n  @callback void onClick(MouseEvent event) {\n    this.emit(click);\n  }\n}\nstruct App {\n  mixin Node!\"div\";\n  @child Button button;\n  @child UnorderedList!Item items;\n  @connect!\"button.click\" void click() {\n    Item* item = allocator.make!Item;\n    item.innerText = \"Item\";\n    items.put(item);\n  }\n}\nmixin Spa!App;\n```\n\nWe added an `UnorderedList!Item` child. This is a standard component and renders an `\u003cul\u003e` node with children.\n\nHere we show how to do event listeners on arrays.\n\n```d\nstruct Item {\n  mixin Node!\"li\";\n  mixin Slot!\"click\";\n  @prop string innerText;\n  @callback void onClick(MouseEvent event) {\n    this.emit(click);\n  }\n}\nstruct Button {\n  mixin Node!\"button\";\n  mixin Slot!\"click\";\n  @prop string innerText = \"Add\";\n  @callback void onClick(MouseEvent event) {\n    this.emit(click);\n  }\n}\nstruct App {\n  mixin Node!\"div\";\n  @child Button button;\n  @child UnorderedList!Item list;\n  @connect!\"button.click\" void click() {\n    Item* item = allocator.make!Item;\n    item.innerText = \"Item\";\n    list.put(item);\n  }\n  @connect!(\"list.items\",\"click\") void itemClick(size_t idx) {\n  }\n}\nmixin Spa!App;\n```\n\nIn the `@connect` annotation we split the part to the underlying DynamicArray in `UnorderedList` and the path to the slot from the Item component. Plus there is an extra argument signifying the index of the item in the array.\n\nThis is works with a simple pointer range search in the array. It introduces no memory overhead or keying.\n\nIn this example we show how we can use standard range algorithms to transform arrays.\n\n```d\nstruct Item {\n  mixin Node!\"li\";\n  mixin Slot!\"click\";\n  @prop string innerText;\n  @style!\"active\" bool active = false;\n  @callback void onClick(MouseEvent event) {\n    this.emit(click);\n  }\n  void toggle() {\n    this.update.active = !active;\n  }\n}\nstruct Button {\n  mixin Node!\"button\";\n  mixin Slot!\"click\";\n  @prop string innerText = \"Add\";\n  @callback void onClick(MouseEvent event) {\n    this.emit(click);\n  }\n}\nstruct App {\n  mixin Node!\"div\";\n  @child Button addButton;\n  @child Button toggleButton = {innerText: \"Only Active\"};\n  @child UnorderedList!Item list;\n  bool onlyActive;\n  DynamicArray!(Item*) items;\n  @connect!\"toggleButton.click\" void toggleClick() {\n    this.update.onlyActive = !onlyActive;\n  }\n  @connect!\"addButton.click\" void addClick() {\n    Item* item = allocator.make!Item;\n    item.innerText = \"Item\";\n    items.put(item);\n    this.update!(items);\n  }\n  @connect!(\"list.items\",\"click\") void itemClick(size_t idx) {\n    list.items[idx].toggle();\n    this.update!(items);\n  }\n  auto transform(ref DynamicArray!(Item*) items, bool onlyActive) {\n    import std.algorithm : filter;\n    items[].filter!(i=\u003e(i.active || !onlyActive)).update(list);\n  }\n}\nmixin Spa!App;\n```\n\nBefore showing the standard range usage we had to make some adjustments and additions to the example.\n\nIn the Item Component we added an `active` bool, and we annotated this with `@style!\"active\"`. Whenever active is true the active style is added, and vice versa. We added a `toggle` function that toggles the `active` bool.\n\nWe reused the Button component in the App for a Toggle, using D's struct initializer to overwrite the innerText property.\n\nWe added the `onlyActive` bool and this is updated by clicking on the toggleButton.\n\nWe also added an `DynamicArray!(Item*) items` field. This will contain our complete list and the UnorderedList's appender will only contain the items we want.\n\nThe `itemClick` function is updated to call the items toggle function and updates the items.\n\nNow we can discuss the `transform` function. This function does the filtering of Item's based on the value of `onlyActive` compared to the Item's `active` bool.\n\nAnytime there is a call to the templated `update` function (e.g. in `toggleClick` and in `addClick`), besides updating what is necessary it will also call any member function which has a parameter which correspronds with the value that is being updated.\n\nSince the transform function has the `items` and `onlyActive` as parameters, the update function will call it whenever `items` or `onlyActive` is changed.\n\nIn the `transform` function we have our normal D range programming with an `update(list)` at the end. This will make sure our `UnorderedList!Item` field will get the items from the range. Essentially the `UnorderedList!Item` acts as an Sink or OutputRange where each element of the InputRange will be placed into, it also does any necessary diffing with the dom.\n\nThere is a little caveat here. Since the transform function works by filtering on the active field of the Item, whenever the active field of an Item changes we need to call `update` on `items` again to ensure the list is updated. Therefore we needed to hoist the toggling from the Item Component into the App Component. The update function only works downwards and it cannot update parent properties.\n\nThe next example shows how we can do inline css styles.\n\n```d\nstruct AppStyle {\n  struct root {\n    auto margin = \"10px\";\n  }\n  struct button {\n    auto backgroundColor = \"white\";\n    @(\"hover\") struct hover {\n      auto backgroundColor = \"gray\";\n    }\n  }\n  struct toggle {\n    auto backgroundColor = \"purple\";\n  }\n}\n@styleset!(AppStyle)\nstruct App {\n  @style!\"root\" mixin Node!\"div\";\n  @child Button button;\n  @connect(\"button.click\") void toggle() {\n    button.update.toggle = !button.toggle;\n  }\n}\n@styleset!(AppStyle)\nstruct Button {\n  mixin Event!\"click\";\n  @style!\"button\" mixin Node!\"button\";\n  @style!\"toggle\" bool toggle;\n  @callback void onClick(MouseEvent event) {\n    this.click.emit;\n  }\n}\nmixin Spa!App;\n```\n\nHere you see the AppStyle struct, which contains some nested structs which themselves contains properties known from css. The idea is that Component can apply any of these nested structs.\n\nBoth the App and the Button struct have a `@styleset!(AppStyle)` annotation.\n\nThe App Component has a `@style!\"root\"` applied to its Node mixin. This means it will get a css class set with all the css properties defined in `AppStyle.root`.\n\nThe Button Component has the `AppStyle.button` on its Node mixin, and the `AppStyle.toggle` applies to the `toggle` bool. Whenever toggle is true, the toggle class is applied and vica versa.\n\nThe css is created at compile time and injected on startup into the html page. The class names are converted to hashes based on css name + css properties. This allows use to deduplicate classes with same css content.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskoppe%2Fspasm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fskoppe%2Fspasm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskoppe%2Fspasm/lists"}