Table of Contents

Develop the browser explorer

This page is for contributors changing the explorer or its managed bridge. Browser consumers should use the release bundle starter. They do not need a source checkout or .NET workload.

Build both parts

Install the stable .NET 10 SDK selected by global.json, Node.js and npm compatible with the root package.json, the npm workspace manifest (the repository scripts under tools/ run on the same Node). Its packageManager field records the preferred npm version; the root lockfile fixes the dependency graph.

From the repository root:

dotnet workload restore ./src/CStructSharp.Wasm/CStructSharpWeb.Wasm.csproj
npm ci
npm run build:wasm
npm --prefix ./apps/explorer run build

npm run build:wasm publishes the C# bridge into artifacts/wasm. The explorer's production build stages it into public/wasm, builds Vue into apps/explorer/dist, and verifies that the build embeds exactly that publication; the inspector's build does the same. The managed solution alone does not build Vue.

After the first build:

npm --prefix ./apps/explorer run dev

Open the address printed by Vite. Changes to Vue update during development; changes to C# require npm run build:wasm and npm --prefix ./apps/explorer run copy:wasm. Run the complete production build before browser tests.

Reproducible publication

A build is reproducible when the same source gives the same output bytes. npm run build:wasm passes -p:Deterministic=true -p:ContinuousIntegrationBuild=true to dotnet publish, and these global properties also apply to the referenced library. Without them the compiler derives each assembly's module version ID from inputs that include the checkout path, so two clones of one commit would publish different .wasm files and boot integrity hashes. With them the compiler maps source paths to /_/. A plain dotnet build of the library, including the Debug build you step through locally, keeps real paths in its symbols.

npm run test:wasm-reproducibility checks this after npm run build:wasm. It copies the working tree (tracked and untracked, non-ignored files) to a temporary directory at a different path, seeds that copy's artifacts/wasm with the current publication plus stale files, and publishes there. The check passes when every published file has the same size and SHA-256 hash and the stale files are gone. The copy's .git file points at the original repository, so both builds read the same commit metadata.

Managed bridge trimming

src/CStructSharp.Wasm/CStructSharpWeb.Wasm.csproj builds with PublishTrimmed and TrimMode=full, declares the bridge assembly trimmable for this publish (the library itself ships as IsTrimmable/IsAotCompatible), roots nothing, and publishes with trim analysis on: the library uses no reflection (typed reads and writes go through ICStructMapped<T>), so the linker has nothing to warn about. The browser value-conversion rules make these settings possible:

  • No C# runtime-binder call site executes in the browser build. Parsed values are StructValue objects, and the bridge and the benchmark exports handle every library result as object. Microsoft.CSharp is not part of the publication, and System.Linq.Expressions is trimmed to the IDynamicMetaObjectProvider surface the value types implement. This avoids loading and initializing the runtime binder for a parse.
  • The library has no reflection-based data binding: write data is a StructValue, a dictionary, or a class implementing ICStructMapped<T>, and ParseJsonValue in CStructJsonConversion.cs always produces dictionary/list shapes, so the browser build never registers a mapped type. (Declaring dynamic code unsupported was measured to add ≈ 820 B of managed allocation to every export call through System.Text.Json, so the project relies on trimming alone.)

Measured effect of the full trim (publication as shipped by publish-wasm.mjs): 33 → 27 files, 5.35 → 4.36 MB raw, 2.05 → 1.66 MB gzip; Node cold start: first public parse 142 → 17 ms, process wall −25 %; runtime creation and first layout compilation unchanged. The gates that must stay green when touching these settings: the JS benchmark harness's fixture verification, browser-contract.mjs, both apps' e2e suites against the production build, verify-wasm-publication.mjs, and measure-web-artifacts.mjs --check. If a change reintroduces a dynamic call site in the bridge, the trimmer fails the publish or the first parse throws a MissingMethodException - the harness verification catches both.

Change lessons and test examples

src/lessons.ts contains authored titles, exercises, and operation presets. The generator's test catalog remains separate. Each lesson references a registered C# documentation scenario; the unit check detects missing references. Real-browser tests compare every offered lesson operation against its expected values, bytes, or error code. Preserve lesson IDs because documentation links use them.

The standalone browser examples are in wasm/starter. Documentation includes those source files directly, and the package builder copies them into the release ZIP. Editing a browser example does not require duplicating it in Markdown.

Verify the change

npm --prefix ./apps/explorer run lint
npm --prefix ./apps/explorer run test:unit
npm --prefix ./apps/explorer run test:demos
npm run test:bootstrap
npm --prefix ./apps/explorer run build
npm --prefix ./apps/explorer run test:e2e

Install Playwright Chromium from the web directory with npx playwright install chromium if it is missing. The suites run in Chromium by default; CSTRUCT_BROWSERS=firefox,webkit (comma-separated engine names) selects other engines for any Playwright config, and the release workflow smokes the packaged starter page and one inspector flow in Firefox and WebKit that way. PR CI stays Chromium-only. For the exact released bundle, create and test an archive:

npm run pack:zip
(cd artifacts/wasm-package && zip -qr ../onboarding-browser.zip .)
node tools/packaging/test-onboarding-browser.mjs --archive-path artifacts/onboarding-browser.zip

The last check extracts the ZIP into a nested static path and tests the starter and inspector using the public JavaScript entry point. It does not substitute the explorer's internal adapter.

Preview matching documentation and explorer sources

For the public npm package, its Node.js loader, and the Vite integration, use the npm build and consumer checks. Application users should start with the JavaScript quick start; they do not need to build this repository.

For source review, run the documentation server with local explorer links in one terminal:

node tools/documentation/build-documentation.mjs --serve --port 8080 --explorer-url http://127.0.0.1:5173/cstructsharp/explorer/

In another terminal, set the explorer's documentation base before starting Vite:

VITE_DOCS_BASE_URL=http://localhost:8080/ npm --prefix ./apps/explorer run dev

Open http://127.0.0.1:5173/cstructsharp/explorer/. The header links open the local docs, and lesson links in the local docs return to this explorer. Build the WASM runtime first if needed, following the instructions above. For production builds, omit these overrides: the explorer links to the sibling ../docs/ directory and documentation links use the published explorer. VITE_DOCS_BASE_URL is a build-time setting when building the frontend. Remove it from the terminal with Remove-Item Env:VITE_DOCS_BASE_URL before making a publication build. The documentation override changes generated HTML only; rebuild without it before publishing.