@stonecrop/schema 0.31.0 → 0.32.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.
Files changed (82) hide show
  1. package/README.md +10 -10
  2. package/dist/cli.js +194 -121
  3. package/dist/cli.js.map +1 -1
  4. package/dist/converter-CLwduvT_.js +2102 -0
  5. package/dist/converter-CLwduvT_.js.map +1 -0
  6. package/dist/flatten-Bx2cfvw3.js +37 -0
  7. package/dist/flatten-Bx2cfvw3.js.map +1 -0
  8. package/dist/index.js +123 -117
  9. package/dist/index.js.map +1 -1
  10. package/dist/record-BQOOi83C.js +134 -0
  11. package/dist/record-BQOOi83C.js.map +1 -0
  12. package/dist/record.js +2 -6
  13. package/dist/tsdoc-metadata.json +1 -1
  14. package/package.json +34 -18
  15. package/dist/flatten-C1MjkzFh.js +0 -10
  16. package/dist/flatten-C1MjkzFh.js.map +0 -1
  17. package/dist/record-Bc0lI9Rq.js +0 -61
  18. package/dist/record-Bc0lI9Rq.js.map +0 -1
  19. package/dist/record.js.map +0 -1
  20. package/dist/schema.tsbuildinfo +0 -1
  21. package/dist/src/badge.d.ts +0 -74
  22. package/dist/src/badge.d.ts.map +0 -1
  23. package/dist/src/badge.js +0 -158
  24. package/dist/src/cli.d.ts +0 -3
  25. package/dist/src/cli.d.ts.map +0 -1
  26. package/dist/src/cli.js +0 -292
  27. package/dist/src/column-schema.d.ts +0 -163
  28. package/dist/src/column-schema.d.ts.map +0 -1
  29. package/dist/src/column-schema.js +0 -0
  30. package/dist/src/component-meta.d.ts +0 -96
  31. package/dist/src/component-meta.d.ts.map +0 -1
  32. package/dist/src/component-meta.js +0 -88
  33. package/dist/src/converter/aggregate.d.ts +0 -127
  34. package/dist/src/converter/aggregate.d.ts.map +0 -1
  35. package/dist/src/converter/aggregate.js +0 -235
  36. package/dist/src/converter/authored.d.ts +0 -43
  37. package/dist/src/converter/authored.d.ts.map +0 -1
  38. package/dist/src/converter/authored.js +0 -52
  39. package/dist/src/converter/heuristics.d.ts +0 -60
  40. package/dist/src/converter/heuristics.d.ts.map +0 -1
  41. package/dist/src/converter/heuristics.js +0 -304
  42. package/dist/src/converter/index.d.ts +0 -51
  43. package/dist/src/converter/index.d.ts.map +0 -1
  44. package/dist/src/converter/index.js +0 -195
  45. package/dist/src/converter/merge.d.ts +0 -102
  46. package/dist/src/converter/merge.d.ts.map +0 -1
  47. package/dist/src/converter/merge.js +0 -136
  48. package/dist/src/converter/scalars.d.ts +0 -46
  49. package/dist/src/converter/scalars.d.ts.map +0 -1
  50. package/dist/src/converter/scalars.js +0 -83
  51. package/dist/src/converter/types.d.ts +0 -157
  52. package/dist/src/converter/types.d.ts.map +0 -1
  53. package/dist/src/converter/types.js +0 -5
  54. package/dist/src/doctype.d.ts +0 -516
  55. package/dist/src/doctype.d.ts.map +0 -1
  56. package/dist/src/doctype.js +0 -343
  57. package/dist/src/field.d.ts +0 -423
  58. package/dist/src/field.d.ts.map +0 -1
  59. package/dist/src/field.js +0 -378
  60. package/dist/src/flatten.d.ts +0 -29
  61. package/dist/src/flatten.d.ts.map +0 -1
  62. package/dist/src/flatten.js +0 -38
  63. package/dist/src/index.d.ts +0 -16
  64. package/dist/src/index.d.ts.map +0 -1
  65. package/dist/src/index.js +0 -20
  66. package/dist/src/mode.d.ts +0 -15
  67. package/dist/src/mode.d.ts.map +0 -1
  68. package/dist/src/mode.js +0 -0
  69. package/dist/src/naming.d.ts +0 -80
  70. package/dist/src/naming.d.ts.map +0 -1
  71. package/dist/src/naming.js +0 -106
  72. package/dist/src/record.d.ts +0 -29
  73. package/dist/src/record.d.ts.map +0 -1
  74. package/dist/src/record.js +0 -55
  75. package/dist/src/table.d.ts +0 -33
  76. package/dist/src/table.d.ts.map +0 -1
  77. package/dist/src/table.js +0 -25
  78. package/dist/src/validation.d.ts +0 -54
  79. package/dist/src/validation.d.ts.map +0 -1
  80. package/dist/src/validation.js +0 -60
  81. package/dist/validation-C9P__pRF.js +0 -994
  82. package/dist/validation-C9P__pRF.js.map +0 -1
package/README.md CHANGED
@@ -17,10 +17,10 @@ This package is schema-only and has no UI dependencies - it can be used in both
17
17
  ## Installation
18
18
 
19
19
  ```bash
20
- # From the monorepo root
21
- rush update
20
+ # From the monorepo root, which links the workspace copy
21
+ pnpm install
22
22
 
23
- # Or with pnpm
23
+ # Or, in a consuming project
24
24
  pnpm add @stonecrop/schema
25
25
  ```
26
26
 
@@ -437,20 +437,20 @@ This package provides the type system used throughout Stonecrop:
437
437
  ## Development
438
438
 
439
439
  ```bash
440
- # Install dependencies
441
- rush update
440
+ # Install dependencies (from the repo root)
441
+ pnpm install
442
442
 
443
- # Build
444
- rushx build
443
+ # Build (a Vite+ task, so `pnpm run build` does not resolve it)
444
+ pnpm exec vp run build
445
445
 
446
446
  # Run tests
447
- rushx test
447
+ pnpm run test
448
448
 
449
449
  # Watch mode
450
- rushx test:watch
450
+ pnpm run test:watch
451
451
 
452
452
  # Generate API documentation
453
- rushx docs
453
+ pnpm run docs
454
454
  ```
455
455
 
456
456
  ## TypeScript Support
package/dist/cli.js CHANGED
@@ -1,124 +1,194 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync as i, existsSync as y, mkdirSync as E, writeFileSync as C } from "node:fs";
3
- import { resolve as u, join as A } from "node:path";
4
- import { parseArgs as J } from "node:util";
5
- import { getIntrospectionQuery as R } from "graphql";
6
- import { c as F, a as G, p as L, m as q, f as D, s as T, v as U } from "./validation-C9P__pRF.js";
7
- async function Q(e, w) {
8
- const o = await fetch(e, {
9
- method: "POST",
10
- headers: {
11
- "Content-Type": "application/json",
12
- ...w
13
- },
14
- body: JSON.stringify({
15
- query: R()
16
- })
17
- });
18
- if (!o.ok)
19
- throw new Error(`Failed to fetch introspection: ${o.status} ${o.statusText}`);
20
- const l = await o.json();
21
- if (l.errors?.length)
22
- throw new Error(`GraphQL errors: ${l.errors.map((r) => r.message).join(", ")}`);
23
- if (!l.data)
24
- throw new Error("No data in introspection response");
25
- return l.data;
2
+ import { G as stripFieldKind, _ as validateDoctype, i as authoredPrimaryKey, n as formatDoctypeDrift, r as mergeIntrospectedDoctype, s as planGeneration, t as convertGraphQLSchema } from "./converter-CLwduvT_.js";
3
+ import { getIntrospectionQuery } from "graphql";
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { join, resolve } from "node:path";
6
+ import { parseArgs } from "node:util";
7
+ //#region src/cli.ts
8
+ /**
9
+ * Stonecrop Schema CLI
10
+ *
11
+ * Converts GraphQL introspection results to Stonecrop doctype JSON schemas.
12
+ *
13
+ * Usage:
14
+ * stonecrop-schema generate --endpoint <url> --output <dir>
15
+ * stonecrop-schema generate --introspection <file.json> --output <dir>
16
+ * stonecrop-schema generate --sdl <file.graphql> --output <dir>
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+ /**
21
+ * Fetch an introspection result from a live GraphQL endpoint.
22
+ *
23
+ * @param endpoint - The GraphQL endpoint URL
24
+ * @param headers - Optional HTTP headers
25
+ * @returns The introspection query result
26
+ */
27
+ async function fetchIntrospection(endpoint, headers) {
28
+ const response = await fetch(endpoint, {
29
+ method: "POST",
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ ...headers
33
+ },
34
+ body: JSON.stringify({ query: getIntrospectionQuery() })
35
+ });
36
+ if (!response.ok) throw new Error(`Failed to fetch introspection: ${response.status} ${response.statusText}`);
37
+ const json = await response.json();
38
+ if (json.errors?.length) throw new Error(`GraphQL errors: ${json.errors.map((e) => e.message).join(", ")}`);
39
+ if (!json.data) throw new Error("No data in introspection response");
40
+ return json.data;
26
41
  }
