Skip to content

Build Product resources

Build Product resources with immutable Merulan CDK factories and give each one exactly one owning Block.

Use this guide when you author resources for a Product in Workspace. You declare each resource with a factory, list it in one Block, and select those Blocks through an immutable Product definition. The examples use the repository-pinned CDK 0.3.3 package; they do not assert public registry availability.

Before you start

You need an authoring repository made by merulan workspace init and a Product made by merulan product init. The repository pins @merulan/cdk to the exact release the CLI was built with. Import only @merulan/cdk/product, @merulan/cdk/workspace, and local modules. A deep import into the package is refused.

Declare each resource

A factory such as entity, relationshipResource, or actionResource returns a typed handle. A handle is a value you pass around, not a name you repeat. A builder method such as withField returns a new snapshot and leaves the old one unchanged. Export the final snapshot from the module that owns it.

import { entity, Types } from '@merulan/cdk/product';

export const account = entity('example_account', { label: 'Account', pluralLabel: 'Accounts', displayField: 'name' })
  .withField('name', { type: Types.text, label: 'Name', required: true });

The factory refuses a retired property, such as kindKey or cedarResourceType, before compiling. The handle already carries that fact.

Assign Block ownership

A Block selects resources by listing their handles. Only a listed handle becomes part of the Product. Folder placement never adds a resource. A module that no Block imports installs nothing, so a draft can sit in the repository without shipping.

import type { BlockContribution } from '@merulan/cdk/product';
import { account } from './schema.ts';

export const context: BlockContribution = { id: 'context', name: 'Context', handles: account.handles() };

Each resource has exactly one owning Block. Another Block that needs the resource refers to it through its handle and does not list it again.

Compose the Product

product() creates a Product definition. Methods such as addBlock return new definitions; synthesize() produces the existing native source and an owners map from each resource key to its Block path. Export the synthesized result and its source from product.ts.

import { product as createProduct } from '@merulan/cdk/product';
import { context } from './context/block.ts';

export const definition = createProduct('example', { version: '1.0.0', name: 'Example Product' })
  .addBlock(context);
export const composed = definition.synthesize();
export const product = composed.source;

Keep Product-owned settings and APQC mappings in product.ts: configure registers a schema-backed configuration, requires declares Source or Integration requirements, dependsOn selects exact Product references, and mapTo adds framework correspondences using resource handles. Pass checked definition.setting(key) or definition.source(key) references into capability factories. A child module need not import its owning Product.

The resulting composed.source is the native compiler input, not a manifest to hand-author. Synthesis creates no records, account connections or permissions.

Checkpoints

Run merulan workspace lint --dir . after each change. The lint reads every module the Product reaches. It reports each refusal with a file, line, and column. Run merulan product build --source src/products/example/product.ts to compile a draft. Read the owners map from merulan product source-inventory --source src/products/example/product.ts --offline to see which Block owns each resource.

Success

The lint returns no diagnostics, and the build returns a draft with a contentDigest. Every resource you declared appears once in the owners map. The registered products.source.inventory operation returns the same inventory as the offline CLI for the same draft.

When it fails

The lint names the code, the file, and the line to repair. Repair that code and run the lint again.

workspace.structure.orphan_resource

A module the Product reaches exports a resource that no Block lists. List the handle in its owning Block, or remove the export.

workspace.structure.ambiguous_owner

Two Blocks list the same resource. Keep it in one Block and refer to its handle from the other.

workspace.structure.unresolved_reference

A listed resource refers to a resource that no Block selects. List the missing handle in its owner, or reference a resource from an exact Product release.

workspace.structure.private_import

A module imports a private path of the package. Import the public entry instead.

Recovery

Generation never overwrites source you changed. merulan product init refuses when a file it would write already holds different bytes. It checks every target before it writes, so a refusal leaves all prior source in place. Move your edits aside or pick another Product key, then run the command again.

Export and delegation

Use Merulan to compile and check Product code when each resource must have one owner in one release. Merulan retains the compiled draft, its digest, and the owner of every resource. You may write the code in any editor or repository. It counts only after the CDK compiles it and the lint admits it. An exported draft keeps its resources and owners, not your repository layout.

Next steps