How to Integrate Custom Export Logic into the DESIGN.md CLI

The DESIGN.md CLI supports custom export formats through a pluggable emitter architecture that requires implementing an execute(state) method, registering your handler in the CLI dispatcher, and validating output with Zod schemas.

The google-labs-code/design.md repository provides a CLI tool for managing design systems through markdown-based specifications. When the built-in export formats—Tailwind v3/v4, CSS variables, and DTCG—do not meet your project requirements, you can integrate custom export logic by leveraging the project's emitter plugin system. This architecture allows you to transform the internal DesignSystemState into any target format while maintaining type safety through validation.

Understanding the Pluggable Emitter Architecture

The CLI processes npx @google/design.md export commands through a dispatcher that follows a strict three-step pipeline. First, it parses the DESIGN.md file into a DesignSystemState object defined in packages/cli/src/linter/model/spec.ts. Then, it selects an emitter based on the --format flag. Finally, it runs the emitter's execute(state) method, which must return a result matching a Zod-validated schema.

The Core Emitter Contract

Built-in emitters demonstrate the required implementation pattern. The TailwindEmitterSpec interface in packages/cli/src/linter/tailwind/spec.ts establishes the contract your custom emitter must follow:

// packages/cli/src/linter/tailwind/spec.ts
export interface TailwindEmitterSpec {
  execute(state: DesignSystemState): TailwindEmitterResult;
}

Your custom emitter must implement this interface, accepting the full design system state and returning a structured result. The DesignSystemState contains typed maps for colors, typography, spacing, and other design tokens extracted from your markdown files.

Result Validation with Zod

The CLI enforces output consistency through the TailwindEmitterResultSchema located in packages/cli/src/linter/tailwind/spec.ts. This Zod schema uses a discriminated union pattern to validate success and error states:

// packages/cli/src/linter/tailwind/spec.ts
export const TailwindEmitterResultSchema = z.discriminatedUnion('success', [ … ]);

Custom emitters must define equivalent schemas to ensure the CLI can safely serialize and output results.

Step-by-Step: Integrate Custom Export Logic

To add a completely new export format—such as my-format—follow these implementation steps derived from the source code structure.

1. Define the Specification

Create a spec file that mirrors the Tailwind implementation. Define a Zod schema for your output and an interface with an execute(state) method. Store this in packages/cli/src/linter/myformat/spec.ts:

// packages/cli/src/linter/myformat/spec.ts
import { z } from 'zod';
import type { DesignSystemState } from '../model/spec.js';

export const MyFormatResultSchema = z.object({
  success: z.literal(true),
  data: z.object({
    // Example: a flat map of token names → raw values
    tokens: z.record(z.string()),
  }),
});

export type MyFormatResult = z.infer<typeof MyFormatResultSchema>;

export interface MyFormatEmitterSpec {
  execute(state: DesignSystemState): MyFormatResult;
}

2. Implement the Handler

Create a handler class that transforms DesignSystemState into your custom representation. Reference packages/cli/src/linter/tailwind/handler.ts as a template for accessing state properties:

// packages/cli/src/linter/myformat/handler.ts
import type { MyFormatEmitterSpec, MyFormatResult } from './spec.js';
import type { DesignSystemState } from '../model/spec.js';

export class MyFormatEmitterHandler implements MyFormatEmitterSpec {
  execute(state: DesignSystemState): MyFormatResult {
    const tokens: Record<string, string> = {};

    // Flatten colors (example)
    for (const [name, color] of state.colors) {
      tokens[`color.${name}`] = color.hex;
    }
    // Add other token types as needed …
    return {
      success: true,
      data: { tokens },
    };
  }
}

3. Register in the CLI Dispatcher

Locate the export command handler in packages/cli/src/cli.ts or the format parsing utility in packages/cli/src/utils.ts. Import your handler and add a case to the format selection logic:

// packages/cli/src/cli.ts (excerpt)
import { MyFormatEmitterHandler } from './linter/myformat/handler.js';
import { MyFormatResultSchema } from './linter/myformat/spec.js';

...
case 'my-format': {
  const handler = new MyFormatEmitterHandler();
  const result = handler.execute(state);
  // Validation ensures a consistent shape
  MyFormatResultSchema.parse(result);
  console.log(JSON.stringify(result, null, 2));
  break;
}
...

4. Export from Module Index

Create an index file at packages/cli/src/linter/myformat/index.ts to expose your spec and handler for clean imports:

export { MyFormatEmitterHandler } from './handler.js';
export { MyFormatResultSchema, type MyFormatEmitterSpec } from './spec.js';

Once registered, invoke your new format:

npx @google/design.md export --format my-format DESIGN.md > my-design.json

Key Source Files for Custom Emitters

Understanding the codebase structure helps when you integrate custom export logic. These files define the core types and utilities:

Summary

  • The DESIGN.md CLI uses a pluggable emitter architecture that parses markdown into DesignSystemState before routing to format-specific handlers.
  • To add custom export logic, implement the TailwindEmitterSpec interface pattern with an execute(state) method in packages/cli/src/linter/<format>/handler.ts.
  • Define a Zod schema for result validation to ensure the CLI receives predictable output shapes.
  • Register your format in the CLI dispatcher at packages/cli/src/cli.ts by mapping the --format flag value to your handler instance.
  • Reference the built-in Tailwind emitters in packages/cli/src/linter/tailwind/ as working templates for contract implementation.

Frequently Asked Questions

What interface must my custom emitter implement?

Your custom emitter must implement an interface matching the pattern found in TailwindEmitterSpec from packages/cli/src/linter/tailwind/spec.ts. This requires a single method execute(state: DesignSystemState) that returns a Zod-validated result object containing at minimum a success boolean and a data payload.

Where do I register a new export format in the CLI?

Register new formats in the CLI dispatcher located in packages/cli/src/cli.ts or within the format parsing utilities in packages/cli/src/utils.ts. Add a case statement that maps your custom --format flag value (e.g., my-format) to a new instance of your emitter handler class.

How does the CLI validate custom emitter output?

The CLI validates output using Zod schemas defined alongside your emitter spec. Your result schema—similar to TailwindEmitterResultSchema in packages/cli/src/linter/tailwind/spec.ts—must validate the shape returned by your execute() method. The dispatcher calls .parse() on this schema before serializing output.

Can I export to formats other than JSON?

Yes. While the built-in emitters primarily produce JSON, your custom handler can generate any text-based format including XML, YAML, CSS, or proprietary formats. Simply adjust the data structure in your Zod schema and modify the serialization logic in your handler's execute() method to return strings in your target format.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →