Skip to content

Commit 40e1668

Browse files
committed
chore: automatic README generation
1 parent cc666d4 commit 40e1668

File tree

6 files changed

+314
-109
lines changed

6 files changed

+314
-109
lines changed

README.md

Lines changed: 26 additions & 25 deletions
Large diffs are not rendered by default.

eslint-rules/enforce-zod-v4.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"use strict";
2+
import path from "path";
3+
4+
// The file that is allowed to import from zod/v4
5+
const configFilePath = path.resolve(import.meta.dirname, "../src/common/config.ts");
6+
7+
// Ref: https://eslint.org/docs/latest/extend/custom-rules
8+
export default {
9+
meta: {
10+
type: "problem",
11+
docs: {
12+
description: "Only allow importing 'zod/v4' in config.ts, all other imports are allowed elsewhere.",
13+
recommended: true,
14+
},
15+
fixable: null,
16+
messages: {
17+
enforceZodV4:
18+
"Only 'zod/v4' imports are allowed in config.ts. Found import from '{{importPath}}'. Use 'zod/v4' instead.",
19+
},
20+
},
21+
create(context) {
22+
const currentFilePath = path.resolve(context.getFilename());
23+
24+
// Only enforce rule in config.ts
25+
if (currentFilePath !== configFilePath) {
26+
return {};
27+
}
28+
29+
return {
30+
ImportDeclaration(node) {
31+
const importPath = node.source.value;
32+
33+
// Check if this is a zod import
34+
if (typeof importPath !== "string") {
35+
return;
36+
}
37+
38+
// If importing from 'zod' or any 'zod/...' except 'zod/v4', only allow 'zod/v4' in config.ts
39+
const isZodImport = importPath === "zod" || importPath.startsWith("zod/");
40+
const isZodV4Import = importPath === "zod/v4";
41+
42+
if (isZodImport && !isZodV4Import) {
43+
context.report({
44+
node,
45+
messageId: "enforceZodV4",
46+
data: {
47+
importPath,
48+
},
49+
});
50+
}
51+
},
52+
};
53+
},
54+
};

eslint.config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import tseslint from "typescript-eslint";
66
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
77
import vitestPlugin from "@vitest/eslint-plugin";
88
import noConfigImports from "./eslint-rules/no-config-imports.js";
9+
import enforceZodV4 from "./eslint-rules/enforce-zod-v4.js";
910

1011
const testFiles = ["tests/**/*.test.ts", "tests/**/*.ts"];
1112

