# Custom Component Psu

> Creates custom PowerShell Universal App Framework components by pairing React/JavaScript components with PowerShell module cmdlets, asset registration, endpoint wiring, state handling, packaging, and validation. Use this skill when a user wants a reusable New-UD* component backed by custom JSX, npm libraries, or UniversalDashboard.register rather than a static frontend or ordinary PSU app script.

- Skill: `devolutions/custom-component-psu` (Agent Skill)
- Install (CLI): `npx skillmds@latest add devolutions/custom-component-psu`
- Raw SKILL.md: https://api.skillmd.com/api/skills/devolutions/custom-component-psu/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: devolutions (https://skillmd.com/u/devolutions)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/devolutions/custom-component-psu

---


# custom-component-psu

Use this skill when creating or modifying a custom PowerShell Universal (PSU) App Framework component: a reusable PowerShell module that exports one or more `New-UD*` functions and renders custom React/JavaScript through `UniversalDashboard.register`.

The goal is to produce a component module that can be imported into PSU and used inside `New-UDApp`, `New-UDPage`, `New-UDDynamic`, and other PSU App Framework content.

## When to use

- The user asks for a custom PSU, Universal Dashboard, or App Framework component.
- The output should be a reusable PowerShell module with one or more `New-UD*` commands.
- The component needs custom React, JSX, JavaScript, CSS, browser APIs, or npm packages.
- The component should wrap a third-party JavaScript library, such as Mermaid, Monaco, charting, maps, editors, visualization, media, or drag-and-drop libraries.
- The task involves `UniversalDashboard.register`, `withComponentFeatures`, `AssetService.RegisterAsset`, plugin hashtables, or custom endpoint props.

## Avoid this skill when

- The user wants a conventional React, Vue, Angular, Svelte, Vite, Tailwind, Bootstrap, or static frontend hosted by PSU published folders. Use `custom-frontend-psu` instead.
- The user only needs a PSU app page built from existing `New-UD*` components.
- The task is a WinForms-to-PSU app migration. Use `winforms-to-psu` instead.
- The task is only to install or run PSU. Use `install-sandbox-psu` when available.

## Gather requirements first

Ask only for details that materially affect implementation. Prefer sensible defaults when the repository or prompt already implies them.

- Component name, command name, and registration type, such as `New-UDMermaid` and `ud-mermaid`.
- Props and PowerShell parameters, including which are mandatory.
- Whether the component supports child content through `-Content` / `children`.
- Event scriptblocks, such as `-OnClick`, `-OnChange`, `-OnSubmit`, or custom callbacks.
- Whether the component must support `Get-UDElement`, `Set-UDElement`, `Add-UDElement`, `Clear-UDElement`, or `Sync-UDElement`.
- Required npm libraries and license constraints.
- Target PSU and PowerShell versions.
- Packaging target, such as local `output\`, a module folder, gallery-ready module, or existing repository structure.
- Validation environment, such as an existing PSU instance or disposable sandbox.

## Architecture

A custom PSU component normally has these pieces:

1. React/JavaScript source in a `Components\` or `components\` folder.
2. An `index.js` entry point that calls `UniversalDashboard.register('<type>', Component)`.
3. A PowerShell module script (`.psm1`) that registers bundled assets and exports `New-UD*` functions returning plugin hashtables.
4. A module manifest (`.psd1`) that exports the public functions.
5. Webpack and Babel configuration that bundles the component but externalizes PSU-provided dependencies.
6. A build script that creates an importable module output folder containing the bundle, `.psm1`, and `.psd1`.

The runtime flow is:

```text
New-UD* function -> plugin hashtable -> registered JavaScript asset -> registered React component -> browser DOM
```

The PowerShell function's `type` value must exactly match the string passed to `UniversalDashboard.register`.

## Recommended project structure

Use the repository's existing component layout if one exists. For a new module, prefer:

```text
UniversalDashboard.MyComponent\
  Components\
    index.js
    myComponent.jsx
  output\
  public\
  .babelrc
  component.build.ps1
  package.json
  webpack.config.js
  UniversalDashboard.MyComponent.psd1
  UniversalDashboard.MyComponent.psm1
```

Keep folder casing consistent. Some examples use `Components\` while webpack entries use `components\`; this works only on case-insensitive file systems. Use one casing everywhere.

## React component rules

Import `withComponentFeatures` from `universal-dashboard` and export the wrapped component.

```jsx
import React from 'react';
import { withComponentFeatures } from 'universal-dashboard';