27
- async function W() {
28
- const { values: e, positionals: w } = J({
29
- allowPositionals: !0,
30
- options: {
31
- endpoint: { type: "string", short: "e" },
32
- introspection: { type: "string", short: "i" },
33
- sdl: { type: "string", short: "s" },
34
- output: { type: "string", short: "o" },
35
- include: { type: "string" },
36
- exclude: { type: "string" },
37
- names: { type: "string" },
38
- "custom-scalars": { type: "string" },
39
- "include-unmapped": { type: "boolean", default: !1 },
40
- "no-aggregates": { type: "boolean", default: !1 },
41
- check: { type: "boolean", default: !1 },
42
- help: { type: "boolean", short: "h" }
43
- }
44
- }), o = w[0];
45
- (e.help || !o) && (I(), process.exit(o ? 0 : 1)), o !== "generate" && (console.error(`Unknown command: ${o}`), console.error("Available commands: generate"), process.exit(1)), [e.endpoint, e.introspection, e.sdl].filter(Boolean).length !== 1 && (console.error("Exactly one of --endpoint, --introspection, or --sdl must be provided"), process.exit(1)), e.output || (console.error("--output <dir> is required"), process.exit(1));
46
- const r = u(e.output), c = {
47
- includeUnmappedMeta: e["include-unmapped"]
48
- };
49
- if (e.include && (c.include = e.include.split(",").map((t) => t.trim())), e.exclude && (c.exclude = e.exclude.split(",").map((t) => t.trim())), e.names) {
50
- const t = u(e.names);
51
- c.doctypeNames = JSON.parse(i(t, "utf-8"));
52
- }
53
- if (c.onWarning = (t) => console.warn(` WARN: ${t}`), e["custom-scalars"]) {
54
- const t = u(e["custom-scalars"]), n = i(t, "utf-8");
55
- c.customScalars = JSON.parse(n);
56
- }
57
- let h;
58
- if (e.endpoint)
59
- console.log(`Fetching introspection from ${e.endpoint}...`), h = await Q(e.endpoint);
60
- else if (e.introspection) {
61
- const t = u(e.introspection), n = i(t, "utf-8"), a = JSON.parse(n);
62
- h = a.data ?? a;
63
- } else {
64
- const t = u(e.sdl);
65
- h = i(t, "utf-8");
66
- }
67
- const v = F(h, c);
68
- v.length === 0 && (console.warn("No entity types found in the schema. Check your include/exclude filters."), process.exit(0));
69
- const O = {};
70
- for (const t of v) {
71
- const n = A(r, `${t.slug}.json`);
72
- if (!y(n)) continue;
73
- const a = G(JSON.parse(i(n, "utf-8")));
74
- a !== void 0 && (O[t.name] = a);
75
- }
76
- const k = L(v, {
77
- noAggregates: e["no-aggregates"],
78
- identity: O,
79
- onWarning: (t) => console.warn(` WARN: ${t}`)
80
- });
81
- y(r) || E(r, { recursive: !0 });
82
- let S = 0, f = 0, m = 0;
83
- const $ = [];
84
- for (const { generated: t, basis: n, subset: a } of k) {
85
- const x = `${t.slug}.json`, p = A(r, x);
86
- let N = t;
87
- if (y(p)) {
88
- const { doctype: s, drift: d } = q(JSON.parse(i(p, "utf-8")), n, {
89
- subset: a
90
- });
91
- N = s, $.push(...D(d));
92
- }
93
- const g = { ...N };
94
- Array.isArray(g.fields) && (g.fields = g.fields.map(T));
95
- const b = JSON.stringify(g, null, " ") + `
96
- `, j = y(p) && i(p, "utf-8") === b;
97
- j || m++, !e.check && !j && C(p, b, "utf-8");
98
- const P = U(N);
99
- if (P.success) {
100
- const s = t.fields.filter((d) => d._unmapped);
101
- s.length > 0 && (S++, console.warn(
102
- ` WARN: ${x} has ${s.length} unmapped field(s): ${s.map((d) => d.fieldname).join(", ")}`
103
- ));
104
- } else {
105
- f++, console.error(` ERROR: ${x} failed validation:`);
106
- for (const s of P.errors)
107
- console.error(` ${s.path.join(".")}: ${s.message}`);
108
- }
109
- }
110
- if ($.length > 0) {
111
- console.log(`
112
- Drift between the authored doctypes and the schema (reported, not applied):`);
113
- for (const t of $) console.log(t);
114
- }
115
- console.log(
116
- `
117
- ${e.check ? "Checked" : "Generated"} ${k.length} doctype(s) in ${r}` + (m ? ` (${m} ${e.check ? "would change" : "written"})` : " (all up to date)") + (S ? ` (${S} with warnings)` : "") + (f ? ` (${f} with errors)` : "")
118
- ), (f > 0 || e.check && m > 0) && process.exit(1);
42
+ async function main() {
43
+ const { values, positionals } = parseArgs({
44
+ allowPositionals: true,
45
+ options: {
46
+ endpoint: {
47
+ type: "string",
48
+ short: "e"
49
+ },
50
+ introspection: {
51
+ type: "string",
52
+ short: "i"
53
+ },
54
+ sdl: {
55
+ type: "string",
56
+ short: "s"
57
+ },
58
+ output: {
59
+ type: "string",
60
+ short: "o"
61
+ },
62
+ include: { type: "string" },
63
+ exclude: { type: "string" },
64
+ names: { type: "string" },
65
+ "custom-scalars": { type: "string" },
66
+ "include-unmapped": {
67
+ type: "boolean",
68
+ default: false
69
+ },
70
+ "no-aggregates": {
71
+ type: "boolean",
72
+ default: false
73
+ },
74
+ check: {
75
+ type: "boolean",
76
+ default: false
77
+ },
78
+ help: {
79
+ type: "boolean",
80
+ short: "h"
81
+ }
82
+ }
83
+ });
84
+ const command = positionals[0];
85
+ if (values.help || !command) {
86
+ printHelp();
87
+ process.exit(command ? 0 : 1);
88
+ }
89
+ if (command !== "generate") {
90
+ console.error(`Unknown command: ${command}`);
91
+ console.error("Available commands: generate");
92
+ process.exit(1);
93
+ }
94
+ if ([
95
+ values.endpoint,
96
+ values.introspection,
97
+ values.sdl
98
+ ].filter(Boolean).length !== 1) {
99
+ console.error("Exactly one of --endpoint, --introspection, or --sdl must be provided");
100
+ process.exit(1);
101
+ }
102
+ if (!values.output) {
103
+ console.error("--output <dir> is required");
104
+ process.exit(1);
105
+ }
106
+ const outputDir = resolve(values.output);
107
+ const options = { includeUnmappedMeta: values["include-unmapped"] };
108
+ if (values.include) options.include = values.include.split(",").map((s) => s.trim());
109
+ if (values.exclude) options.exclude = values.exclude.split(",").map((s) => s.trim());
110
+ if (values.names) {
111
+ const namesPath = resolve(values.names);
112
+ options.doctypeNames = JSON.parse(readFileSync(namesPath, "utf-8"));
113
+ }
114
+ options.onWarning = (message) => console.warn(` WARN: ${message}`);
115
+ if (values["custom-scalars"]) {
116
+ const scalarsPath = resolve(values["custom-scalars"]);
117
+ const scalarsContent = readFileSync(scalarsPath, "utf-8");
118
+ options.customScalars = JSON.parse(scalarsContent);
119
+ }
120
+ let source;
121
+ if (values.endpoint) {
122
+ console.log(`Fetching introspection from ${values.endpoint}...`);
123
+ source = await fetchIntrospection(values.endpoint);
124
+ } else if (values.introspection) {
125
+ const filePath = resolve(values.introspection);
126
+ const content = readFileSync(filePath, "utf-8");
127
+ const parsed = JSON.parse(content);
128
+ source = parsed.data ?? parsed;
129
+ } else {
130
+ const filePath = resolve(values.sdl);
131
+ source = readFileSync(filePath, "utf-8");
132
+ }
133
+ const entities = convertGraphQLSchema(source, options);
134
+ if (entities.length === 0) {
135
+ console.warn("No entity types found in the schema. Check your include/exclude filters.");
136
+ process.exit(0);
137
+ }
138
+ const identity = {};
139
+ for (const entity of entities) {
140
+ const entityPath = join(outputDir, `${entity.slug}.json`);
141
+ if (!existsSync(entityPath)) continue;
142
+ const declared = authoredPrimaryKey(JSON.parse(readFileSync(entityPath, "utf-8")));
143
+ if (declared !== void 0) identity[entity.name] = declared;
144
+ }
145
+ const doctypes = planGeneration(entities, {
146
+ noAggregates: values["no-aggregates"],
147
+ identity,
148
+ onWarning: (message) => console.warn(` WARN: ${message}`)
149
+ });
150
+ if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
151
+ let warnings = 0;
152
+ let errors = 0;
153
+ let changed = 0;
154
+ const driftLines = [];
155
+ for (const { generated, basis, subset } of doctypes) {
156
+ const fileName = `${generated.slug}.json`;
157
+ const filePath = join(outputDir, fileName);
158
+ let output = generated;
159
+ if (existsSync(filePath)) {
160
+ const { doctype: merged, drift } = mergeIntrospectedDoctype(JSON.parse(readFileSync(filePath, "utf-8")), basis, { subset });
161
+ output = merged;
162
+ driftLines.push(...formatDoctypeDrift(drift));
163
+ }
164
+ const serializable = { ...output };
165
+ if (Array.isArray(serializable.fields)) serializable.fields = serializable.fields.map(stripFieldKind);
166
+ const json = JSON.stringify(serializable, null, " ") + "\n";
167
+ const unchanged = existsSync(filePath) && readFileSync(filePath, "utf-8") === json;
168
+ if (!unchanged) changed++;
169
+ if (!values.check && !unchanged) writeFileSync(filePath, json, "utf-8");
170
+ const validation = validateDoctype(output);
171
+ if (!validation.success) {
172
+ errors++;
173
+ console.error(` ERROR: ${fileName} failed validation:`);
174
+ for (const err of validation.errors) console.error(` ${err.path.join(".")}: ${err.message}`);
175
+ } else {
176
+ const unmappedFields = generated.fields.filter((f) => f._unmapped);
177
+ if (unmappedFields.length > 0) {
178
+ warnings++;
179
+ console.warn(` WARN: ${fileName} has ${unmappedFields.length} unmapped field(s): ${unmappedFields.map((f) => f.fieldname).join(", ")}`);
180
+ }
181
+ }
182
+ }
183
+ if (driftLines.length > 0) {
184
+ console.log("\nDrift between the authored doctypes and the schema (reported, not applied):");
185
+ for (const line of driftLines) console.log(line);
186
+ }
187
+ console.log(`\n${values.check ? "Checked" : "Generated"} ${doctypes.length} doctype(s) in ${outputDir}` + (changed ? ` (${changed} ${values.check ? "would change" : "written"})` : " (all up to date)") + (warnings ? ` (${warnings} with warnings)` : "") + (errors ? ` (${errors} with errors)` : ""));
188
+ if (errors > 0 || values.check && changed > 0) process.exit(1);
119
189
  }
120
- function I() {
121
- console.log(`
190
+ function printHelp() {
191
+ console.log(`
122
192
  stonecrop-schema - Convert GraphQL schemas to Stonecrop doctypes
123
193
 
124
194
  USAGE:
@@ -172,7 +242,10 @@ EXAMPLES:
172
242
  --include "User,Post,Comment"
173
243
  `);
174
244
  }
175
- W().catch((e) => {
176
- console.error("Error:", e.message), process.exit(1);
245
+ main().catch((err) => {
246
+ console.error("Error:", err.message);
247
+ process.exit(1);
177
248
  });
178
- //# sourceMappingURL=cli.js.map
249
+ //#endregion
250
+
251
+ //# sourceMappingURL=cli.js.map
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/* oxlint-disable no-console */\n\n/**\n * Stonecrop Schema CLI\n *\n * Converts GraphQL introspection results to Stonecrop doctype JSON schemas.\n *\n * Usage:\n * stonecrop-schema generate --endpoint <url> --output <dir>\n * stonecrop-schema generate --introspection <file.json> --output <dir>\n * stonecrop-schema generate --sdl <file.graphql> --output <dir>\n *\n * @packageDocumentation\n */\n\nimport { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'\nimport { resolve, join } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport { getIntrospectionQuery, type IntrospectionQuery } from 'graphql'\n\nimport { authoredPrimaryKey } from './converter/authored'\nimport { convertGraphQLSchema, formatDoctypeDrift, mergeIntrospectedDoctype, planGeneration } from './converter/index'\nimport { stripFieldKind } from './field'\nimport { validateDoctype } from './validation'\nimport type { GraphQLConversionOptions } from './converter/types'\n\n/**\n * Fetch an introspection result from a live GraphQL endpoint.\n *\n * @param endpoint - The GraphQL endpoint URL\n * @param headers - Optional HTTP headers\n * @returns The introspection query result\n */\nasync function fetchIntrospection(endpoint: string, headers?: Record<string, string>): Promise<IntrospectionQuery> {\n\tconst response = await fetch(endpoint, {\n\t\tmethod: 'POST',\n\t\theaders: {\n\t\t\t'Content-Type': 'application/json',\n\t\t\t...headers,\n\t\t},\n\t\tbody: JSON.stringify({\n\t\t\tquery: getIntrospectionQuery(),\n\t\t}),\n\t})\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to fetch introspection: ${response.status} ${response.statusText}`)\n\t}\n\n\tconst json: { data?: IntrospectionQuery; errors?: Array<{ message: string }> } = await response.json()\n\n\tif (json.errors?.length) {\n\t\tthrow new Error(`GraphQL errors: ${json.errors.map(e => e.message).join(', ')}`)\n\t}\n\n\tif (!json.data) {\n\t\tthrow new Error('No data in introspection response')\n\t}\n\n\treturn json.data\n}\n\nasync function main(): Promise<void> {\n\tconst { values, positionals } = parseArgs({\n\t\tallowPositionals: true,\n\t\toptions: {\n\t\t\tendpoint: { type: 'string', short: 'e' },\n\t\t\tintrospection: { type: 'string', short: 'i' },\n\t\t\tsdl: { type: 'string', short: 's' },\n\t\t\toutput: { type: 'string', short: 'o' },\n\t\t\tinclude: { type: 'string' },\n\t\t\texclude: { type: 'string' },\n\t\t\tnames: { type: 'string' },\n\t\t\t'custom-scalars': { type: 'string' },\n\t\t\t'include-unmapped': { type: 'boolean', default: false },\n\t\t\t'no-aggregates': { type: 'boolean', default: false },\n\t\t\tcheck: { type: 'boolean', default: false },\n\t\t\thelp: { type: 'boolean', short: 'h' },\n\t\t},\n\t})\n\n\tconst command = positionals[0]\n\n\tif (values.help || !command) {\n\t\tprintHelp()\n\t\tprocess.exit(command ? 0 : 1)\n\t}\n\n\tif (command !== 'generate') {\n\t\tconsole.error(`Unknown command: ${command}`)\n\t\tconsole.error('Available commands: generate')\n\t\tprocess.exit(1)\n\t}\n\n\t// Determine source\n\tconst sourceCount = [values.endpoint, values.introspection, values.sdl].filter(Boolean).length\n\tif (sourceCount !== 1) {\n\t\tconsole.error('Exactly one of --endpoint, --introspection, or --sdl must be provided')\n\t\tprocess.exit(1)\n\t}\n\n\tif (!values.output) {\n\t\tconsole.error('--output <dir> is required')\n\t\tprocess.exit(1)\n\t}\n\n\tconst outputDir = resolve(values.output)\n\n\t// Build conversion options\n\tconst options: GraphQLConversionOptions = {\n\t\tincludeUnmappedMeta: values['include-unmapped'],\n\t}\n\n\tif (values.include) {\n\t\toptions.include = values.include.split(',').map(s => s.trim())\n\t}\n\n\tif (values.exclude) {\n\t\toptions.exclude = values.exclude.split(',').map(s => s.trim())\n\t}\n\n\tif (values.names) {\n\t\tconst namesPath = resolve(values.names)\n\t\toptions.doctypeNames = JSON.parse(readFileSync(namesPath, 'utf-8'))\n\t}\n\n\toptions.onWarning = message => console.warn(` WARN: ${message}`)\n\n\tif (values['custom-scalars']) {\n\t\tconst scalarsPath = resolve(values['custom-scalars'])\n\t\tconst scalarsContent = readFileSync(scalarsPath, 'utf-8')\n\t\toptions.customScalars = JSON.parse(scalarsContent)\n\t}\n\n\t// Resolve source\n\tlet source: IntrospectionQuery | string\n\n\tif (values.endpoint) {\n\t\tconsole.log(`Fetching introspection from ${values.endpoint}...`)\n\t\tsource = await fetchIntrospection(values.endpoint)\n\t} else if (values.introspection) {\n\t\tconst filePath = resolve(values.introspection)\n\t\tconst content = readFileSync(filePath, 'utf-8')\n\t\tconst parsed = JSON.parse(content)\n\t\t// Handle both { data: { __schema: ... } } and { __schema: ... } formats\n\t\tsource = parsed.data ?? parsed\n\t} else {\n\t\tconst filePath = resolve(values.sdl!)\n\t\tsource = readFileSync(filePath, 'utf-8')\n\t}\n\n\t// Convert\n\tconst entities = convertGraphQLSchema(source, options)\n\n\tif (entities.length === 0) {\n\t\tconsole.warn('No entity types found in the schema. Check your include/exclude filters.')\n\t\tprocess.exit(0)\n\t}\n\n\t// Read the identity each authored doctype declares, before planning rather than during the write\n\t// loop below. SDL cannot express which UNIQUE column is a table's key, so for a natural-key\n\t// doctype the declaration in the file is the only answer that exists — and planning is what\n\t// decides whether the aggregate can be built at all. Reading it 20 lines later, as the loop\n\t// does, made the \"declare a primaryKey and re-run\" warning a dead end: re-running after\n\t// declaring one changed nothing.\n\tconst identity: Record<string, string> = {}\n\tfor (const entity of entities) {\n\t\tconst entityPath = join(outputDir, `${entity.slug}.json`)\n\t\tif (!existsSync(entityPath)) continue\n\t\tconst declared = authoredPrimaryKey(JSON.parse(readFileSync(entityPath, 'utf-8')))\n\t\tif (declared !== undefined) identity[entity.name] = declared\n\t}\n\n\t// Each table yields two doctypes — the entity and its aggregate — written as peers, one file\n\t// each, along with what each is verified against. See `planGeneration`.\n\tconst doctypes = planGeneration(entities, {\n\t\tnoAggregates: values['no-aggregates'],\n\t\tidentity,\n\t\tonWarning: message => console.warn(` WARN: ${message}`),\n\t})\n\n\t// Write output\n\tif (!existsSync(outputDir)) {\n\t\tmkdirSync(outputDir, { recursive: true })\n\t}\n\n\tlet warnings = 0\n\tlet errors = 0\n\tlet changed = 0\n\tconst driftLines: string[] = []\n\n\tfor (const { generated, basis, subset } of doctypes) {\n\t\tconst fileName = `${generated.slug}.json`\n\t\tconst filePath = join(outputDir, fileName)\n\n\t\t// When a doctype already exists it is the source of truth: generation confirms it and adds\n\t\t// provenance markers, and reports anything it disagrees with rather than applying it. A\n\t\t// doctype legitimately declares identity the schema cannot express — most often a natural\n\t\t// key that is a UNIQUE constraint, not the table's PRIMARY KEY — and overwriting that would\n\t\t// silently re-key the doctype on every run. A first generation has nothing to merge into,\n\t\t// so converter output is written verbatim.\n\t\tlet output: object = generated\n\t\tif (existsSync(filePath)) {\n\t\t\tconst { doctype: merged, drift } = mergeIntrospectedDoctype(JSON.parse(readFileSync(filePath, 'utf-8')), basis, {\n\t\t\t\tsubset,\n\t\t\t})\n\t\t\toutput = merged\n\t\t\tdriftLines.push(...formatDoctypeDrift(drift))\n\t\t}\n\n\t\t// Serialize the merged object directly. Never round-trip it through the Zod parser first:\n\t\t// that runs in strip mode and would silently drop every key this package does not model,\n\t\t// `handler` on an action being the one consumers actually rely on.\n\t\t//\n\t\t// `kind` is dropped on the way out because it is the discriminated-union tag the parser\n\t\t// synthesizes, not authored data — `injectKind` puts it back on every read. Writing it made\n\t\t// the generator and the docbuilder disagree with hand-authored files about what a doctype\n\t\t// looks like, and re-added the key on every regeneration.\n\t\tconst serializable: Record<string, unknown> = { ...output }\n\t\tif (Array.isArray(serializable.fields)) {\n\t\t\tserializable.fields = serializable.fields.map(stripFieldKind)\n\t\t}\n\t\tconst json = JSON.stringify(serializable, null, '\\t') + '\\n'\n\t\tconst unchanged = existsSync(filePath) && readFileSync(filePath, 'utf-8') === json\n\t\tif (!unchanged) changed++\n\n\t\tif (!values.check && !unchanged) {\n\t\t\twriteFileSync(filePath, json, 'utf-8')\n\t\t}\n\n\t\t// Validate the output\n\t\tconst validation = validateDoctype(output)\n\t\tif (!validation.success) {\n\t\t\terrors++\n\t\t\tconsole.error(` ERROR: ${fileName} failed validation:`)\n\t\t\tfor (const err of validation.errors) {\n\t\t\t\tconsole.error(` ${err.path.join('.')}: ${err.message}`)\n\t\t\t}\n\t\t} else {\n\t\t\t// Check for unmapped fields\n\t\t\tconst unmappedFields = generated.fields.filter((f: any) => f._unmapped)\n\t\t\tif (unmappedFields.length > 0) {\n\t\t\t\twarnings++\n\t\t\t\tconsole.warn(\n\t\t\t\t\t` WARN: ${fileName} has ${unmappedFields.length} unmapped field(s): ${unmappedFields\n\t\t\t\t\t\t.map((f: any) => f.fieldname)\n\t\t\t\t\t\t.join(', ')}`\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\tif (driftLines.length > 0) {\n\t\tconsole.log('\\nDrift between the authored doctypes and the schema (reported, not applied):')\n\t\tfor (const line of driftLines) console.log(line)\n\t}\n\n\tconsole.log(\n\t\t`\\n${values.check ? 'Checked' : 'Generated'} ${doctypes.length} doctype(s) in ${outputDir}` +\n\t\t\t(changed ? ` (${changed} ${values.check ? 'would change' : 'written'})` : ' (all up to date)') +\n\t\t\t(warnings ? ` (${warnings} with warnings)` : '') +\n\t\t\t(errors ? ` (${errors} with errors)` : '')\n\t)\n\n\tif (errors > 0 || (values.check && changed > 0)) {\n\t\tprocess.exit(1)\n\t}\n}\n\nfunction printHelp(): void {\n\tconsole.log(`\nstonecrop-schema - Convert GraphQL schemas to Stonecrop doctypes\n\nUSAGE:\n stonecrop-schema generate [options]\n\nSOURCE (exactly one required):\n --endpoint, -e <url> Fetch introspection from a live GraphQL endpoint\n --introspection, -i <file> Read from a saved introspection JSON file\n --sdl, -s <file> Read from a GraphQL SDL (.graphql) file\n\nOUTPUT:\n --output, -o <dir> Directory to write doctype JSON files (required)\n\nOPTIONS:\n --include <types> Comma-separated list of type names to include\n --exclude <types> Comma-separated list of type names to exclude\n --names <file> JSON file mapping GraphQL type name to doctype name\n --custom-scalars <file> JSON file mapping custom scalar names to field templates\n --include-unmapped Include _graphqlType metadata on unmapped fields\n --no-aggregates Emit only the entity doctype, not its aggregate\n --check Report drift and exit non-zero if anything would change; write nothing\n --help, -h Show this help message\n\nEach table generates TWO doctypes: the entity (every column, backs the record form) and its\naggregate (the collection view, identity column only by default), named as simple plurals —\n'task.json' and 'tasks.json'. Widen an aggregate by adding fields to it — curation survives\nregeneration. A doctype whose name is already plural gets no aggregate, because it would claim\nits own name and its own file; the run says so and still writes the entity.\n\nAn aggregate needs an identity column. A table keyed on 'id' gives one up; for a natural key,\ndeclare 'primaryKey' on the field in the entity's own file and re-run — the aggregate is keyed\non whatever that file declares.\n\nNOTE: an existing doctype file is the source of truth. Regeneration verifies it against the\nschema and adds 'source: introspected' markers; it reports disagreements rather than\noverwriting them, so hand-curation survives. Use --check in CI.\n\nEXAMPLES:\n # From a live PostGraphile server\n stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas\n\n # From a saved introspection result\n stonecrop-schema generate -i introspection.json -o ./schemas\n\n # From an SDL file with custom scalars\n stonecrop-schema generate -s schema.graphql -o ./schemas \\\\\n --custom-scalars custom-scalars.json\n\n # Only convert specific types\n stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas \\\\\n --include \"User,Post,Comment\"\n`)\n}\n\nmain().catch(err => {\n\tconsole.error('Error:', err.message)\n\tprocess.exit(1)\n})\n"],"names":["fetchIntrospection","endpoint","headers","response","getIntrospectionQuery","json","e","main","values","positionals","parseArgs","command","printHelp","outputDir","resolve","options","s","namesPath","readFileSync","message","scalarsPath","scalarsContent","source","filePath","content","parsed","entities","convertGraphQLSchema","identity","entity","entityPath","join","existsSync","declared","authoredPrimaryKey","doctypes","planGeneration","mkdirSync","warnings","errors","changed","driftLines","generated","basis","subset","fileName","output","merged","drift","mergeIntrospectedDoctype","formatDoctypeDrift","serializable","stripFieldKind","unchanged","writeFileSync","validation","validateDoctype","unmappedFields","f","err","line"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/* oxlint-disable no-console */\n\n/**\n * Stonecrop Schema CLI\n *\n * Converts GraphQL introspection results to Stonecrop doctype JSON schemas.\n *\n * Usage:\n * stonecrop-schema generate --endpoint <url> --output <dir>\n * stonecrop-schema generate --introspection <file.json> --output <dir>\n * stonecrop-schema generate --sdl <file.graphql> --output <dir>\n *\n * @packageDocumentation\n */\n\nimport { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'\nimport { resolve, join } from 'node:path'\nimport { parseArgs } from 'node:util'\nimport { getIntrospectionQuery, type IntrospectionQuery } from 'graphql'\n\nimport { authoredPrimaryKey } from './converter/authored'\nimport { convertGraphQLSchema, formatDoctypeDrift, mergeIntrospectedDoctype, planGeneration } from './converter/index'\nimport { stripFieldKind } from './field'\nimport { validateDoctype } from './validation'\nimport type { GraphQLConversionOptions } from './converter/types'\n\n/**\n * Fetch an introspection result from a live GraphQL endpoint.\n *\n * @param endpoint - The GraphQL endpoint URL\n * @param headers - Optional HTTP headers\n * @returns The introspection query result\n */\nasync function fetchIntrospection(endpoint: string, headers?: Record<string, string>): Promise<IntrospectionQuery> {\n\tconst response = await fetch(endpoint, {\n\t\tmethod: 'POST',\n\t\theaders: {\n\t\t\t'Content-Type': 'application/json',\n\t\t\t...headers,\n\t\t},\n\t\tbody: JSON.stringify({\n\t\t\tquery: getIntrospectionQuery(),\n\t\t}),\n\t})\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to fetch introspection: ${response.status} ${response.statusText}`)\n\t}\n\n\tconst json: { data?: IntrospectionQuery; errors?: Array<{ message: string }> } = await response.json()\n\n\tif (json.errors?.length) {\n\t\tthrow new Error(`GraphQL errors: ${json.errors.map(e => e.message).join(', ')}`)\n\t}\n\n\tif (!json.data) {\n\t\tthrow new Error('No data in introspection response')\n\t}\n\n\treturn json.data\n}\n\nasync function main(): Promise<void> {\n\tconst { values, positionals } = parseArgs({\n\t\tallowPositionals: true,\n\t\toptions: {\n\t\t\tendpoint: { type: 'string', short: 'e' },\n\t\t\tintrospection: { type: 'string', short: 'i' },\n\t\t\tsdl: { type: 'string', short: 's' },\n\t\t\toutput: { type: 'string', short: 'o' },\n\t\t\tinclude: { type: 'string' },\n\t\t\texclude: { type: 'string' },\n\t\t\tnames: { type: 'string' },\n\t\t\t'custom-scalars': { type: 'string' },\n\t\t\t'include-unmapped': { type: 'boolean', default: false },\n\t\t\t'no-aggregates': { type: 'boolean', default: false },\n\t\t\tcheck: { type: 'boolean', default: false },\n\t\t\thelp: { type: 'boolean', short: 'h' },\n\t\t},\n\t})\n\n\tconst command = positionals[0]\n\n\tif (values.help || !command) {\n\t\tprintHelp()\n\t\tprocess.exit(command ? 0 : 1)\n\t}\n\n\tif (command !== 'generate') {\n\t\tconsole.error(`Unknown command: ${command}`)\n\t\tconsole.error('Available commands: generate')\n\t\tprocess.exit(1)\n\t}\n\n\t// Determine source\n\tconst sourceCount = [values.endpoint, values.introspection, values.sdl].filter(Boolean).length\n\tif (sourceCount !== 1) {\n\t\tconsole.error('Exactly one of --endpoint, --introspection, or --sdl must be provided')\n\t\tprocess.exit(1)\n\t}\n\n\tif (!values.output) {\n\t\tconsole.error('--output <dir> is required')\n\t\tprocess.exit(1)\n\t}\n\n\tconst outputDir = resolve(values.output)\n\n\t// Build conversion options\n\tconst options: GraphQLConversionOptions = {\n\t\tincludeUnmappedMeta: values['include-unmapped'],\n\t}\n\n\tif (values.include) {\n\t\toptions.include = values.include.split(',').map(s => s.trim())\n\t}\n\n\tif (values.exclude) {\n\t\toptions.exclude = values.exclude.split(',').map(s => s.trim())\n\t}\n\n\tif (values.names) {\n\t\tconst namesPath = resolve(values.names)\n\t\toptions.doctypeNames = JSON.parse(readFileSync(namesPath, 'utf-8'))\n\t}\n\n\toptions.onWarning = message => console.warn(` WARN: ${message}`)\n\n\tif (values['custom-scalars']) {\n\t\tconst scalarsPath = resolve(values['custom-scalars'])\n\t\tconst scalarsContent = readFileSync(scalarsPath, 'utf-8')\n\t\toptions.customScalars = JSON.parse(scalarsContent)\n\t}\n\n\t// Resolve source\n\tlet source: IntrospectionQuery | string\n\n\tif (values.endpoint) {\n\t\tconsole.log(`Fetching introspection from ${values.endpoint}...`)\n\t\tsource = await fetchIntrospection(values.endpoint)\n\t} else if (values.introspection) {\n\t\tconst filePath = resolve(values.introspection)\n\t\tconst content = readFileSync(filePath, 'utf-8')\n\t\tconst parsed = JSON.parse(content)\n\t\t// Handle both { data: { __schema: ... } } and { __schema: ... } formats\n\t\tsource = parsed.data ?? parsed\n\t} else {\n\t\tconst filePath = resolve(values.sdl!)\n\t\tsource = readFileSync(filePath, 'utf-8')\n\t}\n\n\t// Convert\n\tconst entities = convertGraphQLSchema(source, options)\n\n\tif (entities.length === 0) {\n\t\tconsole.warn('No entity types found in the schema. Check your include/exclude filters.')\n\t\tprocess.exit(0)\n\t}\n\n\t// Read the identity each authored doctype declares, before planning rather than during the write\n\t// loop below. SDL cannot express which UNIQUE column is a table's key, so for a natural-key\n\t// doctype the declaration in the file is the only answer that exists — and planning is what\n\t// decides whether the aggregate can be built at all. Reading it 20 lines later, as the loop\n\t// does, made the \"declare a primaryKey and re-run\" warning a dead end: re-running after\n\t// declaring one changed nothing.\n\tconst identity: Record<string, string> = {}\n\tfor (const entity of entities) {\n\t\tconst entityPath = join(outputDir, `${entity.slug}.json`)\n\t\tif (!existsSync(entityPath)) continue\n\t\tconst declared = authoredPrimaryKey(JSON.parse(readFileSync(entityPath, 'utf-8')))\n\t\tif (declared !== undefined) identity[entity.name] = declared\n\t}\n\n\t// Each table yields two doctypes — the entity and its aggregate — written as peers, one file\n\t// each, along with what each is verified against. See `planGeneration`.\n\tconst doctypes = planGeneration(entities, {\n\t\tnoAggregates: values['no-aggregates'],\n\t\tidentity,\n\t\tonWarning: message => console.warn(` WARN: ${message}`),\n\t})\n\n\t// Write output\n\tif (!existsSync(outputDir)) {\n\t\tmkdirSync(outputDir, { recursive: true })\n\t}\n\n\tlet warnings = 0\n\tlet errors = 0\n\tlet changed = 0\n\tconst driftLines: string[] = []\n\n\tfor (const { generated, basis, subset } of doctypes) {\n\t\tconst fileName = `${generated.slug}.json`\n\t\tconst filePath = join(outputDir, fileName)\n\n\t\t// When a doctype already exists it is the source of truth: generation confirms it and adds\n\t\t// provenance markers, and reports anything it disagrees with rather than applying it. A\n\t\t// doctype legitimately declares identity the schema cannot express — most often a natural\n\t\t// key that is a UNIQUE constraint, not the table's PRIMARY KEY — and overwriting that would\n\t\t// silently re-key the doctype on every run. A first generation has nothing to merge into,\n\t\t// so converter output is written verbatim.\n\t\tlet output: object = generated\n\t\tif (existsSync(filePath)) {\n\t\t\tconst { doctype: merged, drift } = mergeIntrospectedDoctype(JSON.parse(readFileSync(filePath, 'utf-8')), basis, {\n\t\t\t\tsubset,\n\t\t\t})\n\t\t\toutput = merged\n\t\t\tdriftLines.push(...formatDoctypeDrift(drift))\n\t\t}\n\n\t\t// Serialize the merged object directly. Never round-trip it through the Zod parser first:\n\t\t// that runs in strip mode and would silently drop every key this package does not model,\n\t\t// `handler` on an action being the one consumers actually rely on.\n\t\t//\n\t\t// `kind` is dropped on the way out because it is the discriminated-union tag the parser\n\t\t// synthesizes, not authored data — `injectKind` puts it back on every read. Writing it made\n\t\t// the generator and the docbuilder disagree with hand-authored files about what a doctype\n\t\t// looks like, and re-added the key on every regeneration.\n\t\tconst serializable: Record<string, unknown> = { ...output }\n\t\tif (Array.isArray(serializable.fields)) {\n\t\t\tserializable.fields = serializable.fields.map(stripFieldKind)\n\t\t}\n\t\tconst json = JSON.stringify(serializable, null, '\\t') + '\\n'\n\t\tconst unchanged = existsSync(filePath) && readFileSync(filePath, 'utf-8') === json\n\t\tif (!unchanged) changed++\n\n\t\tif (!values.check && !unchanged) {\n\t\t\twriteFileSync(filePath, json, 'utf-8')\n\t\t}\n\n\t\t// Validate the output\n\t\tconst validation = validateDoctype(output)\n\t\tif (!validation.success) {\n\t\t\terrors++\n\t\t\tconsole.error(` ERROR: ${fileName} failed validation:`)\n\t\t\tfor (const err of validation.errors) {\n\t\t\t\tconsole.error(` ${err.path.join('.')}: ${err.message}`)\n\t\t\t}\n\t\t} else {\n\t\t\t// Check for unmapped fields\n\t\t\tconst unmappedFields = generated.fields.filter((f: any) => f._unmapped)\n\t\t\tif (unmappedFields.length > 0) {\n\t\t\t\twarnings++\n\t\t\t\tconsole.warn(\n\t\t\t\t\t` WARN: ${fileName} has ${unmappedFields.length} unmapped field(s): ${unmappedFields\n\t\t\t\t\t\t.map((f: any) => f.fieldname)\n\t\t\t\t\t\t.join(', ')}`\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\tif (driftLines.length > 0) {\n\t\tconsole.log('\\nDrift between the authored doctypes and the schema (reported, not applied):')\n\t\tfor (const line of driftLines) console.log(line)\n\t}\n\n\tconsole.log(\n\t\t`\\n${values.check ? 'Checked' : 'Generated'} ${doctypes.length} doctype(s) in ${outputDir}` +\n\t\t\t(changed ? ` (${changed} ${values.check ? 'would change' : 'written'})` : ' (all up to date)') +\n\t\t\t(warnings ? ` (${warnings} with warnings)` : '') +\n\t\t\t(errors ? ` (${errors} with errors)` : '')\n\t)\n\n\tif (errors > 0 || (values.check && changed > 0)) {\n\t\tprocess.exit(1)\n\t}\n}\n\nfunction printHelp(): void {\n\tconsole.log(`\nstonecrop-schema - Convert GraphQL schemas to Stonecrop doctypes\n\nUSAGE:\n stonecrop-schema generate [options]\n\nSOURCE (exactly one required):\n --endpoint, -e <url> Fetch introspection from a live GraphQL endpoint\n --introspection, -i <file> Read from a saved introspection JSON file\n --sdl, -s <file> Read from a GraphQL SDL (.graphql) file\n\nOUTPUT:\n --output, -o <dir> Directory to write doctype JSON files (required)\n\nOPTIONS:\n --include <types> Comma-separated list of type names to include\n --exclude <types> Comma-separated list of type names to exclude\n --names <file> JSON file mapping GraphQL type name to doctype name\n --custom-scalars <file> JSON file mapping custom scalar names to field templates\n --include-unmapped Include _graphqlType metadata on unmapped fields\n --no-aggregates Emit only the entity doctype, not its aggregate\n --check Report drift and exit non-zero if anything would change; write nothing\n --help, -h Show this help message\n\nEach table generates TWO doctypes: the entity (every column, backs the record form) and its\naggregate (the collection view, identity column only by default), named as simple plurals —\n'task.json' and 'tasks.json'. Widen an aggregate by adding fields to it — curation survives\nregeneration. A doctype whose name is already plural gets no aggregate, because it would claim\nits own name and its own file; the run says so and still writes the entity.\n\nAn aggregate needs an identity column. A table keyed on 'id' gives one up; for a natural key,\ndeclare 'primaryKey' on the field in the entity's own file and re-run — the aggregate is keyed\non whatever that file declares.\n\nNOTE: an existing doctype file is the source of truth. Regeneration verifies it against the\nschema and adds 'source: introspected' markers; it reports disagreements rather than\noverwriting them, so hand-curation survives. Use --check in CI.\n\nEXAMPLES:\n # From a live PostGraphile server\n stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas\n\n # From a saved introspection result\n stonecrop-schema generate -i introspection.json -o ./schemas\n\n # From an SDL file with custom scalars\n stonecrop-schema generate -s schema.graphql -o ./schemas \\\\\n --custom-scalars custom-scalars.json\n\n # Only convert specific types\n stonecrop-schema generate -e http://localhost:5000/graphql -o ./schemas \\\\\n --include \"User,Post,Comment\"\n`)\n}\n\nmain().catch(err => {\n\tconsole.error('Error:', err.message)\n\tprocess.exit(1)\n})\n"],"mappings":"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"}