@@ -72,9 +73,15 @@ export default defineConfig([
7273
"no-config-imports": noConfigImports,
7374
},
7475
},
76+
"enforce-zod-v4": {
77+
rules: {
78+
"enforce-zod-v4": enforceZodV4,
79+
},
80+
},
7581
},
7682
rules: {
7783
"no-config-imports/no-config-imports": "error",
84+
"enforce-zod-v4/enforce-zod-v4": "error",
7885
},
7986
},
8087
globalIgnores([

scripts/generateArguments.ts

Lines changed: 107 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@
33
/**
44
* This script generates argument definitions and updates:
55
* - server.json arrays
6-
* - TODO: README.md configuration table
6+
* - README.md configuration table
77
*
88
* It uses the Zod schema and OPTIONS defined in src/common/config.ts
99
*/
1010

1111
import { readFileSync, writeFileSync } from "fs";
1212
import { join, dirname } from "path";
1313
import { fileURLToPath } from "url";
14-
import { OPTIONS, UserConfigSchema } from "../src/common/config.js";
15-
import type { ZodObject, ZodRawShape } from "zod";
14+
import { OPTIONS, UserConfigSchema, defaultUserConfig, configRegistry } from "../src/common/config.js";
15+
import assert from "assert";
16+
import { execSync } from "child_process";
1617

1718
const __filename = fileURLToPath(import.meta.url);
1819
const __dirname = dirname(__filename);
@@ -21,14 +22,12 @@ function camelCaseToSnakeCase(str: string): string {
2122
return str.replace(/[A-Z]/g, (letter) => `_${letter}`).toUpperCase();
2223
}
2324

24-
// List of configuration keys that contain sensitive/secret information
25+
// List of mongosh OPTIONS that contain sensitive/secret information
2526
// These should be redacted in logs and marked as secret in environment variable definitions
26-
const SECRET_CONFIG_KEYS = new Set([
27+
const SECRET_OPTIONS_KEYS = new Set([
2728
"connectionString",
2829
"username",
2930
"password",
30-
"apiClientId",
31-
"apiClientSecret",
3231
"tlsCAFile",
3332
"tlsCertificateKeyFile",
3433
"tlsCertificateKeyFilePassword",
@@ -37,56 +36,74 @@ const SECRET_CONFIG_KEYS = new Set([
3736
"sslPEMKeyFile",
3837
"sslPEMKeyPassword",
3938
"sslCRLFile",
40-
"voyageApiKey",
4139
]);
4240

43-
interface EnvironmentVariable {
41+
interface ArgumentInfo {
4442
name: string;
4543
description: string;
4644
isRequired: boolean;
4745
format: string;
4846
isSecret: boolean;
4947
configKey: string;
5048
defaultValue?: unknown;
49+
defaultValueDescription?: string;
5150
}
5251

5352
interface ConfigMetadata {
5453
description: string;
5554
defaultValue?: unknown;
55+
defaultValueDescription?: string;
56+
isSecret?: boolean;
5657
}
5758

5859
function extractZodDescriptions(): Record<string, ConfigMetadata> {
5960
const result: Record<string, ConfigMetadata> = {};
6061

6162
// Get the shape of the Zod schema
62-
const shape = (UserConfigSchema as ZodObject<ZodRawShape>).shape;
63+
const shape = UserConfigSchema.shape;
6364

6465
for (const [key, fieldSchema] of Object.entries(shape)) {
6566
const schema = fieldSchema;
6667
// Extract description from Zod schema
67-
const description = schema.description || `Configuration option: ${key}`;
68+
let description = schema.description || `Configuration option: ${key}`;
69+
70+
if ("innerType" in schema.def) {
71+
if (schema.def.innerType.def.type === "array") {
72+
assert(
73+
description.startsWith("An array of"),
74+
`Field description for field "${key}" with array type does not start with 'An array of'`
75+
);
76+
description = description.replace("An array of", "Comma separated values of");
77+
}
78+
}
6879

6980
// Extract default value if present
7081
let defaultValue: unknown = undefined;
71-
if (schema._def && "defaultValue" in schema._def) {
72-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
73-
defaultValue = schema._def.defaultValue() as unknown;
82+
let defaultValueDescription: string | undefined = undefined;
83+
let isSecret: boolean | undefined = undefined;
84+
if (schema.def && "defaultValue" in schema.def) {
85+
defaultValue = schema.def.defaultValue;
86+
}
87+
// Get metadata from custom registry
88+
const registryMeta = configRegistry.get(schema);
89+
if (registryMeta) {
90+
defaultValueDescription = registryMeta.defaultValueDescription;
91+
isSecret = registryMeta.isSecret;
7492
}
7593

7694
result[key] = {
7795
description,
7896
defaultValue,
97+
defaultValueDescription,
98+
isSecret,
7999
};
80100
}
81101

82102
return result;
83103
}
84104

85-
function generateEnvironmentVariables(
86-
options: typeof OPTIONS,
87-
zodMetadata: Record<string, ConfigMetadata>
88-
): EnvironmentVariable[] {
89-
const envVars: EnvironmentVariable[] = [];
105+
function getArgumentInfo(options: typeof OPTIONS, zodMetadata: Record<string, ConfigMetadata>): ArgumentInfo[] {
106+
const argumentInfos: ArgumentInfo[] = [];
90107
const processedKeys = new Set<string>();
91108

92109
// Helper to add env var
@@ -107,14 +124,15 @@ function generateEnvironmentVariables(
107124
format = "string"; // Arrays are passed as comma-separated strings
108125
}
109126

110-
envVars.push({
127+
argumentInfos.push({
111128
name: envVarName,
112129
description: metadata.description,
113130
isRequired: false,
114131
format: format,
115-
isSecret: SECRET_CONFIG_KEYS.has(key),
132+
isSecret: metadata.isSecret ?? SECRET_OPTIONS_KEYS.has(key),
116133
configKey: key,
117134
defaultValue: metadata.defaultValue,
135+
defaultValueDescription: metadata.defaultValueDescription,
118136
});
119137
};
120138

@@ -139,10 +157,10 @@ function generateEnvironmentVariables(
139157
}
140158

141159
// Sort by name for consistent output
142-
return envVars.sort((a, b) => a.name.localeCompare(b.name));
160+
return argumentInfos.sort((a, b) => a.name.localeCompare(b.name));
143161
}
144162

145-
function generatePackageArguments(envVars: EnvironmentVariable[]): unknown[] {
163+
function generatePackageArguments(envVars: ArgumentInfo[]): unknown[] {
146164
const packageArguments: unknown[] = [];
147165

148166
// Generate positional arguments from the same config options (only documented ones)
@@ -168,7 +186,7 @@ function generatePackageArguments(envVars: EnvironmentVariable[]): unknown[] {
168186
return packageArguments;
169187
}
170188

171-
function updateServerJsonEnvVars(envVars: EnvironmentVariable[]): void {
189+
function updateServerJsonEnvVars(envVars: ArgumentInfo[]): void {
172190
const serverJsonPath = join(__dirname, "..", "server.json");
173191
const packageJsonPath = join(__dirname, "..", "package.json");
174192

@@ -179,7 +197,7 @@ function updateServerJsonEnvVars(envVars: EnvironmentVariable[]): void {
179197
packages: {
180198
registryType?: string;
181199
identifier: string;
182-
environmentVariables: EnvironmentVariable[];
200+
environmentVariables: ArgumentInfo[];
183201
packageArguments?: unknown[];
184202
version?: string;
185203
}[];
@@ -207,7 +225,7 @@ function updateServerJsonEnvVars(envVars: EnvironmentVariable[]): void {
207225
// Update environmentVariables, packageArguments, and version for all packages
208226
if (serverJson.packages && Array.isArray(serverJson.packages)) {
209227
for (const pkg of serverJson.packages) {
210-
pkg.environmentVariables = envVarsArray as EnvironmentVariable[];
228+
pkg.environmentVariables = envVarsArray as ArgumentInfo[];
211229
pkg.packageArguments = packageArguments;
212230

213231
// For OCI packages, update the version tag in the identifier and not a version field
@@ -224,11 +242,72 @@ function updateServerJsonEnvVars(envVars: EnvironmentVariable[]): void {
224242
console.log(`✓ Updated server.json (version ${version})`);
225243
}
226244

245+
function generateReadmeConfigTable(argumentInfos: ArgumentInfo[]): string {
246+
const rows = [
247+
"| CLI Option | Environment Variable | Default | Description |",
248+
"| -------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |",
249+
];
250+
251+
// Filter to only include options that are in the Zod schema (documented options)
252+
const documentedVars = argumentInfos.filter((v) => !v.description.startsWith("Configuration option:"));
253+
254+
for (const argumentInfo of documentedVars) {
255+
const cliOption = `\`${argumentInfo.configKey}\``;
256+
const envVarName = `\`${argumentInfo.name}\``;
257+
258+
// Get default value from Zod schema or fallback to defaultUserConfig
259+
const config = defaultUserConfig as unknown as Record<string, unknown>;
260+
const defaultValue = argumentInfo.defaultValue ?? config[argumentInfo.configKey];
261+
262+
let defaultValueString = argumentInfo.defaultValueDescription ?? "`<not set>`";
263+
if (!argumentInfo.defaultValueDescription && defaultValue !== undefined && defaultValue !== null) {
264+
if (Array.isArray(defaultValue)) {
265+
defaultValueString = `\`"${defaultValue.join(",")}"\``;
266+
} else if (typeof defaultValue === "number") {
267+
defaultValueString = `\`${defaultValue}\``;
268+
} else if (typeof defaultValue === "boolean") {
269+
defaultValueString = `\`${defaultValue}\``;
270+
} else if (typeof defaultValue === "string") {
271+
defaultValueString = `\`"${defaultValue}"\``;
272+
} else {
273+
throw new Error(`Unsupported default value type: ${typeof defaultValue}`);
274+
}
275+
}
276+
277+
const desc = argumentInfo.description.replace(/\|/g, "\\|"); // Escape pipes in description
278+
rows.push(
279+
`| ${cliOption.padEnd(38)} | ${envVarName.padEnd(51)} | ${defaultValueString.padEnd(75)} | ${desc.padEnd(199)} |`
280+
);
281+
}
282+
283+
return rows.join("\n");
284+
}
285+
286+
function updateReadmeConfigTable(envVars: ArgumentInfo[]): void {
287+
const readmePath = join(__dirname, "..", "README.md");
288+
let content = readFileSync(readmePath, "utf-8");
289+
290+
const newTable = generateReadmeConfigTable(envVars);
291+
292+
// Find and replace the configuration options table
293+
const tableRegex = /### Configuration Options\n\n\| CLI Option[\s\S]*?\n\n####/;
294+
const replacement = `### Configuration Options\n\n${newTable}\n\n####`;
295+
296+
content = content.replace(tableRegex, replacement);
297+
298+
writeFileSync(readmePath, content, "utf-8");
299+
console.log("✓ Updated README.md configuration table");
300+
301+
// Run prettier on the README.md file
302+
execSync("npx prettier --write README.md", { cwd: join(__dirname, "..") });
303+
}
304+
227305
function main(): void {
228306
const zodMetadata = extractZodDescriptions();
229307

230-
const envVars = generateEnvironmentVariables(OPTIONS, zodMetadata);
231-
updateServerJsonEnvVars(envVars);
308+
const argumentInfo = getArgumentInfo(OPTIONS, zodMetadata);
309+
updateServerJsonEnvVars(argumentInfo);
310+
updateReadmeConfigTable(argumentInfo);
232311
}
233312

234313
main();

0 commit comments

Comments
 (0)