const UDSample = (props) => {
  const { id, text } = props;

  return (
    <div id={id}>
      {text}
    </div>
  );
};

export default withComponentFeatures(UDSample);
```

Use React hooks normally, but ensure initialization and cleanup are tied to prop changes. When wrapping third-party libraries, clean up timers, listeners, observers, subscriptions, and library instances in the `useEffect` cleanup function.

## Component registration

Register the component once from the JavaScript entry point.

```javascript
import UDSample from './sample';

UniversalDashboard.register('ud-sample', UDSample);
```

The registration name is the plugin type used by PowerShell:

```powershell
@{
    type = 'ud-sample'
}
```

## PowerShell module rules

Register the built JavaScript bundle when the module loads and return a plugin hashtable from each `New-UD*` function.

```powershell
Get-ChildItem "$PSScriptRoot\*.js" | ForEach-Object {
    $Item = [UniversalDashboard.Services.AssetService]::Instance.RegisterAsset($_.FullName)
    if ($_.Name.StartsWith('index.') -and $_.Name.EndsWith('.bundle.js')) {
        $AssetId = $Item
    }
}

function New-UDSample {
    param(
        [Parameter()]
        [string]$Id = (New-Guid).ToString(),

        [Parameter(Mandatory)]
        [string]$Text
    )

    @{
        assetId = $AssetId
        isPlugin = $true
        type = 'ud-sample'
        id = $Id
        text = $Text
    }
}
```

Required plugin properties:

| Property | Requirement |
|---|---|
| `assetId` | The ID returned by `AssetService.RegisterAsset` for the bundled JavaScript. |
| `isPlugin` | Set to `$true`. |
| `type` | Must match `UniversalDashboard.register(...)`. |
| `id` | Unique component instance ID, usually defaulted to `(New-Guid).ToString()`. |

Additional hashtable keys become React props. Keep prop names stable and use simple JSON-serializable values when possible.

## Endpoint scriptblocks

For event parameters, use `[Endpoint]` and call `.Register($Id, $PSCmdlet)` before returning the hashtable.

```powershell
function New-UDSampleButton {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$Id = (New-Guid).ToString(),

        [Parameter()]
        [string]$Text,

        [Parameter()]
        [Endpoint]$OnClick
    )

    if ($OnClick) {
        $OnClick.Register($Id, $PSCmdlet)
    }

    @{
        assetId = $AssetId
        isPlugin = $true
        type = 'ud-sample-button'
        id = $Id
        text = $Text
        onClick = $OnClick
    }
}
```

Call the endpoint prop from JavaScript.

```jsx
import React from 'react';
import { withComponentFeatures } from 'universal-dashboard';

const UDSampleButton = (props) => {
  return (
    <button id={props.id} onClick={() => props.onClick?.()}>
      {props.text}
    </button>
  );
};

export default withComponentFeatures(UDSampleButton);
```

If the event has data, pass a serializable object to the endpoint function. Keep secrets and privileged operations on the server side.

## State and child content

Use `props.setState` when the component owns mutable browser state that should be visible to PSU through `Get-UDElement`.

```jsx
const onChange = (event) => {
  props.setState({ value: event.target.value });
};
```

For components that accept nested PSU content, pass through `props.children`. This preserves support for `Add-UDElement`, `Remove-UDElement`, and `Clear-UDElement`.

```jsx
const UDPanel = (props) => {
  return <section id={props.id}>{props.children}</section>;
};
```

Use stable `id` values for anything that will be read, updated, synced, or targeted from PSU.

## Webpack and Babel

Externalize dependencies provided by PSU so React and Universal Dashboard are not bundled twice.

```javascript
const path = require('path');

const BUILD_DIR = path.resolve(__dirname, 'public');

module.exports = (env) => {
  const isDev = env === 'development';

  return {
    entry: {
      index: path.resolve(__dirname, 'Components', 'index.js')
    },
    output: {
      path: BUILD_DIR,
      filename: isDev ? 'component.[name].bundle.js' : '[name].[contenthash].bundle.js',
      library: 'udcomponent',
      libraryTarget: 'var',
      publicPath: ''
    },
    module: {
      rules: [
        { test: /\.(js|jsx|mjs)$/, exclude: /public/, loader: 'babel-loader' },
        { test: /\.css$/, use: ['style-loader', 'css-loader'] }
      ]
    },
    externals: {
      UniversalDashboard: 'UniversalDashboard',
      react: 'react',
      'react-dom': 'reactdom'
    },
    resolve: {
      extensions: ['.js', '.jsx', '.mjs', '.json']
    }
  };
};
```

Use a Babel configuration that supports JSX.

```json
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ],
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-syntax-dynamic-import"
  ]
}
```

Match the config to the existing repository's Webpack and Babel versions. Do not upgrade build tooling unless the task requires it.

## Build script pattern

Create an output folder that contains only the importable module files and generated assets.

```powershell
$OutputPath = "$PSScriptRoot\output"

