Codegen & deployments
Current generated-file behavior.
Import generated bindings for your Move packages and the services in your stack from one place:
src/generated. Run devstack codegen to (re)write that tree from your Move source. It's
deterministic — running it twice on the same source produces the same files — and needs no running
stack, Docker, or on-chain ids. It does require the
Sui CLI (sui) on your PATH
for the sui move summary step.
The tree lives at one fixed location, shared across every stack:
src/generated/Codegen emits a set of files, one per domain:
config.ts # sui networks + package handles (id-free; deployment lookups)
config-runtime.ts # deployment loader (fixed; reads __DEVSTACK_DEPLOYMENT__)
deployment.ts # the strict app-specific deployment type
coins.ts # coin types and metadata
deepbook.ts # pool ids and market config
seal.ts # seal key-server / vault handles
walrus.ts # walrus endpoints and config
bindings/<pkg>/<module>.ts # generated Move bindings, one file per moduleThe config.ts, config-runtime.ts, deployment.ts, and bindings/ outputs are always present.
The service files (coins.ts, deepbook.ts, seal.ts, walrus.ts) appear only when your stack
declares the corresponding plugin — an app with no seal(...)/walrus(...) member emits no
seal.ts/walrus.ts.
The bindings/ tree is standard Sui Move codegen — MoveStruct BCS definitions and typed function
builders, one file per module. Devstack drives the
@mysten/codegen
generator for these; see its docs for how Move types, BCS, and function builders map to TypeScript.
This page covers the devstack layer on top: the file layout, the @generated alias, the
includePhantomTypeParameters knob, and how on-chain ids reach the generated tree.
Importing generated code
Import generated files like any other source, through the @generated alias. It always resolves to
src/generated, whichever stack is active; per-stack runtime state under .devstack/ is not
importable through it.
import { config } from '@generated/config.js';
import { Vault } from '@generated/bindings/my_pkg/vault.js';The alias is wired by devstackVitePlugin() and a matching tsconfig paths entry. Because the
generated tree is id-free (see On-chain ids), the same import works under every
stack — only the injected deployment differs, never the file location.
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import { devstackVitePlugin } from '@mysten-incubation/devstack/vite';
export default defineConfig({ plugins: [react(), devstackVitePlugin()] });The alias prefix is coordinated in two places, both pointing at the same fixed src/generated path:
devstackVitePlugin()invite.config.ts(andvitest.config.ts— Vitest runs its own Vite pipeline; see Vitest).- A
tsconfigpathsentry so the type checker resolves it:tsconfig.json { "compilerOptions": { "paths": { "@generated/*": ["./src/generated/*"] } } }
After changing Move source, regenerate the tree:
devstack codegendevstack codegen rewrites src/generated only — it doesn't boot a stack or touch the deployment
file. To refresh the live deployment (real ids) for a running stack, use
devstack apply instead.
Regenerating the deployment with apply
devstack apply re-emits the per-stack deployment.json from a live or one-shot stack. It writes
the per-stack deployment, not the src/generated tree.
devstack applyWhen a matching devstack up stack is live, apply asks it to emit from the current runtime state.
Otherwise, apply boots the stack once, emits the deployment, and exits.
On-chain ids
The generated config.ts carries no on-chain ids. Each id is resolved at runtime from the
build-time-injected __DEVSTACK_DEPLOYMENT__ global through config-runtime.ts. So the generated
tree stays the same whether you're on local, testnet, or a real deploy — only the injected ids
differ — and each command owns one job:
pnpm codegen/devstack codegen— regeneratessrc/generatedfrom Move source. Deterministic, no running stack, no Docker (requires thesuiCLI onPATH). Writes the generated tree only, not the deployment file.pnpm dev/devstack up— boots the stack and writes a gitignored, per-stack.devstack/stacks/<stack>/deployment.json; the Vite plugin injects it automatically in dev. It doesn't rewrite the generated tree.devstack apply— re-emits the per-stack deployment from a live (or one-shot) stack. It doesn't write the generated tree.pnpm build(tsc -b && vite build) — works on a clean clone with no running stack.
The deployment loader, the strict deployment.ts type, the missing-id error behavior, and the
dapp-kit handoff are covered in Config & deployments. To
author a deployment for a real network, see Going to production.
Phantom type parameters
includePhantomTypeParameters is a devstack codegen knob that controls how the bindings render Move
structs with phantom type parameters — Vault<phantom T>, coin and pool markers, capability
types. By default the bindings drop them: a struct whose type parameters are all phantom renders as
a plain const, with the phantom slots baked into its BCS name as placeholders:
// default (includePhantomTypeParameters: false)
export const Vault = new MoveStruct({ name: `${$moduleName}::Vault<phantom T>`, fields: { ... } });That class can parse Vault fields, but it cannot express a concrete type tag like
Vault<USDC> — the phantom is gone from both the type and the runtime name.
Turn on includePhantomTypeParameters to keep them:
export default defineDevstack({
members: [localnet, pkg],
codegen: {
includePhantomTypeParameters: true,
},
});Phantom-parameterized structs now generate as factories whose phantom parameters are required
arguments, and the returned class's .name is the fully-qualified type tag:
export function Vault<T extends BcsType<any>>(...typeParameters: [T]) {
return new MoveStruct({ name: `${$moduleName}::Vault<${typeParameters[0].name}>`, fields: { ... } });
}This lets the generated classes double as type tags: the phantom is tracked at the type level (not
widened to string), and tags compose by nesting.
Type arguments: strings or generated classes
Function builders type typeArguments as strings, so a hand-written fully-qualified tag always
works — including nested ones:
deposit({
arguments: { vault, coin },
typeArguments: ['0x2::sui::SUI'],
});
burnReceipt({
arguments: { receipt },
typeArguments: ['0x…::vault::Receipt<0x…::vault::Vault<0x2::sui::SUI>>'],
});With the flag on, the same tags can be composed from the generated BCS classes instead of
spelled by hand — structs without type parameters (one-time witnesses, markers) stay plain consts
and slot straight in, factories nest, and .name yields the string:
import { Receipt, Vault } from '@generated/bindings/my_pkg/vault.js';
import { USDC } from '@generated/bindings/my_pkg/usdc.js';
const vaultType = Vault(USDC); // MoveStruct for Vault<…::usdc::USDC>
vaultType.name; // '0x…::vault::Vault<0x…::usdc::USDC>'
deposit({ arguments: { vault, coin }, typeArguments: [vaultType.name] });
// Nested tags compose the same way — Receipt<Vault<USDC>>:
burnReceipt({ arguments: { receipt }, typeArguments: [Receipt(Vault(USDC)).name] });Anything BcsType-shaped fits a phantom slot, so primitives work too: Vault(bcs.u64()) renders
Vault<u64>. And the factory result is still a full codec for the struct's real fields —
Vault(USDC).parse(bytes) and the typed object readers (Vault(USDC).get({ client, objectId }))
keep working, now with the phantom tracked in the type.
One consequence to plan for when enabling the flag on an existing app: structs whose only type
parameters are phantom switch shape from const to factory in the regenerated bindings, so call
sites change from Vault to Vault(USDC). That reshape is why the option is opt-in.