@amritk/mjst 0.18.0 → 0.19.0

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
@@ -394,7 +394,7 @@ The exit code is `0` on success, `1` when compilation fails (unloadable module,
394
394
  <td align="center"><code>false</code></td>
395
395
  </tr>
396
396
  <tr>
397
- <td colspan="4">Allow generated files to overwrite files that already exist in the output destination. By default a collision with a file mjst did not generate (a hand-written index.ts, say) aborts the run before anything is written, because the generator would otherwise replace it — and, under build, delete it along with the other intermediate sources. Regenerating never needs this flag: each run records what it wrote in a .mjst-manifest.json at the root of the output directory, and anything listed there is replaced freely.</td>
397
+ <td colspan="4">Deprecated and ignored. Generated files always replace whatever is at their path, so this flag no longer does anything; it is still accepted so existing scripts and config files keep running. Reverting an unwanted replacement is version control's job. Compilation under build still only removes the intermediate sources the run itself generated.</td>
398
398
  </tr>
399
399
  <tr>
400
400
  <td>⚠️ <code>logWarnings</code></td>
@@ -85,15 +85,11 @@ export type CliConfig = {
85
85
  */
86
86
  readonly build?: boolean;
87
87
  /**
88
- * When true, generated files may overwrite files that already exist in the
89
- * output destination. By default a collision with a file mjst did not generate
90
- * (a hand-written `index.ts`, say) aborts the run rather than silently
91
- * replacing it — and with `build`, deleting it afterwards. Regeneration does
92
- * not need this: each run records what it wrote in a `.mjst-manifest.json` at
93
- * the root of the output directory, and reclaims those paths freely. With
94
- * `outFile` the "output directory" is the directory holding that file, so the
95
- * manifest lands beside it — generated examples and the `build` output already
96
- * do.
88
+ * @deprecated No longer has any effect, and is accepted only so existing
89
+ * scripts and config files keep running. Generated files always replace
90
+ * whatever is at their path; version control is where an unwanted replacement
91
+ * shows up and gets reverted. `build` still only ever deletes the intermediate
92
+ * sources the run itself committed.
97
93
  */
98
94
  readonly force?: boolean;
99
95
  /**
package/dist/cli.js CHANGED
@@ -67,7 +67,7 @@ const commitOrDiscard = async (writer, stageAll) => {
67
67
  return writer.commit();
68
68
  };
69
69
  const runExamples = async (config, outputDir, tasks) => {
70
- const writer = await createOutputWriter(outputDir, config.force);
70
+ const writer = await createOutputWriter(outputDir);
71
71
  const written = await commitOrDiscard(writer, async () => {
72
72
  for (const task of tasks) {
73
73
  await emitExamples({
@@ -156,7 +156,7 @@ const runSingle = async (config, schemaPath, outputDir) => {
156
156
  const rootTypeName = config.rootType ?? deriveRootTypeName(schema, schemaBaseName(schemaPath));
157
157
  console.log(`Root type: ${rootTypeName}`);
158
158
  const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, helpersMode, void 0, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive, void 0, config.unknownKeys);
159
- const writer = await createOutputWriter(outputDir, config.force);
159
+ const writer = await createOutputWriter(outputDir);
160
160
  const written = await commitOrDiscard(writer, async () => {
161
161
  for (const file of files) {
162
162
  const isHelper = file.filename.startsWith("_helpers/");
@@ -186,7 +186,7 @@ const runSingleFile = async (config, schemaPath, outFilePath) => {
186
186
  console.log(`Root type: ${rootTypeName}`);
187
187
  const files = await buildSchema(schema, rootTypeName, void 0, config.typesOnly, config.logWarnings, config.strict, config.helpers ?? "package", void 0, config.readonly, config.stripUnknown, config.typeSuffix, resolveImportExt(config), config.caseInsensitive, void 0, config.unknownKeys);
188
188
  const combined = combineGeneratedFiles(files);
189
- const writer = await createOutputWriter(outputDir, config.force);
189
+ const writer = await createOutputWriter(outputDir);
190
190
  const written = await commitOrDiscard(writer, async () => {
191
191
  await writer.stage(basename(outFilePath), config.banner ? resolveBanner(config.banner) + combined : combined);
192
192
  });
@@ -214,7 +214,7 @@ const runRecursive = async (config, schemaDir, outputDir) => {
214
214
  await ensureOutputDir(outputDir);
215
215
  const helpersMode = resolveHelpersMode(config, outputDir);
216
216
  const sharedHelpers = /* @__PURE__ */ new Map();
217
- const writer = await createOutputWriter(outputDir, config.force);
217
+ const writer = await createOutputWriter(outputDir);
218
218
  const exampleTasks = [];
219
219
  const stageAll = async () => {
220
220
  for (const schemaFile of schemaFiles) {
@@ -279,7 +279,7 @@ const runAsyncApi = async (config, documentPath, outputDir) => {
279
279
  await ensureOutputDir(outputDir);
280
280
  const helpersMode = resolveHelpersMode(config, outputDir);
281
281
  const sharedHelpers = /* @__PURE__ */ new Map();
282
- const writer = await createOutputWriter(outputDir, config.force);
282
+ const writer = await createOutputWriter(outputDir);
283
283
  const exampleTasks = [];
284
284
  const contractIssues = [];
285
285
  let contractMessageCount = 0;
@@ -385,6 +385,9 @@ const run = async () => {
385
385
  console.error("Error: provide only one of --out-dir or --out-file, not both.");
386
386
  process.exit(1);
387
387
  }
388
+ if (config.force) {
389
+ console.warn("Warning: --force is deprecated and does nothing; generated files always replace what is at their path.");
390
+ }
388
391
  if (config.rootType !== void 0 && !ROOT_TYPE_PATTERN.test(config.rootType)) {
389
392
  console.error(`Error: invalid --root-type "${config.rootType}". Expected a TypeScript identifier (letters, digits, _ or $, not starting with a digit).`);
390
393
  process.exit(1);
@@ -3,22 +3,21 @@
3
3
  * final location under a temporary name, and only once the whole set is on disk
4
4
  * are the temporaries renamed into place.
5
5
  *
6
- * Two failure modes drive the design:
6
+ * The failure mode it exists for is **partial trees**. Writing straight to the
7
+ * final paths meant a failure halfway through (a `_helpers` path occupied by a
8
+ * regular file, a full disk) left some generated files behind and others missing
9
+ * — a tree that compiles against a schema that no longer exists. Staging first
10
+ * means the run either lands completely or leaves the destination as it found it.
11
+ * Renaming is not one atomic operation across the whole set, but every rename
12
+ * happens after all the risky work (generating, creating directories, writing
13
+ * bytes) has already succeeded, so it is the cheapest place to get the guarantee.
7
14
  *
8
- * 1. **Partial trees.** Writing straight to the final paths meant a failure
9
- * halfway through (a `_helpers` path occupied by a regular file, a full disk)
10
- * left some generated files behind and others missing — a tree that compiles
11
- * against a schema that no longer exists. Staging first means the run either
12
- * lands completely or leaves the destination as it found it. Renaming is not
13
- * one atomic operation across the whole set, but every rename happens after
14
- * all the risky work (generating, creating directories, writing bytes) has
15
- * already succeeded, so it is the cheapest place to get the guarantee.
16
- * 2. **Clobbering hand-written files.** A generated name can collide with a file
17
- * the user wrote (`index.ts` is the common one), which used to be overwritten
18
- * without a word — and then, under `--build`, deleted along with the other
19
- * intermediate sources. A file is only replaced when a previous run claimed it
20
- * (see {@link MANIFEST_FILENAME}) or `--force` says so, and `--build` is only
21
- * ever handed the paths this run actually committed.
15
+ * A generated path that already exists is simply replaced. The writer keeps no
16
+ * record of what it produced last time: an output directory is generated output,
17
+ * and version control is where a user sees — and reverts — a replacement they did
18
+ * not want. What the writer still guarantees is that `--build` only ever deletes
19
+ * intermediate sources this run committed, so nothing that was already sitting in
20
+ * the output directory is removed.
22
21
  */
23
22
  export type OutputWriter = {
24
23
  /**
@@ -34,7 +33,10 @@ export type OutputWriter = {
34
33
  /**
35
34
  * Creates an {@link OutputWriter} rooted at `rootDir`.
36
35
  *
36
+ * Still async with nothing to await: it read the previous run's manifest here
37
+ * until that was removed, and every call site already awaits it. Keeping the
38
+ * signature leaves room for setup that does need I/O.
39
+ *
37
40
  * @param rootDir - Output root. Every write must resolve inside it.
38
- * @param force - Overwrite pre-existing files instead of refusing (`--force`).
39
41
  */
40
- export declare const createOutputWriter: (rootDir: string, force?: boolean) => Promise<OutputWriter>;
42
+ export declare const createOutputWriter: (rootDir: string) => Promise<OutputWriter>;
@@ -1,23 +1,8 @@
1
- import { readFile, rename, rm, stat, writeFile } from "node:fs/promises";
2
- import { dirname, join, relative, resolve, sep } from "node:path";
1
+ import { rename, rm, writeFile } from "node:fs/promises";
2
+ import { dirname, resolve, sep } from "node:path";
3
3
  import { ensureOutputDir } from "./ensure-output-dir.js";
4
- const MANIFEST_FILENAME = ".mjst-manifest.json";
5
- const pathExists = async (path) => stat(path).then(() => true, () => false);
6
- const readManifest = async (root) => {
7
- const raw = await readFile(join(root, MANIFEST_FILENAME), "utf-8").catch(() => void 0);
8
- if (raw === void 0)
9
- return /* @__PURE__ */ new Set();
10
- try {
11
- const parsed = JSON.parse(raw);
12
- const files = Array.isArray(parsed.files) ? parsed.files : [];
13
- return new Set(files.filter((file) => typeof file === "string").map((file) => resolve(root, file)));
14
- } catch {
15
- return /* @__PURE__ */ new Set();
16
- }
17
- };
18
- const createOutputWriter = async (rootDir, force = false) => {
4
+ const createOutputWriter = async (rootDir) => {
19
5
  const root = resolve(rootDir);
20
- const owned = await readManifest(root);
21
6
  const staged = [];
22
7
  const stamp = `${process.pid}-${Date.now().toString(36)}`;
23
8
  const stage = async (relativePath, content) => {
@@ -25,9 +10,6 @@ const createOutputWriter = async (rootDir, force = false) => {
25
10
  if (targetPath !== root && !targetPath.startsWith(root + sep)) {
26
11
  throw new Error(`Refusing to write "${relativePath}": it resolves outside the output directory ${root}.`);
27
12
  }
28
- if (!force && !owned.has(targetPath) && await pathExists(targetPath)) {
29
- throw new Error(`Refusing to overwrite ${targetPath}: it already exists and was not generated by mjst. Move it aside, choose another output directory, or pass --force to overwrite it.`);
30
- }
31
13
  const tempPath = `${targetPath}.mjst-${stamp}-${staged.length}.tmp`;
32
14
  await ensureOutputDir(dirname(targetPath));
33
15
  await writeFile(tempPath, content, "utf-8");
@@ -36,12 +18,6 @@ const createOutputWriter = async (rootDir, force = false) => {
36
18
  const commit = async () => {
37
19
  for (const file of staged) {
38
20
  await rename(file.tempPath, file.targetPath);
39
- owned.add(file.targetPath);
40
- }
41
- if (staged.length > 0) {
42
- const files = [...owned].map((path) => relative(root, path)).sort();
43
- await writeFile(join(root, MANIFEST_FILENAME), `${JSON.stringify({ version: 1, files }, null, 2)}
44
- `, "utf-8");
45
21
  }
46
22
  return staged.map((file) => file.relativePath);
47
23
  };
@@ -11,10 +11,9 @@ export type EmitExamplesOptions = {
11
11
  /**
12
12
  * The run's writer, rooted at the output destination. Examples used to be
13
13
  * written straight to disk with `mkdir` + `writeFile`, which meant the one
14
- * output tree mjst emits that skipped the ownership check — a hand-written
15
- * `examples/index.ts` was overwritten without a word. Taking the writer instead
16
- * of a directory makes that impossible to reintroduce: there is no path here
17
- * that does not go through staging, the manifest, and `--force`.
14
+ * output tree mjst emits that skipped staging entirely — a failure part-way
15
+ * through left a half-written `examples/` tree behind. Taking the writer
16
+ * instead of a directory makes that impossible to reintroduce.
18
17
  */
19
18
  readonly writer: OutputWriter;
20
19
  /**
@@ -4,4 +4,4 @@
4
4
  * documents these flags (config.schema.json) is not shipped in the published
5
5
  * package; the help-text test cross-checks that every flag is listed.
6
6
  */
7
- export declare const HELP_TEXT = "mjst \u2014 generate TypeScript parsers and type definitions from JSON Schemas\n\nUsage:\n mjst --schema <path> --out-dir <dir> [options]\n mjst --schema-dir <dir> --out-dir <dir> [options]\n mjst --schema <path> --out-file <file> --types-only [options]\n\nSubcommands (each with its own flags and --help):\n mjst lint <files> Lint JSON/YAML documents against a ruleset\n mjst compile-api <module> --out <file> Compile @amritk/api route contracts into a fetch-handler module\n\nInput:\n --schema <path> Schema to process: a JSON Schema file, or a module when --input is set\n --schema-dir <dir> Directory of JSON Schemas, processed recursively (instead of --schema)\n --input <format> Schema source format: json (default), typebox, zod, valibot, effect, avro,\n or asyncapi (an AsyncAPI 2.x/3.0 document, JSON or YAML)\n --export <name> Which export of the schema module to use when --input names a module format\n\nOutput:\n --out-dir <dir> Output directory for the generated files\n --out-file <file> Single-file output instead of a directory (requires --types-only)\n --types-only Generate type definitions only, without parser functions\n --validators Also emit validateX/isX validators into a validators/ subdir\n --examples Also emit fast-check arbitraries + example values under examples/\n --message-contracts Also emit @amritk/api channel contracts under contracts/ (--input asyncapi only)\n --discriminator <p> Property naming which message a frame is, for --message-contracts (default: type)\n --build Compile the generated files to .js/.d.ts (implies --import-ext js)\n --force Overwrite pre-existing files in the output destination\n --import-ext <ext> Extension on generated relative imports: ts (default) or js\n --helpers <mode> Runtime helpers: package or embedded (default: auto-detect from package.json)\n --root-type <name> Root type name for a single --schema run (default: schema title or filename)\n --type-suffix <s> Suffix appended to every $ref-derived type name\n --banner [text] Prepend a header comment to every generated file\n --readonly Emit deeply readonly type definitions\n\nValidation:\n --strict Throw on type/shape mismatches instead of coercing to defaults\n --strip-unknown Silently drop undeclared input keys at every nesting level\n --case-insensitive Normalize mis-cased enum/const strings to their exact casing\n --log-warnings console.warn on input keys not declared in the schema\n --unknown-keys <s> How closed objects are proven free of undeclared keys:\n count-keys (default, fastest on Bun) or count-enumerable (fastest on Node)\n\nReference resolution:\n --resolve-remote Fetch http(s) $refs while resolving cross-file/remote refs (off by default)\n --allowed-hosts <h> Restrict remote $ref fetches to these hosts (comma-separated; implies --resolve-remote)\n --allow-private-hosts Permit remote $refs to private/loopback hosts (SSRF guard, off by default)\n --allowed-roots <d> Extra directories a local $ref may resolve into, on top of the schema's own\n (comma-separated; relative to the current directory)\n\nMisc:\n --config <path> JSON config file with the same keys; CLI flags take precedence\n --version, -v Print the CLI version\n --help, -h Print this help\n\nDocs: https://github.com/amritk/mjst/tree/main/packages/cli#readme\n";
7
+ export declare const HELP_TEXT = "mjst \u2014 generate TypeScript parsers and type definitions from JSON Schemas\n\nUsage:\n mjst --schema <path> --out-dir <dir> [options]\n mjst --schema-dir <dir> --out-dir <dir> [options]\n mjst --schema <path> --out-file <file> --types-only [options]\n\nSubcommands (each with its own flags and --help):\n mjst lint <files> Lint JSON/YAML documents against a ruleset\n mjst compile-api <module> --out <file> Compile @amritk/api route contracts into a fetch-handler module\n\nInput:\n --schema <path> Schema to process: a JSON Schema file, or a module when --input is set\n --schema-dir <dir> Directory of JSON Schemas, processed recursively (instead of --schema)\n --input <format> Schema source format: json (default), typebox, zod, valibot, effect, avro,\n or asyncapi (an AsyncAPI 2.x/3.0 document, JSON or YAML)\n --export <name> Which export of the schema module to use when --input names a module format\n\nOutput:\n --out-dir <dir> Output directory for the generated files\n --out-file <file> Single-file output instead of a directory (requires --types-only)\n --types-only Generate type definitions only, without parser functions\n --validators Also emit validateX/isX validators into a validators/ subdir\n --examples Also emit fast-check arbitraries + example values under examples/\n --message-contracts Also emit @amritk/api channel contracts under contracts/ (--input asyncapi only)\n --discriminator <p> Property naming which message a frame is, for --message-contracts (default: type)\n --build Compile the generated files to .js/.d.ts (implies --import-ext js)\n --force Deprecated; ignored (generated files always overwrite)\n --import-ext <ext> Extension on generated relative imports: ts (default) or js\n --helpers <mode> Runtime helpers: package or embedded (default: auto-detect from package.json)\n --root-type <name> Root type name for a single --schema run (default: schema title or filename)\n --type-suffix <s> Suffix appended to every $ref-derived type name\n --banner [text] Prepend a header comment to every generated file\n --readonly Emit deeply readonly type definitions\n\nValidation:\n --strict Throw on type/shape mismatches instead of coercing to defaults\n --strip-unknown Silently drop undeclared input keys at every nesting level\n --case-insensitive Normalize mis-cased enum/const strings to their exact casing\n --log-warnings console.warn on input keys not declared in the schema\n --unknown-keys <s> How closed objects are proven free of undeclared keys:\n count-keys (default, fastest on Bun) or count-enumerable (fastest on Node)\n\nReference resolution:\n --resolve-remote Fetch http(s) $refs while resolving cross-file/remote refs (off by default)\n --allowed-hosts <h> Restrict remote $ref fetches to these hosts (comma-separated; implies --resolve-remote)\n --allow-private-hosts Permit remote $refs to private/loopback hosts (SSRF guard, off by default)\n --allowed-roots <d> Extra directories a local $ref may resolve into, on top of the schema's own\n (comma-separated; relative to the current directory)\n\nMisc:\n --config <path> JSON config file with the same keys; CLI flags take precedence\n --version, -v Print the CLI version\n --help, -h Print this help\n\nDocs: https://github.com/amritk/mjst/tree/main/packages/cli#readme\n";
package/dist/help-text.js CHANGED
@@ -25,7 +25,7 @@ Output:
25
25
  --message-contracts Also emit @amritk/api channel contracts under contracts/ (--input asyncapi only)
26
26
  --discriminator <p> Property naming which message a frame is, for --message-contracts (default: type)
27
27
  --build Compile the generated files to .js/.d.ts (implies --import-ext js)
28
- --force Overwrite pre-existing files in the output destination
28
+ --force Deprecated; ignored (generated files always overwrite)
29
29
  --import-ext <ext> Extension on generated relative imports: ts (default) or js
30
30
  --helpers <mode> Runtime helpers: package or embedded (default: auto-detect from package.json)
31
31
  --root-type <name> Root type name for a single --schema run (default: schema title or filename)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/mjst",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -53,7 +53,7 @@
53
53
  "@amritk/api": "^0.16.3",
54
54
  "@amritk/asyncapi": "^0.2.0",
55
55
  "@amritk/generate-examples": "^0.8.3",
56
- "@amritk/generate-parsers": "^0.21.1",
56
+ "@amritk/generate-parsers": "^0.22.0",
57
57
  "@amritk/generate-validators": "^0.16.1",
58
58
  "@amritk/helpers": "^0.20.0",
59
59
  "@amritk/lint": "^0.5.3",