Development Guide<script lang="d">

<script lang="d">

The most important sentence in this guide is also the easiest to skip: <script lang="d"> is libwasm D. svelte-d does not translate TypeScript into D. It does not accept a subset of JavaScript and lower it. It libdparse-checks the body you wrote and prints that body onto the same struct that owns the markup’s NodeDef graph. If the body is not D, it will not compile as D. If it is D but it import vibe.*, it is in the wrong cell. The script is the component.

<script lang="d">
  string title = "Home";
 
  void onClick(MouseEvent ev) {
    this.update.title = "Clicked";
  }
</script>
 
<section class="container">
  <h1>{title}</h1>
  <button on:click={onClick}>Ping</button>
</section>

string title = "Home" is a field of the printed struct, with the same type and the same initializer. void onClick is a method of that struct. this.update.title = "Clicked" is already the libwasm mutation primitive; the printer keeps it. A bare title = "Clicked" inside a handler is rewritten to this.update.title (and, when {title} exists as a child NamedNode, to titleSpan.update.title) so the live handle updates. svelte.config.js in the engine blanks the lang=d block so vscode-svelte and svelte-check do not try to parse D as TypeScript. That blanking is an editor courtesy. It is not a compile step.

The printed module is nothrow: @safe:. Almost every engine golden is nothrow, and LDC 1.43’s default --foptimize-nothrow on wasm deletes landing pads inside nothrow functions. The wasm cell therefore compiles with --foptimize-nothrow=false. Even so, throwing APIs such as std.conv.to belong inside try/catch, because the struct itself remains nothrow. import std.algorithm (and the rest of the spa-phobos set) is lifted to module scope, above nothrow. It is not written inside the struct. std.file, std.stdio, and std.socket are not wasm Phobos on this cell; they are a diagnostic.

try/catch is ordinary D on wasm-eh, with one hard constraint that throwBoundary will repeat: the try, the throw, and the catch must be in the same function on the same struct. Personality is first-catch-type only. An exception that escapes nothrow is not a boundary; it is an abort.

Lifetime hooks are libwasm’s, not Svelte’s. void construct() runs during compile!, before handles exist — seed child @prop fields here, do not read the DOM. void onMount() / void onUnmount() run after render. App.ready() runs after first paint and is where wireEach / wireAwait attach, because those need live handles. There is no JS onMount import. Printing one would be a second runtime.

Lodash, document(), window(), moment(), and Eval("window.X") are procedural leaves. They live in a void go() body. They are not how a <button> is represented. A printer that emitted _.map to mean {#each} would have abandoned the IR contract. Heavy arms wrap ScopedPool(m_pool) and copy survivors onto the graph; language new bumps WasmAllocator and is never recycled.

What the script must not contain follows from the cell split. import vibe.* is the host cell. npm import is lang=ts plus Vite (those specifiers fall through onto dest package.json and dest node_modules). Same-file TS exports are called by the simple name (callTs); a window host object that is not a lang=ts export is Eval("window.$"). .await is printed in wireAwait when the etcimon Binaryen fork asyncified the wasm-eh module; wrap it in try/catch and you are asking Flatten and a landing pad to share a frame. Stock Binaryen 132 still cannot --asyncify try_table, so that path keeps JsPromise.then.

The runtime those identifiers belong to — mixin Spa, NodeDef, Lodash.execute, JsPromise, WasmAllocator, the handle table — is libwasm. lang=“d” on the wasm cell walks a full script against that API. Open svelte-engine-ws in VS Code with code-d to complete the same names; see Getting Started.