Generated Outputs
Current generated-file behavior.
There is no root factory for generated output. The supervisor collects each member's codegenable
capability and emits files during devstack up or devstack apply.
Default root for the primary stack (the one your config declares via stackName):
src/generated/Secondary stacks (any run with an explicit --stack/$DEVSTACK_STACK override that diverges from
the config's stackName) emit to a per-stack directory instead, so two stacks of the same app can
run side by side without clobbering each other's package-id and wallet-pair-token literals:
.devstack/stacks/<stack>/generated/The supervisor records the absolute output dir it chose in that stack's manifest as
codegen.generatedDir, so the reader (the Vite alias below) consults
the same location the writer chose. pnpm dev (primary stack) and pnpm test:e2e (an e2e stack)
therefore coexist with distinct generated trees.
Common outputs:
sui/network.ts
accounts/<name>.ts
coins/<symbol>.ts
package/<mvr-placeholder>.ts
dapp-kit/config.ts
walrus/network.ts
seal/<name>.ts
deepbook/<name>.ts
extras.tsThe primary/secondary rule above is the default. An app can pin an explicit output dir in the stack
options; outputDir (and the optional stackSubdir) are then honored verbatim, and per-stack
isolation becomes the app's responsibility:
import { defineDevstack, account, sui } from '@mysten-incubation/devstack';
const localnet = sui();
const alice = account('alice');
export default defineDevstack({
members: [localnet, alice],
codegen: {
outputDir: 'src/generated',
stackSubdir: null,
},
extras: {
featureFlag: 'local-demo',
},
});Importing generated code
Generated files are normal source imports, resolved through a configurable @generated alias.
Runtime state under .devstack/ is not importable app code.
import { suiNetwork } from '@generated/sui/network.js';
import { dappKitConfig } from '@generated/dapp-kit/config.js';The alias is wired by devstackVitePlugin(), which points @generated at whichever stack is
active. It reads the active stack's manifest-recorded codegen.generatedDir (falling back to
src/generated/ before the first up), so an import resolves to the primary stack under pnpm dev
and to .devstack/stacks/<stack>/generated/ under a secondary stack — automatically, with no import
changes.
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 three places, all using the same string (default @generated,
customizable via devstackVitePlugin({ alias: '@gen' })):
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/*"] } } } - The import specifiers themselves:
@generated/....
To force a re-emit, run apply:
devstack applyWhen a matching devstack up supervisor is live, apply asks it to emit from the current runtime
state. Otherwise, apply boots the stack once, emits, and exits.
Phantom type parameters and type tags
Move structs routinely carry phantom type parameters — Vault<phantom T>, coin and pool markers,
capability types. By default the generated 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 is what makes the generated classes work well as type tags: you can no longer forget the
phantom, the tag is tracked at the type level (not widened to string), and tags compose by
nesting.
Type arguments: strings or generated classes
Generated 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.