@amritk/mjst 0.7.0 β†’ 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -95,22 +95,159 @@ npx mjst --config ./mjst.config.json
95
95
  ## Configuration reference
96
96
 
97
97
  <!-- config-table-start -->
98
- | | Property | CLI Flag | Type | Required | Default | Description |
99
- |:---:|:---|:---|:---:|:---:|:---:|:---|
100
- | πŸ“„ | `schema` | `--schema <path>` | `string` | β€” | β€” | Path to the schema to process. With the default 'json' input this is a JSON Schema file that is read and parsed. With any other input format it is a JS/TS module that exports a schema, which is loaded and converted to JSON Schema via the matching adapter. Either 'schema' or 'schemaDir' is required. |
101
- | πŸ—‚οΈ | `schemaDir` | `--schema-dir <dir>` | `string` | β€” | β€” | Path to a directory of JSON Schema files. When set, the CLI walks the directory recursively, generates parsers for every '*.json' schema it finds, and mirrors the directory layout under outDir (each schema lands in its own subdirectory). The runtime helpers are emitted once into a shared outDir/_helpers/ that every nested parser imports from. Mutually exclusive with 'schema'; when both are present 'schemaDir' wins. Only JSON Schema input is supported in this mode. |
102
- | πŸ”Œ | `input` | `--input <format>` | `string` | β€” | `"json"` | Source format of the schema. 'json' (default) reads a JSON Schema file directly. Any other format loads 'schema' as a module and converts it to JSON Schema with the matching adapter. Supported: 'typebox', 'zod' (zod v4+), 'valibot' (with @valibot/to-json-schema), and 'effect' β€” each requires the corresponding library installed in your project. |
103
- | πŸ“¦ | `export` | `--export <name>` | `string` | β€” | β€” | Which export of the schema module to use when 'input' is not 'json'. Defaults to the default export, or the sole named export when the module has exactly one. |
104
- | πŸ“ | `outDir` | `--out-dir <dir>` | `string` | β€” | β€” | Output directory for generated TypeScript files. The directory is created automatically if it does not exist. Subdirectories are created as needed when a generated file includes a nested path. Mutually exclusive with 'outFile'. |
105
- | πŸ“„ | `outFile` | `--out-file <file>` | `string` | β€” | β€” | Output everything to a single TypeScript file instead of a directory. Every generated definition is concatenated into one self-contained file and the cross-file imports are dropped. Mutually exclusive with 'outDir'. Currently supported only together with 'typesOnly'. |
106
- | 🏷️ | `typesOnly` | `--types-only` | `boolean` | β€” | `false` | Generate only TypeScript type definitions without parser functions. Useful when you only need the type shapes and do not need runtime validation. |
107
- | πŸ”¨ | `build` | `--build` | `boolean` | β€” | `false` | Compile the generated TypeScript files to .js and .d.ts output. A temporary tsconfig is written to the output directory, tsc is invoked, and the intermediate .ts source files are removed when compilation succeeds. |
108
- | ⚠️ | `logWarnings` | `--log-warnings` | `boolean` | β€” | `false` | Emit a console.warn in the generated parsers for every input key that is not declared in the schema's properties. Useful for detecting schema drift or unexpected data shapes at runtime. |
109
- | 🚫 | `strict` | `--strict` | `boolean` | β€” | `false` | Generate parsers that throw on type/shape mismatches (wrong type, missing required property, enum/pattern/min/max violations) instead of coercing invalid input to default values. Unknown extra keys are still allowed. |
110
- | πŸ”’ | `readonly` | `--readonly` | `boolean` | β€” | `false` | Emit every property, array, and record in the generated type definitions as readonly, producing deeply immutable types. Affects type definitions only; the generated parsers still build and return plain objects. |
111
- | 🧰 | `helpers` | `--helpers <mode>` | `string` | β€” | β€” | Controls how generated parsers reference their runtime helpers. 'package' emits imports from @amritk/helpers (requires it to be installed in the consumer project). 'embedded' ships the helper source under outDir/_helpers/ so the output is self-contained. When omitted, the CLI auto-detects: it picks 'package' if @amritk/helpers resolves from outDir, otherwise 'embedded'. |
112
- | 🏷️ | `typeSuffix` | `--type-suffix <suffix>` | `string` | β€” | `""` | Suffix appended to every generated type name derived from a $ref (e.g. 'Object' turns Contact into ContactObject). Defaults to no suffix. The root type name is used verbatim and is unaffected. |
113
- | βš™οΈ | `config` | `--config <path>` | `string` | β€” | β€” | Path to a JSON config file. Keys match the option names in this schema (schema, schemaDir, outDir, outFile, typesOnly, build, logWarnings, strict, readonly, helpers, typeSuffix). CLI flags take precedence over config file values. |
98
+ <table>
99
+ <thead>
100
+ <tr>
101
+ <th>Property</th>
102
+ <th>CLI Flag</th>
103
+ <th>Type</th>
104
+ <th align="center">Required</th>
105
+ <th align="center">Default</th>
106
+ </tr>
107
+ </thead>
108
+ <tbody>
109
+ <tr>
110
+ <td>πŸ“„ <code>schema</code></td>
111
+ <td><code>--schema &lt;path&gt;</code></td>
112
+ <td><code>string</code></td>
113
+ <td align="center">β€”</td>
114
+ <td align="center">β€”</td>
115
+ </tr>
116
+ <tr>
117
+ <td colspan="5">Path to the schema to process. With the default 'json' input this is a JSON Schema file that is read and parsed. With any other input format it is a JS/TS module that exports a schema, which is loaded and converted to JSON Schema via the matching adapter. Either 'schema' or 'schemaDir' is required.</td>
118
+ </tr>
119
+ <tr>
120
+ <td>πŸ—‚οΈ <code>schemaDir</code></td>
121
+ <td><code>--schema-dir &lt;dir&gt;</code></td>
122
+ <td><code>string</code></td>
123
+ <td align="center">β€”</td>
124
+ <td align="center">β€”</td>
125
+ </tr>
126
+ <tr>
127
+ <td colspan="5">Path to a directory of JSON Schema files. When set, the CLI walks the directory recursively, generates parsers for every '*.json' schema it finds, and mirrors the directory layout under outDir (each schema lands in its own subdirectory). The runtime helpers are emitted once into a shared outDir/_helpers/ that every nested parser imports from. Mutually exclusive with 'schema'; when both are present 'schemaDir' wins. Only JSON Schema input is supported in this mode.</td>
128
+ </tr>
129
+ <tr>
130
+ <td>πŸ”Œ <code>input</code></td>
131
+ <td><code>--input &lt;format&gt;</code></td>
132
+ <td><code>string</code></td>
133
+ <td align="center">β€”</td>
134
+ <td align="center"><code>"json"</code></td>
135
+ </tr>
136
+ <tr>
137
+ <td colspan="5">Source format of the schema. 'json' (default) reads a JSON Schema file directly. Any other format loads 'schema' as a module and converts it to JSON Schema with the matching adapter. Supported: 'typebox', 'zod' (zod v4+), 'valibot' (with @valibot/to-json-schema), and 'effect' β€” each requires the corresponding library installed in your project.</td>
138
+ </tr>
139
+ <tr>
140
+ <td>πŸ“¦ <code>export</code></td>
141
+ <td><code>--export &lt;name&gt;</code></td>
142
+ <td><code>string</code></td>
143
+ <td align="center">β€”</td>
144
+ <td align="center">β€”</td>
145
+ </tr>
146
+ <tr>
147
+ <td colspan="5">Which export of the schema module to use when 'input' is not 'json'. Defaults to the default export, or the sole named export when the module has exactly one.</td>
148
+ </tr>
149
+ <tr>
150
+ <td>πŸ“ <code>outDir</code></td>
151
+ <td><code>--out-dir &lt;dir&gt;</code></td>
152
+ <td><code>string</code></td>
153
+ <td align="center">β€”</td>
154
+ <td align="center">β€”</td>
155
+ </tr>
156
+ <tr>
157
+ <td colspan="5">Output directory for generated TypeScript files. The directory is created automatically if it does not exist. Subdirectories are created as needed when a generated file includes a nested path. Mutually exclusive with 'outFile'.</td>
158
+ </tr>
159
+ <tr>
160
+ <td>πŸ“„ <code>outFile</code></td>
161
+ <td><code>--out-file &lt;file&gt;</code></td>
162
+ <td><code>string</code></td>
163
+ <td align="center">β€”</td>
164
+ <td align="center">β€”</td>
165
+ </tr>
166
+ <tr>
167
+ <td colspan="5">Output everything to a single TypeScript file instead of a directory. Every generated definition is concatenated into one self-contained file and the cross-file imports are dropped. Mutually exclusive with 'outDir'. Currently supported only together with 'typesOnly'.</td>
168
+ </tr>
169
+ <tr>
170
+ <td>🏷️ <code>typesOnly</code></td>
171
+ <td><code>--types-only</code></td>
172
+ <td><code>boolean</code></td>
173
+ <td align="center">β€”</td>
174
+ <td align="center"><code>false</code></td>
175
+ </tr>
176
+ <tr>
177
+ <td colspan="5">Generate only TypeScript type definitions without parser functions. Useful when you only need the type shapes and do not need runtime validation.</td>
178
+ </tr>
179
+ <tr>
180
+ <td>πŸ”¨ <code>build</code></td>
181
+ <td><code>--build</code></td>
182
+ <td><code>boolean</code></td>
183
+ <td align="center">β€”</td>
184
+ <td align="center"><code>false</code></td>
185
+ </tr>
186
+ <tr>
187
+ <td colspan="5">Compile the generated TypeScript files to .js and .d.ts output. A temporary tsconfig is written to the output directory, tsc is invoked, and the intermediate .ts source files are removed when compilation succeeds.</td>
188
+ </tr>
189
+ <tr>
190
+ <td>⚠️ <code>logWarnings</code></td>
191
+ <td><code>--log-warnings</code></td>
192
+ <td><code>boolean</code></td>
193
+ <td align="center">β€”</td>
194
+ <td align="center"><code>false</code></td>
195
+ </tr>
196
+ <tr>
197
+ <td colspan="5">Emit a console.warn in the generated parsers for every input key that is not declared in the schema's properties. Useful for detecting schema drift or unexpected data shapes at runtime.</td>
198
+ </tr>
199
+ <tr>
200
+ <td>🚫 <code>strict</code></td>
201
+ <td><code>--strict</code></td>
202
+ <td><code>boolean</code></td>
203
+ <td align="center">β€”</td>
204
+ <td align="center"><code>false</code></td>
205
+ </tr>
206
+ <tr>
207
+ <td colspan="5">Generate parsers that throw on type/shape mismatches (wrong type, missing required property, enum/pattern/min/max violations) instead of coercing invalid input to default values. Unknown extra keys are still allowed.</td>
208
+ </tr>
209
+ <tr>
210
+ <td>πŸ”’ <code>readonly</code></td>
211
+ <td><code>--readonly</code></td>
212
+ <td><code>boolean</code></td>
213
+ <td align="center">β€”</td>
214
+ <td align="center"><code>false</code></td>
215
+ </tr>
216
+ <tr>
217
+ <td colspan="5">Emit every property, array, and record in the generated type definitions as readonly, producing deeply immutable types. Affects type definitions only; the generated parsers still build and return plain objects.</td>
218
+ </tr>
219
+ <tr>
220
+ <td>🧰 <code>helpers</code></td>
221
+ <td><code>--helpers &lt;mode&gt;</code></td>
222
+ <td><code>string</code></td>
223
+ <td align="center">β€”</td>
224
+ <td align="center">β€”</td>
225
+ </tr>
226
+ <tr>
227
+ <td colspan="5">Controls how generated parsers reference their runtime helpers. 'package' emits imports from @amritk/helpers (requires it to be installed in the consumer project). 'embedded' ships the helper source under outDir/_helpers/ so the output is self-contained. When omitted, the CLI auto-detects: it picks 'package' if @amritk/helpers resolves from outDir, otherwise 'embedded'.</td>
228
+ </tr>
229
+ <tr>
230
+ <td>🏷️ <code>typeSuffix</code></td>
231
+ <td><code>--type-suffix &lt;suffix&gt;</code></td>
232
+ <td><code>string</code></td>
233
+ <td align="center">β€”</td>
234
+ <td align="center"><code>""</code></td>
235
+ </tr>
236
+ <tr>
237
+ <td colspan="5">Suffix appended to every generated type name derived from a $ref (e.g. 'Object' turns Contact into ContactObject). Defaults to no suffix. The root type name is used verbatim and is unaffected.</td>
238
+ </tr>
239
+ <tr>
240
+ <td>βš™οΈ <code>config</code></td>
241
+ <td><code>--config &lt;path&gt;</code></td>
242
+ <td><code>string</code></td>
243
+ <td align="center">β€”</td>
244
+ <td align="center">β€”</td>
245
+ </tr>
246
+ <tr>
247
+ <td colspan="5">Path to a JSON config file. Keys match the option names in this schema (schema, schemaDir, outDir, outFile, typesOnly, build, logWarnings, strict, readonly, helpers, typeSuffix). CLI flags take precedence over config file values.</td>
248
+ </tr>
249
+ </tbody>
250
+ </table>
114
251
  <!-- config-table-end -->
