| name | custom-component-psu |
| description | 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. |
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:
- React/JavaScript source in a
Components\ or components\ folder.
- An
index.js entry point that calls UniversalDashboard.register('<type>', Component).
- A PowerShell module script (
.psm1) that registers bundled assets and exports New-UD* functions returning plugin hashtables.
- A module manifest (
.psd1) that exports the public functions.
- Webpack and Babel configuration that bundles the component but externalizes PSU-provided dependencies.
- A build script that creates an importable module output folder containing the bundle,
.psm1, and .psd1.
The runtime flow is:
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:
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.
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.
import UDSample from './sample';
UniversalDashboard.register('ud-sample', UDSample);
The registration name is the plugin type used by 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.
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.
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.
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.
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.
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.
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'
},
: {
: [, , , ]
}
};
};
Use a Babel configuration that supports JSX.
{
"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.
$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.
@{
RootModule = 'UniversalDashboard.MyComponent.psm1'
ModuleVersion = '1.0.0'
Author = 'Your Name'
Description = 'Custom PowerShell Universal component.'
FunctionsToExport = @('New-UDSample')
}
Implementation workflow
- Inspect the existing repository for component modules, package manager, webpack version, naming, and build conventions.
- Choose or confirm the command name, plugin type, props, endpoint events, child-content behavior, and state behavior.
- Add or update React components and register them from
Components\index.js.
- Add or update
New-UD* functions in the .psm1, including asset IDs, plugin hashtables, endpoint registration, and exported functions.
- Update the
.psd1, package manifest, build script, and documentation or examples.
- Run the smallest build or test that proves the component bundle and module output work.
- 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.
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.