.agents/skills/kbn-ui-package/SKILL.md
The kbn-ui system distributes Kibana UI components as versioned standalone packages for external consumers (e.g. Cloud UI). Each package lives under src/platform/kbn-ui/ and has two layers:
package.json, kibana.jsonc, src/, index.ts)packaging/) that bundles the source into a standalone .tgz via webpackCanonical reference: src/platform/kbn-ui/side-navigation/
Use AskUserQuestion to collect three values. packageName is always auto-derived — never ask the user for it.
| Variable | Example | Source |
|---|---|---|
sourcePath | src/platform/packages/private/kbn-grid-layout | User input |
folderName | grid-layout | User input |
packageName | @kbn/ui-grid-layout | Auto-derived: @kbn/ui-{folderName} |
description | Standalone Elastic grid layout component for non-Kibana applications | User input |
Questions to ask:
src/platform/packages/private/kbn-grid-layout)grid-layout — the package name will be @kbn/ui-{answer})Read these files before creating anything:
{sourcePath}/package.json → derive oldName (current workspace name, e.g. @kbn/grid-layout), peerDependencies, dependencies{sourcePath}/kibana.jsonc → derive owner, group{sourcePath}/index.ts → list all exported symbols (components, types, utilities)Run to find all internal @kbn/* imports used by the source (search the whole package, not just a src/ subdirectory — the source dir may have any name):
grep -roh "from '@kbn/[^']*'" {sourcePath} --include="*.ts" --include="*.tsx" \
--exclude-dir=node_modules --exclude-dir=target --exclude-dir=packaging | sort -u
Partition the results:
peerDependencies (consumer will provide it)packaging/react/services/)Count how many Kibana files will need import updates:
grep -r "from '{oldName}'" src/ x-pack/ packages/ --include="*.ts" --include="*.tsx" -l | wc -l
Show a summary and use AskUserQuestion to confirm before touching any files:
Moving: {sourcePath}/ → src/platform/kbn-ui/{folderName}/
Renaming: {oldName} → {packageName}
@kbn/* stubs to generate: [list from Phase 2]
Kibana files with imports to update: [count from Phase 2]
git mv {sourcePath} src/platform/kbn-ui/{folderName}
After moving, the source directory inside the package must always be named src/. Detect the actual source directory name — it is the non-metadata subdirectory (i.e. not packaging, target, __tests__, etc.):
# List top-level subdirectories in the moved package (excluding known non-source dirs)
ls -d src/platform/kbn-ui/{folderName}/*/ | grep -vE "/(packaging|target|node_modules)/$"
If the source directory is not named src, rename it:
git mv src/platform/kbn-ui/{folderName}/{actualDirName} src/platform/kbn-ui/{folderName}/src
Store the result as srcDir = "src" — all packaging templates must reference ../../src/ from inside packaging/react/.
Overwrite src/platform/kbn-ui/{folderName}/package.json with:
{
"name": "{packageName}",
"version": "1.0.0",
"private": true,
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
}
Keep owner, group, type, visibility from the moved file. Update only the id field to {packageName}.
mkdir -p src/platform/kbn-ui/{folderName}/packaging/scripts
mkdir -p src/platform/kbn-ui/{folderName}/packaging/react/services
packaging/package.json{
"name": "{packageName}",
"version": "0.1.0",
"private": true,
"description": "{description}",
"main": "index.js",
"types": "index.d.ts",
"files": [
"index.js",
"index.js.map",
"index.d.ts",
"metadata.json",
"package.json"
],
"peerDependencies": {
"@elastic/eui": ">=112.0.0",
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
},
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
}
Merge any additional peer deps found in the source package.json (e.g. @emotion/react, @emotion/css).
packaging/webpack.config.jsStart from the side-navigation template (src/platform/kbn-ui/side-navigation/packaging/webpack.config.js). Customize:
externals: one entry per peer dep — '@pkg/name': 'commonjs @pkg/name'alias: one entry per stubbed @kbn/* package, e.g.:
'@kbn/i18n$': path.resolve(__dirname, 'react/services/i18n.tsx'),
packaging/tsconfig.jsonStart from the side-navigation template. Update paths to match the alias map:
{
"paths": {
"@kbn/some-dep": ["./react/services/some-dep.ts"]
}
}
Update include to cover ../src/**/*.ts(x) relative to the new package root.
packaging/scripts/build.shCopy verbatim from src/platform/kbn-ui/side-navigation/packaging/scripts/build.sh. Update only the top comment line to reference {packageName}. The path resolution is fully relative and generic — no other changes needed.
packaging/react/index.tsxRe-export the main component under a distribution-friendly name alias. Always import from ../../src/ — never from the original source directory name. Derive component name, props type, and all re-exported types from the index.ts analysis in Phase 2:
/*
* [Elastic license header]
*/
// Build-time type validation
import './type_validation';
import React from 'react';
import { {SourceComponent}, type {SourceComponentProps} } from '../../src/{path-to-component}';
export type { /* all public types from ../../index.ts */ };
void React;
/** Alias for the external package. */
export type {ExportedComponentName}Props = {SourceComponentProps};
export const {ExportedComponentName} = (props: {ExportedComponentName}Props) => {
return <{SourceComponent} {...props} />;
};
packaging/react/types.tsWrite standalone inline type definitions (no @kbn/* or @elastic/eui imports):
index.tsIconType) with stringimport type * as React from 'react' allowedexport declare function {ExportedComponentName}(props: ...): React.ReactNode;packaging/react/type_validation.tsFollow the side-navigation pattern exactly:
Source prefix, packaged types with Packaged prefixconst _foo: PackagedType = {} as SourceType;@ts-expect-error for intentional simplifications (e.g. IconType → string)export const TYPE_VALIDATION_PASSED = true;mkdir -p src/platform/kbn-ui/{folderName}/packaging/example/src
mkdir -p src/platform/kbn-ui/{folderName}/packaging/example/public
The example is a minimal runnable app that imports from ../../target (the built package), so consumers can see the component in action without a full Kibana setup.
packaging/example/package.json — replace {folderName}:
{
"name": "{folderName}-example",
"version": "1.0.0",
"private": true,
"license": "SEE LICENSE IN LICENSE.txt",
"description": "Example application demonstrating {ExportedComponentName} usage. Uses dependencies from Kibana root.",
"scripts": {
"start": "./start.sh"
}
}
packaging/example/tsconfig.json — copy verbatim from side-navigation (it's fully generic).
packaging/example/webpack.config.js — copy from side-navigation, update only the alias:
alias: {
'{packageName}': path.resolve(__dirname, '../../target'),
},
packaging/example/start.sh — copy verbatim from side-navigation (fully relative, no substitution needed).
packaging/example/public/index.html — copy from side-navigation, update <title> to {ExportedComponentName} Example.
packaging/example/src/index.tsx — copy verbatim from side-navigation (generic React bootstrap).
packaging/example/src/app.tsx — generate a minimal working demo from the component's public API (derived from packaging/react/types.ts in step 4d):
EuiProvider'{packageName}' (the webpack alias resolves to ../../target)onChange, onItemClick) with useState and display the current value<EuiText> block listing manual test cases relevant to the componentpackaging/example/README.md — copy from side-navigation, substituting {packageName} and {folderName}.
For each @kbn/* package identified for stubbing in Phase 2:
Known stubs — copy directly from side-navigation:
@kbn/i18n and @kbn/i18n-react → copy src/platform/kbn-ui/side-navigation/packaging/react/services/i18n.tsx verbatimUnknown stubs — for each unfamiliar @kbn/* package:
index.ts (search under src/platform/packages/) to list named exportspackaging/react/services/{package-slug}.ts:
export const fnName = (..._args: unknown[]) => undefined as unknown as ReturnType;export const CONST_NAME = '';export const CONST_NAME = 0;export const CONST_NAME = false;export const CONST_NAME = {}; / []// Stub for @kbn/{name} — no-op implementation for standalone bundleFind and update every file importing the old package name:
# Collect affected files
grep -rl "from '${oldName}'" src/ x-pack/ packages/ --include="*.ts" --include="*.tsx"
# Replace static imports
find src/ x-pack/ packages/ -name "*.ts" -o -name "*.tsx" | \
xargs grep -l "from '${oldName}'" | \
xargs sed -i "s|from '${oldName}'|from '${packageName}'|g"
# Replace dynamic imports
find src/ x-pack/ packages/ -name "*.ts" -o -name "*.tsx" | \
xargs grep -l "import('${oldName}')" | \
xargs sed -i "s|import('${oldName}')|import('${packageName}')|g"
Also update kbn_references in tsconfig.json files:
grep -rl '"${oldName}"' src/ x-pack/ packages/ --include="tsconfig.json" | \
xargs sed -i "s|\"${oldName}\"|\"${packageName}\"|g"
ls {sourcePath} 2>/dev/null && echo "ERROR: old path still exists" || echo "OK: old path removed"
Check for any remaining tsconfig.json composite project references to the old path:
grep -rl '{sourcePath}' . --include="tsconfig.json" | head -5
Remove any stale references found.
Remind the engineer:
packaging/react/types.ts — EUI/complex type simplifications need manual verification.tgz in the consumer app (e.g. Cloud UI) before merging