115
252
 
116
253
  ---
@@ -73,4 +73,11 @@ export type CliConfig = {
73
73
  * (no suffix). The root type name is used verbatim and is unaffected.
74
74
  */
75
75
  readonly typeSuffix?: string;
76
+ /**
77
+ * Prepends a comment header to every generated file (excluding `_helpers/`).
78
+ * - `true` β€” use the default message: "This file was auto-generated by @amritk/mjst…"
79
+ * - A string β€” use that text as the message body (wrapped in a JSDoc block).
80
+ * - `false` / omitted β€” no header.
81
+ */
82
+ readonly banner?: boolean | string;
76
83
  };
package/dist/cli.js CHANGED
@@ -90440,6 +90440,31 @@ var getUnbrandedType = (schema, options = {}) => {
90440
90440
  case "object":
90441
90441
  if (schema.properties) {
90442
90442
  const readonlyPrefix = options.readonly ? "readonly " : "";
90443
+ const hasDescriptions = Object.values(schema.properties).some((p) => isSchemaObject2(p) && (typeof p.description === "string" || typeof p.$comment === "string"));
90444
+ if (hasDescriptions) {
90445
+ let properties2 = "";
90446
+ let first22 = true;
90447
+ for (const key in schema.properties) {
90448
+ const propSchema = schema.properties[key];
90449
+ const isRequired = schema.required?.includes(key) ?? false;
90450
+ const optional3 = isRequired ? "" : "?";
90451
+ const propType = getTypeScriptType(propSchema, options);
90452
+ const inlineDescription = isSchemaObject2(propSchema) && typeof propSchema.description === "string" ? propSchema.description : isSchemaObject2(propSchema) && typeof propSchema.$comment === "string" ? propSchema.$comment : undefined;
90453
+ if (!first22)
90454
+ properties2 += `
90455
+ `;
90456
+ first22 = false;
90457
+ if (inlineDescription) {
90458
+ properties2 += " /** " + inlineDescription + ` */
90459
+ ` + readonlyPrefix + safeKey(key) + optional3 + ": " + propType + ";";
90460
+ } else {
90461
+ properties2 += " " + readonlyPrefix + safeKey(key) + optional3 + ": " + propType + ";";
90462
+ }
90463
+ }
90464
+ return `{
90465
+ ` + properties2 + `
90466
+ }`;
90467
+ }
90443
90468
  let properties = "";
90444
90469
  let first5 = true;
90445
90470
  for (const key in schema.properties) {
@@ -90481,8 +90506,9 @@ var generateTypeDefinition = (schema, typeName, options = {}) => {
90481
90506
  if (!isObjectLikeSchema(schema)) {
90482
90507
  const tsType = getTypeScriptType(schema, options);
90483
90508
  let result = "";
90484
- if (isSchemaObject2(schema) && schema.$comment && typeof schema.$comment === "string") {
90485
- result += buildJsDocBlock(typeName, schema.$comment);
90509
+ const topLevelComment = isSchemaObject2(schema) && typeof schema.description === "string" && schema.description || isSchemaObject2(schema) && typeof schema.$comment === "string" && schema.$comment || undefined;
90510
+ if (topLevelComment) {
90511
+ result += buildJsDocBlock(typeName, topLevelComment);
90486
90512
  }
90487
90513
  result += `export type ${typeName} = ${tsType};`;
90488
90514
  return result;
@@ -90493,9 +90519,10 @@ var generateTypeDefinition = (schema, typeName, options = {}) => {
90493
90519
  const conditionalThenRef = conditionalResult?.thenRef ?? null;
90494
90520
  let jsDocTitle;
90495
90521
  let jsDocDescription;
90496
- if (isSchemaObject2(schema) && schema.$comment && typeof schema.$comment === "string") {
90522
+ const topLevelComment = isSchemaObject2(schema) && typeof schema.description === "string" && schema.description || isSchemaObject2(schema) && typeof schema.$comment === "string" && schema.$comment || undefined;
90523
+ if (topLevelComment) {
90497
90524
  jsDocTitle = typeName;
90498
- jsDocDescription = schema.$comment;
90525
+ jsDocDescription = topLevelComment;
90499
90526
  }
90500
90527
  const hasProperties2 = normalizedSchema.properties && Object.keys(normalizedSchema.properties).length > 0;
90501
90528
  const hasAdditionalProperties2 = normalizedSchema.additionalProperties && typeof normalizedSchema.additionalProperties === "object";
@@ -92665,7 +92692,9 @@ var parseCliArgs = (args2) => {
92665
92692
  if (equalsIndex !== -1) {
92666
92693
  const key2 = toCamelCase(arg.slice(2, equalsIndex));
92667
92694
  const value10 = arg.slice(equalsIndex + 1);
92668
- if (BOOLEAN_KEYS.has(key2)) {
92695
+ if (key2 === "banner") {
92696
+ config3.banner = value10 === "false" ? false : value10 === "true" ? true : value10;
92697
+ } else if (BOOLEAN_KEYS.has(key2)) {
92669
92698
  assignBoolean(config3, key2, value10 !== "false");
92670
92699
  } else {
92671
92700
  assignValue(config3, key2, value10);
@@ -92673,6 +92702,16 @@ var parseCliArgs = (args2) => {
92673
92702
  continue;
92674
92703
  }
92675
92704
  const key = toCamelCase(arg.slice(2));
92705
+ if (key === "banner") {
92706
+ const nextArg = args2[i + 1];
92707
+ if (nextArg && !nextArg.startsWith("--")) {
92708
+ config3.banner = nextArg;
92709
+ i++;
92710
+ } else {
92711
+ config3.banner = true;
92712
+ }
92713
+ continue;
92714
+ }
92676
92715
  if (BOOLEAN_KEYS.has(key)) {
92677
92716
  assignBoolean(config3, key, true);
92678
92717
  continue;
@@ -92720,6 +92759,18 @@ var loadSchema = async (config3, schemaPath) => {
92720
92759
  const source = await loadSchemaModule(schemaPath, config3.export);
92721
92760
  return getAdapter(inputFormat).toJSONSchema(source);
92722
92761
  };
92762
+ var DEFAULT_BANNER_TEXT = `This file was auto-generated by @amritk/mjst.
92763
+ * Do not edit it manually β€” your changes will be overwritten on the next generation.`;
92764
+ var resolveBanner = (banner) => {
92765
+ if (!banner)
92766
+ return "";
92767
+ const text = banner === true ? DEFAULT_BANNER_TEXT : banner;
92768
+ return `/**
92769
+ * ${text}
92770
+ */
92771
+
92772
+ `;
92773
+ };
92723
92774
  var writeGeneratedFile = async (baseDir, filename, content) => {
92724
92775
  const filePath = join8(baseDir, filename);
92725
92776
  await mkdir(dirname2(filePath), { recursive: true });
@@ -92776,7 +92827,9 @@ var runSingle = async (config3, schemaPath, outputDir) => {
92776
92827
  console.log(`Root type: ${rootTypeName}`);
92777
92828
  const files = await buildSchema(schema, rootTypeName, undefined, config3.typesOnly, config3.logWarnings, config3.strict, helpersMode, undefined, config3.readonly, config3.typeSuffix);
92778
92829
  for (const file2 of files) {
92779
- await writeGeneratedFile(outputDir, file2.filename, file2.content);
92830
+ const isHelper = file2.filename.startsWith("_helpers/");
92831
+ const content = config3.banner && !isHelper ? resolveBanner(config3.banner) + file2.content : file2.content;
92832
+ await writeGeneratedFile(outputDir, file2.filename, content);
92780
92833
  console.log(`Generated: ${file2.filename}`);
92781
92834
  }
92782
92835
  if (config3.build) {
@@ -92793,7 +92846,8 @@ var runSingleFile = async (config3, schemaPath, outFilePath) => {
92793
92846
  const rootTypeName = deriveRootTypeName(schema);
92794
92847
  console.log(`Root type: ${rootTypeName}`);
92795
92848
  const files = await buildSchema(schema, rootTypeName, undefined, config3.typesOnly, config3.logWarnings, config3.strict, config3.helpers ?? "package", undefined, config3.readonly, config3.typeSuffix);
92796
- await writeFile(outFilePath, combineGeneratedFiles(files), "utf-8");
92849
+ const combined = combineGeneratedFiles(files);
92850
+ await writeFile(outFilePath, config3.banner ? resolveBanner(config3.banner) + combined : combined, "utf-8");
92797
92851
  console.log(`Generated: ${relative(process.cwd(), outFilePath)}`);
92798
92852
  if (config3.build) {
92799
92853
  await buildOutput(outputDir, [basename(outFilePath)], config3.typesOnly);
@@ -92833,7 +92887,8 @@ ${relPath} β†’ ${relNoExt}/ (root type: ${rootTypeName})`);
92833
92887
  sharedHelpers.set(file2.filename, file2.content);
92834
92888
  continue;
92835
92889
  }
92836
- await writeGeneratedFile(schemaSubDir, file2.filename, file2.content);
92890
+ const content = config3.banner ? resolveBanner(config3.banner) + file2.content : file2.content;
92891
+ await writeGeneratedFile(schemaSubDir, file2.filename, content);
92837
92892
  const relativeFilename = join8(relNoExt, file2.filename);
92838
92893
  writtenTsFiles.push(relativeFilename);
92839
92894
  console.log(`Generated: ${relativeFilename}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/mjst",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,10 +42,10 @@
42
42
  "generate-readme": "bun run ./scripts/generate-readme.ts"
43
43
  },
44
44
  "dependencies": {
45
- "@amritk/adapters": "0.2.3",
46
- "@amritk/generate-parsers": "0.6.0",
47
- "@amritk/generate-markdown": "0.2.1",
48
- "@amritk/helpers": "0.6.0"
45
+ "@amritk/adapters": "0.2.4",
46
+ "@amritk/generate-parsers": "0.6.2",
47
+ "@amritk/generate-markdown": "0.2.2",
48
+ "@amritk/helpers": "0.6.1"
49
49
  },
50
50
  "devDependencies": {
51
51
  "json-schema-typed": "^8.0.1"
package/src/cli-config.ts CHANGED
@@ -74,4 +74,11 @@ export type CliConfig = {
74
74
  * (no suffix). The root type name is used verbatim and is unaffected.
75
75
  */
76
76
  readonly typeSuffix?: string
77
+ /**
78
+ * Prepends a comment header to every generated file (excluding `_helpers/`).
79
+ * - `true` β€” use the default message: "This file was auto-generated by @amritk/mjst…"
80
+ * - A string β€” use that text as the message body (wrapped in a JSDoc block).
81
+ * - `false` / omitted β€” no header.
82
+ */
83
+ readonly banner?: boolean | string
77
84
  }
package/src/cli.ts CHANGED
@@ -55,6 +55,15 @@ const loadSchema = async (config: Partial<CliConfig>, schemaPath: string): Promi
55
55
  return getAdapter(inputFormat).toJSONSchema(source)
56
56
  }
57
57
 
58
+ const DEFAULT_BANNER_TEXT =
59
+ 'This file was auto-generated by @amritk/mjst.\n * Do not edit it manually β€” your changes will be overwritten on the next generation.'
60
+
61
+ const resolveBanner = (banner: boolean | string | undefined): string => {
62
+ if (!banner) return ''
63
+ const text = banner === true ? DEFAULT_BANNER_TEXT : banner
64
+ return `/**\n * ${text}\n */\n\n`
65
+ }
66
+
58
67
  /** Writes a generated file, creating any parent directories the filename implies. */
59
68
  const writeGeneratedFile = async (baseDir: string, filename: string, content: string): Promise<void> => {
60
69
  const filePath = join(baseDir, filename)
@@ -151,7 +160,9 @@ const runSingle = async (config: Partial<CliConfig>, schemaPath: string, outputD
151
160
  )
152
161
 
153
162
  for (const file of files) {
154
- await writeGeneratedFile(outputDir, file.filename, file.content)
163
+ const isHelper = file.filename.startsWith('_helpers/')
164
+ const content = config.banner && !isHelper ? resolveBanner(config.banner) + file.content : file.content
165
+ await writeGeneratedFile(outputDir, file.filename, content)
155
166
  console.log(`Generated: ${file.filename}`)
156
167
  }
157
168
 
@@ -192,7 +203,8 @@ const runSingleFile = async (config: Partial<CliConfig>, schemaPath: string, out
192
203
  config.typeSuffix,
193
204
  )
194
205
 
195
- await writeFile(outFilePath, combineGeneratedFiles(files), 'utf-8')
206
+ const combined = combineGeneratedFiles(files)
207
+ await writeFile(outFilePath, config.banner ? resolveBanner(config.banner) + combined : combined, 'utf-8')
196
208
  console.log(`Generated: ${relative(process.cwd(), outFilePath)}`)
197
209
 
198
210
  if (config.build) {
@@ -263,7 +275,8 @@ const runRecursive = async (config: Partial<CliConfig>, schemaDir: string, outpu
263
275
  continue
264
276
  }
265
277
 
266
- await writeGeneratedFile(schemaSubDir, file.filename, file.content)
278
+ const content = config.banner ? resolveBanner(config.banner) + file.content : file.content
279
+ await writeGeneratedFile(schemaSubDir, file.filename, content)
267
280
  const relativeFilename = join(relNoExt, file.filename)
268
281
  writtenTsFiles.push(relativeFilename)
269
282
  console.log(`Generated: ${relativeFilename}`)
@@ -271,4 +271,25 @@ describe('parse-cli-args', () => {
271
271
  expect(parseCliArgs(['--readonly=true'])).toEqual({ readonly: true })
272
272
  expect(parseCliArgs(['--readonly=false'])).toEqual({ readonly: false })
273
273
  })
274
+
275
+ it('parses --banner with no value as true (default message)', () => {
276
+ expect(parseCliArgs(['--banner'])).toEqual({ banner: true })
277
+ })
278
+
279
+ it('parses --banner=true and --banner=false', () => {
280
+ expect(parseCliArgs(['--banner=true'])).toEqual({ banner: true })
281
+ expect(parseCliArgs(['--banner=false'])).toEqual({ banner: false })
282
+ })
283
+
284
+ it('parses --banner=<text> as a custom message string', () => {
285
+ expect(parseCliArgs(['--banner=Do not edit this file.'])).toEqual({ banner: 'Do not edit this file.' })
286
+ })
287
+
288
+ it('parses --banner <text> (space-separated) as a custom message string', () => {
289
+ expect(parseCliArgs(['--banner', 'Do not edit this file.'])).toEqual({ banner: 'Do not edit this file.' })
290
+ })
291
+
292
+ it('parses --banner followed by a flag as true (default message)', () => {
293
+ expect(parseCliArgs(['--banner', '--out-dir', 'dist'])).toEqual({ banner: true, outDir: 'dist' })
294
+ })
274
295
  })
@@ -19,6 +19,7 @@ type MutableConfig = {
19
19
  readonly?: boolean
20
20
  helpers?: 'package' | 'embedded'
21
21
  typeSuffix?: string
22
+ banner?: boolean | string
22
23
  }
23
24
 
24
25
  // Boolean flags toggle on by presence and accept `--flag=false` to opt out.
@@ -127,7 +128,10 @@ export const parseCliArgs = (args: readonly string[]): Partial<CliConfig> => {
127
128
  if (equalsIndex !== -1) {
128
129
  const key = toCamelCase(arg.slice(2, equalsIndex))
129
130
  const value = arg.slice(equalsIndex + 1)
130
- if (BOOLEAN_KEYS.has(key as keyof MutableConfig)) {
131
+ if (key === 'banner') {
132
+ // --banner=false β†’ false, --banner=true β†’ true, --banner=<text> β†’ custom string
133
+ config.banner = value === 'false' ? false : value === 'true' ? true : value
134
+ } else if (BOOLEAN_KEYS.has(key as keyof MutableConfig)) {
131
135
  assignBoolean(config, key, value !== 'false')
132
136
  } else {
133
137
  assignValue(config, key, value)
@@ -137,6 +141,19 @@ export const parseCliArgs = (args: readonly string[]): Partial<CliConfig> => {
137
141
 
138
142
  const key = toCamelCase(arg.slice(2))
139
143
 
144
+ // --banner: presence alone enables the default message; an immediately
145
+ // following non-flag argument is treated as a custom message string.
146
+ if (key === 'banner') {
147
+ const nextArg = args[i + 1]
148
+ if (nextArg && !nextArg.startsWith('--')) {
149
+ config.banner = nextArg
150
+ i++
151
+ } else {
152
+ config.banner = true
153
+ }
154
+ continue
155
+ }
156
+
140
157
  // Boolean flag: presence alone enables it, no value needed
141
158
  if (BOOLEAN_KEYS.has(key as keyof MutableConfig)) {
142
159
  assignBoolean(config, key, true)