persona-community-5/.pnpm-store/v3/files/1a/bb94c69db0055fffbc81e14b9901d117dbf6983b371e427be653d5c57eb55f2dc9a031c6caa81425b5ddcfa08bbd7885e1c3832ad10a30179c9e770814a380
rdev-worker a1d0d1bf1c
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /implement-feature community-ui --requirements 'Build the React commu...
2026-02-24 08:22:30 +00:00

617 lines
21 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { OasRef, Referenced } from "@redocly/openapi-core";
import { parseRef } from "@redocly/openapi-core/lib/ref-utils.js";
import ts, { type LiteralTypeNode, type TypeLiteralNode } from "typescript";
import type { ParameterObject } from "../types.js";
export const JS_PROPERTY_INDEX_RE = /^[A-Za-z_$][A-Za-z_$0-9]*$/;
export const JS_ENUM_INVALID_CHARS_RE = /[^A-Za-z_$0-9]+(.)?/g;
export const JS_PROPERTY_INDEX_INVALID_CHARS_RE = /[^A-Za-z_$0-9]+/g;
export const SPECIAL_CHARACTER_MAP: Record<string, string> = {
"+": "Plus",
// Add more mappings as needed
};
export const BOOLEAN = ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
export const FALSE = ts.factory.createLiteralTypeNode(ts.factory.createFalse());
export const NEVER = ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword);
export const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull());
export const NUMBER = ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
export const QUESTION_TOKEN = ts.factory.createToken(ts.SyntaxKind.QuestionToken);
export const STRING = ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
export const TRUE = ts.factory.createLiteralTypeNode(ts.factory.createTrue());
export const UNDEFINED = ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword);
export const UNKNOWN = ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
const LB_RE = /\r?\n/g;
const COMMENT_RE = /\*\//g;
export interface AnnotatedSchemaObject {
const?: unknown; // jsdoc without value
default?: unknown; // jsdoc with value
deprecated?: boolean; // jsdoc without value
description?: string; // jsdoc with value
enum?: unknown[]; // jsdoc without value
example?: string; // jsdoc with value
examples?: unknown;
format?: string; // not jsdoc
nullable?: boolean; // Node information
summary?: string; // not jsdoc
title?: string; // not jsdoc
type?: string | string[]; // Type of node
}
/**
* Preparing comments from fields
* @see {comment} for output examples
* @returns void if not comments or jsdoc format comment string
*/
export function addJSDocComment(schemaObject: AnnotatedSchemaObject, node: ts.PropertySignature): void {
if (!schemaObject || typeof schemaObject !== "object" || Array.isArray(schemaObject)) {
return;
}
const output: string[] = [];
// Not JSDoc tags: [title, format]
if (schemaObject.title) {
output.push(schemaObject.title.trim().replace(LB_RE, "\n * "));
}
if (schemaObject.summary) {
output.push(schemaObject.summary.trim().replace(LB_RE, "\n * "));
}
if (schemaObject.format) {
output.push(`Format: ${schemaObject.format}`);
}
// JSDoc tags without value
// 'Deprecated' without value
if (schemaObject.deprecated) {
output.push("@deprecated");
}
// JSDoc tags with value
const supportedJsDocTags = ["description", "default", "example"] as const;
for (const field of supportedJsDocTags) {
const allowEmptyString = field === "default" || field === "example";
if (schemaObject[field] === undefined) {
continue;
}
if (schemaObject[field] === "" && !allowEmptyString) {
continue;
}
const serialized =
typeof schemaObject[field] === "object" ? JSON.stringify(schemaObject[field], null, 2) : schemaObject[field];
output.push(`@${field} ${String(serialized).trim().replace(LB_RE, "\n * ")}`);
}
if (Array.isArray(schemaObject.examples)) {
for (const example of schemaObject.examples) {
const serialized = typeof example === "object" ? JSON.stringify(example, null, 2) : example;
output.push(`@example ${String(serialized).trim().replace(LB_RE, "\n * ")}`);
}
}
// JSDoc 'Constant' without value
if ("const" in schemaObject) {
output.push("@constant");
}
// JSDoc 'Enum' with type
if (schemaObject.enum) {
let type = "unknown";
if (Array.isArray(schemaObject.type)) {
type = schemaObject.type.join("|");
} else if (typeof schemaObject.type === "string") {
type = schemaObject.type;
}
output.push(`@enum {${type}${schemaObject.nullable ? "|null" : ""}}`);
}
// attach comment if it has content
if (output.length) {
// Check if any output item contains multi-line content (has internal line breaks)
const hasMultiLineContent = output.some((item) => item.includes("\n"));
let text =
output.length === 1 && !hasMultiLineContent ? `* ${output.join("\n")} ` : `*\n * ${output.join("\n * ")}\n `;
text = text.replace(COMMENT_RE, "*\\/"); // prevent inner comments from leaking
ts.addSyntheticLeadingComment(
/* node */ node,
/* kind */ ts.SyntaxKind.MultiLineCommentTrivia, // note: MultiLine just refers to a "/* */" comment
/* text */ text,
/* hasTrailingNewLine */ true,
);
}
}
function isOasRef<T>(obj: Referenced<T>): obj is OasRef {
return Boolean((obj as OasRef).$ref);
}
type OapiRefResolved = Referenced<ParameterObject>;
function isParameterObject(obj: OapiRefResolved | undefined): obj is ParameterObject {
return Boolean(obj && !isOasRef(obj) && obj.in);
}
function addIndexedAccess(node: ts.TypeNode, ...segments: readonly string[]) {
return segments.reduce<ts.TypeNode>((acc, segment) => {
return ts.factory.createIndexedAccessTypeNode(
acc,
ts.factory.createLiteralTypeNode(
typeof segment === "number"
? ts.factory.createNumericLiteral(segment)
: ts.factory.createStringLiteral(segment),
),
);
}, node);
}
/**
* Wrap a type with Extract<T, { propertyName: unknown }> to narrow a union type
* before accessing a property that only exists on some variants.
*/
function wrapWithExtract(type: ts.TypeNode, propertyName: string): ts.TypeNode {
return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Extract"), [
type,
ts.factory.createTypeLiteralNode([
ts.factory.createPropertySignature(
/* modifiers */ undefined,
/* name */ ts.factory.createIdentifier(propertyName),
/* questionToken */ undefined,
/* type */ ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
),
]),
]);
}
export interface OapiRefOptions {
/** Whether to wrap with FlattenedDeepRequired<> (default: false) */
deep?: boolean;
/** Array of property names to wrap with Extract<> when accessing */
extractProperties?: string[];
}
/**
* Convert OpenAPI ref into TS indexed access node (ex: `components["schemas"]["Foo"]`)
* `path` is a JSON Pointer to a location within an OpenAPI document.
* Transform it into a TypeScript type reference into the generated types.
*
* In most cases the structures of the openapi-typescript generated types and the
* JSON Pointer paths into the OpenAPI document are the same. However, in some cases
* special transformations are necessary to account for the ways they differ.
* * Object schemas
* $refs into the `properties` of object schemas are valid, but openapi-typescript
* flattens these objects, so we omit so the index into the schema skips ["properties"]
* * Parameters
* $refs into the `parameters` of paths are valid, but openapi-ts represents
* them according to their type; path, query, header, etc… so in these cases we
* must check the parameter definition to determine the how to index into
* the openapi-typescript type.
* * Union variant properties (oneOf/anyOf)
* When accessing properties that may only exist on some variants of a union type,
* we use Extract<> to narrow the type before each property access.
**/
export function oapiRef(path: string, resolved?: OapiRefResolved, options: OapiRefOptions = {}): ts.TypeNode {
const { pointer } = parseRef(path);
if (pointer.length === 0) {
throw new Error(`Error parsing $ref: ${path}. Is this a valid $ref?`);
}
const parametersObject = isParameterObject(resolved);
const extractSet = new Set(options.extractProperties ?? []);
// Initial segments are handled in a fixed , then remaining segments are treated
// according to heuristics based on the initial segments
const initialSegment = pointer[0];
const leadingSegments = pointer.slice(1, 3);
const restSegments = pointer.slice(3);
const leadingType = addIndexedAccess(
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier(
options.deep ? `FlattenedDeepRequired<${String(initialSegment)}>` : String(initialSegment),
),
),
...leadingSegments,
);
return restSegments.reduce<ts.TypeNode>((acc, segment, index, original) => {
// Skip `properties` items when in the middle of the pointer
// See: https://github.com/openapi-ts/openapi-typescript/issues/1742
if (segment === "properties") {
return acc;
}
if (parametersObject && index === original.length - 1) {
return addIndexedAccess(acc, resolved.in, resolved.name);
}
// If this segment is in the extractProperties list,
// wrap the current type with Extract<T, { segment: unknown }> before accessing.
// This narrows union types to variants that have this property.
if (extractSet.has(segment)) {
const narrowedType = wrapWithExtract(acc, segment);
return addIndexedAccess(narrowedType, segment);
}
return addIndexedAccess(acc, segment);
}, leadingType);
}
export interface AstToStringOptions {
fileName?: string;
sourceText?: string;
formatOptions?: ts.PrinterOptions;
}
/** Convert TypeScript AST to string */
export function astToString(
ast: ts.Node | ts.Node[] | ts.TypeElement | ts.TypeElement[],
options?: AstToStringOptions,
): string {
const sourceFile = ts.createSourceFile(
options?.fileName ?? "openapi-ts.ts",
options?.sourceText ?? "",
ts.ScriptTarget.ESNext,
false,
ts.ScriptKind.TS,
);
// @ts-expect-error its OK to overwrite statements once
sourceFile.statements = ts.factory.createNodeArray(Array.isArray(ast) ? ast : [ast]);
const printer = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed,
removeComments: false,
...options?.formatOptions,
});
return printer.printFile(sourceFile);
}
/** Convert an arbitrary string to TS (assuming its valid) */
export function stringToAST(source: string): unknown[] {
return ts.createSourceFile(
/* fileName */ "stringInput",
/* sourceText */ source,
/* languageVersion */ ts.ScriptTarget.ESNext,
/* setParentNodes */ undefined,
/* scriptKind */ undefined,
).statements as any;
}
/**
* Deduplicate simple primitive types from an array of nodes
* Note: wont deduplicate complex types like objects
*/
export function tsDedupe(types: ts.TypeNode[]): ts.TypeNode[] {
const encounteredTypes = new Set<number>();
const filteredTypes: ts.TypeNode[] = [];
for (const t of types) {
// only mark for deduplication if this is not a const ("text" means it is a const)
if (!("text" in ((t as LiteralTypeNode).literal ?? t))) {
const { kind } = (t as LiteralTypeNode).literal ?? t;
if (encounteredTypes.has(kind)) {
continue;
}
if (tsIsPrimitive(t)) {
encounteredTypes.add(kind);
}
}
filteredTypes.push(t);
}
return filteredTypes;
}
export const enumCache = new Map<string, ts.EnumDeclaration>();
/** Create a TS enum (with sanitized name and members) */
export function tsEnum(
name: string,
members: (string | number)[],
metadata?: { name?: string; description?: string | null }[],
options?: { export?: boolean; shouldCache?: boolean },
) {
let enumName = sanitizeMemberName(name);
enumName = `${enumName[0].toUpperCase()}${enumName.substring(1)}`;
let key = "";
if (options?.shouldCache) {
key = `${members
.slice(0)
.sort()
.map((v, i) => {
return `${metadata?.[i]?.name ?? String(v)}:${metadata?.[i]?.description || ""}`;
})
.join(",")}`;
if (enumCache.has(key)) {
return enumCache.get(key) as ts.EnumDeclaration;
}
}
const enumDeclaration = ts.factory.createEnumDeclaration(
/* modifiers */ options ? tsModifiers({ export: options.export ?? false }) : undefined,
/* name */ enumName,
/* members */ members.map((value, i) => tsEnumMember(value, metadata?.[i])),
);
options?.shouldCache && enumCache.set(key, enumDeclaration);
return enumDeclaration;
}
/** Create an exported TS array literal expression */
export function tsArrayLiteralExpression(
name: string,
elementType: ts.TypeNode,
values: (string | number)[],
options?: { export?: boolean; readonly?: boolean; injectFooter?: ts.Node[] },
) {
let variableName = sanitizeMemberName(name);
variableName = `${variableName[0].toLowerCase()}${variableName.substring(1)}`;
if (
options?.injectFooter &&
!options.injectFooter.some(
(node) => ts.isTypeAliasDeclaration(node) && node?.name?.escapedText === "FlattenedDeepRequired",
)
) {
const helper = stringToAST(
"type FlattenedDeepRequired<T> = { [K in keyof T]-?: FlattenedDeepRequired<T[K] extends unknown[] | undefined | null ? Extract<T[K], unknown[]>[number] : T[K]>; };",
)[0] as any;
options.injectFooter.push(helper);
}
const arrayType = options?.readonly
? tsReadonlyArray(elementType, options.injectFooter)
: ts.factory.createArrayTypeNode(elementType);
return ts.factory.createVariableStatement(
options ? tsModifiers({ export: options.export ?? false }) : undefined,
ts.factory.createVariableDeclarationList(
[
ts.factory.createVariableDeclaration(
variableName,
undefined,
arrayType,
ts.factory.createArrayLiteralExpression(
values.map((value) => {
if (typeof value === "number") {
if (value < 0) {
return ts.factory.createPrefixUnaryExpression(
ts.SyntaxKind.MinusToken,
ts.factory.createNumericLiteral(Math.abs(value)),
);
} else {
return ts.factory.createNumericLiteral(value);
}
} else {
return ts.factory.createStringLiteral(value);
}
}),
),
),
],
ts.NodeFlags.Const,
),
);
}
function sanitizeMemberName(name: string) {
let sanitizedName = name.replace(JS_ENUM_INVALID_CHARS_RE, (c) => {
const last = c[c.length - 1];
return JS_PROPERTY_INDEX_INVALID_CHARS_RE.test(last) ? "" : last.toUpperCase();
});
if (Number(name[0]) >= 0) {
sanitizedName = `Value${name}`;
}
return sanitizedName;
}
/** Sanitize TS enum member expression */
export function tsEnumMember(value: string | number, metadata: { name?: string; description?: string | null } = {}) {
let name = metadata.name ?? String(value);
if (!JS_PROPERTY_INDEX_RE.test(name)) {
if (Number(name[0]) >= 0) {
name = `Value${name}`.replace(".", "_"); // don't forged decimals;
} else if (name[0] === "-") {
name = `ValueMinus${name.slice(1)}`;
}
const invalidCharMatch = name.match(JS_PROPERTY_INDEX_INVALID_CHARS_RE);
if (invalidCharMatch) {
if (invalidCharMatch[0] === name) {
name = `"${name}"`;
} else {
name = name.replace(JS_PROPERTY_INDEX_INVALID_CHARS_RE, (s) => {
return s in SPECIAL_CHARACTER_MAP ? SPECIAL_CHARACTER_MAP[s] : "_";
});
}
}
}
let member: ts.EnumMember;
if (typeof value === "number") {
const literal =
value < 0
? ts.factory.createPrefixUnaryExpression(
ts.SyntaxKind.MinusToken,
ts.factory.createNumericLiteral(Math.abs(value)),
)
: ts.factory.createNumericLiteral(value);
member = ts.factory.createEnumMember(name, literal);
} else {
member = ts.factory.createEnumMember(name, ts.factory.createStringLiteral(value));
}
const trimmedDescription = metadata.description?.trim();
if (trimmedDescription === undefined || trimmedDescription === null || trimmedDescription === "") {
return member;
}
return ts.addSyntheticLeadingComment(member, ts.SyntaxKind.SingleLineCommentTrivia, ` ${trimmedDescription}`, true);
}
/** Create an intersection type */
export function tsIntersection(types: ts.TypeNode[]): ts.TypeNode {
if (types.length === 0) {
return NEVER;
}
if (types.length === 1) {
return types[0];
}
return ts.factory.createIntersectionTypeNode(tsDedupe(types));
}
/** Is this a primitive type (string, number, boolean, null, undefined)? */
export function tsIsPrimitive(type: ts.TypeNode): boolean {
if (!type) {
return true;
}
return (
ts.SyntaxKind[type.kind] === "BooleanKeyword" ||
ts.SyntaxKind[type.kind] === "NeverKeyword" ||
ts.SyntaxKind[type.kind] === "NullKeyword" ||
ts.SyntaxKind[type.kind] === "NumberKeyword" ||
ts.SyntaxKind[type.kind] === "StringKeyword" ||
ts.SyntaxKind[type.kind] === "UndefinedKeyword" ||
("literal" in type && tsIsPrimitive(type.literal as TypeLiteralNode))
);
}
/** Create a literal type */
export function tsLiteral(value: unknown): ts.TypeNode {
if (typeof value === "string") {
// workaround for UTF-8: https://github.com/microsoft/TypeScript/issues/36174
return ts.factory.createIdentifier(JSON.stringify(value)) as unknown as ts.TypeNode;
}
if (typeof value === "number") {
const literal =
value < 0
? ts.factory.createPrefixUnaryExpression(
ts.SyntaxKind.MinusToken,
ts.factory.createNumericLiteral(Math.abs(value)),
)
: ts.factory.createNumericLiteral(value);
return ts.factory.createLiteralTypeNode(literal);
}
if (typeof value === "boolean") {
return value === true ? TRUE : FALSE;
}
if (value === null) {
return NULL;
}
if (Array.isArray(value)) {
if (value.length === 0) {
return ts.factory.createArrayTypeNode(NEVER);
}
return ts.factory.createTupleTypeNode(value.map((v: unknown) => tsLiteral(v)));
}
if (typeof value === "object") {
const keys: ts.TypeElement[] = [];
for (const [k, v] of Object.entries(value)) {
keys.push(
ts.factory.createPropertySignature(
/* modifiers */ undefined,
/* name */ tsPropertyIndex(k),
/* questionToken */ undefined,
/* type */ tsLiteral(v),
),
);
}
return keys.length ? ts.factory.createTypeLiteralNode(keys) : tsRecord(STRING, NEVER);
}
return UNKNOWN;
}
/** Modifiers (readonly) */
export function tsModifiers(modifiers: { readonly?: boolean; export?: boolean }): ts.Modifier[] {
const typeMods: ts.Modifier[] = [];
if (modifiers.export) {
typeMods.push(ts.factory.createModifier(ts.SyntaxKind.ExportKeyword));
}
if (modifiers.readonly) {
typeMods.push(ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword));
}
return typeMods;
}
/** Create a T | null union */
export function tsNullable(types: ts.TypeNode[]): ts.TypeNode {
return ts.factory.createUnionTypeNode([...types, NULL]);
}
/** Create a TS Omit<X, Y> type */
export function tsOmit(type: ts.TypeNode, keys: string[]): ts.TypeNode {
return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Omit"), [
type,
ts.factory.createUnionTypeNode(keys.map((k) => tsLiteral(k))),
]);
}
/** Create a TS Record<X, Y> type */
export function tsRecord(key: ts.TypeNode, value: ts.TypeNode) {
return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Record"), [key, value]);
}
/** Create a valid property index */
export function tsPropertyIndex(index: string | number) {
if (
(typeof index === "number" && !(index < 0)) ||
(typeof index === "string" && String(Number(index)) === index && index[0] !== "-")
) {
return ts.factory.createNumericLiteral(index);
}
return typeof index === "string" && JS_PROPERTY_INDEX_RE.test(index)
? ts.factory.createIdentifier(index)
: ts.factory.createStringLiteral(String(index));
}
/** Create a union type */
export function tsUnion(types: ts.TypeNode[]): ts.TypeNode {
if (types.length === 0) {
return NEVER;
}
if (types.length === 1) {
return types[0];
}
return ts.factory.createUnionTypeNode(tsDedupe(types));
}
/** Create a WithRequired<X, Y> type */
export function tsWithRequired(
type: ts.TypeNode,
keys: string[],
injectFooter: ts.Node[], // needed to inject type helper if used
): ts.TypeNode {
if (keys.length === 0) {
return type;
}
// inject helper, if needed
if (!injectFooter.some((node) => ts.isTypeAliasDeclaration(node) && node?.name?.escapedText === "WithRequired")) {
const helper = stringToAST("type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };")[0] as any;
injectFooter.push(helper);
}
return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("WithRequired"), [
type,
tsUnion(keys.map((k) => tsLiteral(k))),
]);
}
/**
* Enhanced ReadonlyArray.
* eg: type Foo = ReadonlyArray<T>; type Bar = ReadonlyArray<T[]>
* Foo and Bar are both of type `readonly T[]`
*/
export function tsReadonlyArray(type: ts.TypeNode, injectFooter?: ts.Node[]): ts.TypeNode {
if (
injectFooter &&
!injectFooter.some((node) => ts.isTypeAliasDeclaration(node) && node?.name?.escapedText === "ReadonlyArray")
) {
const helper = stringToAST(
"type ReadonlyArray<T> = [Exclude<T, undefined>] extends [unknown[]] ? Readonly<Exclude<T, undefined>> : Readonly<Exclude<T, undefined>[]>;",
)[0] as any;
injectFooter.push(helper);
}
return ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("ReadonlyArray"), [type]);
}