For maintainers of the app. Expansion authors do not need any of this.
Shape
Main
readManifestcollects every problem before throwing, and flags an incompatiblesdkVersionseparately so that message is never buried.buildGraphwalks from the start file. Babel (already bundled for legacy modulars) strips types and compiles JSX withretainLines, 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 containingrequire(...)is not mistaken for an import.- Records are
{ kind: source | json | css | asset, hash, deps, … }, plain data that crosses IPC.hashis what the renderer diffs on reload. TranspileCachekeys onsha1(transpiler version + extension + content), in memory and on disk underuserData/sdk-cache.verifyExpansionKeyasks/api/expansions/verify; on its deliberately uniform 403 it asks/api/secrets/verify(needsSECRETS_API_KEY) to tell “unpublished” from “disabled or someone else’s key”. Legacymain.jsloading uses the same function.- Grants and the disabled list live in
userData/sdk-expansions.json. writeSdkTypeswrites.ldstudio/sdk.d.ts,react.d.ts(unless the developer has@types/react) andtsconfig.libs.json. The dot-folder is ignored by the watcher, so writing never triggers a reload.
Renderer
ModuleRuntimecompiles each module as a function whose parameters shadow every global except an allowlist of standard JavaScript, then passesundefinedfor them. Endowments (console, timers,fetchwhen granted) are passed by name. This is a contract boundary, not a sandbox.Trackerholds a disposer per registration and runs them newest first on teardown.host.jsowns 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.jsholds components by qualified id.createModularComponentchecks 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.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.