@amritk/mjst 0.12.2 → 0.12.3

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.
@@ -1,39 +1,28 @@
1
- // Require whitespace after `import` so a property literally named `import` (e.g.
2
- // `import?: string`) is not mistaken for an import statement.
3
1
  const IMPORT_RE = /^import\s/;
4
2
  const RELATIVE_IMPORT_RE = /\bfrom\s+['"]\.\.?\//;
5
- /**
6
- * Merges the per-schema {@link GeneratedFile}s into a single self-contained
7
- * TypeScript module. Each generated file imports its sibling definitions via
8
- * relative paths; since everything now lives in one file those imports are
9
- * dropped, while any external (package) imports are hoisted to the top and
10
- * deduplicated. The `index.ts` barrel is skipped because it only re-exports the
11
- * declarations that are already inlined here.
12
- *
13
- * Intended for types-only output, where files contain only type definitions and
14
- * type-only relative imports (no runtime helpers).
15
- */
16
- export const combineGeneratedFiles = (files) => {
17
- const externalImports = new Set();
18
- const bodies = [];
19
- for (const file of files) {
20
- if (file.filename === 'index.ts')
21
- continue;
22
- const bodyLines = [];
23
- for (const line of file.content.split('\n')) {
24
- if (IMPORT_RE.test(line.trimStart())) {
25
- // Relative imports point at sibling files that are now inlined — drop them.
26
- if (RELATIVE_IMPORT_RE.test(line))
27
- continue;
28
- externalImports.add(line.trim());
29
- continue;
30
- }
31
- bodyLines.push(line);
32
- }
33
- const body = bodyLines.join('\n').trim();
34
- if (body)
35
- bodies.push(body);
3
+ const combineGeneratedFiles = (files) => {
4
+ const externalImports = /* @__PURE__ */ new Set();
5
+ const bodies = [];
6
+ for (const file of files) {
7
+ if (file.filename === "index.ts")
8
+ continue;
9
+ const bodyLines = [];
10
+ for (const line of file.content.split("\n")) {
11
+ if (IMPORT_RE.test(line.trimStart())) {
12
+ if (RELATIVE_IMPORT_RE.test(line))
13
+ continue;
14
+ externalImports.add(line.trim());
15
+ continue;
16
+ }
17
+ bodyLines.push(line);
36
18
  }
37
- const importBlock = [...externalImports].sort().join('\n');
38
- return [importBlock, bodies.join('\n\n')].filter(Boolean).join('\n\n') + '\n';
19
+ const body = bodyLines.join("\n").trim();
20
+ if (body)
21
+ bodies.push(body);
22
+ }
23
+ const importBlock = [...externalImports].sort().join("\n");
24
+ return [importBlock, bodies.join("\n\n")].filter(Boolean).join("\n\n") + "\n";
25
+ };
26
+ export {
27
+ combineGeneratedFiles
39
28
  };
@@ -1,45 +1,32 @@
1
- import { readFileSync } from 'node:fs';
2
- import { dirname, join, parse as parsePath, resolve } from 'node:path';
3
- /** Reads a package.json's declared dependency names, or `undefined` if unreadable. */
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join, parse as parsePath, resolve } from "node:path";
4
3
  const readDeclaredDeps = (packageJsonPath) => {
5
- try {
6
- const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
7
- return new Set([
8
- ...Object.keys(pkg.dependencies ?? {}),
9
- ...Object.keys(pkg.devDependencies ?? {}),
10
- ...Object.keys(pkg.peerDependencies ?? {}),
11
- ...Object.keys(pkg.optionalDependencies ?? {}),
12
- ]);
13
- }
14
- catch {
15
- return undefined;
16
- }
4
+ try {
5
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
6
+ return /* @__PURE__ */ new Set([
7
+ ...Object.keys(pkg.dependencies ?? {}),
8
+ ...Object.keys(pkg.devDependencies ?? {}),
9
+ ...Object.keys(pkg.peerDependencies ?? {}),
10
+ ...Object.keys(pkg.optionalDependencies ?? {})
11
+ ]);
12
+ } catch {
13
+ return void 0;
14
+ }
17
15
  };
18
- /**
19
- * Detects whether generated parsers should import `@amritk/helpers` from the
20
- * package (`'package'`) or ship the helper sources alongside them (`'embedded'`).
21
- *
22
- * We select `'package'` only when `@amritk/helpers` is a *declared* dependency
23
- * (dependencies / devDependencies / peer / optional) of the target project's
24
- * nearest `package.json` above `outputDir`. Merely being *resolvable* is not
25
- * enough: under npm/bun's hoisted layouts `@amritk/helpers` can sit in
26
- * `node_modules` purely as a transitive dependency of `@amritk/mjst`, which
27
- * makes `import '@amritk/helpers/...'` work locally but break under
28
- * pnpm/isolated installs where undeclared packages are not reachable. Requiring
29
- * a declaration keeps auto-detected `'package'` output portable across install
30
- * layouts; everything else falls back to the self-contained `'embedded'` mode.
31
- */
32
- export const detectHelpersMode = (outputDir) => {
33
- let dir = resolve(outputDir);
34
- const { root } = parsePath(dir);
35
- while (true) {
36
- const declared = readDeclaredDeps(join(dir, 'package.json'));
37
- if (declared) {
38
- return declared.has('@amritk/helpers') ? 'package' : 'embedded';
39
- }
40
- if (dir === root)
41
- break;
42
- dir = dirname(dir);
16
+ const detectHelpersMode = (outputDir) => {
17
+ let dir = resolve(outputDir);
18
+ const { root } = parsePath(dir);
19
+ while (true) {
20
+ const declared = readDeclaredDeps(join(dir, "package.json"));
21
+ if (declared) {
22
+ return declared.has("@amritk/helpers") ? "package" : "embedded";
43
23
  }
44
- return 'embedded';
24
+ if (dir === root)
25
+ break;
26
+ dir = dirname(dir);
27
+ }
28
+ return "embedded";
29
+ };
30
+ export {
31
+ detectHelpersMode
45
32
  };
@@ -1,29 +1,19 @@
1
- import { mkdir, writeFile } from 'node:fs/promises';
2
- import { dirname, join, relative } from 'node:path';
3
- import { buildExampleSchema } from '@amritk/generate-examples';
4
- /**
5
- * Emits fast-check arbitrary + concrete example files for one schema into an
6
- * `examples/` subdirectory of `outputDir`.
7
- *
8
- * The dedicated subdirectory keeps the test-data output from colliding with the
9
- * parser files, which otherwise share the same `<name>.ts` / `index.ts` names.
10
- * Each generated file exports a `FooArbitrary` (a `fast-check` arbitrary that
11
- * produces schema-valid values) and a static `fooExample` value; an `index.ts`
12
- * barrel re-exports them. The arbitraries import `fast-check`, which consumers
13
- * must install as a (dev) dependency.
14
- *
15
- * @returns The written file paths, relative to `outputDir`.
16
- */
17
- export const emitExamples = async (options) => {
18
- const { schema, rootTypeName, outputDir, subDir = '', typeSuffix, bannerPrefix = '' } = options;
19
- const files = await buildExampleSchema(schema, rootTypeName, typeSuffix);
20
- const exampleDir = join(outputDir, 'examples', subDir);
21
- const written = [];
22
- for (const file of files) {
23
- const filePath = join(exampleDir, file.filename);
24
- await mkdir(dirname(filePath), { recursive: true });
25
- await writeFile(filePath, bannerPrefix + file.content, 'utf-8');
26
- written.push(relative(outputDir, filePath));
27
- }
28
- return written;
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, join, relative } from "node:path";
3
+ import { buildExampleSchema } from "@amritk/generate-examples";
4
+ const emitExamples = async (options) => {
5
+ const { schema, rootTypeName, outputDir, subDir = "", typeSuffix, bannerPrefix = "" } = options;
6
+ const files = await buildExampleSchema(schema, rootTypeName, typeSuffix);
7
+ const exampleDir = join(outputDir, "examples", subDir);
8
+ const written = [];
9
+ for (const file of files) {
10
+ const filePath = join(exampleDir, file.filename);
11
+ await mkdir(dirname(filePath), { recursive: true });
12
+ await writeFile(filePath, bannerPrefix + file.content, "utf-8");
13
+ written.push(relative(outputDir, filePath));
14
+ }
15
+ return written;
16
+ };
17
+ export {
18
+ emitExamples
29
19
  };
@@ -1,22 +1,17 @@
1
- /** The reference keywords whose value may point at another document (a non-`#` target). */
2
- const REF_KEYWORDS = ['$ref', '$dynamicRef', '$recursiveRef'];
3
- /**
4
- * Whether `data` contains a reference into another document (a `$ref` value that
5
- * is not a same-document `#...` fragment). Same-document references resolve
6
- * without a disk round-trip, so callers can skip the file-backed resolver — and
7
- * preserve behavior that depends on those internal refs — unless an external
8
- * (cross-file or remote) target exists.
9
- */
10
- export const hasExternalRefs = (data) => {
11
- if (data === null || typeof data !== 'object')
12
- return false;
13
- if (Array.isArray(data))
14
- return data.some(hasExternalRefs);
15
- const obj = data;
16
- for (const keyword of REF_KEYWORDS) {
17
- const value = obj[keyword];
18
- if (typeof value === 'string' && !value.startsWith('#'))
19
- return true;
20
- }
21
- return Object.values(obj).some(hasExternalRefs);
1
+ const REF_KEYWORDS = ["$ref", "$dynamicRef", "$recursiveRef"];
2
+ const hasExternalRefs = (data) => {
3
+ if (data === null || typeof data !== "object")
4
+ return false;
5
+ if (Array.isArray(data))
6
+ return data.some(hasExternalRefs);
7
+ const obj = data;
8
+ for (const keyword of REF_KEYWORDS) {
9
+ const value = obj[keyword];
10
+ if (typeof value === "string" && !value.startsWith("#"))
11
+ return true;
12
+ }
13
+ return Object.values(obj).some(hasExternalRefs);
14
+ };
15
+ export {
16
+ hasExternalRefs
22
17
  };
package/dist/help-text.js CHANGED
@@ -1,10 +1,4 @@
1
- /**
2
- * Usage text printed for `--help`/`-h` and when the CLI is invoked with no
3
- * arguments. Kept as a hand-written constant because the config schema that
4
- * documents these flags (config.schema.json) is not shipped in the published
5
- * package; the help-text test cross-checks that every flag is listed.
6
- */
7
- export const HELP_TEXT = `mjst — generate TypeScript parsers and type definitions from JSON Schemas
1
+ const HELP_TEXT = `mjst \u2014 generate TypeScript parsers and type definitions from JSON Schemas
8
2
 
9
3
  Usage:
10
4
  mjst --schema <path> --out-dir <dir> [options]
@@ -49,3 +43,6 @@ Misc:
49
43
 
50
44
  Docs: https://github.com/amritk/mjst/tree/main/packages/cli#readme
51
45
  `;
46
+ export {
47
+ HELP_TEXT
48
+ };
@@ -1,126 +1,93 @@
1
- import { readFileSync } from 'node:fs';
2
- import { resolve as resolvePath } from 'node:path';
3
- import { createDocument, DiagnosticSeverity, resolveSourceOriginFromMap, } from '@amritk/lint';
4
- import { resolveRefs, resolveRefsFromFile } from '@amritk/resolve-refs';
5
- import { parse as parseYaml } from '@amritk/yaml';
6
- import { hasExternalRefs } from '../has-external-refs.js';
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve as resolvePath } from "node:path";
3
+ import { createDocument, DiagnosticSeverity, resolveSourceOriginFromMap } from "@amritk/lint";
4
+ import { resolveRefs, resolveRefsFromFile } from "@amritk/resolve-refs";
5
+ import { parse as parseYaml } from "@amritk/yaml";
6
+ import { hasExternalRefs } from "../has-external-refs.js";
7
7
  const isRemote = (location) => /^https?:\/\//i.test(location);
8
- /**
9
- * Parses a referenced document by extension: YAML for `.yaml`/`.yml`, JSON for
10
- * `.json`, and for anything else (extensionless, remote) tries JSON first, then
11
- * YAML. YAML is a JSON superset, so this accepts every document the linter does.
12
- */
13
8
  const parseDoc = (content, location) => {
14
- if (/\.ya?ml$/i.test(location))
15
- return parseYaml(content);
16
- if (/\.json$/i.test(location))
17
- return JSON.parse(content);
18
- try {
19
- return JSON.parse(content);
20
- }
21
- catch {
22
- return parseYaml(content);
23
- }
9
+ if (/\.ya?ml$/i.test(location))
10
+ return parseYaml(content);
11
+ if (/\.json$/i.test(location))
12
+ return JSON.parse(content);
13
+ try {
14
+ return JSON.parse(content);
15
+ } catch {
16
+ return parseYaml(content);
17
+ }
24
18
  };
25
- /** Whether the file at `absolute` reads back byte-for-byte as `input` (the document we linted). */
26
19
  const readsBackAs = (absolute, input) => {
27
- try {
28
- return readFileSync(absolute, 'utf8') === input;
29
- }
30
- catch {
31
- return false;
32
- }
20
+ try {
21
+ return readFileSync(absolute, "utf8") === input;
22
+ } catch {
23
+ return false;
24
+ }
33
25
  };
34
- /**
35
- * Builds the {@link ISourceSet} the runner uses to map a finding on a resolved
36
- * (dereferenced) node back to the file it came from. Origins from the resolver
37
- * attribute each inlined node to its source document + path; `get` lazily parses
38
- * each source file (with a position map) so findings on cross-file nodes report
39
- * that file's own `line:column`. The root document is already parsed by the
40
- * linter, so it is reused directly. Remote documents are not re-fetched here, so
41
- * a finding inlined from a URL falls back to the root document's position.
42
- */
43
26
  const buildSourceSet = (rootDocument, resolved, origins, rootLocation) => {
44
- const documents = new Map([[rootLocation, rootDocument]]);
45
- const originMap = origins ?? new Map();
46
- const load = (location) => {
47
- if (documents.has(location))
48
- return documents.get(location);
49
- let document;
50
- if (location !== '' && !isRemote(location)) {
51
- try {
52
- document = createDocument(readFileSync(location, 'utf8'), { source: location });
53
- }
54
- catch {
55
- document = undefined;
56
- }
57
- }
58
- documents.set(location, document);
59
- return document;
60
- };
61
- return {
62
- get: load,
63
- origin: (path) => resolveSourceOriginFromMap(resolved, originMap, rootLocation, path),
64
- };
27
+ const documents = /* @__PURE__ */ new Map([[rootLocation, rootDocument]]);
28
+ const originMap = origins ?? /* @__PURE__ */ new Map();
29
+ const load = (location) => {
30
+ if (documents.has(location))
31
+ return documents.get(location);
32
+ let document;
33
+ if (location !== "" && !isRemote(location)) {
34
+ try {
35
+ document = createDocument(readFileSync(location, "utf8"), { source: location });
36
+ } catch {
37
+ document = void 0;
38
+ }
39
+ }
40
+ documents.set(location, document);
41
+ return document;
42
+ };
43
+ return {
44
+ get: load,
45
+ origin: (path) => resolveSourceOriginFromMap(resolved, originMap, rootLocation, path)
46
+ };
65
47
  };
66
- /** A zero-width range at the document start, for resolve errors with no recoverable position. */
67
48
  const DOCUMENT_START = { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };
68
- /**
69
- * Maps resolver failures ({@link ResolveError}) into lint findings so an
70
- * unresolvable `$ref` — a typo'd pointer, a missing file, a refused/failed
71
- * remote fetch — surfaces as a diagnostic instead of being silently dropped. An
72
- * error carrying a JSON path is anchored to that node's position in the root
73
- * document (best effort, `closest`); otherwise the finding is document-level.
74
- */
75
49
  const toFindings = (errors, root) => errors.map((error) => {
76
- const location = error.path.length > 0 ? root.getLocationForJsonPath(error.path, true) : undefined;
77
- const finding = {
78
- code: 'unresolved-ref',
79
- message: error.message,
80
- path: error.path,
81
- severity: DiagnosticSeverity.Error,
82
- range: location?.range ?? DOCUMENT_START,
83
- };
84
- if (root.source !== undefined)
85
- finding.source = root.source;
86
- return finding;
50
+ const location = error.path.length > 0 ? root.getLocationForJsonPath(error.path, true) : void 0;
51
+ const finding = {
52
+ code: "unresolved-ref",
53
+ message: error.message,
54
+ path: error.path,
55
+ severity: DiagnosticSeverity.Error,
56
+ range: location?.range ?? DOCUMENT_START
57
+ };
58
+ if (root.source !== void 0)
59
+ finding.source = root.source;
60
+ return finding;
87
61
  });
88
- /**
89
- * Builds a {@link LintResolver} backed by `@amritk/resolve-refs`, dereferencing
90
- * `$ref`, `$dynamicRef`/`$dynamicAnchor`, and `$recursiveRef`/`$recursiveAnchor`
91
- * so rules with `resolved: true` (the ruleset default) see through references.
92
- *
93
- * Internal-only documents resolve in memory. A document with cross-file/remote
94
- * refs is resolved from disk **only when its source file reads back exactly as
95
- * the linted input** — so a piped document (`--stdin-filepath`) never resolves
96
- * against a different on-disk file — with remote fetching gated by `options`.
97
- */
98
- export const createLintResolver = (options = {}) => {
99
- const fromFileOptions = {
100
- remote: options.remote ?? false,
101
- ...(options.allowedHosts ? { allowedHosts: options.allowedHosts } : {}),
102
- ...(options.allowPrivateHosts ? { allowPrivateHosts: options.allowPrivateHosts } : {}),
103
- parse: parseDoc,
104
- trackOrigins: true,
105
- };
106
- return async (document, { input }) => {
107
- const { source } = document;
108
- if (source && hasExternalRefs(document.data)) {
109
- const absolute = resolvePath(source);
110
- if (readsBackAs(absolute, input)) {
111
- const { resolved, origins, errors } = await resolveRefsFromFile(absolute, fromFileOptions);
112
- return {
113
- resolved,
114
- sources: buildSourceSet(document, resolved, origins, absolute),
115
- ...(errors.length > 0 ? { diagnostics: toFindings(errors, document) } : {}),
116
- };
117
- }
118
- }
119
- const { resolved, origins, errors } = resolveRefs(document.data, { trackOrigins: true });
62
+ const createLintResolver = (options = {}) => {
63
+ const fromFileOptions = {
64
+ remote: options.remote ?? false,
65
+ ...options.allowedHosts ? { allowedHosts: options.allowedHosts } : {},
66
+ ...options.allowPrivateHosts ? { allowPrivateHosts: options.allowPrivateHosts } : {},
67
+ parse: parseDoc,
68
+ trackOrigins: true
69
+ };
70
+ return async (document, { input }) => {
71
+ const { source } = document;
72
+ if (source && hasExternalRefs(document.data)) {
73
+ const absolute = resolvePath(source);
74
+ if (readsBackAs(absolute, input)) {
75
+ const { resolved: resolved2, origins: origins2, errors: errors2 } = await resolveRefsFromFile(absolute, fromFileOptions);
120
76
  return {
121
- resolved,
122
- sources: buildSourceSet(document, resolved, origins, ''),
123
- ...(errors.length > 0 ? { diagnostics: toFindings(errors, document) } : {}),
77
+ resolved: resolved2,
78
+ sources: buildSourceSet(document, resolved2, origins2, absolute),
79
+ ...errors2.length > 0 ? { diagnostics: toFindings(errors2, document) } : {}
124
80
  };
81
+ }
82
+ }
83
+ const { resolved, origins, errors } = resolveRefs(document.data, { trackOrigins: true });
84
+ return {
85
+ resolved,
86
+ sources: buildSourceSet(document, resolved, origins, ""),
87
+ ...errors.length > 0 ? { diagnostics: toFindings(errors, document) } : {}
125
88
  };
89
+ };
90
+ };
91
+ export {
92
+ createLintResolver
126
93
  };
@@ -1,31 +1,33 @@
1
- import { existsSync } from 'node:fs';
2
- import { readFile } from 'node:fs/promises';
3
- import { dirname, isAbsolute, join, resolve } from 'node:path';
4
- import { pathToFileURL } from 'node:url';
5
- import { parseWithPointers } from '@amritk/lint';
6
- const RULESET_FILENAMES = ['.lint.yaml', '.lint.yml', '.lint.json', '.lint.js', '.lint.mjs'];
7
- /** Loads a ruleset definition from a YAML, JSON, or JS/MJS file. */
8
- export const loadRuleset = async (path) => {
9
- const absolute = isAbsolute(path) ? path : resolve(process.cwd(), path);
10
- if (/\.(c|m)?js$/.test(absolute)) {
11
- const module = await import(pathToFileURL(absolute).href);
12
- return (module.default ?? module);
13
- }
14
- const content = await readFile(absolute, 'utf8');
15
- return parseWithPointers(content).data;
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { parseWithPointers } from "@amritk/lint";
6
+ const RULESET_FILENAMES = [".lint.yaml", ".lint.yml", ".lint.json", ".lint.js", ".lint.mjs"];
7
+ const loadRuleset = async (path) => {
8
+ const absolute = isAbsolute(path) ? path : resolve(process.cwd(), path);
9
+ if (/\.(c|m)?js$/.test(absolute)) {
10
+ const module = await import(pathToFileURL(absolute).href);
11
+ return module.default ?? module;
12
+ }
13
+ const content = await readFile(absolute, "utf8");
14
+ return parseWithPointers(content).data;
16
15
  };
17
- /** Walks up from a directory looking for a `.lint.*` ruleset file. */
18
- export const discoverRuleset = (startDir) => {
19
- let dir = resolve(startDir);
20
- while (true) {
21
- for (const name of RULESET_FILENAMES) {
22
- const candidate = join(dir, name);
23
- if (existsSync(candidate))
24
- return candidate;
25
- }
26
- const parent = dirname(dir);
27
- if (parent === dir)
28
- return undefined;
29
- dir = parent;
16
+ const discoverRuleset = (startDir) => {
17
+ let dir = resolve(startDir);
18
+ while (true) {
19
+ for (const name of RULESET_FILENAMES) {
20
+ const candidate = join(dir, name);
21
+ if (existsSync(candidate))
22
+ return candidate;
30
23
  }
24
+ const parent = dirname(dir);
25
+ if (parent === dir)
26
+ return void 0;
27
+ dir = parent;
28
+ }
29
+ };
30
+ export {
31
+ discoverRuleset,
32
+ loadRuleset
31
33
  };