From bb170200e4bbac2cd003f9c8b507bca34297a192 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Fri, 16 Jan 2026 14:07:29 -0500 Subject: [PATCH 1/4] feat(api): add api endpoint for component props --- package-lock.json | 72 ++++---- .../api/[version]/[section]/[page]/props.ts | 41 +++++ src/pages/api/[version]/[section]/names.ts | 41 +++++ src/pages/api/index.ts | 70 +++++++ src/pages/api/openapi.json.ts | 173 +++++++++++++++++- 5 files changed, 354 insertions(+), 43 deletions(-) create mode 100644 src/pages/api/[version]/[section]/[page]/props.ts create mode 100644 src/pages/api/[version]/[section]/names.ts diff --git a/package-lock.json b/package-lock.json index 9bc293c..49cfc0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -134,7 +134,8 @@ "version": "2.13.0", "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.0.tgz", "integrity": "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@astrojs/internal-helpers": { "version": "0.7.2", @@ -353,6 +354,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1409,6 +1411,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -3510,6 +3513,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", "dev": true, + "peer": true, "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.0.0", @@ -4070,6 +4074,7 @@ "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.2.2.tgz", "integrity": "sha512-JUrZ57JQ4bkmed1kxaciXb0ZpIVYyCHc2HjtzoKQ5UNRlx204zR2isATSHjdw2GFcWvwpkC5/fU2BR+oT3opbg==", "license": "MIT", + "peer": true, "dependencies": { "@patternfly/react-icons": "^6.2.2", "@patternfly/react-styles": "^6.2.2", @@ -5016,7 +5021,6 @@ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -5033,7 +5037,6 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -5044,7 +5047,6 @@ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5199,8 +5201,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -5265,19 +5266,6 @@ "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", "license": "MIT" }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -5445,6 +5433,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.1.tgz", "integrity": "sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.19.8" } @@ -5466,6 +5455,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5476,6 +5466,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -5723,6 +5714,7 @@ "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", @@ -6333,6 +6325,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6789,6 +6782,7 @@ "resolved": "https://registry.npmjs.org/astro/-/astro-5.15.9.tgz", "integrity": "sha512-XLDXxu0282cC/oYHswWZm3johGlRvk9rLRS7pWVWSne+HsZe9JgrpHI+vewAJSSNHBGd1aCyaQOElT5RNGe7IQ==", "license": "MIT", + "peer": true, "dependencies": { "@astrojs/compiler": "^2.13.0", "@astrojs/internal-helpers": "0.7.5", @@ -7853,6 +7847,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -8841,6 +8836,7 @@ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, + "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -9511,8 +9507,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/domexception": { "version": "4.0.0", @@ -10155,6 +10150,7 @@ "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10232,6 +10228,7 @@ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -13519,6 +13516,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -15322,16 +15320,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "license": "MIT", - "optional": true, - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -15763,7 +15751,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -17277,6 +17264,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": "^18.0.0 || >=20.0.0" } @@ -21203,6 +21191,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "devOptional": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -21231,6 +21220,7 @@ "resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.14.1.tgz", "integrity": "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==", "devOptional": true, + "peer": true, "dependencies": { "@astrojs/compiler": "^2.9.1", "prettier": "^3.0.0", @@ -21246,7 +21236,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -21262,7 +21251,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -21273,7 +21261,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -21286,8 +21273,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/pretty-ms": { "version": "9.2.0", @@ -21475,6 +21461,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -21535,6 +21522,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -22211,6 +22199,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.9.tgz", "integrity": "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.6" }, @@ -22361,6 +22350,7 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.90.0.tgz", "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -22413,6 +22403,7 @@ "integrity": "sha512-KRhQG9cUazPavJiJEFIJ3XAMjgfd0fcK3B+T26qOl8L0UG5aZUjeRfREO0KM5InGtYwxqiiytkJrbcYoLDEv0A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.0-beta.1", "@semantic-release/error": "^4.0.0", @@ -23902,6 +23893,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -24097,6 +24089,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -24178,6 +24171,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz", "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.15.0", "@typescript-eslint/types": "8.15.0", @@ -24295,6 +24289,7 @@ "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.19.tgz", "integrity": "sha512-t/OMHBNAkknVCI7bVB9OWjUUAwhVv9vsPIAGnNUxnu3FxPQN11rjh0sksLMzc3g7IlTgvHmOTl4JM7JHpcv5wA==", "license": "MIT", + "peer": true, "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.7", @@ -24678,6 +24673,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -25228,6 +25224,7 @@ "integrity": "sha512-8qoE56hf9QHS2llMM1tybjhvFEX5vnNUa1PpuyxeNC9F0dn9/qb9eDqN/z3sBPgpYK8vfQU9J8KOxczA+qo/cQ==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "bin": { "workerd": "bin/workerd" }, @@ -26134,6 +26131,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/pages/api/[version]/[section]/[page]/props.ts b/src/pages/api/[version]/[section]/[page]/props.ts new file mode 100644 index 0000000..bf84367 --- /dev/null +++ b/src/pages/api/[version]/[section]/[page]/props.ts @@ -0,0 +1,41 @@ +import type { APIRoute } from 'astro' +import { createJsonResponse } from '../../../../../utils/apiHelpers' +import { getConfig } from '../../../../../../cli/getConfig' +import { join } from 'node:path' +import { readFileSync } from 'node:fs' +import { sentenceCase } from 'change-case' + +export const prerender = false + +export const GET: APIRoute = async ({ params }) => { + const { version, section, page } = params + + if (!version || !section || !page) { + return createJsonResponse( + { error: 'Version, section, and page parameters are required' }, + 400, + ) + } + + try { + const config = await getConfig(`${process.cwd()}/pf-docs.config.mjs`) + const outputDir = config?.outputDir || join(process.cwd(), 'dist') + + const propsFilePath = join(outputDir, 'props.json') + const propsDataFile = readFileSync(propsFilePath) + const props = JSON.parse(propsDataFile.toString()) + + const propsData = props[sentenceCase(page)] + return createJsonResponse(propsData) + + } catch (error) { + const details = error instanceof Error ? error.message : String(error) + return createJsonResponse( + { error: 'Props data not found', details }, + 500, + ) + } +} + + + diff --git a/src/pages/api/[version]/[section]/names.ts b/src/pages/api/[version]/[section]/names.ts new file mode 100644 index 0000000..9558119 --- /dev/null +++ b/src/pages/api/[version]/[section]/names.ts @@ -0,0 +1,41 @@ +import type { APIRoute } from 'astro' +import { createJsonResponse } from '../../../../utils/apiHelpers' +import { getConfig } from '../../../../../cli/getConfig' +import { join } from 'node:path' +import { readFileSync } from 'node:fs' + +export const prerender = false + +export const GET: APIRoute = async ({ params }) => { + const { version, section } = params + + if (!version || !section) { + return createJsonResponse( + { error: 'Version and section parameters are required' }, + 400, + ) + } + + try { + const config = await getConfig(`${process.cwd()}/pf-docs.config.mjs`) + const outputDir = config?.outputDir || join(process.cwd(), 'dist') + + const propsFilePath = join(outputDir, 'props.json') + const propsDataFile = readFileSync(propsFilePath) + const props = JSON.parse(propsDataFile.toString()) + + const propsKey = new RegExp("Props", 'i'); // ignore ComponentProps objects + const names = Object.keys(props).filter(name => !propsKey.test(name)) + + return createJsonResponse(names) + } catch (error) { + const details = error instanceof Error ? error.message : String(error) + return createJsonResponse( + { error: 'Component names data not found', details }, + 500, + ) + } +} + + + diff --git a/src/pages/api/index.ts b/src/pages/api/index.ts index cead58f..cdd59c4 100644 --- a/src/pages/api/index.ts +++ b/src/pages/api/index.ts @@ -98,6 +98,36 @@ export const GET: APIRoute = async () => example: ['alert', 'button', 'card'], }, }, + { + path: '/api/{version}/{section}/names', + method: 'GET', + description: 'Get component names that have props data', + parameters: [ + { + name: 'version', + in: 'path', + required: true, + type: 'string', + example: 'v6', + }, + { + name: 'section', + in: 'path', + required: true, + type: 'string', + example: 'components', + } + ], + returns: { + type: 'array', + items: 'string', + description: 'All component names with props data', + example: [ + 'Alert', + 'AlertGroup' + ], + }, + }, { path: '/api/{version}/{section}/{page}', method: 'GET', @@ -132,6 +162,46 @@ export const GET: APIRoute = async () => example: ['react', 'react-demos', 'html'], }, }, + { + path: '/api/{version}/{section}/{page}/props', + method: 'GET', + description: 'Get props for a specific component', + parameters: [ + { + name: 'version', + in: 'path', + required: true, + type: 'string', + example: 'v6', + }, + { + name: 'section', + in: 'path', + required: true, + type: 'string', + example: 'components', + }, + { + name: 'page', + in: 'path', + required: true, + type: 'string', + example: 'alert', + }, + ], + returns: { + type: 'array', + items: 'object', + description: 'Props for a specific component', + example: [ + { + name: 'actionClose', + type: 'React.ReactNode', + description: 'Close button; use the alert action close button component.', + }, + ], + }, + }, { path: '/api/{version}/{section}/{page}/{tab}', method: 'GET', diff --git a/src/pages/api/openapi.json.ts b/src/pages/api/openapi.json.ts index 0262f0d..fc365ea 100644 --- a/src/pages/api/openapi.json.ts +++ b/src/pages/api/openapi.json.ts @@ -23,7 +23,7 @@ export const GET: APIRoute = async ({ url }) => { const details = error instanceof Error ? error.message : String(error) return createJsonResponse( { error: 'Failed to load API index', details }, - 500 + 500, ) } @@ -103,7 +103,8 @@ export const GET: APIRoute = async ({ url }) => { '/openapi.json': { get: { summary: 'Get OpenAPI specification', - description: 'Returns the complete OpenAPI 3.0 specification for this API', + description: + 'Returns the complete OpenAPI 3.0 specification for this API', operationId: 'getOpenApiSpec', responses: { '200': { @@ -234,6 +235,70 @@ export const GET: APIRoute = async ({ url }) => { }, }, }, + '/{version}/{section}/names': { + get: { + summary: 'Get component names', + description: 'Returns the component names that have props data', + operationId: 'getNames', + parameters: [ + { + name: 'version', + in: 'path', + required: true, + description: 'Documentation version', + schema: { + type: 'string', + enum: versions, + }, + example: 'v6', + }, + { + name: 'section', + in: 'path', + required: true, + description: 'Documentation section', + schema: { + type: 'string', + }, + example: 'components', + }, + ], + responses: { + '200': { + description: 'Component names with props data', + content: { + array: { + schema: { + type: 'array', + items: { + type: 'string' + }, + }, + example: [ + 'Alert', + 'AlertGroup' + ], + }, + }, + }, + '404': { + description: 'Props not found', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, '/{version}/{section}/{page}': { get: { summary: 'List tabs for a page', @@ -306,6 +371,102 @@ export const GET: APIRoute = async ({ url }) => { }, }, }, + '/{version}/{section}/{page}/props': { + get: { + summary: 'Get component props', + description: 'Returns the props for the specified component', + operationId: 'getProps', + parameters: [ + { + name: 'version', + in: 'path', + required: true, + description: 'Documentation version', + schema: { + type: 'string', + enum: versions, + }, + example: 'v6', + }, + { + name: 'section', + in: 'path', + required: true, + description: 'Documentation section', + schema: { + type: 'string', + }, + example: 'components', + }, + { + name: 'page', + in: 'path', + required: true, + description: 'Page ID (kebab-cased)', + schema: { + type: 'string', + }, + example: 'alert', + }, + ], + responses: { + '200': { + description: 'Props for the specified component', + content: { + array: { + schema: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + type: { type: 'string' }, + description: { type: 'string' }, + defaultValue: { type: 'string' }, + }, + }, + }, + example: [ + { + name: 'actionClose', + type: 'React.ReactNode', + description: + 'Close button; use the alert action close button component.', + }, + { + name: 'actionLinks', + type: 'React.ReactNode', + description: + 'Action links; use a single alert action link component or multiple wrapped in an array\nor React fragment.', + }, + { + name: 'children', + type: 'React.ReactNode', + description: 'Content rendered inside the alert.', + defaultValue: "''", + }, + ], + }, + }, + }, + '404': { + description: 'Props not found', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, '/{version}/{section}/{page}/{tab}': { get: { summary: 'Validate and redirect to text endpoint', @@ -357,7 +518,8 @@ export const GET: APIRoute = async ({ url }) => { ], responses: { '302': { - description: 'Redirects to /{version}/{section}/{page}/{tab}/text', + description: + 'Redirects to /{version}/{section}/{page}/{tab}/text', }, '404': { description: 'Tab not found', @@ -554,8 +716,7 @@ export const GET: APIRoute = async ({ url }) => { '/{version}/{section}/{page}/{tab}/examples/{example}': { get: { summary: 'Get example code', - description: - 'Returns the raw source code for a specific example', + description: 'Returns the raw source code for a specific example', operationId: 'getExampleCode', parameters: [ { @@ -619,7 +780,7 @@ export const GET: APIRoute = async ({ url }) => { type: 'string', }, example: - 'import React from \'react\';\nimport { Alert } from \'@patternfly/react-core\';\n\nexport const AlertBasic = () => ;', + "import React from 'react';\nimport { Alert } from '@patternfly/react-core';\n\nexport const AlertBasic = () => ;", }, }, }, From a2deb1e02c89d2f6e3331f341d8fcb77a2d77d2a Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Fri, 16 Jan 2026 14:50:53 -0500 Subject: [PATCH 2/4] revert lock --- package-lock.json | 72 ++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index 49cfc0d..9bc293c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -134,8 +134,7 @@ "version": "2.13.0", "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.13.0.tgz", "integrity": "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@astrojs/internal-helpers": { "version": "0.7.2", @@ -354,7 +353,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1411,7 +1409,6 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -3513,7 +3510,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", "dev": true, - "peer": true, "dependencies": { "@octokit/auth-token": "^5.0.0", "@octokit/graphql": "^8.0.0", @@ -4074,7 +4070,6 @@ "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.2.2.tgz", "integrity": "sha512-JUrZ57JQ4bkmed1kxaciXb0ZpIVYyCHc2HjtzoKQ5UNRlx204zR2isATSHjdw2GFcWvwpkC5/fU2BR+oT3opbg==", "license": "MIT", - "peer": true, "dependencies": { "@patternfly/react-icons": "^6.2.2", "@patternfly/react-styles": "^6.2.2", @@ -5021,6 +5016,7 @@ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -5037,6 +5033,7 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -5047,6 +5044,7 @@ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5201,7 +5199,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -5266,6 +5265,19 @@ "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", "license": "MIT" }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -5433,7 +5445,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.1.tgz", "integrity": "sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.19.8" } @@ -5455,7 +5466,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5466,7 +5476,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -5714,7 +5723,6 @@ "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", @@ -6325,7 +6333,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6782,7 +6789,6 @@ "resolved": "https://registry.npmjs.org/astro/-/astro-5.15.9.tgz", "integrity": "sha512-XLDXxu0282cC/oYHswWZm3johGlRvk9rLRS7pWVWSne+HsZe9JgrpHI+vewAJSSNHBGd1aCyaQOElT5RNGe7IQ==", "license": "MIT", - "peer": true, "dependencies": { "@astrojs/compiler": "^2.13.0", "@astrojs/internal-helpers": "0.7.5", @@ -7847,7 +7853,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -8836,7 +8841,6 @@ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -9507,7 +9511,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/domexception": { "version": "4.0.0", @@ -10150,7 +10155,6 @@ "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10228,7 +10232,6 @@ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -13516,7 +13519,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -15320,6 +15322,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", + "optional": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -15751,6 +15763,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -17264,7 +17277,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": "^18.0.0 || >=20.0.0" } @@ -21191,7 +21203,6 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "devOptional": true, - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -21220,7 +21231,6 @@ "resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.14.1.tgz", "integrity": "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==", "devOptional": true, - "peer": true, "dependencies": { "@astrojs/compiler": "^2.9.1", "prettier": "^3.0.0", @@ -21236,6 +21246,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -21251,6 +21262,7 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -21261,6 +21273,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -21273,7 +21286,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/pretty-ms": { "version": "9.2.0", @@ -21461,7 +21475,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -21522,7 +21535,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -22199,7 +22211,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.9.tgz", "integrity": "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.6" }, @@ -22350,7 +22361,6 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.90.0.tgz", "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -22403,7 +22413,6 @@ "integrity": "sha512-KRhQG9cUazPavJiJEFIJ3XAMjgfd0fcK3B+T26qOl8L0UG5aZUjeRfREO0KM5InGtYwxqiiytkJrbcYoLDEv0A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.0-beta.1", "@semantic-release/error": "^4.0.0", @@ -23893,7 +23902,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -24089,7 +24097,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -24171,7 +24178,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz", "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.15.0", "@typescript-eslint/types": "8.15.0", @@ -24289,7 +24295,6 @@ "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.19.tgz", "integrity": "sha512-t/OMHBNAkknVCI7bVB9OWjUUAwhVv9vsPIAGnNUxnu3FxPQN11rjh0sksLMzc3g7IlTgvHmOTl4JM7JHpcv5wA==", "license": "MIT", - "peer": true, "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.7", @@ -24673,7 +24678,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -25224,7 +25228,6 @@ "integrity": "sha512-8qoE56hf9QHS2llMM1tybjhvFEX5vnNUa1PpuyxeNC9F0dn9/qb9eDqN/z3sBPgpYK8vfQU9J8KOxczA+qo/cQ==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "bin": { "workerd": "bin/workerd" }, @@ -26131,7 +26134,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 9d93f9b955f8e5c42871c8aa183d3a947eee4496 Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Mon, 19 Jan 2026 13:30:27 -0500 Subject: [PATCH 3/4] coderabbit feedback --- src/pages/api/[version]/[section]/[page]/props.ts | 10 +++++++++- src/pages/api/openapi.json.ts | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pages/api/[version]/[section]/[page]/props.ts b/src/pages/api/[version]/[section]/[page]/props.ts index bf84367..53adaf7 100644 --- a/src/pages/api/[version]/[section]/[page]/props.ts +++ b/src/pages/api/[version]/[section]/[page]/props.ts @@ -3,7 +3,7 @@ import { createJsonResponse } from '../../../../../utils/apiHelpers' import { getConfig } from '../../../../../../cli/getConfig' import { join } from 'node:path' import { readFileSync } from 'node:fs' -import { sentenceCase } from 'change-case' +import { sentenceCase } from '../../../../../utils/case' export const prerender = false @@ -26,6 +26,14 @@ export const GET: APIRoute = async ({ params }) => { const props = JSON.parse(propsDataFile.toString()) const propsData = props[sentenceCase(page)] + + if(propsData === undefined) { + return createJsonResponse( + { error: `Props data for ${page} not found` }, + 404, + ) + } + return createJsonResponse(propsData) } catch (error) { diff --git a/src/pages/api/openapi.json.ts b/src/pages/api/openapi.json.ts index fc365ea..babd078 100644 --- a/src/pages/api/openapi.json.ts +++ b/src/pages/api/openapi.json.ts @@ -267,7 +267,7 @@ export const GET: APIRoute = async ({ url }) => { '200': { description: 'Component names with props data', content: { - array: { + 'application/json': { schema: { type: 'array', items: { @@ -413,7 +413,7 @@ export const GET: APIRoute = async ({ url }) => { '200': { description: 'Props for the specified component', content: { - array: { + 'application/json': { schema: { type: 'array', items: { From 7f30bf4cf5fb561b569ef808be784db81a17da8e Mon Sep 17 00:00:00 2001 From: Katie McFaul Date: Mon, 26 Jan 2026 11:53:31 -0500 Subject: [PATCH 4/4] update error logic & add tests --- .../[version]/[section]/[page]/props.test.ts | 357 ++++++++++++++++++ .../[version]/[section]/names.test.ts | 270 +++++++++++++ .../api/[version]/[section]/[page]/props.ts | 14 +- src/pages/api/[version]/[section]/names.ts | 17 +- 4 files changed, 638 insertions(+), 20 deletions(-) create mode 100644 src/__tests__/pages/api/__tests__/[version]/[section]/[page]/props.test.ts create mode 100644 src/__tests__/pages/api/__tests__/[version]/[section]/names.test.ts diff --git a/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/props.test.ts b/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/props.test.ts new file mode 100644 index 0000000..ea5c090 --- /dev/null +++ b/src/__tests__/pages/api/__tests__/[version]/[section]/[page]/props.test.ts @@ -0,0 +1,357 @@ +import { GET } from '../../../../../../../pages/api/[version]/[section]/[page]/props' +import { getConfig } from '../../../../../../../../cli/getConfig' +import { sentenceCase } from '../../../../../../../utils/case' + +/** + * Mock getConfig to return a test configuration + */ +jest.mock('../../../../../../../../cli/getConfig', () => ({ + getConfig: jest.fn().mockResolvedValue({ + outputDir: '/mock/output/dir', + }), +})) + +/** + * Mock node:path join function + */ +const mockJoin = jest.fn((...paths: string[]) => paths.join('/')) +jest.mock('node:path', () => ({ + join: (...args: any[]) => mockJoin(...args), +})) + +/** + * Mock node:fs readFileSync function + */ +const mockReadFileSync = jest.fn() +jest.mock('node:fs', () => ({ + readFileSync: (...args: any[]) => mockReadFileSync(...args), +})) + +/** + * Mock sentenceCase utility + */ +jest.mock('../../../../../../../utils/case', () => ({ + sentenceCase: jest.fn((id: string) => + // Simple mock: convert kebab-case to Sentence Case + id + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') + ), +})) + +const mockData = { + Alert: { + name: 'Alert', + description: '', + props: [ + { + name: 'variant', + type: 'string', + description: 'Alert variant style', + }, + ], + }, + Button: { + name: 'Button', + description: '', + props: [ + { + name: 'onClick', + type: 'function', + description: 'Click handler function', + }, + ], + }, + 'Sample Data Row': { + name: 'SampleDataRow', + description: '', + props: [ + { + name: 'applications', + type: 'number', + description: null, + required: true, + }, + ], + }, + 'Dashboard Wrapper': { + name: 'DashboardWrapper', + description: '', + props: [ + { + name: 'hasDefaultBreadcrumb', + type: 'boolean', + description: 'Flag to render sample breadcrumb if custom breadcrumb not passed', + }, + ], + }, + 'Keyboard Handler': { + name: 'KeyboardHandler', + description: '', + props: [ + { + name: 'containerRef', + type: 'React.RefObject', + description: 'Reference of the container to apply keyboard interaction', + defaultValue: 'null', + }, + ], + }, +} + +beforeEach(() => { + jest.clearAllMocks() + // Reset process.cwd mock + process.cwd = jest.fn(() => '/mock/workspace') + // Reset mockReadFileSync to return default mock data + mockReadFileSync.mockReturnValue(JSON.stringify(mockData)) +}) + +it('returns props data for a valid page', async () => { + const response = await GET({ + params: { version: 'v6', section: 'components', page: 'alert' }, + url: new URL('http://localhost:4321/api/v6/components/alert/props'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(response.headers.get('Content-Type')).toBe('application/json; charset=utf-8') + expect(body).toHaveProperty('name') + expect(body).toHaveProperty('description') + expect(body).toHaveProperty('props') + expect(body.name).toBe('Alert') + expect(Array.isArray(body.props)).toBe(true) + expect(sentenceCase).toHaveBeenCalledWith('alert') +}) + +it('converts kebab-case page name to sentence case for lookup', async () => { + const response = await GET({ + params: { version: 'v6', section: 'components', page: 'sample-data-row' }, + url: new URL('http://localhost:4321/api/v6/components/sample-data-row/props'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body.name).toBe('SampleDataRow') + expect(sentenceCase).toHaveBeenCalledWith('sample-data-row') +}) + +it('handles multi-word page names correctly', async () => { + const response = await GET({ + params: { version: 'v6', section: 'components', page: 'dashboard-wrapper' }, + url: new URL('http://localhost:4321/api/v6/components/dashboard-wrapper/props'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body.name).toBe('DashboardWrapper') + expect(sentenceCase).toHaveBeenCalledWith('dashboard-wrapper') +}) + +it('returns 404 error when props data is not found', async () => { + const response = await GET({ + params: { version: 'v6', section: 'components', page: 'nonexistent' }, + url: new URL('http://localhost:4321/api/v6/components/nonexistent/props'), + } as any) + const body = await response.json() + + expect(response.status).toBe(404) + expect(body).toHaveProperty('error') + expect(body.error).toContain('nonexistent') + expect(body.error).toContain('not found') +}) + +it('returns 400 error when page parameter is missing', async () => { + const response = await GET({ + params: { version: 'v6', section: 'components' }, + url: new URL('http://localhost:4321/api/v6/components/props'), + } as any) + const body = await response.json() + + expect(response.status).toBe(400) + expect(body).toHaveProperty('error') + expect(body.error).toContain('Page parameter is required') +}) + +it('returns 500 error when props.json file is not found', async () => { + mockReadFileSync.mockImplementation(() => { + const error = new Error('ENOENT: no such file or directory') + ; (error as any).code = 'ENOENT' + throw error + }) + + const response = await GET({ + params: { version: 'v6', section: 'components', page: 'alert' }, + url: new URL('http://localhost:4321/api/v6/components/alert/props'), + } as any) + const body = await response.json() + + expect(response.status).toBe(500) + expect(body).toHaveProperty('error') + expect(body.error).toBe('Props data not found') + expect(body).toHaveProperty('details') + expect(body.details).toContain('ENOENT') +}) + +it('returns 500 error when props.json contains invalid JSON', async () => { + mockReadFileSync.mockReturnValue('invalid json content') + + const response = await GET({ + params: { version: 'v6', section: 'components', page: 'alert' }, + url: new URL('http://localhost:4321/api/v6/components/alert/props'), + } as any) + const body = await response.json() + + expect(response.status).toBe(500) + expect(body).toHaveProperty('error') + expect(body.error).toBe('Props data not found') + expect(body).toHaveProperty('details') +}) + +it('returns 500 error when file read throws an error', async () => { + mockReadFileSync.mockImplementation(() => { + throw new Error('Permission denied') + }) + + const response = await GET({ + params: { version: 'v6', section: 'components', page: 'alert' }, + url: new URL('http://localhost:4321/api/v6/components/alert/props'), + } as any) + const body = await response.json() + + expect(response.status).toBe(500) + expect(body).toHaveProperty('error') + expect(body.error).toBe('Props data not found') + expect(body).toHaveProperty('details') + expect(body.details).toContain('Permission denied') +}) + +it('uses default outputDir when config does not provide one', async () => { + jest.mocked(getConfig).mockResolvedValueOnce({ + content: [], + propsGlobs: [], + outputDir: '', + }) + + const response = await GET({ + params: { version: 'v6', section: 'components', page: 'alert' }, + url: new URL('http://localhost:4321/api/v6/components/alert/props'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body).toHaveProperty('name') + expect(mockJoin).toHaveBeenCalledWith('/mock/workspace/dist', 'props.json') +}) + +it('uses custom outputDir from config when provided', async () => { + jest.mocked(getConfig).mockResolvedValueOnce({ + outputDir: '/custom/output/path', + content: [], + propsGlobs: [], + }) + + const response = await GET({ + params: { version: 'v6', section: 'components', page: 'alert' }, + url: new URL('http://localhost:4321/api/v6/components/alert/props'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body).toHaveProperty('name') + // Verify that join was called with custom outputDir + expect(mockJoin).toHaveBeenCalledWith('/custom/output/path', 'props.json') +}) + +it('reads props.json from the correct file path', async () => { + await GET({ + params: { version: 'v6', section: 'components', page: 'alert' }, + url: new URL('http://localhost:4321/api/v6/components/alert/props'), + } as any) + + // Verify readFileSync was called with the correct path + expect(mockReadFileSync).toHaveBeenCalledWith('/mock/output/dir/props.json') +}) + +it('returns full props structure with all fields', async () => { + const response = await GET({ + params: { version: 'v6', section: 'components', page: 'keyboard-handler' }, + url: new URL('http://localhost:4321/api/v6/components/keyboard-handler/props'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body).toHaveProperty('name') + expect(body).toHaveProperty('description') + expect(body).toHaveProperty('props') + expect(Array.isArray(body.props)).toBe(true) + expect(body.props.length).toBeGreaterThan(0) + expect(body.props[0]).toHaveProperty('name') + expect(body.props[0]).toHaveProperty('type') + expect(body.props[0]).toHaveProperty('description') +}) + +it('handles props with defaultValue field', async () => { + const response = await GET({ + params: { version: 'v6', section: 'components', page: 'keyboard-handler' }, + url: new URL('http://localhost:4321/api/v6/components/keyboard-handler/props'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + const propWithDefault = body.props.find((p: any) => p.defaultValue !== undefined) + if (propWithDefault) { + expect(propWithDefault).toHaveProperty('defaultValue') + } +}) + +it('handles props with required field', async () => { + const response = await GET({ + params: { version: 'v6', section: 'components', page: 'sample-data-row' }, + url: new URL('http://localhost:4321/api/v6/components/sample-data-row/props'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + const requiredProp = body.props.find((p: any) => p.required === true) + if (requiredProp) { + expect(requiredProp.required).toBe(true) + } +}) + +it('handles components with empty props array', async () => { + const emptyPropsData = { + 'Empty Component': { + name: 'EmptyComponent', + description: '', + props: [], + }, + } + mockReadFileSync.mockReturnValueOnce(JSON.stringify(emptyPropsData)) + + const response = await GET({ + params: { version: 'v6', section: 'components', page: 'empty-component' }, + url: new URL('http://localhost:4321/api/v6/components/empty-component/props'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body.name).toBe('EmptyComponent') + expect(Array.isArray(body.props)).toBe(true) + expect(body.props).toEqual([]) +}) + +it('handles request when tab is in URL path but not in params', async () => { + // Note: props.ts route is at [page] level, so tab parameter is not available + // This test verifies the route works correctly with just page parameter + const response = await GET({ + params: { version: 'v6', section: 'components', page: 'alert' }, + url: new URL('http://localhost:4321/api/v6/components/alert/react/props'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body).toHaveProperty('name') + expect(body.name).toBe('Alert') +}) diff --git a/src/__tests__/pages/api/__tests__/[version]/[section]/names.test.ts b/src/__tests__/pages/api/__tests__/[version]/[section]/names.test.ts new file mode 100644 index 0000000..23edfde --- /dev/null +++ b/src/__tests__/pages/api/__tests__/[version]/[section]/names.test.ts @@ -0,0 +1,270 @@ +import { GET } from '../../../../../../pages/api/[version]/[section]/names' +import { getConfig } from '../../../../../../../cli/getConfig' + +/** + * Mock getConfig to return a test configuration + */ +jest.mock('../../../../../../../cli/getConfig', () => ({ + getConfig: jest.fn().mockResolvedValue({ + outputDir: '/mock/output/dir', + }), +})) + +/** + * Mock node:path join function + */ +const mockJoin = jest.fn((...paths: string[]) => paths.join('/')) +jest.mock('node:path', () => ({ + join: (...args: any[]) => mockJoin(...args), +})) + +/** + * Mock node:fs readFileSync function + */ +const mockReadFileSync = jest.fn() +jest.mock('node:fs', () => ({ + readFileSync: (...args: any[]) => mockReadFileSync(...args), +})) + +const mockData = { + Alert: { + name: 'Alert', + description: '', + props: [ + { + name: 'variant', + type: 'string', + description: 'Alert variant', + }, + ], + }, + Button: { + name: 'Button', + description: '', + props: [ + { + name: 'onClick', + type: 'function', + description: 'Click handler', + }, + ], + }, + Card: { + name: 'Card', + description: '', + props: [ + { + name: 'title', + type: 'string', + description: 'Card title', + }, + ], + }, + AlertProps: { + name: 'AlertProps', + description: '', + props: [ + { + name: 'someProp', + type: 'string', + description: null, + }, + ], + }, + ButtonComponentProps: { + name: 'ButtonComponentProps', + description: '', + props: [ + { + name: 'anotherProp', + type: 'string', + description: null, + }, + ], + }, +} + +beforeEach(() => { + jest.clearAllMocks() + // Reset process.cwd mock + process.cwd = jest.fn(() => '/mock/workspace') + // Reset mockReadFileSync to return default mock data + mockReadFileSync.mockReturnValue(JSON.stringify(mockData)) +}) + +it('returns filtered component names from props.json data', async () => { + const response = await GET({ + params: { version: 'v6', section: 'components' }, + url: new URL('http://localhost:4321/api/v6/components/names'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(response.headers.get('Content-Type')).toBe('application/json; charset=utf-8') + expect(Array.isArray(body)).toBe(true) + expect(body).toContain('Alert') + expect(body).toContain('Button') + expect(body).toContain('Card') + expect(body).not.toContain('AlertProps') + expect(body).not.toContain('ButtonComponentProps') +}) + +it('filters out all keys containing "Props" case-insensitively', async () => { + const testData = { + Alert: {}, + Button: {}, + AlertProps: {}, + ALERTPROPS: {}, + alertprops: {}, + ComponentProps: {}, + SomeComponentProps: {}, + } + + mockReadFileSync.mockReturnValue(JSON.stringify(testData)) + + const response = await GET({ + params: { version: 'v6', section: 'components' }, + url: new URL('http://localhost:4321/api/v6/components/names'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(body).toEqual(['Alert', 'Button']) + expect(body).not.toContain('AlertProps') + expect(body).not.toContain('ALERTPROPS') + expect(body).not.toContain('alertprops') + expect(body).not.toContain('ComponentProps') + expect(body).not.toContain('SomeComponentProps') +}) + +it('returns empty array when props.json has no valid component names', async () => { + const testData = { + AlertProps: {}, + ButtonProps: {}, + ComponentProps: {}, + } + + mockReadFileSync.mockReturnValue(JSON.stringify(testData)) + + const response = await GET({ + params: { version: 'v6', section: 'components' }, + url: new URL('http://localhost:4321/api/v6/components/names'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(Array.isArray(body)).toBe(true) + expect(body).toEqual([]) +}) + +it('returns empty array when props.json is empty', async () => { + mockReadFileSync.mockReturnValue(JSON.stringify({})) + + const response = await GET({ + params: { version: 'v6', section: 'components' }, + url: new URL('http://localhost:4321/api/v6/components/names'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(Array.isArray(body)).toBe(true) + expect(body).toEqual([]) +}) + +it('returns 500 error when props.json file is not found', async () => { + mockReadFileSync.mockImplementation(() => { + const error = new Error('ENOENT: no such file or directory') + ; (error as any).code = 'ENOENT' + throw error + }) + + const response = await GET({ + params: { version: 'v6', section: 'components' }, + url: new URL('http://localhost:4321/api/v6/components/names'), + } as any) + const body = await response.json() + + expect(response.status).toBe(500) + expect(body).toHaveProperty('error') + expect(body.error).toBe('Component names data not found') + expect(body).toHaveProperty('details') + expect(body.details).toContain('ENOENT') +}) + +it('returns 500 error when props.json contains invalid JSON', async () => { + mockReadFileSync.mockReturnValue('invalid json content') + + const response = await GET({ + params: { version: 'v6', section: 'components' }, + url: new URL('http://localhost:4321/api/v6/components/names'), + } as any) + const body = await response.json() + + expect(response.status).toBe(500) + expect(body).toHaveProperty('error') + expect(body.error).toBe('Component names data not found') + expect(body).toHaveProperty('details') +}) + +it('returns 500 error when file read throws an error', async () => { + mockReadFileSync.mockImplementation(() => { + throw new Error('Permission denied') + }) + + const response = await GET({ + params: { version: 'v6', section: 'components' }, + url: new URL('http://localhost:4321/api/v6/components/names'), + } as any) + const body = await response.json() + + expect(response.status).toBe(500) + expect(body).toHaveProperty('error') + expect(body.error).toBe('Component names data not found') + expect(body).toHaveProperty('details') + expect(body.details).toContain('Permission denied') +}) + +it('uses default outputDir when config does not provide one', async () => { + jest.mocked(getConfig).mockResolvedValueOnce({ + content: [], + propsGlobs: [], + outputDir: '', + }) + + const response = await GET({ + params: { version: 'v6', section: 'components' }, + url: new URL('http://localhost:4321/api/v6/components/names'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(Array.isArray(body)).toBe(true) + expect(mockJoin).toHaveBeenCalledWith('/mock/workspace/dist', 'props.json') +}) + +it('uses custom outputDir from config when provided', async () => { + jest.mocked(getConfig).mockResolvedValueOnce({ + outputDir: '/custom/output/path', + content: [], + propsGlobs: [], + }) + + const response = await GET({ + params: { version: 'v6', section: 'components' }, + url: new URL('http://localhost:4321/api/v6/components/names'), + } as any) + const body = await response.json() + + expect(response.status).toBe(200) + expect(Array.isArray(body)).toBe(true) + expect(mockJoin).toHaveBeenCalledWith('/custom/output/path', 'props.json') +}) + +it('reads props.json from the correct file path', async () => { + await GET({ + params: { version: 'v6', section: 'components' }, + url: new URL('http://localhost:4321/api/v6/components/names'), + } as any) + + expect(mockReadFileSync).toHaveBeenCalledWith('/mock/output/dir/props.json') +}) diff --git a/src/pages/api/[version]/[section]/[page]/props.ts b/src/pages/api/[version]/[section]/[page]/props.ts index 53adaf7..27493e0 100644 --- a/src/pages/api/[version]/[section]/[page]/props.ts +++ b/src/pages/api/[version]/[section]/[page]/props.ts @@ -8,11 +8,11 @@ import { sentenceCase } from '../../../../../utils/case' export const prerender = false export const GET: APIRoute = async ({ params }) => { - const { version, section, page } = params + const { page } = params - if (!version || !section || !page) { + if (!page) { return createJsonResponse( - { error: 'Version, section, and page parameters are required' }, + { error: 'Page parameter is required' }, 400, ) } @@ -20,14 +20,14 @@ export const GET: APIRoute = async ({ params }) => { try { const config = await getConfig(`${process.cwd()}/pf-docs.config.mjs`) const outputDir = config?.outputDir || join(process.cwd(), 'dist') - + const propsFilePath = join(outputDir, 'props.json') const propsDataFile = readFileSync(propsFilePath) const props = JSON.parse(propsDataFile.toString()) - + const propsData = props[sentenceCase(page)] - if(propsData === undefined) { + if (propsData === undefined) { return createJsonResponse( { error: `Props data for ${page} not found` }, 404, @@ -35,7 +35,7 @@ export const GET: APIRoute = async ({ params }) => { } return createJsonResponse(propsData) - + } catch (error) { const details = error instanceof Error ? error.message : String(error) return createJsonResponse( diff --git a/src/pages/api/[version]/[section]/names.ts b/src/pages/api/[version]/[section]/names.ts index 9558119..af41ded 100644 --- a/src/pages/api/[version]/[section]/names.ts +++ b/src/pages/api/[version]/[section]/names.ts @@ -6,27 +6,18 @@ import { readFileSync } from 'node:fs' export const prerender = false -export const GET: APIRoute = async ({ params }) => { - const { version, section } = params - - if (!version || !section) { - return createJsonResponse( - { error: 'Version and section parameters are required' }, - 400, - ) - } - +export const GET: APIRoute = async ({ }) => { try { const config = await getConfig(`${process.cwd()}/pf-docs.config.mjs`) const outputDir = config?.outputDir || join(process.cwd(), 'dist') - + const propsFilePath = join(outputDir, 'props.json') const propsDataFile = readFileSync(propsFilePath) const props = JSON.parse(propsDataFile.toString()) - + const propsKey = new RegExp("Props", 'i'); // ignore ComponentProps objects const names = Object.keys(props).filter(name => !propsKey.test(name)) - + return createJsonResponse(names) } catch (error) { const details = error instanceof Error ? error.message : String(error)