Remove-Item -Path $OutputPath -Force -ErrorAction SilentlyContinue -Recurse
Remove-Item -Path "$PSScriptRoot\public" -Force -ErrorAction SilentlyContinue -Recurse

npm install --legacy-peer-deps
npm run build

New-Item -Path $OutputPath -ItemType Directory | Out-Null

Copy-Item "$PSScriptRoot\public\*.*" $OutputPath
Copy-Item "$PSScriptRoot\UniversalDashboard.MyComponent.psd1" $OutputPath
Copy-Item "$PSScriptRoot\UniversalDashboard.MyComponent.psm1" $OutputPath
```

Do not copy source files, `.env` files, tests, local config, or development artifacts into the distributed output.

## Module manifest

Export only the public component commands.

```powershell
@{
    RootModule = 'UniversalDashboard.MyComponent.psm1'
    ModuleVersion = '1.0.0'
    Author = 'Your Name'
    Description = 'Custom PowerShell Universal component.'
    FunctionsToExport = @('New-UDSample')
}
```

## Implementation workflow

1. Inspect the existing repository for component modules, package manager, webpack version, naming, and build conventions.
2. Choose or confirm the command name, plugin type, props, endpoint events, child-content behavior, and state behavior.
3. Add or update React components and register them from `Components\index.js`.
4. Add or update `New-UD*` functions in the `.psm1`, including asset IDs, plugin hashtables, endpoint registration, and exported functions.
5. Update the `.psd1`, package manifest, build script, and documentation or examples.
6. Run the smallest build or test that proves the component bundle and module output work.
7. If a PSU runtime check is needed, use an existing PSU instance or the `install-sandbox-psu` skill.

## Troubleshooting checklist

- Component does not render: verify `type`, `UniversalDashboard.register`, `assetId`, `isPlugin = $true`, and that the generated bundle exists beside the `.psm1` in the output module.
- Browser console errors: inspect missing npm dependencies, externals, incorrect imports, JSX transpilation, or third-party library initialization.
- Props are missing: verify hashtable key names, serialization shape, and React prop casing.
- Endpoint does not fire: verify `[Endpoint]`, `$OnClick.Register($Id, $PSCmdlet)`, the returned hashtable key, and the JavaScript prop call.
- `Get-UDElement` is stale: call `props.setState` whenever browser-owned state changes.
- Child updates fail: render `props.children` directly for child-content containers.
- Works on Windows but not Linux: fix path separators in scripts and folder casing mismatches such as `Components` vs `components`.
- Build fails with OpenSSL errors on older webpack: set `NODE_OPTIONS=--openssl-legacy-provider` only when required by the installed toolchain.

## Security and reliability

- Do not embed PSU app tokens, secrets, credentials, or privileged data in JavaScript bundles or props.
- Validate and authorize server-side in endpoint scriptblocks and PSU APIs; hiding UI elements is not an authorization boundary.
- Keep long-running operations out of browser event handlers. Use PSU scripts/jobs and expose status through refreshable components or endpoints.
- Make event payloads small and serializable.
- Clean up browser resources in React effects to avoid memory leaks across page navigation and dynamic rerenders.

## Useful prompt pattern

When delegating component creation to another agent or tool, include PSU-specific constraints.

```text
Create a custom PowerShell Universal App Framework component module named UniversalDashboard.Sample. Export New-UDSample, register the React component as ud-sample with UniversalDashboard.register, wrap the React component with withComponentFeatures, externalize react, react-dom, and UniversalDashboard in webpack, register the generated bundle with AssetService, return a plugin hashtable with assetId, isPlugin, type, id, and props, register any [Endpoint] parameters with $Endpoint.Register($Id, $PSCmdlet), call props.setState for browser-owned state, and build an output folder containing the bundle, PSM1, and PSD1.
```

## Final response guidance

When reporting completion, include:

- The module and exported `New-UD*` command names.
- The JavaScript registration type or types.
- Props, endpoint events, state behavior, and child-content support.
- The build command and output folder.
- The validation performed, such as `npm run build`, module import, PSU sandbox render, browser console check, endpoint event test, or `Get-UDElement` test.

