MCPcopy Index your code
hub / github.com/carbon-design-system/sveld

github.com/carbon-design-system/sveld @v0.34.1

Chat with this repo
repository ↗ · DeepWiki ↗ · release v0.34.1 ↗ · + Follow
1,223 symbols 2,462 edges 539 files 116 documented · 9% 2 cross-repo links
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

sveld

[![NPM][npm]][npm-url] npm downloads to date

sveld generates TypeScript definitions and component documentation (Markdown/JSON) for Svelte components. It statically analyzes props, events, slots, and the rest. Add types with JSDoc when inference is not enough.

The goal is to get third-party Svelte libraries working with the Svelte Language Server and TypeScript with minimal effort from the author. Generated .d.ts files give you autocomplete in VS Code and other IDEs.

Carbon Components Svelte uses this library to auto-generate component types and API metadata.

sveld uses the Svelte 5 compiler to parse .svelte files. That single parse path powers docgen and TypeScript output for Svelte 3, Svelte 4, Svelte 5 without runes (export let, <slot>, $$restProps, …), and Svelte 5 Runes ($props(), $bindable(), {@render ...}, callback props such as onclick, …).

For lang="ts" components, sveld keeps source-level prop type annotations when it can, instead of forcing JSDoc. That covers legacy export let props, typed $props() destructuring, typed whole-object $props() captures, local interface/type declarations, and imported type references in emitted .d.ts files.

Syntax mode Supported
Svelte 3
Svelte 4
Svelte 5 (non-Runes)
Svelte 5 Runes

Generated .d.ts files extend SvelteComponentTyped from svelte, so TypeScript and the Svelte Language Server work whether consumers use Svelte 3, Svelte 4, or Svelte 5.


From a Svelte component, sveld can infer basic prop types and emit definitions the Svelte Language Server understands:

Button.svelte

<script>
  export let type = "button";
  export let primary = false;
</script>

<button {...$$restProps} {type} class:primary on:click>
  <slot>Click me</slot>
</button>

The following generated .d.ts extends SvelteComponentTyped:

Button.svelte.d.ts

import { SvelteComponentTyped } from "svelte";
import type { SvelteHTMLElements } from "svelte/elements";

type $RestProps = SvelteHTMLElements["button"];

type $Props = {
  /**
   * @default "button"
   */
  type?: string;

  /**
   * @default false
   */
  primary?: boolean;

  [key: `data-${string}`]: unknown;
};

export type ButtonProps = Omit<$RestProps, keyof $Props> & $Props;

export default class Button extends SvelteComponentTyped<
  ButtonProps,
  { click: WindowEventMap["click"] },
  { default: Record<string, never> }
> {}

Inference only gets you so far. Use JSDoc to document prop, event, and slot types when you need more precision.

/** @type {"button" | "submit" | "reset"} */
export let type = "button";

/**
 * Set to `true` to use the primary variant
 */
export let primary = false;

With JSDoc, the output looks like this:

import type { SvelteHTMLElements } from "svelte/elements";

type $RestProps = SvelteHTMLElements["button"];

type $Props = {
  /**
   * @default "button"
   */
  type?: "button" | "submit" | "reset";

  /**
   * Set to `true` to use the primary variant
   * @default false
   */
  primary?: boolean;
};

export type ButtonProps = Omit<$RestProps, keyof $Props> & $Props;

export default class Button extends SvelteComponentTyped<
  ButtonProps,
  { click: WindowEventMap["click"] },
  { default: Record<string, never> }
> {}

Table of Contents

Approach

sveld uses the Svelte compiler to statically analyze exported components and emit docs for consumers.

It extracts:

  • props
  • slots
  • forwarded events
  • dispatched events
  • context (setContext/getContext)
  • $$restProps

When inference fails, props fall back to any rather than guessing wrong. Authors can tighten types with JSDoc. Comments are optional from the compiler's point of view, so plain JavaScript components still parse.

When both TypeScript syntax and JSDoc are present, sveld resolves prop types in this order:

  1. explicit TypeScript annotation
  2. explicit JSDoc annotation
  3. initializer inference
  4. any

