mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-23 19:44:30 -07:00
chore: autogenerate settings documentation (#12451)
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { generateSettingsSchema } from './generate-settings-schema.js';
|
||||
import {
|
||||
escapeBackticks,
|
||||
formatDefaultValue,
|
||||
formatWithPrettier,
|
||||
normalizeForCompare,
|
||||
} from './utils/autogen.js';
|
||||
|
||||
import type {
|
||||
SettingDefinition,
|
||||
SettingsSchema,
|
||||
SettingsSchemaType,
|
||||
} from '../packages/cli/src/config/settingsSchema.js';
|
||||
|
||||
const START_MARKER = '<!-- SETTINGS-AUTOGEN:START -->';
|
||||
const END_MARKER = '<!-- SETTINGS-AUTOGEN:END -->';
|
||||
|
||||
const MANUAL_TOP_LEVEL = new Set(['mcpServers', 'telemetry', 'extensions']);
|
||||
|
||||
interface DocEntry {
|
||||
path: string;
|
||||
type: string;
|
||||
description: string;
|
||||
defaultValue: string;
|
||||
requiresRestart: boolean;
|
||||
enumValues?: string[];
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv.slice(2)) {
|
||||
const checkOnly = argv.includes('--check');
|
||||
|
||||
await generateSettingsSchema({ checkOnly });
|
||||
|
||||
const repoRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..',
|
||||
);
|
||||
const docPath = path.join(repoRoot, 'docs/get-started/configuration.md');
|
||||
|
||||
const { getSettingsSchema } = await loadSettingsSchemaModule();
|
||||
const schema = getSettingsSchema();
|
||||
const sections = collectEntries(schema);
|
||||
const generatedBlock = renderSections(sections);
|
||||
|
||||
const doc = await readFile(docPath, 'utf8');
|
||||
const startIndex = doc.indexOf(START_MARKER);
|
||||
const endIndex = doc.indexOf(END_MARKER);
|
||||
|
||||
if (startIndex === -1 || endIndex === -1 || startIndex >= endIndex) {
|
||||
throw new Error(
|
||||
`Could not locate documentation markers (${START_MARKER}, ${END_MARKER}).`,
|
||||
);
|
||||
}
|
||||
|
||||
const before = doc.slice(0, startIndex + START_MARKER.length);
|
||||
const after = doc.slice(endIndex);
|
||||
const formattedDoc = await formatWithPrettier(
|
||||
`${before}\n${generatedBlock}\n${after}`,
|
||||
docPath,
|
||||
);
|
||||
|
||||
if (normalizeForCompare(doc) === normalizeForCompare(formattedDoc)) {
|
||||
if (!checkOnly) {
|
||||
console.log('Settings documentation already up to date.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkOnly) {
|
||||
console.error(
|
||||
'Settings documentation is out of date. Run `npm run docs:settings` to regenerate.',
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
await writeFile(docPath, formattedDoc);
|
||||
console.log('Settings documentation regenerated.');
|
||||
}
|
||||
|
||||
async function loadSettingsSchemaModule() {
|
||||
const modulePath = '../packages/cli/src/config/settingsSchema.ts';
|
||||
return import(modulePath);
|
||||
}
|
||||
|
||||
function collectEntries(schema: SettingsSchemaType) {
|
||||
const sections = new Map<string, DocEntry[]>();
|
||||
|
||||
const visit = (
|
||||
current: SettingsSchema,
|
||||
pathSegments: string[],
|
||||
topLevel?: string,
|
||||
) => {
|
||||
for (const [key, definition] of Object.entries(current)) {
|
||||
if (pathSegments.length === 0 && MANUAL_TOP_LEVEL.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newPathSegments = [...pathSegments, key];
|
||||
const sectionKey = topLevel ?? key;
|
||||
const hasChildren =
|
||||
definition.type === 'object' &&
|
||||
definition.properties &&
|
||||
Object.keys(definition.properties).length > 0;
|
||||
|
||||
if (!hasChildren) {
|
||||
if (!sections.has(sectionKey)) {
|
||||
sections.set(sectionKey, []);
|
||||
}
|
||||
|
||||
sections.get(sectionKey)!.push({
|
||||
path: newPathSegments.join('.'),
|
||||
type: formatType(definition),
|
||||
description: formatDescription(definition),
|
||||
defaultValue: formatDefaultValue(definition.default, {
|
||||
quoteStrings: true,
|
||||
}),
|
||||
requiresRestart: Boolean(definition.requiresRestart),
|
||||
enumValues: definition.options?.map((option) =>
|
||||
formatDefaultValue(option.value, { quoteStrings: true }),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (hasChildren && definition.properties) {
|
||||
visit(definition.properties, newPathSegments, sectionKey);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
visit(schema, []);
|
||||
return sections;
|
||||
}
|
||||
|
||||
function formatDescription(definition: SettingDefinition) {
|
||||
if (definition.description?.trim()) {
|
||||
return definition.description.trim();
|
||||
}
|
||||
return 'Description not provided.';
|
||||
}
|
||||
|
||||
function formatType(definition: SettingDefinition): string {
|
||||
switch (definition.ref) {
|
||||
case 'StringOrStringArray':
|
||||
return 'string | string[]';
|
||||
case 'BooleanOrString':
|
||||
return 'boolean | string';
|
||||
default:
|
||||
return definition.type;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSections(sections: Map<string, DocEntry[]>) {
|
||||
const lines: string[] = [];
|
||||
|
||||
for (const [section, entries] of sections) {
|
||||
if (entries.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
lines.push(`#### \`${section}\``);
|
||||
lines.push('');
|
||||
|
||||
for (const entry of entries) {
|
||||
lines.push(`- **\`${entry.path}\`** (${entry.type}):`);
|
||||
lines.push(` - **Description:** ${entry.description}`);
|
||||
lines.push(` - **Default:** \`${escapeBackticks(entry.defaultValue)}\``);
|
||||
|
||||
if (entry.enumValues && entry.enumValues.length > 0) {
|
||||
const values = entry.enumValues
|
||||
.map((value) => `\`${escapeBackticks(value)}\``)
|
||||
.join(', ');
|
||||
lines.push(` - **Values:** ${values}`);
|
||||
}
|
||||
|
||||
if (entry.requiresRestart) {
|
||||
lines.push(' - **Requires restart:** Yes');
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n').trimEnd();
|
||||
}
|
||||
|
||||
if (process.argv[1]) {
|
||||
const entryUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
||||
if (entryUrl === import.meta.url) {
|
||||
await main();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
|
||||
import {
|
||||
getSettingsSchema,
|
||||
type SettingCollectionDefinition,
|
||||
type SettingDefinition,
|
||||
type SettingsSchema,
|
||||
type SettingsSchemaType,
|
||||
SETTINGS_SCHEMA_DEFINITIONS,
|
||||
type SettingsJsonSchemaDefinition,
|
||||
} from '../packages/cli/src/config/settingsSchema.js';
|
||||
import {
|
||||
formatDefaultValue,
|
||||
formatWithPrettier,
|
||||
normalizeForCompare,
|
||||
} from './utils/autogen.js';
|
||||
|
||||
const OUTPUT_RELATIVE_PATH = ['schemas', 'settings.schema.json'];
|
||||
const SCHEMA_ID =
|
||||
'https://raw.githubusercontent.com/google-gemini/gemini-cli/main/schemas/settings.schema.json';
|
||||
|
||||
type JsonPrimitive = string | number | boolean | null;
|
||||
type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
||||
|
||||
interface JsonSchema {
|
||||
[key: string]: JsonValue | JsonSchema | JsonSchema[] | undefined;
|
||||
$schema?: string;
|
||||
$id?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
markdownDescription?: string;
|
||||
type?: string | string[];
|
||||
enum?: JsonPrimitive[];
|
||||
default?: JsonValue;
|
||||
properties?: Record<string, JsonSchema>;
|
||||
items?: JsonSchema;
|
||||
additionalProperties?: boolean | JsonSchema;
|
||||
required?: string[];
|
||||
$ref?: string;
|
||||
anyOf?: JsonSchema[];
|
||||
}
|
||||
|
||||
interface GenerateOptions {
|
||||
checkOnly: boolean;
|
||||
}
|
||||
|
||||
export async function generateSettingsSchema(
|
||||
options: GenerateOptions,
|
||||
): Promise<void> {
|
||||
const repoRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..',
|
||||
);
|
||||
const outputPath = path.join(repoRoot, ...OUTPUT_RELATIVE_PATH);
|
||||
await mkdir(path.dirname(outputPath), { recursive: true });
|
||||
|
||||
const schemaObject = buildSchemaObject(getSettingsSchema());
|
||||
const formatted = await formatWithPrettier(
|
||||
JSON.stringify(schemaObject, null, 2),
|
||||
outputPath,
|
||||
);
|
||||
|
||||
let existing: string | undefined;
|
||||
try {
|
||||
existing = await readFile(outputPath, 'utf8');
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
existing &&
|
||||
normalizeForCompare(existing) === normalizeForCompare(formatted)
|
||||
) {
|
||||
if (!options.checkOnly) {
|
||||
console.log('Settings JSON schema already up to date.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.checkOnly) {
|
||||
console.error(
|
||||
'Settings JSON schema is out of date. Run `npm run schema:settings` to regenerate.',
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
await writeFile(outputPath, formatted);
|
||||
console.log('Settings JSON schema regenerated.');
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv.slice(2)): Promise<void> {
|
||||
const checkOnly = argv.includes('--check');
|
||||
await generateSettingsSchema({ checkOnly });
|
||||
}
|
||||
|
||||
function buildSchemaObject(schema: SettingsSchemaType): JsonSchema {
|
||||
const defs = new Map<string, JsonSchema>(
|
||||
Object.entries(SETTINGS_SCHEMA_DEFINITIONS as Record<string, JsonSchema>),
|
||||
);
|
||||
|
||||
const root: JsonSchema = {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
$id: SCHEMA_ID,
|
||||
title: 'Gemini CLI Settings',
|
||||
description:
|
||||
'Configuration file schema for Gemini CLI settings. This schema enables IDE completion for `settings.json`.',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {},
|
||||
};
|
||||
|
||||
for (const [key, definition] of Object.entries(schema)) {
|
||||
root.properties![key] = buildSettingSchema(definition, [key], defs);
|
||||
}
|
||||
|
||||
if (defs.size > 0) {
|
||||
root.$defs = Object.fromEntries(defs.entries());
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
function buildSettingSchema(
|
||||
definition: SettingDefinition,
|
||||
pathSegments: string[],
|
||||
defs: Map<string, JsonSchema>,
|
||||
): JsonSchema {
|
||||
const base: JsonSchema = {
|
||||
title: definition.label,
|
||||
description: definition.description,
|
||||
markdownDescription: buildMarkdownDescription(definition),
|
||||
};
|
||||
|
||||
if (definition.default !== undefined) {
|
||||
base.default = definition.default as JsonValue;
|
||||
}
|
||||
|
||||
const schemaShape = definition.ref
|
||||
? buildRefSchema(definition.ref, defs)
|
||||
: buildSchemaForType(definition, pathSegments, defs);
|
||||
|
||||
return { ...base, ...schemaShape };
|
||||
}
|
||||
|
||||
function buildCollectionSchema(
|
||||
collection: SettingCollectionDefinition,
|
||||
pathSegments: string[],
|
||||
defs: Map<string, JsonSchema>,
|
||||
): JsonSchema {
|
||||
if (collection.ref) {
|
||||
return buildRefSchema(collection.ref, defs);
|
||||
}
|
||||
return buildSchemaForType(collection, pathSegments, defs);
|
||||
}
|
||||
|
||||
function buildSchemaForType(
|
||||
source: SettingDefinition | SettingCollectionDefinition,
|
||||
pathSegments: string[],
|
||||
defs: Map<string, JsonSchema>,
|
||||
): JsonSchema {
|
||||
switch (source.type) {
|
||||
case 'boolean':
|
||||
case 'string':
|
||||
case 'number':
|
||||
return { type: source.type };
|
||||
case 'enum':
|
||||
return buildEnumSchema(source.options);
|
||||
case 'array': {
|
||||
const itemPath = [...pathSegments, '<items>'];
|
||||
const items = isSettingDefinition(source)
|
||||
? source.items
|
||||
? buildCollectionSchema(source.items, itemPath, defs)
|
||||
: {}
|
||||
: source.properties
|
||||
? buildInlineObjectSchema(source.properties, itemPath, defs)
|
||||
: {};
|
||||
return { type: 'array', items };
|
||||
}
|
||||
case 'object':
|
||||
return isSettingDefinition(source)
|
||||
? buildObjectDefinitionSchema(source, pathSegments, defs)
|
||||
: buildObjectCollectionSchema(source, pathSegments, defs);
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function buildEnumSchema(
|
||||
options:
|
||||
| SettingDefinition['options']
|
||||
| SettingCollectionDefinition['options'],
|
||||
): JsonSchema {
|
||||
const values = options?.map((option) => option.value) ?? [];
|
||||
const inferred = inferTypeFromValues(values);
|
||||
return {
|
||||
type: inferred ?? undefined,
|
||||
enum: values,
|
||||
};
|
||||
}
|
||||
|
||||
function buildObjectDefinitionSchema(
|
||||
definition: SettingDefinition,
|
||||
pathSegments: string[],
|
||||
defs: Map<string, JsonSchema>,
|
||||
): JsonSchema {
|
||||
const properties = definition.properties
|
||||
? buildObjectProperties(definition.properties, pathSegments, defs)
|
||||
: undefined;
|
||||
|
||||
const schema: JsonSchema = {
|
||||
type: 'object',
|
||||
};
|
||||
|
||||
if (properties && Object.keys(properties).length > 0) {
|
||||
schema.properties = properties;
|
||||
}
|
||||
|
||||
if (definition.additionalProperties) {
|
||||
schema.additionalProperties = buildCollectionSchema(
|
||||
definition.additionalProperties,
|
||||
[...pathSegments, '<additionalProperties>'],
|
||||
defs,
|
||||
);
|
||||
} else if (!definition.properties) {
|
||||
schema.additionalProperties = true;
|
||||
} else {
|
||||
schema.additionalProperties = false;
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
function buildObjectCollectionSchema(
|
||||
collection: SettingCollectionDefinition,
|
||||
pathSegments: string[],
|
||||
defs: Map<string, JsonSchema>,
|
||||
): JsonSchema {
|
||||
if (collection.properties) {
|
||||
return buildInlineObjectSchema(collection.properties, pathSegments, defs);
|
||||
}
|
||||
return { type: 'object', additionalProperties: true };
|
||||
}
|
||||
|
||||
function buildObjectProperties(
|
||||
properties: SettingsSchema,
|
||||
pathSegments: string[],
|
||||
defs: Map<string, JsonSchema>,
|
||||
): Record<string, JsonSchema> {
|
||||
const result: Record<string, JsonSchema> = {};
|
||||
for (const [childKey, childDefinition] of Object.entries(properties)) {
|
||||
result[childKey] = buildSettingSchema(
|
||||
childDefinition,
|
||||
[...pathSegments, childKey],
|
||||
defs,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildInlineObjectSchema(
|
||||
properties: SettingsSchema,
|
||||
pathSegments: string[],
|
||||
defs: Map<string, JsonSchema>,
|
||||
): JsonSchema {
|
||||
const childSchemas = buildObjectProperties(properties, pathSegments, defs);
|
||||
return {
|
||||
type: 'object',
|
||||
properties: childSchemas,
|
||||
additionalProperties: false,
|
||||
};
|
||||
}
|
||||
|
||||
function buildRefSchema(
|
||||
ref: string,
|
||||
defs: Map<string, JsonSchema>,
|
||||
): JsonSchema {
|
||||
ensureDefinition(ref, defs);
|
||||
return { $ref: `#/$defs/${ref}` };
|
||||
}
|
||||
|
||||
function isSettingDefinition(
|
||||
source: SettingDefinition | SettingCollectionDefinition,
|
||||
): source is SettingDefinition {
|
||||
return 'label' in source;
|
||||
}
|
||||
|
||||
function buildMarkdownDescription(definition: SettingDefinition): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
if (definition.description?.trim()) {
|
||||
lines.push(definition.description.trim());
|
||||
} else {
|
||||
lines.push('Description not provided.');
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push(`- Category: \`${definition.category}\``);
|
||||
lines.push(
|
||||
`- Requires restart: \`${definition.requiresRestart ? 'yes' : 'no'}\``,
|
||||
);
|
||||
|
||||
if (definition.default !== undefined) {
|
||||
lines.push(`- Default: \`${formatDefaultValue(definition.default)}\``);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function inferTypeFromValues(
|
||||
values: Array<string | number>,
|
||||
): string | undefined {
|
||||
if (values.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (values.every((value) => typeof value === 'string')) {
|
||||
return 'string';
|
||||
}
|
||||
if (values.every((value) => typeof value === 'number')) {
|
||||
return 'number';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function ensureDefinition(ref: string, defs: Map<string, JsonSchema>): void {
|
||||
if (defs.has(ref)) {
|
||||
return;
|
||||
}
|
||||
const predefined = SETTINGS_SCHEMA_DEFINITIONS[ref] as
|
||||
| SettingsJsonSchemaDefinition
|
||||
| undefined;
|
||||
if (predefined) {
|
||||
defs.set(ref, predefined as JsonSchema);
|
||||
} else {
|
||||
defs.set(ref, { description: `Definition for ${ref}` });
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1]) {
|
||||
const entryUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
||||
if (entryUrl === import.meta.url) {
|
||||
await main();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { main as generateDocs } from '../generate-settings-doc.ts';
|
||||
|
||||
describe('generate-settings-doc', () => {
|
||||
it('keeps documentation in sync in check mode', async () => {
|
||||
const previousExitCode = process.exitCode;
|
||||
await expect(generateDocs(['--check'])).resolves.toBeUndefined();
|
||||
expect(process.exitCode).toBe(previousExitCode);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { main as generateSchema } from '../generate-settings-schema.ts';
|
||||
|
||||
describe('generate-settings-schema', () => {
|
||||
it('keeps schema in sync in check mode', async () => {
|
||||
const previousExitCode = process.exitCode;
|
||||
await expect(generateSchema(['--check'])).resolves.toBeUndefined();
|
||||
expect(process.exitCode).toBe(previousExitCode);
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@ export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['scripts/tests/**/*.test.js'],
|
||||
include: ['scripts/tests/**/*.test.{js,ts}'],
|
||||
setupFiles: ['scripts/tests/test-setup.ts'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import prettier from 'prettier';
|
||||
|
||||
export async function formatWithPrettier(content: string, filePath: string) {
|
||||
const options = await prettier.resolveConfig(filePath);
|
||||
return prettier.format(content, {
|
||||
...options,
|
||||
filepath: filePath,
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeForCompare(content: string): string {
|
||||
return content.replace(/\r\n/g, '\n').trimEnd();
|
||||
}
|
||||
|
||||
export function escapeBackticks(value: string): string {
|
||||
return value.replace(/\\/g, '\\\\').replace(/`/g, '\\`');
|
||||
}
|
||||
|
||||
export interface FormatDefaultValueOptions {
|
||||
/**
|
||||
* When true, string values are JSON-stringified, including surrounding quotes.
|
||||
* Defaults to false to return raw string content.
|
||||
*/
|
||||
quoteStrings?: boolean;
|
||||
}
|
||||
|
||||
export function formatDefaultValue(
|
||||
value: unknown,
|
||||
options: FormatDefaultValueOptions = {},
|
||||
): string {
|
||||
const { quoteStrings = false } = options;
|
||||
|
||||
if (value === undefined) {
|
||||
return 'undefined';
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return quoteStrings ? JSON.stringify(value) : value;
|
||||
}
|
||||
|
||||
if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
return '[]';
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
try {
|
||||
const json = JSON.stringify(value);
|
||||
if (json === '{}') {
|
||||
return '{}';
|
||||
}
|
||||
return json;
|
||||
} catch {
|
||||
return '[object Object]';
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user