Skip to main content
For maintainers of the app. Expansion authors do not need any of this.

Shape

Main never runs expansion code. It validates, verifies, compiles and resolves, then hands the renderer a table of modules. The renderer evaluates it.

Main

  • readManifest collects every problem before throwing, and flags an incompatible sdkVersion separately so that message is never buried.
  • buildGraph walks from the start file. Babel (already bundled for legacy modulars) strips types and compiles JSX with retainLines, which is why error cards can name the author’s line without a source map. Requires are collected from the output AST, so a string containing require(...) is not mistaken for an import.
  • Records are { kind: source | json | css | asset, hash, deps, … }, plain data that crosses IPC. hash is what the renderer diffs on reload.
  • TranspileCache keys on sha1(transpiler version + extension + content), in memory and on disk under userData/sdk-cache.
  • verifyExpansionKey asks /api/expansions/verify; on its deliberately uniform 403 it asks /api/secrets/verify (needs SECRETS_API_KEY) to tell “unpublished” from “disabled or someone else’s key”. Legacy main.js loading uses the same function.
  • Grants and the disabled list live in userData/sdk-expansions.json.
  • writeSdkTypes writes .ldstudio/sdk.d.ts, react.d.ts (unless the developer has @types/react) and tsconfig.libs.json. The dot-folder is ignored by the watcher, so writing never triggers a reload.

Renderer

  • ModuleRuntime compiles each module as a function whose parameters shadow every global except an allowlist of standard JavaScript, then passes undefined for them. Endowments (console, timers, fetch when granted) are passed by name. This is a contract boundary, not a sandbox.
  • Tracker holds a disposer per registration and runs them newest first on teardown.
  • host.js owns start, reload, stop, per-expansion logs, app-event fan-out and the statuses the Expansions page renders. activate() decides what a default export is: class, object, function or nothing.
  • registry.js holds components by qualified id. createModularComponent checks it first, which is why every existing mount point (pages, editors, creators, injections, item-editor modules) renders SDK components without knowing they exist.
  • Hot reload re-evaluates changed modules, their importers, and every module importing sdk, so registrations come back after teardown. CSS keeps a <style> per module path, replaced in place.

IPC

Adding an SDK point

1

Implement it

Implement it in renderer/src/sdk/sdk/createSdk.js. Register through the existing app registry or postMessage, and give the tracker a disposer.
2

Gate it

Gate it with need('permission', 'point.name') if it touches user data, and add the permission to src/shared/sdk/permissions.js with a plain-language label.
3

Declare it

Declare it in packages/sdk-types/index.d.ts.
4

Document it

Document it in the sdk reference, and in migration if it replaces a legacy call.
5

Test it

Add a test in tests/unit/sdk/createSdk.test.jsx, including that teardown undoes it.
Bump SDK_VERSION in src/shared/sdk/version.js: minor for an addition, major for a change in shape or a removal.

Tests

tests/unit/sdk/ covers the manifest, resolver, libraries, CSS scoping, graph and runtime (including circular imports, hidden globals and hot-reload dirtiness), the sdk module, the host lifecycle, verification and the generated type files. Every shipped example and template is built as part of the suite, so an example that stops loading fails CI.

Still missing

Export formats and server reload commands (legacy-only today), a CLI, background scripts, the NMCrate library system, and hard isolation. See the repository’s plan notes.