sveld stays AST-only. It copies imported and local type text into generated .d.ts output but does not run project-wide semantic resolution with the TypeScript compiler. Opaque imported whole-object $props() types can therefore stay in declarations without being fully expanded into JSON metadata.

Opt-in semantic resolution (resolveTypes)

Imported whole-object $props() types stay opaque in JSON by default ("props": []). Turn on resolveTypes when a docs site or prop table needs the individual fields.

await sveld({ json: true, resolveTypes: true });
<script lang="ts">
  import type { Props } from "./types";

  let props: Props = $props();
</script>

Without resolveTypes, JSON lists no props. With it, each field shows up with "typeSource": "typescript":

{
  "props": [
    { "name": "disabled", "type": "boolean", "isRequired": false, "typeSource": "typescript" },
    { "name": "href", "type": "string", "isRequired": true, "typeSource": "typescript" },
    { "name": "variant", "type": "\"primary\" | \"secondary\"", "isRequired": true, "typeSource": "typescript" }
  ]
}

Performance. Off by default. This is the only path that loads TypeScript. It needs typescript and a tsconfig.json, runs slower than the AST-only pipeline, and gets slower as your types grow. Use it only when you need expanded JSON. .d.ts output is unchanged.

Persistent parse cache (cache)

By default, every component is re-parsed on every run. With cache, parsed output is written to disk and reused when the source file has not changed. That applies across runs, including CI on a fresh checkout.

await sveld({ json: true, cache: true });

cache: true writes to node_modules/.cache/sveld/parse-cache.json. Pass a string to use a different location, e.g. cache: ".cache/sveld.json". Also available as --cache / --cache=<path> on the CLI.

If a component @extendProps / @extends another file, it is re-parsed when that dependency changes, same as in watch mode. Bumping the sveld or Svelte version clears the cache.

Compile-checked @example blocks (checkExamples)

@example blocks are just text. Rename a prop and the sample code can sit there broken for months. Set checkExamples: true to run plain TS/JS @example blocks through the TypeScript program. Broken examples show up as example-compile-error diagnostics.

await sveld({ json: true, checkExamples: true });
<script>
  /**
   * Formats a value.
   * @param {string} value
   * @returns {string}
   * @example
   * ```js
   * formatValue("ok");
   * ```
   */
  export function formatValue(value) {
    return value;
  }
</script>

If formatValue is later renamed and the example is never updated, checkExamples reports it:

@example blocks that failed to compile (1):
  ./Component.svelte
    - Line 1: Cannot find name 'formatValue'.

Plain TS/JS only. Examples fenced as svelte or html, or bare markup like <Button />, are skipped. Checking those needs svelte2tsx or similar, and sveld stays AST-only.

The check is narrow on purpose. It catches renamed or removed symbols and wrong argument counts. It is not full type checking and never pulls in types sveld cannot see.

Needs typescript and a tsconfig.json, same as resolveTypes. Use --strict (or the strict option) to fail CI when an example breaks.

Type inference diagnostics

sveld collects unresolved-type diagnostics on every run: props that fall back to any, context values typed as any, @event tags with no dispatch or callback, and (when checkExamples is enabled) example-compile-error. They are always returned from the programmatic sveld() API in SveldResult.diagnostics.

With reportDiagnostics or strict, the grouped summary looks like this:

sveld: 4 unresolved types found.

Props without inferred types (1):
  ./icons/Add.svelte
    - Prop "title" type could not be inferred; falling back to "any".

Context values typed as `any` (1):
  ./ThemeProvider.svelte
    - Context "theme" variable "themeStore" has no type annotation; defaulted to "any".

@event tags with no dispatch or callback (2):
  ./Modal.svelte
    - @event "open" has no matching dispatch or callback prop.
    - @event "close" has no matching dispatch or callback prop.

When checkExamples is also enabled, @example compile failures appear as a fourth group:

@example blocks that failed to compile (1):
  ./Component.svelte
    - Line 1: Cannot find name 'formatValue'.

By default, nothing is printed. Opt in when you are working on types or want CI output:

