diff --git a/.agents/skills/fix-package-docs/SKILL.md b/.agents/skills/fix-package-docs/SKILL.md new file mode 100644 index 0000000000000..15e8d0fb16717 --- /dev/null +++ b/.agents/skills/fix-package-docs/SKILL.md @@ -0,0 +1,309 @@ +--- +name: fix-package-docs +description: Fix API documentation issues in a Kibana plugin or package. Use when asked to fix, improve, or add JSDoc/API documentation for a Kibana plugin or package, or when check_package_docs validation fails. +disable-model-invocation: true +--- + +# Fix Package Docs + +Systematically find and fix all actionable API documentation issues in a Kibana plugin or package using `check_package_docs`. + +## Non-negotiable conventions + +- **Only add JSDoc to exported public API** — skip internal helpers that happen to be exported. +- **Never change runtime behavior** — documentation edits only (no logic, type, or signature changes). +- **Use `/** ... */` style** (not `//`). Single-line `/** Description. */` for simple items; multi-line for complex ones. +- **Descriptions are sentences**: start with a capital letter, end with a period. +- **`@param name - Description.`** (hyphen separator, not colon). Match every parameter in the signature. +- **Destructured object parameters**: document nested properties with dot-notation tags (e.g., `@param options.foo - Description.`). The tooling supports arbitrary nesting (e.g., `@param fns.fn1.foo.param`). Each level must have its own `@param` tag. +- **`@returns` for non-void functions**: always present; omit only for `void`/`Promise`. +- **Use `{@link OtherType}`** for cross-references to other Kibana types. +- **`missingExports`** items need human judgment to decide whether to export — skip them and note in PR. + +## Workflow + +### 1. Resolve the target + +Given a plugin ID (e.g., `dashboard`), manifest ID (e.g., `@kbn/dashboard-plugin`), or file path: + +- Plugin ID → use `--plugin ` +- Manifest ID / package name → use `--package ` +- File path → find its package first (look for nearest `kibana.jsonc`) + +To confirm: search for the `plugin.id` in `kibana.jsonc` files if unsure. + +```bash +grep -r '"id": "dashboard"' --include="kibana.jsonc" -l +``` + +### 2. Generate the stats file + +```bash +node scripts/check_package_docs.js --plugin --write +# or for packages: +node scripts/check_package_docs.js --package --write +``` + +This writes `/target/api_docs/stats.json` with structured issue data. Exit code 1 is expected when issues exist — that's normal. + +### 3. Read and plan + +Read the stats file: + +``` +/target/api_docs/stats.json +``` + +The file has this shape: + +```json +{ + "counts": { + "apiCount": 145, + "missingComments": 28, + "missingReturns": 10, + "paramDocMismatches": 5, + "missingComplexTypeInfo": 8, + "isAnyType": 3, + "noReferences": 144, + "missingExports": 11, + "unnamedExports": 2 + }, + "missingComments": [ + { "id": "dashboard.SomeType", "label": "SomeType", "path": "src/.../types.ts", "type": "Interface", "lineNumber": 42, "link": "..." } + ], + "missingReturns": [...], + "paramDocMismatches": [...], + "missingComplexTypeInfo": [...], + "isAnyType": [...], + "noReferences": [...], + "missingExports": [{ "source": "...", "references": [...] }], + "unnamedExports": [{ "pluginId": "dashboard", "scope": "public", "path": "src/.../index.ts", "lineNumber": 12, "textSnippet": "export default { ... }" }] +} +``` + +Report a summary of the issues in a table before proceeding with fixes. + +Group all issues by `path` so you edit each file once. Prioritize: +1. `missingComments` — highest volume, most impact +2. `missingReturns` — quick wins (add `@returns` to existing JSDoc) +3. `paramDocMismatches` — add missing `@param` tags so all params are documented +4. `missingComplexTypeInfo` — add JSDoc to undocumented interface, object, and union type declarations +5. `isAnyType` — replace `any` with specific types (careful: may require reading more context) +6. `unnamedExports` — skip; flag for human review (requires restructuring exports, which changes the public API surface) +7. `missingExports` — skip; flag for human review +8. `noReferences` — informational only; not a validation failure, no action required + +### 4. Fix issues file by file + +For each file, read it fully first, then make all edits in one pass. + +#### missingComments — add JSDoc above the declaration + +**Functions/methods:** +```typescript +/** + * Cleans filters before serialization by removing empty arrays and null values. + * + * @param filters - Array of filter objects to sanitize. + * @returns Cleaned filter array safe for serialization. + */ +export function cleanFiltersForSerialize(filters: Filter[]): Filter[] { +``` + +**Interfaces/types:** +```typescript +/** + * Parameters for retrieving a dashboard by its saved object ID. + */ +export interface GetDashboardParams { +``` + +**Interface properties** (inline `/** ... */`): +```typescript +export interface DashboardLocatorParams { + /** The saved object ID of the dashboard to navigate to. */ + dashboardId?: string; + /** When true, the dashboard opens in view mode. */ + viewMode?: boolean; +} +``` + +**Constants/variables:** +```typescript +/** Maximum number of panels allowed on a single dashboard. */ +export const MAX_PANELS = 100; +``` + +**Classes:** +```typescript +/** + * Provides the public API for the Dashboard plugin. + */ +export class DashboardPlugin implements Plugin { +``` + +#### missingReturns — add `@returns` to existing JSDoc + +Find the existing JSDoc block and add the `@returns` line before the closing `*/`. Match the actual return type: + +```typescript +/** + * Retrieves the locator params for the current dashboard state. + * + * @param state - Current dashboard application state. + * @returns Locator params derived from the state, suitable for deep-linking. + */ +``` + +#### paramDocMismatches — complete partial `@param` documentation + +This flag means some (but not all) parameters already have `@param` tags — the function has inconsistent docs. Functions where *no* params are documented are not flagged here; they fall under `missingComments` instead. + +The fix is always to add the missing `@param` tags so every parameter is covered. Read the function signature and add a `@param` for each undocumented parameter: + +```typescript +// Before: only `id` is documented, `includeRoles` and `timeout` are missing +/** + * Fetches user data from the API. + * + * @param id - The user ID. + */ +export const getUser = (id: string, includeRoles: boolean, timeout?: number): Promise => { /* ... */ }; + +// After: all params documented +/** + * Fetches user data from the API. + * + * @param id - The user ID. + * @param includeRoles - When true, includes the user's assigned roles in the response. + * @param timeout - Optional request timeout in milliseconds. + */ +export const getUser = (id: string, includeRoles: boolean, timeout?: number): Promise => { /* ... */ }; +``` + +Also remove any stale `@param` entries for parameters that no longer exist in the signature. + +**Destructured object parameters** — document each nested property with dot-notation tags. Every level of nesting needs its own `@param`: + +```typescript +/** + * Runs a search with the given options. + * + * @param query - The query object. + * @param query.text - The search string. + * @param query.language - Query language (e.g., `kuery`, `lucene`). + * @param options - Runtime options. + * @param options.signal - Abort signal for cancellation. + */ +export const runSearch = ( + query: { text: string; language: string }, + options: { signal: AbortSignal }, +) => { /* ... */ }; +``` + +For deeply nested properties, continue the dot chain as far as needed (e.g., `@param fns.fn1.foo.param - Description.`). + +#### missingComplexTypeInfo — add JSDoc to undocumented complex type declarations + +A prioritized subset of `missingComments` for type declarations that lack a top-level JSDoc description. Flagged: interfaces, inline object types, and union/intersection types. Excluded: primitives and functions (self-documenting or tracked elsewhere). Fixing one reduces both `missingComplexTypeInfo` and `missingComments` counts. + +The fix is to add a JSDoc block to the **type declaration itself**: + +```typescript +// Before — no description on SearchOptions or FilterSpec +export interface SearchOptions { + query: string; + filters: FilterSpec; +} + +export type FilterSpec = { + field: string; + operator: 'eq' | 'neq'; +}; + +// After +/** + * Options for configuring a search request. + */ +export interface SearchOptions { + query: string; + filters: FilterSpec; +} + +/** + * Describes a single filter condition applied to a search query. + */ +export type FilterSpec = { + field: string; + operator: 'eq' | 'neq'; +}; +``` + +Inline property docs (e.g., `/** ... */` on each field) are a separate concern covered under `missingComments` for interface members. + +#### unnamedExports — flag for human review + +This flags an exported declaration that has no identifiable name — meaning `ts-morph` cannot call `getName()` on the node. The most common cause is an anonymous `export default` expression (e.g., `export default { ... }` or `export default function() { ... }`). + +**Do not attempt to fix these.** Naming an anonymous default export or removing `export default` changes the module's public API surface, which is a runtime behavior change outside the scope of documentation fixes. Report them in the PR for a developer to handle. + +#### isAnyType — replace `any` with specific types + +Read the context to understand the actual type, then replace. Common patterns: + +```typescript +// Before +callback: (result: any) => void +// After +callback: (result: DashboardCreationResult) => void +``` + +If the correct type is genuinely unknown, use `unknown` instead of `any`. Only change if you're confident — don't guess. + +### 5. After each file, verify locally (optional) + +For large plugins, re-run after batches of files to track progress: + +```bash +node scripts/check_package_docs.js --plugin --write +``` + +Read the updated `stats.json` counts to confirm the numbers are going down. + +### 6. Final verification + +When all actionable issues are addressed: + +```bash +node scripts/check_package_docs.js --plugin +``` + +Confirm `All packages passed validation.` (or only `missingExports` remain, which are pending human review). + +Then run: + +```bash +node scripts/check_changes.ts +``` + +### 7. PR notes + +In the PR description, include: +- Before/after issue counts from `stats.json` +- Any `missingExports` or `unnamedExports` skipped (always skipped — flag for a developer) +- Any `isAnyType` items skipped because the correct type was ambiguous + +## Example: full run on the dashboard plugin + +```bash +# Generate stats +node scripts/check_package_docs.js --plugin dashboard --write +# Read src/platform/plugins/shared/dashboard/target/api_docs/stats.json with the Read tool + +# Fix all actionable issues across the plugin files per the rules above + +# Verify +node scripts/check_package_docs.js --plugin dashboard +# → "All packages passed validation." +``` diff --git a/packages/kbn-docs-utils/src/check_package_docs_cli.ts b/packages/kbn-docs-utils/src/check_package_docs_cli.ts index 489703bf4e8ba..dbf1ac5d426e0 100644 --- a/packages/kbn-docs-utils/src/check_package_docs_cli.ts +++ b/packages/kbn-docs-utils/src/check_package_docs_cli.ts @@ -27,6 +27,7 @@ import { } from './cli'; import type { PluginOrPackage, MissingApiItemMap } from './types'; import type { AllPluginStats } from './cli/types'; +import { writeFlatStatsFiles } from './cli/tasks/flat_stats'; type ValidationCheck = 'any' | 'comments' | 'exports' | 'unnamed'; @@ -143,6 +144,10 @@ export const runCheckPackageDocs = async (log: ToolingLog, flags: CliFlags) => { optionsWithChecks ); + if (optionsWithChecks.writeStats) { + writeFlatStatsFiles(setupResult.plugins, apiMapResult, allPluginStats); + } + reportMetrics(setupResult, apiMapResult, allPluginStats, log, transaction, { ...optionsWithChecks, stats: checks, @@ -189,11 +194,13 @@ export const runCheckPackageDocsCli = () => { }, flags: { string: ['plugin', 'package', 'check'], + boolean: ['write'], help: ` --plugin Optionally, run for only a specific plugin by its plugin ID (plugin.id in kibana.jsonc). --package Optionally, run for only a specific package by its package ID (id in kibana.jsonc, e.g., @kbn/core). --check Optional. Specify validation checks: any, comments, exports, or all (default). Can be provided multiple times to combine checks. + --write Write stats to a flat JSON file in each plugin's target/api_docs/ directory. `, }, } diff --git a/packages/kbn-docs-utils/src/cli/parse_cli_flags.ts b/packages/kbn-docs-utils/src/cli/parse_cli_flags.ts index 580a7671b9f8a..3bf8005893296 100644 --- a/packages/kbn-docs-utils/src/cli/parse_cli_flags.ts +++ b/packages/kbn-docs-utils/src/cli/parse_cli_flags.ts @@ -108,6 +108,7 @@ const normalizeStats = (value: unknown | string[]) => { */ export function parseCliFlags(flags: CliFlags): CliOptions { const collectReferences = flags.references === true; + const writeStats = flags.write === true; const pluginFilter = dedupe(normalizeStringList(flags.plugin, 'plugin')); const packageFilter = dedupe(normalizeStringList(flags.package, 'package')); const rawStats = normalizeStats(flags.stats); @@ -122,5 +123,6 @@ export function parseCliFlags(flags: CliFlags): CliOptions { stats, pluginFilter, packageFilter, + writeStats, }; } diff --git a/packages/kbn-docs-utils/src/cli/tasks/flat_stats.ts b/packages/kbn-docs-utils/src/cli/tasks/flat_stats.ts new file mode 100644 index 0000000000000..3f5ec682630e5 --- /dev/null +++ b/packages/kbn-docs-utils/src/cli/tasks/flat_stats.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import fs from 'fs'; +import Path from 'path'; + +import type { + ApiDeclaration, + MissingApiItemMap, + PluginOrPackage, + UnnamedExport, +} from '../../types'; +import type { AllPluginStats, BuildApiMapResult } from '../types'; +import { getLink } from './get_link'; + +/** Shape of a single stat entry in the flat JSON output. */ +export interface FlatStatEntry { + id: string; + label: string; + path: string; + type: ApiDeclaration['type']; + lineNumber?: number; + columnNumber?: number; + link: string; +} + +/** Shape of a missing-export entry in the flat JSON output. */ +export interface FlatMissingExportEntry { + source: string; + references: string[]; +} + +/** Shape of an unnamed-export entry in the flat JSON output. */ +export type FlatUnnamedExportEntry = Pick< + UnnamedExport, + 'pluginId' | 'scope' | 'path' | 'lineNumber' | 'textSnippet' +>; + +/** Complete flat stats JSON written per plugin/package. */ +export interface FlatStats { + counts: { + apiCount: number; + missingExports: number; + missingComments: number; + isAnyType: number; + noReferences: number; + missingReturns: number; + paramDocMismatches: number; + missingComplexTypeInfo: number; + unnamedExports: number; + }; + missingComments: FlatStatEntry[]; + isAnyType: FlatStatEntry[]; + noReferences: FlatStatEntry[]; + missingReturns: FlatStatEntry[]; + paramDocMismatches: FlatStatEntry[]; + missingComplexTypeInfo: FlatStatEntry[]; + missingExports: FlatMissingExportEntry[]; + unnamedExports: FlatUnnamedExportEntry[]; +} + +const mapStat = (dec: ApiDeclaration): FlatStatEntry => ({ + id: dec.id, + label: dec.label, + path: dec.path, + type: dec.type, + lineNumber: dec.lineNumber, + columnNumber: dec.columnNumber, + link: getLink(dec), +}); + +export const buildFlatStatsForPlugin = ( + pluginId: string, + pluginStats: AllPluginStats[string], + missingApiItems: MissingApiItemMap +): FlatStats => { + const pluginMissing = missingApiItems[pluginId] ?? {}; + const missingExportsSources = Object.keys(pluginMissing); + const missingExportsCount = missingExportsSources.length; + const missingExportsList = missingExportsSources.map((source) => ({ + source, + references: pluginMissing[source], + })); + + const unnamedExportsList: FlatUnnamedExportEntry[] = (pluginStats.unnamedExports ?? []).map( + ({ pluginId: itemPluginId, scope, path, lineNumber, textSnippet }) => ({ + pluginId: itemPluginId, + scope, + path, + lineNumber, + textSnippet, + }) + ); + + return { + counts: { + apiCount: pluginStats.apiCount, + missingExports: missingExportsCount, + missingComments: pluginStats.missingComments.length, + isAnyType: pluginStats.isAnyType.length, + noReferences: pluginStats.noReferences.length, + missingReturns: pluginStats.missingReturns.length, + paramDocMismatches: pluginStats.paramDocMismatches.length, + missingComplexTypeInfo: pluginStats.missingComplexTypeInfo.length, + unnamedExports: unnamedExportsList.length, + }, + missingComments: pluginStats.missingComments.map(mapStat), + isAnyType: pluginStats.isAnyType.map(mapStat), + noReferences: pluginStats.noReferences.map(mapStat), + missingReturns: pluginStats.missingReturns.map(mapStat), + paramDocMismatches: pluginStats.paramDocMismatches.map(mapStat), + missingComplexTypeInfo: pluginStats.missingComplexTypeInfo.map(mapStat), + missingExports: missingExportsList, + unnamedExports: unnamedExportsList, + }; +}; + +export const writeFlatStatsFiles = ( + plugins: PluginOrPackage[], + apiMapResult: BuildApiMapResult, + allPluginStats: AllPluginStats +) => { + for (const plugin of plugins) { + const stats = allPluginStats[plugin.id]; + if (!stats) { + continue; + } + const flat = buildFlatStatsForPlugin(plugin.id, stats, apiMapResult.missingApiItems); + const pluginTargetDir = Path.resolve(plugin.directory, 'target', 'api_docs'); + fs.mkdirSync(pluginTargetDir, { recursive: true }); + const target = Path.join(pluginTargetDir, 'stats.json'); + fs.writeFileSync(target, JSON.stringify(flat, null, 2)); + } +}; diff --git a/packages/kbn-docs-utils/src/cli/tasks/get_link.ts b/packages/kbn-docs-utils/src/cli/tasks/get_link.ts new file mode 100644 index 0000000000000..a881d95f78250 --- /dev/null +++ b/packages/kbn-docs-utils/src/cli/tasks/get_link.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { ApiDeclaration } from '../../types'; + +/** + * Generates a link to the GitHub source for an API declaration. + * + * When a `lineNumber` is available, produces a direct `#L42`-style anchor. + * Otherwise falls back to a text-fragment search (`#:~:text=...`). + * + * TODO: clintandrewhall - allow `base` to be overridden in the instance of a CI build + * associated with a PR. + * + * @param declaration - API declaration to generate link for. + * @returns GitHub link to the source code. + */ +export const getLink = (declaration: ApiDeclaration): string => { + const base = `https://github.com/elastic/kibana/blob/main/${declaration.path}`; + if (declaration.lineNumber) { + return `${base}#L${declaration.lineNumber}`; + } + return `https://github.com/elastic/kibana/tree/main/${ + declaration.path + }#:~:text=${encodeURIComponent(declaration.label)}`; +}; diff --git a/packages/kbn-docs-utils/src/cli/tasks/report_metrics.ts b/packages/kbn-docs-utils/src/cli/tasks/report_metrics.ts index 1bb3da436de88..76a507a651739 100644 --- a/packages/kbn-docs-utils/src/cli/tasks/report_metrics.ts +++ b/packages/kbn-docs-utils/src/cli/tasks/report_metrics.ts @@ -10,27 +10,8 @@ import type { Transaction } from 'elastic-apm-node'; import type { ToolingLog } from '@kbn/tooling-log'; import { CiStatsReporter } from '@kbn/ci-stats-reporter'; -import type { ApiDeclaration } from '../../types'; import type { AllPluginStats, BuildApiMapResult, CliOptions, SetupProjectResult } from '../types'; - -/** - * Generates a link to the GitHub source for an API declaration. - * - * TODO: clintandrewhall - allow `base` to be overridden in the instance of a CI build - * associated with a PR. - * - * @param declaration - API declaration to generate link for. - * @returns GitHub link to the source code. - */ -function getLink(declaration: ApiDeclaration): string { - const base = `https://github.com/elastic/kibana/blob/main/${declaration.path}`; - if (declaration.lineNumber) { - return `${base}#L${declaration.lineNumber}`; - } - return `https://github.com/elastic/kibana/tree/main/${ - declaration.path - }#:~:text=${encodeURIComponent(declaration.label)}`; -} +import { getLink } from './get_link'; /** * Reports metrics to CI stats and logs validation results. diff --git a/packages/kbn-docs-utils/src/cli/types.ts b/packages/kbn-docs-utils/src/cli/types.ts index ea6bfb18cd878..8f43d1bdba302 100644 --- a/packages/kbn-docs-utils/src/cli/types.ts +++ b/packages/kbn-docs-utils/src/cli/types.ts @@ -38,6 +38,8 @@ export interface CliFlags { plugin?: string | string[]; /** Package filter: single package ID or array of package IDs (id from kibana.jsonc). */ package?: string | string[]; + /** Whether to write stats to a flat JSON file. */ + write?: boolean; } /** @@ -52,6 +54,8 @@ export interface CliOptions { pluginFilter?: string[]; /** Package filter IDs (id from kibana.jsonc, e.g., @kbn/package-name). */ packageFilter?: string[]; + /** Whether to write stats to a flat JSON file. */ + writeStats?: boolean; } /** diff --git a/packages/kbn-docs-utils/src/types.ts b/packages/kbn-docs-utils/src/types.ts index 8016cda471e72..c473aa0b012a0 100644 --- a/packages/kbn-docs-utils/src/types.ts +++ b/packages/kbn-docs-utils/src/types.ts @@ -317,8 +317,8 @@ export interface ApiStats { /** * Represents an exported declaration that has no identifiable name. - * This typically occurs when a JSDoc-style comment appears above a line - * that isn't a proper declaration. + * This typically occurs with anonymous `export default` expressions (e.g., + * `export default { ... }` or `export default function() { ... }`). */ export interface UnnamedExport { pluginId: string; diff --git a/packages/kbn-json-ast/src/compiler_options.ts b/packages/kbn-json-ast/src/compiler_options.ts index b83347589d00f..7975bd9c5ec6c 100644 --- a/packages/kbn-json-ast/src/compiler_options.ts +++ b/packages/kbn-json-ast/src/compiler_options.ts @@ -28,6 +28,16 @@ export function getCompilerOptions(ast: T.ObjectExpression) { return compilerOptions.value; } +/** + * Sets a compiler option in a JSONC tsconfig source string. If the + * `compilerOptions` property does not exist, it is created. If the option + * already exists, its value is replaced. + * @param source - The JSONC tsconfig source text to modify. + * @param name - The compiler option name to set. + * @param value - The compiler option value. Intentionally typed as `any` because valid + * compiler option values include booleans, strings, numbers, arrays, and objects. + * @returns The modified JSONC source text. + */ export function setCompilerOption(source: string, name: string, value: any) { const ast = getAst(source); if (!getProp(ast, 'compilerOptions')) { @@ -78,6 +88,13 @@ export function setCompilerOption(source: string, name: string, value: any) { return left + `,\n ${JSON.stringify(name)}: ${redentJson(value, ' ')}` + right; } +/** + * Removes a compiler option from a JSONC tsconfig source string. Throws if the + * `compilerOptions` block or the named option does not exist. + * @param source - The JSONC tsconfig source text to modify. + * @param name - The compiler option name to remove. + * @returns The modified JSONC source text. + */ export function removeCompilerOption(source: string, name: string) { const ast = getAst(source); const compilerOptions = getCompilerOptions(ast); diff --git a/packages/kbn-json-ast/src/extends.ts b/packages/kbn-json-ast/src/extends.ts index 19d23c3be1c29..9d6ff1278157f 100644 --- a/packages/kbn-json-ast/src/extends.ts +++ b/packages/kbn-json-ast/src/extends.ts @@ -9,6 +9,13 @@ import { setProp } from './props'; +/** + * Sets the `extends` property at the top of a JSONC tsconfig source string. + * If the property already exists, its value is replaced. + * @param jsonc - The JSONC tsconfig source text to modify. + * @param value - The path to set as the `extends` value. + * @returns The modified JSONC source text. + */ export function setExtends(jsonc: string, value: string) { return setProp(jsonc, 'extends', value, { insertAtTop: true, diff --git a/packages/kbn-json-ast/src/props.ts b/packages/kbn-json-ast/src/props.ts index 685d4a7f2cd65..f1b738cdd53f8 100644 --- a/packages/kbn-json-ast/src/props.ts +++ b/packages/kbn-json-ast/src/props.ts @@ -28,14 +28,18 @@ export function getEndOfLastProp(obj: T.ObjectExpression) { } /** - * Removes a property from a JSONc object. If the property does not exist the source is just returned + * Removes a named property from a JSONC object. If the property does not exist + * the source is returned unchanged. + * @param source - The JSONC source text to modify. + * @param key - The property key to remove. + * @param opts - Optional configuration. + * @param opts.node - The AST node of the object to modify. When provided, skips + * re-parsing `source`. + * @returns The modified JSONC source text, or the original if the property does not exist. */ export function removeProp( - /** the jsonc to modify */ source: string, - /** The key to set */ key: string, - /** extra key-value options */ opts?: { node?: T.ObjectExpression; } @@ -49,22 +53,32 @@ export function removeProp( return snip(source, [getExpandedEnds(source, prop)]); } +/** + * Sets a property in a JSONC source string. If the property already exists its + * value is replaced; otherwise it is inserted according to the `opts` configuration. + * @param source - The JSONC source text to modify. + * @param key - The property key to set. + * @param value - The property value. Intentionally typed as `any` because valid JSON + * property values include booleans, strings, numbers, arrays, and objects. + * @param opts - Optional configuration. + * @param opts.insertAtTop - When `true`, new properties are inserted at the top of the + * object rather than at the bottom. Defaults to `false`. + * @param opts.insertAfter - An existing property node after which the new property is + * inserted. Takes precedence over `insertAtTop` when the key is new. + * @param opts.node - The AST node of the object to modify. When provided, skips + * re-parsing `source`. + * @param opts.spaces - Overrides the default `" "` indentation used for new or + * multi-line properties. + * @returns The modified JSONC source text. + */ export function setProp( - /** the jsonc to modify */ source: string, - /** The key to set */ key: string, - /** the value of the key */ value: any, - /** extra key-value options */ opts?: { - /** by default, if the key isn't already in the json, it will be added at the bottom. Set this to true to add the key at the top instead */ insertAtTop?: boolean; - /** by default, if the key isn't already in the json, it will be added at the bottom. Set this to an existing property node to have the key added after this node */ insertAfter?: T.ObjectProperty; - /** In order to set the property an object other than the root object, parse the source and pass the node of the desired object here (make sure to also pass spaces) */ node?: T.ObjectExpression; - /** This overrides the default " " spacing used for multi line or new properties that are added */ spaces?: string; } ) { @@ -90,6 +104,12 @@ export function setProp( return snip(source, [[...getEnds(prop), newPropJson]]); } +/** + * Parses a JSONC source string and returns the AST node for a named property. + * @param source - The JSONC source text to parse. + * @param name - The property name to look up. + * @returns The matching `ObjectProperty` node, or `undefined` if not found. + */ export function getPropFromSource(source: string, name: string) { return getProp(getAst(source), name); } diff --git a/packages/kbn-json-ast/src/references.ts b/packages/kbn-json-ast/src/references.ts index 3a4e78d44b335..ad524c4b158de 100644 --- a/packages/kbn-json-ast/src/references.ts +++ b/packages/kbn-json-ast/src/references.ts @@ -15,6 +15,13 @@ import { snip } from './snip'; const PROP = 'kbn_references'; +/** + * Adds package IDs to the `kbn_references` array in a JSONC source string. + * If the `kbn_references` property does not exist, it is created. + * @param source - The JSONC source text to modify. + * @param refsToAdd - The package IDs to add. + * @returns The modified JSONC source text. + */ export function addReferences(source: string, refsToAdd: string[]) { const ast = getAst(source); @@ -57,6 +64,12 @@ export function addReferences(source: string, refsToAdd: string[]) { return source.slice(0, start) + refsSrc + source.slice(end); } +/** + * Removes the entire `kbn_references` property from a JSONC source string. + * If the property does not exist, the source is returned unchanged. + * @param source - The JSONC source text to modify. + * @returns The modified JSONC source text. + */ export function removeAllReferences(source: string) { const ast = getAst(source); const existing = getProp(ast, PROP); @@ -66,6 +79,13 @@ export function removeAllReferences(source: string) { return snip(source, [getExpandedEnds(source, existing)]); } +/** + * Removes specific entries from the `kbn_references` array in a JSONC source string. + * Throws if the property does not exist or any of the specified refs are not found. + * @param source - The JSONC source text to modify. + * @param refs - The package IDs to remove. + * @returns The modified JSONC source text. + */ export function removeReferences(source: string, refs: string[]) { const ast = getAst(source); @@ -88,6 +108,14 @@ export function removeReferences(source: string, refs: string[]) { ); } +/** + * Replaces object-style reference entries (those with a `path` property) in + * `kbn_references` with plain package ID strings. + * @param source - The JSONC source text to modify. + * @param replacements - Tuples of `[path, pkgId]` where `path` identifies the + * existing object entry to replace and `pkgId` is the string to replace it with. + * @returns The modified JSONC source text. + */ export function replaceReferences( source: string, replacements: Array<[path: string, pkgId: string]> diff --git a/packages/kbn-json-ast/src/snip.ts b/packages/kbn-json-ast/src/snip.ts index 13b9f996a2c84..a3bbb40c0f675 100644 --- a/packages/kbn-json-ast/src/snip.ts +++ b/packages/kbn-json-ast/src/snip.ts @@ -10,7 +10,13 @@ type Snip = [start: number, end: number] | [start: number, end: number, replacement: string]; /** - * Replace or remove specific points of the source code + * Replaces or removes specific character ranges in a source string. Snips are + * applied in order and non-overlapping deletion snips are automatically merged. + * @param source - The source text to modify. + * @param snips - An array of `[start, end]` ranges to delete or `[start, end, + * replacement]` tuples to replace with `replacement`. Ranges must be + * non-reversed (`start <= end`), and replacement snips must not overlap. + * @returns The modified source text. */ export function snip(source: string, snips: Snip[]) { const queue = snips diff --git a/src/platform/packages/shared/kbn-rison/kbn_rison.ts b/src/platform/packages/shared/kbn-rison/kbn_rison.ts index 88e31e9d6d5c4..19a53ff4c977d 100644 --- a/src/platform/packages/shared/kbn-rison/kbn_rison.ts +++ b/src/platform/packages/shared/kbn-rison/kbn_rison.ts @@ -11,6 +11,10 @@ // eslint-disable-next-line @kbn/eslint/module_migration import Rison from 'rison-node'; +/** + * Any value that can be represented in RISON — a superset of JSON primitives + * that also supports nested objects and arrays. + */ export type RisonValue = | boolean | string @@ -19,12 +23,28 @@ export type RisonValue = | { [key: string]: RisonValue } | null; +/** + * RISON-encode a JavaScript value, returning `undefined` when the value cannot + * be represented in RISON (e.g. functions, symbols, or circular references). + * Use {@link encode} when the input is expected to always be encodable. + * + * @param obj - The value to encode. Typed as `any` because this function is + * intentionally used to probe whether an arbitrary runtime value is + * RISON-encodable before committing to encoding it. + * @returns The RISON-encoded string, or `undefined` if the value is not encodable. + */ export function encodeUnknown(obj: any): string | undefined { return Rison.encode(obj); } /** - * rison-encode a javascript structure + * RISON-encode a JavaScript value, throwing if the value cannot be encoded. + * For values of unknown encodability, prefer {@link encodeUnknown}. + * + * @param obj - The JavaScript value to encode. Typed as `any` because RISON + * encoding is used to serialize arbitrary application state (e.g. URL + * parameters) whose shape is not known at compile time. + * @returns The RISON-encoded string. */ export function encode(obj: any) { const rison = encodeUnknown(obj); @@ -37,14 +57,21 @@ export function encode(obj: any) { } /** - * parse a rison string into a javascript structure. + * Parse a RISON string into a JavaScript structure. + * + * @param rison - The RISON-encoded string to decode. + * @returns The decoded JavaScript value as a {@link RisonValue}. */ export function decode(rison: string): RisonValue { return Rison.decode(rison); } /** - * safely parse a rison string into a javascript structure, never throws + * Parse a RISON string into a JavaScript structure, returning `null` instead + * of throwing when the input is invalid. + * + * @param rison - The RISON-encoded string to decode. + * @returns The decoded {@link RisonValue}, or `null` if the string is not valid RISON. */ export function safeDecode(rison: string): RisonValue { try { @@ -55,16 +82,24 @@ export function safeDecode(rison: string): RisonValue { } /** - * rison-encode a javascript array without surrounding parens + * RISON-encode a JavaScript array using A-RISON format (without surrounding + * parentheses), suitable for use in URL array parameters. + * + * @param array - The array to encode. Typed as `any[]` because the array + * elements may be arbitrary application state values. + * @returns The A-RISON-encoded string. */ export function encodeArray(array: any[]) { return Rison.encode_array(array); } /** - * parse an a-rison string into a javascript structure. + * Parse an A-RISON string (a RISON array without surrounding parentheses) + * into a JavaScript array. This prepends array markup before passing to the + * standard RISON decoder. * - * this simply adds array markup around the string before parsing. + * @param rison - The A-RISON-encoded string to decode. + * @returns The decoded array of {@link RisonValue} elements. */ export function decodeArray(rison: string): RisonValue[] { return Rison.decode_array(rison); diff --git a/src/platform/packages/shared/kbn-ui-theme/src/theme.ts b/src/platform/packages/shared/kbn-ui-theme/src/theme.ts index a3aea84ffe223..21fc7f212016c 100644 --- a/src/platform/packages/shared/kbn-ui-theme/src/theme.ts +++ b/src/platform/packages/shared/kbn-ui-theme/src/theme.ts @@ -12,18 +12,44 @@ import { default as borealisDark } from '@elastic/eui-theme-borealis/lib/eui_the const globals: any = typeof window === 'undefined' ? {} : window; +/** + * The shape of EUI theme variables, derived from the Borealis light theme token set. + */ export type Theme = typeof borealisLight; // in the Kibana app we can rely on this global being defined, but in // some cases (like jest) the global is undefined -/** @deprecated theme can be dynamic now, access is discouraged */ + +/** + * The raw theme tag string read from `window.__kbnThemeTag__` at page load. + * + * @deprecated Theme can be dynamic now; direct access is discouraged. + */ export const tag: string = globals.__kbnThemeTag__ || 'borealislight'; -/** @deprecated theme can be dynamic now, access is discouraged */ + +/** + * The Kibana UI theme major version number. + * + * @deprecated Theme can be dynamic now; direct access is discouraged. + */ export const version = 8; -/** @deprecated theme can be dynamic now, access is discouraged */ + +/** + * Whether the dark theme was active at page load, derived from {@link tag}. + * + * @deprecated Theme can be dynamic now; direct access is discouraged. Use + * {@link euiThemeVars} for values that respond to runtime theme changes. + */ export const darkMode = tag.endsWith('dark'); let isDarkMode = darkMode; + +/** + * Sets the dark mode state used internally by {@link euiThemeVars}. + * This is an internal API used by the Kibana theme service; do not call directly. + * + * @param mode - When `true`, {@link euiThemeVars} will return dark theme variables. + */ export const _setDarkMode = (mode: boolean) => { isDarkMode = mode; }; @@ -35,7 +61,9 @@ const getThemeVars = (): { light: Theme; dark: Theme } => { }; }; +/** Static snapshot of EUI light theme variables. Prefer {@link euiThemeVars} for values that respond to runtime theme changes. */ export const euiLightVars: Theme = getThemeVars().light; +/** Static snapshot of EUI dark theme variables. Prefer {@link euiThemeVars} for values that respond to runtime theme changes. */ export const euiDarkVars: Theme = getThemeVars().dark; /** @@ -54,6 +82,14 @@ export const euiThemeVars: Theme = new Proxy( } ); +/** + * Returns the EUI theme variable set for the given theme configuration. + * + * @param theme - The active theme configuration. + * @param theme.name - The name of the theme (e.g., `'borealis'`). + * @param theme.darkMode - When `true`, returns dark theme variables; otherwise returns light theme variables. + * @returns The {@link Theme} variable set matching the given configuration. + */ export function getEuiThemeVars(theme: { name: string; darkMode: boolean }) { return theme.darkMode ? borealisDark : borealisLight; }