npm package: @qds.dev/tools
Description: Tools and utilities for Qwik Design System
Latest version: 0.14.3

# @qds.dev/tools

> Build tools and utilities for Qwik Design System

[![npm version](https://img.shields.io/npm/v/@qds.dev/tools)](https://www.npmjs.com/package/@qds.dev/tools)

## Overview

Building QDS packages requires specialized build-time transformations for icons, component patterns, and interactive playgrounds. Setting these up from scratch means dealing with AST parsing, virtual modules, and complex code generation. You need to transform icon JSX into inline SVG, hoist child props for polymorphic components, and extract prop metadata for documentation.

`@qds.dev/tools` provides ready-to-use Vite and Rolldown plugins that transform your code at build time. You get battle-tested transformations for icon management, asChild pattern implementation, and playground utilities without manual setup.

**What makes this useful:** These are the same build-time transformations QDS uses internally. If you're building Qwik libraries or need advanced code transformation utilities, you can reuse proven plugins rather than reinventing them.

## Installation

```bash
npm install @qds.dev/tools
```

**Note:** This is primarily for internal/advanced use. Most users don't need to install directly. Use the [`create-qds`](https://www.npmjs.com/package/create-qds) CLI instead, which includes these tools preconfigured.

## Quick Start

### Simplest case: Using the icons plugin

```typescript
// vite.config.ts
import { icons } from "@qds.dev/tools/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [icons()]
});
```

### Common pattern: Full plugin setup

```typescript
// rolldown.config.ts
import { icons, asChild } from "@qds.dev/tools/vite";
import { defineConfig } from "rolldown";

export default defineConfig({
  plugins: [icons(), asChild()]
});
```

## For Contributors

**If you're editing files in `libs/components/`, read this section first.**

When you edit a QDS component, several build plugins transform your code before it runs. Understanding these transformations prevents confusion when you see patterns like `<Lucide.Check />` or `asChild` in the codebase.

### Quick Reference

| Pattern You See                          | Plugin         | What Happens                            |
| ---------------------------------------- | -------------- | --------------------------------------- |
| `<Lucide.Check />`, `<Heroicons.Star />` | Icons          | Becomes inline `<svg>` at build time    |
| `<Button asChild><a href="...">`         | AsChild        | Child props hoisted to parent           |
| `component$()`                           | Qwik Optimizer | Not in @qds.dev/tools (built into Qwik) |

### When to Read More

- **Adding/changing icons?** See [Icons Plugin Transformation](#icons-plugin-jsx-to-svg-transformation)
- **Using asChild pattern?** See [AsChild Plugin Transformation](#aschild-plugin-prop-hoisting)
- **Build errors with icons?** Check that icon name matches [Iconify collection](https://icon-sets.iconify.design/)
- **Build errors with asChild?** Ensure exactly one child element exists

### When You Edit a Component File

Here's what happens when you save a `.tsx` file in `libs/components/`:

#### 1. Vite Dev Server Detects Change

```
File changed: libs/components/checkbox/checkbox-indicator.tsx
```

#### 2. Icons Plugin Runs (if icons present)

**Triggered by:** Import from `@qds.dev/ui` like `Lucide`, `Heroicons`, `Tabler`

```tsx
// Your code:
import { Lucide } from "@qds.dev/ui";
<Lucide.Check width={20} class="text-green-500" />;

// After icons plugin:
import __qds_i_lucide_check from "virtual:icons/lucide/check";
<svg
  width={20}
  class="text-green-500"
  height="1em"
  viewBox="0 0 24 24"
  dangerouslySetInnerHTML={__qds_i_lucide_check}
/>;
```

**Why this matters:** The icon component you write doesn't exist at runtime. It's replaced with an actual `<svg>` element. If you're debugging icon issues, check the transformed output in browser devtools.

#### 3. AsChild Plugin Runs (if asChild present)

**Triggered by:** `asChild` prop on any JSX element with exactly one child

```tsx
// Your code:
<Checkbox.Trigger asChild>
  <label class="flex items-center">Toggle</label>
</Checkbox.Trigger>

// After asChild plugin:
<Checkbox.Trigger jsxType="label" movedProps={{ "class": "flex items-center" }}>
  Toggle
</Checkbox.Trigger>
```

**Why this matters:** The child element's wrapper is removed. Props move to `movedProps`, element type moves to `jsxType`. The Checkbox.Trigger component receives these and renders the correct element type.

#### 4. Qwik Optimizer Runs

After @qds.dev/tools plugins finish, Qwik's optimizer extracts `component$()` bodies into lazy-loaded chunks. This is not part of @qds.dev/tools but happens next in the build pipeline.

#### 5. Browser Receives Transformed Code

What you wrote is NOT what runs in the browser. The transformations above happen at build time. Use browser devtools to see the actual rendered output.

### Common Debugging Scenarios

| Issue                                                       | Likely Cause                              | Solution                                                           |
| ----------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------ |
| Icon doesn't render                                         | Icon name not in collection               | Check [Iconify](https://icon-sets.iconify.design/) for valid names |
| Icon has wrong viewBox                                      | Icon collection uses different dimensions | Specify explicit `viewBox` prop                                    |
| asChild throws error                                        | More than one child element               | Ensure exactly one JSX child (whitespace is ignored)               |
| asChild props missing                                       | Props not on direct child                 | Move props to the immediate child element                          |
| Build error: "asChild elements must have exactly one child" | Multiple children or JSX expression       | Use single element child, not `{condition && <el>}` patterns       |

## How Plugins Transform Your Code

All transformations happen at build time. Your source code uses clean JSX patterns, and the plugins transform them into optimized runtime code.

### Icons Plugin: JSX-to-SVG Transformation

Transforms icon JSX elements (`<Lucide.Check />`) into inline SVG at build time with virtual imports.

**Before transformation (your code):**

```typescript
import { Lucide } from "@qds.dev/ui";

export default component$(() => {
  return <Lucide.Check width={24} class="text-green-500" />;
});
```

**After transformation (build output):**

```typescript
import __qds_i_lucide_check from 'virtual:icons/lucide/check';

export default component$(() => {
  return <svg width={24} class="text-green-500" height="1em" viewBox="0 0 24 24" dangerouslySetInnerHTML={__qds_i_lucide_check} />;
});
```

**What this does:**

- Transforms icon JSX into inline SVG elements
- Generates virtual imports for icon data
- Adds default dimensions (1em) if not specified
- Preserves all props (class, aria attributes, etc.)
- Deduplicates icon imports (multiple uses = one import)

### AsChild Plugin: Prop Hoisting

Hoists child element props to parent component when using asChild pattern for polymorphic rendering.

**Before transformation (your code):**

```typescript
export const Link = component$(() => {
  // Gives styles of button, but renders as the link child.
  // Useful when sharing styles OR existing logic inside of button that needs to be shared.
  return (
    <Button asChild>
      <a href="/test">Link</a>
    </Button>
  );
});
```

**After transformation (build output):**

```typescript
export const Link = component$(() => {
  return (
    <Button jsxType="a" movedProps={{ "href": "/test" }}>
      Link
    </Button>
  );
});
```

**What this does:**

- Removes child element wrapper
- Hoists child props to `movedProps` object
- Adds `jsxType` to identify element type
- Preserves grandchildren (text, nested elements)
- Supports conditional expressions (`isLink ? <a> : <button>`)

## API

| Export                         | Description                                                                                                |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| Main export (`@qds.dev/tools`) | Core type utilities (AsChildTypes)                                                                         |
| `/vite`                        | Mostly re-exported from rolldown to stay modular: `icons`, `asChild`, `inlineAsset`, `minifyContentPlugin` |
| `/rolldown`                    | Rolldown plugins: `icons`, `asChild`, `inlineAsset`, `inlineCssPlugin`, `qwikRolldown`                     |
| `/playground`                  | Prop extraction and scenario injection plugins for component playgrounds                                   |
| `/utils`                       | General utility functions for code transformation                                                          |

**Key plugins:**

### `icons(options?)`

Transforms icon JSX elements into inline SVG with virtual imports.

**Options:**

- `packs?: Record<string, { iconifyPrefix: string }>` - Custom icon pack configuration
- `importSources?: string[]` - Additional import sources to scan (default: `["@qds.dev/ui"]`)
- `debug?: boolean` - Enable debug logging

**Default icon packs:** Lucide, Heroicons, Tabler, Hugeicons, MaterialSymbols, AkarIcons

### `asChild(options?)`

Hoists child element props to parent component for polymorphic rendering.

**Options:**

- `debug?: boolean` - Enable debug logging

**Use case:** Implement polymorphic "as" pattern where components can render as different elements while preserving parent component logic.

### `playground(options)`

Factory function returning two plugins for interactive component playgrounds.

**Returns:** `[propExtraction(options), scenarioInjection()]`

**Options:**

- `componentsDir: string` - Directory containing component files
- `outputDir: string` - Directory for generated metadata JSON
- `debug?: boolean` - Enable debug logging

**What it does:**

- Extracts TypeScript prop types from component files → JSON metadata
- Injects scenario imports into MDX playground files
- Enables interactive component demos with prop controls

## Playground Utilities

The `/playground` export provides specialized plugins for building interactive component documentation.

### Usage Example

```typescript
// vite.config.ts
import { playground } from "@qds.dev/tools/playground";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    playground({
      componentsDir: "libs/components/src",
      outputDir: ".playground-metadata"
    })
  ]
});
```

### propExtraction Plugin

Extracts TypeScript prop types from component files and generates JSON metadata.

**Process:**

1. Reads component `.tsx` files from `componentsDir`
2. Parses TypeScript prop interfaces and types
3. Extracts JSDoc comments for prop descriptions
4. Generates JSON files in `outputDir`

**Output type:**

```typescript
interface ComponentMetadata {
  componentName: string;
  pieces: ComponentPiece[];
}

interface ComponentPiece {
  name: string;
  props: PropType[];
  jsdoc?: string;
}

interface PropType {
  name: string;
  type: string;
  required: boolean;
  description?: string;
}
```

### scenarioInjection Plugin

Transforms MDX files containing `<Playground />` components by auto-injecting scenario imports.

**Process:**

1. Scans for `<Playground />` in MDX files
2. Looks for adjacent `scenarios/` directory
3. Auto-imports scenario components and source code
4. Injects `scenarios` prop with component + code pairs

**Example MDX transformation:**

Before:

```mdx
# Button Component

<Playground component={Button} />
```

After (build time):

```mdx
import * as Scenario1 from "./scenarios/basic.tsx";
import Scenario1Code from "./scenarios/basic.tsx?raw";

<Playground
  component={Button}
  scenarios={[{ component: Scenario1, code: Scenario1Code }]}
/>
```

## Architecture

For package internals, dependency relationships, and design decisions, see [ARCHITECTURE.md](./ARCHITECTURE.md).

## Why This Pattern?

**Separate tools package:** Build utilities shouldn't be bundled with runtime code. Separating build-time tooling keeps package sizes small and allows sharing configurations across multiple packages.

**Build-time transformation:** Instead of runtime overhead for icon resolution or asChild logic, transformations happen during build. Your production bundle contains optimized code with zero transformation cost.

**Plugin architecture:** Vite and Rolldown plugins are composable. You can mix QDS plugins with your own custom build logic. Each plugin handles one concern (icons, transformations, metadata extraction) so you only include what you need.

## Related Packages

**Used by:**

- [`create-qds`](https://www.npmjs.com/package/create-qds) - CLI that scaffolds projects with these tools preconfigured
- All QDS packages - Internal build tooling for @qds.dev/ui, @qds.dev/motion, @qds.dev/code

**Complements:**

- [@qds.dev/ui](https://www.npmjs.com/package/@qds.dev/ui) - Uses these build tools for icon transformations
- [@qds.dev/base](https://www.npmjs.com/package/@qds.dev/base) - Uses these build tools for component patterns

**Build dependencies:**

- `vite` - Vite plugin compatibility
- `rolldown` - Rolldown plugin compatibility
- `oxc-*` - Fast JavaScript/TypeScript parsing and transformation

## Known Limitations

- **Internal package:** API may change without major version bumps. This package prioritizes QDS internal needs over public API stability.

- **Advanced use only:** Most users should use `create-qds` CLI instead, which configures these tools automatically. Direct usage requires understanding Vite/Rolldown plugin architecture.

- **Node.js only:** These are build-time tools, not browser utilities. They run during your build process, not in the browser.

- **Qwik-specific optimizations:** While plugins work with any Vite/Rolldown project, some features (like AsChild transformations) are designed for Qwik component patterns.

## Documentation

For usage within QDS projects, see the [QDS documentation](/docs). For general Qwik tooling questions, refer to [Qwik documentation](https://qwik.dev).

This is primarily internal tooling. For most use cases, start with `create-qds` which includes these tools preconfigured.