await sveld({ json: true, reportDiagnostics: true });

Use strict: true (or --strict) to exit with code 1 when diagnostics exist. strict implies reportDiagnostics, so CI always shows why the run failed.

await sveld({ json: true, strict: true });

CLI equivalent:

npx sveld --json --report-diagnostics
npx sveld --json --strict

--check is separate: it diffs COMPONENT_API.json for API drift and semver classification, not inference warnings.

Usage

Installation

Install sveld as a development dependency.

# npm
npm i -D sveld

# pnpm
pnpm i -D sveld

# Bun
bun i -D sveld

# Yarn
yarn add -D sveld

Vite

Import and add sveld as a plugin to your vite.config.ts. The plugin only runs during vite build (not the dev server).

// vite.config.ts
import { svelte } from "@sveltejs/vite-plugin-svelte";
import sveld from "sveld";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [svelte(), sveld()],
});

Since Vite uses Rollup for production builds, the same plugin works in Rollup configs.

By default, sveld will use the "svelte" field from your package.json to determine the entry point. You can override this by specifying an explicit entry option:

sveld({
  entry: "src/index.js",
});

When building the library, TypeScript definitions are emitted to the types folder by default.

Customize the output folder using the typesOptions.outDir option. Use typesOptions.printWidth to control Prettier wrapping for generated .d.ts files. The default is 80.

The following example emits the output to the dist folder:

sveld({
+  typesOptions: {
+    outDir: 'dist',
+    printWidth: 80
+  }
})

CLI

The CLI uses the "svelte" field from your package.json as the entry point:

npx sveld

Generate documentation in JSON and/or Markdown formats using the following flags:

npx sveld --json --markdown

CI: API-drift checks (--check)

--check diffs the parsed component API against a committed COMPONENT_API.json snapshot, assigns a semver bump to each change, and exits 1 when it finds a breaking change.

  1. Generate and commit the snapshot once: npx sveld --json, then commit COMPONENT_API.json.
  2. Add npx sveld --check to CI:
npx sveld --check
sveld --check: 2 API changes detected against "COMPONENT_API.json".
Suggested semver bump: major.

  Button
    [BREAKING] prop "target" added (required)
    [BREAKING] prop "href" removed

Removed props, events, or slots, and props that become required, are breaking (major). New optional props, new events, and widened union types are additive (minor). Changes to generics, @restProps, @extends, or context shapes are not classified further. If any of those changed, --check calls it breaking.

--check does not write the snapshot. Run sveld --json (or sveld --json --check) and commit the file when you want to update it. If there is no snapshot yet, --check prints a notice and exits 0.

Use --check=<path> to diff against a snapshot at a custom location (defaults to jsonOptions.outFile, or COMPONENT_API.json).

Node.js

You can also call sveld from Node.js. The package is ESM-only; require("sveld") does not work. Use import or dynamic import().

If no input is specified, sveld will infer the entry point based on the package.json#svelte field.

```js import { sveld } from "sveld"; import pkg from "./package.json" with { type: "json" };

const { diagnostics } = await sveld({ input: "./src/index.js", glob: true, markdown: true, markdownOptions: { onAppend: (type, document, components) => { if (type === "h1") document.append( "quote", `${components.size} components exported from ${pkg.name}@${

Extension points exported contracts — how you extend this code

Core symbols most depended-on inside this repo

Shape

Class 728
Function 221
Method 149
Interface 125

Languages

TypeScript100%

Modules by API surface

src/ComponentParser.ts142 symbols
src/writer/writer-ts-definitions-core.ts31 symbols
src/resolve-types.ts20 symbols
src/parse-entry-exports.ts20 symbols
src/plugin.ts18 symbols
src/check.ts18 symbols
src/bundle.ts17 symbols
src/parse-cache.ts15 symbols
src/writer/MarkdownWriterBase.ts14 symbols
src/writer/WriterMarkdown.ts11 symbols
src/ast-guards.ts11 symbols
src/writer/markdown-format-utils.ts9 symbols

Used by 2 indexed graphs manifest dependencies, hub-wide

For agents

$ claude mcp add sveld \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page