pocketbomb-dynamite 0.2.0 → 0.3.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 (41) hide show
  1. package/README.md +24 -1
  2. package/dist/cli/check.cjs +160 -0
  3. package/dist/cli/check.cjs.map +1 -0
  4. package/dist/cli/check.d.cts +21 -0
  5. package/dist/cli/check.d.ts +21 -0
  6. package/dist/cli/check.js +157 -0
  7. package/dist/cli/check.js.map +1 -0
  8. package/dist/cli/codegen.cjs +2 -1
  9. package/dist/cli/codegen.cjs.map +1 -1
  10. package/dist/cli/codegen.js +3 -2
  11. package/dist/cli/codegen.js.map +1 -1
  12. package/dist/cli/index.cjs +32 -0
  13. package/dist/cli/index.cjs.map +1 -1
  14. package/dist/cli/index.d.cts +2 -1
  15. package/dist/cli/index.d.ts +2 -1
  16. package/dist/cli/index.js +32 -1
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/customer-account/customer-account.cjs +55 -0
  19. package/dist/customer-account/customer-account.cjs.map +1 -1
  20. package/dist/customer-account/customer-account.d.cts +33 -0
  21. package/dist/customer-account/customer-account.d.ts +33 -0
  22. package/dist/customer-account/customer-account.js +52 -1
  23. package/dist/customer-account/customer-account.js.map +1 -1
  24. package/dist/customer-account/index.cjs +4 -0
  25. package/dist/customer-account/index.d.cts +2 -2
  26. package/dist/customer-account/index.d.ts +2 -2
  27. package/dist/customer-account/index.js +1 -1
  28. package/dist/react/hooks.cjs +37 -5
  29. package/dist/react/hooks.cjs.map +1 -1
  30. package/dist/react/hooks.d.cts +12 -2
  31. package/dist/react/hooks.d.ts +12 -2
  32. package/dist/react/hooks.js +37 -5
  33. package/dist/react/hooks.js.map +1 -1
  34. package/dist/react/index.d.cts +2 -2
  35. package/dist/react/index.d.ts +2 -2
  36. package/dist/ts-plugin/index.d.cts +2 -1
  37. package/dist/vite/index.cjs +6 -2
  38. package/dist/vite/index.cjs.map +1 -1
  39. package/dist/vite/index.js +6 -2
  40. package/dist/vite/index.js.map +1 -1
  41. package/package.json +7 -3
package/README.md CHANGED
@@ -36,4 +36,27 @@ Entries: `pocketbomb-dynamite` (core + storefront client + GraphQL), `pocketbomb
36
36
  (DynamiteProvider, useStorefront, useCustomer, useStorefrontQuery, Pagination, FileImage), `pocketbomb-dynamite/vite`
37
37
  (env + private key guard, dev proxy, codegen, fuse) and `pocketbomb-dynamite/ts-plugin` (graphqlsp for the editor).
38
38
 
39
- CLI: `dynamite codegen [--watch] [--fuse --seed <module>]`, `dynamite dev [--seed] [--entry server.ts]`, `dynamite info`.
39
+ CLI: `dynamite codegen [--watch] [--fuse --seed <module>]`, `dynamite dev [--seed] [--entry server.ts]`,
40
+ `dynamite check [--schema <file>]`, `dynamite info`.
41
+
42
+ ## Global ids
43
+
44
+ GraphQL is read-only and its `id` fields are Relay global ids: base64url of `{collectionName}:{recordId}`. The REST
45
+ API takes record ids, so convert them when a GraphQL result drives a write:
46
+
47
+ ```ts
48
+ import { fromGlobalId, toGlobalId } from "pocketbomb-dynamite/graphql";
49
+
50
+ const { todos } = await customer.query(TODOS); // todos.nodes[0].id = "dG9kb3M6NDFoa2s..."
51
+ const { collectionName, id } = fromGlobalId(todos.nodes[0].id); // "todos", "41hkk..." (throws if it is invalid)
52
+ await customer.client.collection(collectionName).update(id, { done: true });
53
+
54
+ await storefront.query(TODO, { variables: { id: toGlobalId("todos", id) } }); // the other way around
55
+ ```
56
+
57
+ ## Checking the documents
58
+
59
+ `tsc` types the results of the `graphql()` documents but doesn't validate the documents themselves (an unknown field
60
+ is flagged only by the editor plugin). `dynamite check` validates every `graphql()` document of the project
61
+ against `.dynamite/schema.graphql` and fails with `file:line:column`; add it to your typecheck script.
62
+ It needs `typescript` installed in the project.
@@ -0,0 +1,160 @@
1
+ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
2
+ require("../shared/constants.cjs");
3
+ let node_fs = require("node:fs");
4
+ node_fs = require_runtime.__toESM(node_fs, 1);
5
+ let node_path = require("node:path");
6
+ node_path = require_runtime.__toESM(node_path, 1);
7
+ let graphql = require("graphql");
8
+ // src/cli/check.ts
9
+ const IGNORED_DIRS = /* @__PURE__ */ new Set([
10
+ "node_modules",
11
+ "build",
12
+ "dist",
13
+ ".git",
14
+ ".react-router",
15
+ ".turbo",
16
+ ".next",
17
+ ".dynamite"
18
+ ]);
19
+ function sourceFiles(dir, result = []) {
20
+ for (const entry of node_fs.default.readdirSync(dir, { withFileTypes: true })) if (entry.isDirectory()) {
21
+ if (!IGNORED_DIRS.has(entry.name)) sourceFiles(node_path.default.join(dir, entry.name), result);
22
+ } else if (/\.(ts|tsx|mts|cts)$/.test(entry.name) && !entry.name.endsWith(".d.ts")) result.push(node_path.default.join(dir, entry.name));
23
+ return result;
24
+ }
25
+ async function loadTypeScript() {
26
+ try {
27
+ const ts = await import("typescript");
28
+ return ts.default || ts;
29
+ } catch {
30
+ throw new Error("dynamite check needs the typescript package installed in the project.");
31
+ }
32
+ }
33
+ function findDocuments(ts, file) {
34
+ const source = ts.createSourceFile(file, node_fs.default.readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true);
35
+ const result = [];
36
+ const visitNode = (node) => {
37
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "graphql") {
38
+ const arg = node.arguments[0];
39
+ if (arg && (ts.isNoSubstitutionTemplateLiteral(arg) || ts.isStringLiteral(arg))) {
40
+ const start = source.getLineAndCharacterOfPosition(arg.getStart(source) + 1);
41
+ result.push({
42
+ file,
43
+ line: start.line,
44
+ column: start.character,
45
+ text: arg.text
46
+ });
47
+ } else if (arg) {
48
+ const start = source.getLineAndCharacterOfPosition(arg.getStart(source));
49
+ result.push({
50
+ file,
51
+ line: start.line,
52
+ column: start.character,
53
+ text: ""
54
+ });
55
+ }
56
+ }
57
+ ts.forEachChild(node, visitNode);
58
+ };
59
+ visitNode(source);
60
+ return result;
61
+ }
62
+ function usedFragments(root, all) {
63
+ const used = /* @__PURE__ */ new Map();
64
+ const collect = (node) => {
65
+ (0, graphql.visit)(node, { FragmentSpread(spread) {
66
+ const fragment = all.get(spread.name.value);
67
+ if (fragment && !used.has(spread.name.value)) {
68
+ used.set(spread.name.value, fragment);
69
+ collect(fragment);
70
+ }
71
+ } });
72
+ };
73
+ collect(root);
74
+ return used;
75
+ }
76
+ function problemOf(doc, error) {
77
+ const location = error.locations?.[0];
78
+ return {
79
+ file: doc.file,
80
+ line: doc.line + (location?.line ?? 1),
81
+ column: location?.line === 1 ? doc.column + location.column : location?.column ?? 1,
82
+ message: error.message
83
+ };
84
+ }
85
+ /**
86
+ * Validates every `graphql()` document of a project (fragments are resolved by name across
87
+ * the project; a document that isn't a plain string can't be checked and is reported).
88
+ */
89
+ async function check(options = {}) {
90
+ const cwd = node_path.default.resolve(options.cwd || process.cwd());
91
+ const schemaFile = node_path.default.resolve(cwd, options.schemaFile || ".dynamite/schema.graphql");
92
+ const ts = await loadTypeScript();
93
+ const documents = sourceFiles(cwd).flatMap((file) => findDocuments(ts, file));
94
+ if (!documents.length) return {
95
+ documents: 0,
96
+ problems: []
97
+ };
98
+ if (!options.schema && !node_fs.default.existsSync(schemaFile)) throw new Error("Missing " + node_path.default.relative(cwd, schemaFile) + ": run `dynamite codegen` first.");
99
+ const schema = options.schema || (0, graphql.buildSchema)(node_fs.default.readFileSync(schemaFile, "utf8"));
100
+ const problems = [];
101
+ const parsed = [];
102
+ const fragments = /* @__PURE__ */ new Map();
103
+ const fragmentOwners = /* @__PURE__ */ new Map();
104
+ for (const doc of documents) {
105
+ if (!doc.text) {
106
+ problems.push({
107
+ file: doc.file,
108
+ line: doc.line + 1,
109
+ column: doc.column + 1,
110
+ message: "graphql() needs a plain string document to be checked (no ${} interpolation)."
111
+ });
112
+ continue;
113
+ }
114
+ try {
115
+ const ast = (0, graphql.parse)(doc.text);
116
+ parsed.push({
117
+ doc,
118
+ ast
119
+ });
120
+ for (const definition of ast.definitions) {
121
+ if (definition.kind !== graphql.Kind.FRAGMENT_DEFINITION) continue;
122
+ const name = definition.name.value;
123
+ if (fragmentOwners.has(name)) problems.push({
124
+ file: doc.file,
125
+ line: doc.line + (definition.loc?.startToken.line ?? 1),
126
+ column: 1,
127
+ message: `Duplicated fragment "${name}" (also in ${node_path.default.relative(cwd, fragmentOwners.get(name).file)}).`
128
+ });
129
+ fragments.set(name, definition);
130
+ fragmentOwners.set(name, doc);
131
+ }
132
+ } catch (err) {
133
+ problems.push(problemOf(doc, err));
134
+ }
135
+ }
136
+ const rules = graphql.specifiedRules.filter((rule) => rule !== graphql.NoUnusedFragmentsRule);
137
+ for (const { doc, ast } of parsed) {
138
+ const own = new Set(ast.definitions.flatMap((d) => d.kind === graphql.Kind.FRAGMENT_DEFINITION ? [d.name.value] : []));
139
+ const imported = [...usedFragments(ast, fragments).entries()].filter(([name]) => !own.has(name)).map(([, fragment]) => fragment);
140
+ const errors = (0, graphql.validate)(schema, {
141
+ ...ast,
142
+ definitions: [...ast.definitions, ...imported]
143
+ }, rules);
144
+ for (const error of errors) {
145
+ if (imported.some((f) => error.nodes?.some((n) => isInside(n, f)))) continue;
146
+ problems.push(problemOf(doc, error));
147
+ }
148
+ }
149
+ problems.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
150
+ return {
151
+ documents: documents.length,
152
+ problems
153
+ };
154
+ }
155
+ function isInside(node, fragment) {
156
+ return !!node.loc && node.loc.source === fragment.loc?.source;
157
+ }
158
+ exports.check = check;
159
+
160
+ //# sourceMappingURL=check.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.cjs","names":["fs","path","buildSchema","parse","Kind","specifiedRules","NoUnusedFragmentsRule","validate"],"sources":["../../src/cli/check.ts"],"sourcesContent":["// `dynamite check`: validates the graphql() documents of a project against the codegen schema.\n//\n// Why not gql.tada's `gql-tada check`: it isn't a dependency (@gql.tada/cli-utils), and it finds the schema in a\n// tsconfig plugin entry named after gql.tada/graphqlsp, while the projects here use pocketbomb-dynamite/ts-plugin.\n// The check itself is small: the TypeScript parser finds the documents and graphql-js validates them.\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport {\n buildSchema,\n GraphQLError,\n Kind,\n NoUnusedFragmentsRule,\n parse,\n specifiedRules,\n validate,\n visit,\n type DocumentNode,\n type FragmentDefinitionNode,\n type GraphQLSchema,\n} from \"graphql\";\nimport { DEFAULT_SCHEMA_FILE } from \"./codegen\";\n\nconst IGNORED_DIRS = new Set([\n \"node_modules\",\n \"build\",\n \"dist\",\n \".git\",\n \".react-router\",\n \".turbo\",\n \".next\",\n \".dynamite\",\n]);\n\nexport interface CheckProblem {\n file: string;\n line: number;\n column: number;\n message: string;\n}\n\nexport interface CheckResult {\n documents: number;\n problems: Array<CheckProblem>;\n}\n\ninterface FoundDocument {\n file: string;\n /** The zero based position of the document text start in its file. */\n line: number;\n column: number;\n text: string;\n}\n\nfunction sourceFiles(dir: string, result: Array<string> = []): Array<string> {\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n if (entry.isDirectory()) {\n if (!IGNORED_DIRS.has(entry.name)) sourceFiles(path.join(dir, entry.name), result);\n } else if (/\\.(ts|tsx|mts|cts)$/.test(entry.name) && !entry.name.endsWith(\".d.ts\")) {\n result.push(path.join(dir, entry.name));\n }\n }\n return result;\n}\n\nasync function loadTypeScript(): Promise<typeof import(\"typescript\")> {\n try {\n const ts: any = await import(\"typescript\");\n return ts.default || ts;\n } catch {\n throw new Error(\"dynamite check needs the typescript package installed in the project.\");\n }\n}\n\nfunction findDocuments(ts: typeof import(\"typescript\"), file: string): Array<FoundDocument> {\n const source = ts.createSourceFile(\n file,\n fs.readFileSync(file, \"utf8\"),\n ts.ScriptTarget.Latest,\n true,\n );\n const result: Array<FoundDocument> = [];\n\n const visitNode = (node: import(\"typescript\").Node) => {\n if (\n ts.isCallExpression(node) &&\n ts.isIdentifier(node.expression) &&\n node.expression.text === \"graphql\"\n ) {\n const arg = node.arguments[0];\n if (arg && (ts.isNoSubstitutionTemplateLiteral(arg) || ts.isStringLiteral(arg))) {\n // +1: the opening quote\n const start = source.getLineAndCharacterOfPosition(arg.getStart(source) + 1);\n result.push({ file, line: start.line, column: start.character, text: arg.text });\n } else if (arg) {\n const start = source.getLineAndCharacterOfPosition(arg.getStart(source));\n result.push({ file, line: start.line, column: start.character, text: \"\" });\n }\n }\n ts.forEachChild(node, visitNode);\n };\n visitNode(source);\n\n return result;\n}\n\nfunction usedFragments(root: DocumentNode, all: Map<string, FragmentDefinitionNode>) {\n const used = new Map<string, FragmentDefinitionNode>();\n const collect = (node: DocumentNode | FragmentDefinitionNode) => {\n visit(node, {\n FragmentSpread(spread) {\n const fragment = all.get(spread.name.value);\n if (fragment && !used.has(spread.name.value)) {\n used.set(spread.name.value, fragment);\n collect(fragment);\n }\n },\n });\n };\n collect(root);\n return used;\n}\n\nfunction problemOf(doc: FoundDocument, error: GraphQLError): CheckProblem {\n const location = error.locations?.[0];\n return {\n file: doc.file,\n // the first line of the text continues the source line of the opening quote\n line: doc.line + (location?.line ?? 1),\n column: location?.line === 1 ? doc.column + location.column : (location?.column ?? 1),\n message: error.message,\n };\n}\n\n/**\n * Validates every `graphql()` document of a project (fragments are resolved by name across\n * the project; a document that isn't a plain string can't be checked and is reported).\n */\nexport async function check(\n options: { cwd?: string; schemaFile?: string; schema?: GraphQLSchema } = {},\n): Promise<CheckResult> {\n const cwd = path.resolve(options.cwd || process.cwd());\n const schemaFile = path.resolve(cwd, options.schemaFile || DEFAULT_SCHEMA_FILE);\n const ts = await loadTypeScript();\n\n const documents = sourceFiles(cwd).flatMap((file) => findDocuments(ts, file));\n if (!documents.length) {\n return { documents: 0, problems: [] };\n }\n\n if (!options.schema && !fs.existsSync(schemaFile)) {\n throw new Error(\n \"Missing \" + path.relative(cwd, schemaFile) + \": run `dynamite codegen` first.\",\n );\n }\n const schema = options.schema || buildSchema(fs.readFileSync(schemaFile, \"utf8\"));\n const problems: Array<CheckProblem> = [];\n const parsed: Array<{ doc: FoundDocument; ast: DocumentNode }> = [];\n const fragments = new Map<string, FragmentDefinitionNode>();\n const fragmentOwners = new Map<string, FoundDocument>();\n\n for (const doc of documents) {\n if (!doc.text) {\n problems.push({\n file: doc.file,\n line: doc.line + 1,\n column: doc.column + 1,\n message: \"graphql() needs a plain string document to be checked (no ${} interpolation).\",\n });\n continue;\n }\n try {\n const ast = parse(doc.text);\n parsed.push({ doc, ast });\n for (const definition of ast.definitions) {\n if (definition.kind !== Kind.FRAGMENT_DEFINITION) continue;\n const name = definition.name.value;\n if (fragmentOwners.has(name)) {\n problems.push({\n file: doc.file,\n line: doc.line + (definition.loc?.startToken.line ?? 1),\n column: 1,\n message: `Duplicated fragment \"${name}\" (also in ${path.relative(cwd, fragmentOwners.get(name)!.file)}).`,\n });\n }\n fragments.set(name, definition);\n fragmentOwners.set(name, doc);\n }\n } catch (err) {\n problems.push(problemOf(doc, err as GraphQLError));\n }\n }\n\n // a document only holding fragments is valid on its own\n const rules = specifiedRules.filter((rule) => rule !== NoUnusedFragmentsRule);\n\n for (const { doc, ast } of parsed) {\n const own = new Set(\n ast.definitions.flatMap((d) => (d.kind === Kind.FRAGMENT_DEFINITION ? [d.name.value] : [])),\n );\n const imported = [...usedFragments(ast, fragments).entries()]\n .filter(([name]) => !own.has(name))\n .map(([, fragment]) => fragment);\n\n const errors = validate(\n schema,\n { ...ast, definitions: [...ast.definitions, ...imported] },\n rules,\n );\n for (const error of errors) {\n // an error inside an imported fragment is reported once, by the document that owns it\n if (imported.some((f) => error.nodes?.some((n) => isInside(n, f)))) continue;\n problems.push(problemOf(doc, error));\n }\n }\n\n problems.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);\n\n return { documents: documents.length, problems };\n}\n\nfunction isInside(node: { loc?: { source: unknown } }, fragment: FragmentDefinitionNode): boolean {\n return !!node.loc && node.loc.source === fragment.loc?.source;\n}\n"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { GraphQLSchema } from "graphql";
2
+ export interface CheckProblem {
3
+ file: string;
4
+ line: number;
5
+ column: number;
6
+ message: string;
7
+ }
8
+ export interface CheckResult {
9
+ documents: number;
10
+ problems: Array<CheckProblem>;
11
+ }
12
+ /**
13
+ * Validates every `graphql()` document of a project (fragments are resolved by name across
14
+ * the project; a document that isn't a plain string can't be checked and is reported).
15
+ */
16
+ export declare function check(options?: {
17
+ cwd?: string;
18
+ schemaFile?: string;
19
+ schema?: GraphQLSchema;
20
+ }): Promise<CheckResult>;
21
+ //# sourceMappingURL=check.d.cts.map
@@ -0,0 +1,21 @@
1
+ import { GraphQLSchema } from "graphql";
2
+ export interface CheckProblem {
3
+ file: string;
4
+ line: number;
5
+ column: number;
6
+ message: string;
7
+ }
8
+ export interface CheckResult {
9
+ documents: number;
10
+ problems: Array<CheckProblem>;
11
+ }
12
+ /**
13
+ * Validates every `graphql()` document of a project (fragments are resolved by name across
14
+ * the project; a document that isn't a plain string can't be checked and is reported).
15
+ */
16
+ export declare function check(options?: {
17
+ cwd?: string;
18
+ schemaFile?: string;
19
+ schema?: GraphQLSchema;
20
+ }): Promise<CheckResult>;
21
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1,157 @@
1
+ import "../shared/constants.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { Kind, NoUnusedFragmentsRule, buildSchema, parse, specifiedRules, validate, visit } from "graphql";
5
+ // src/cli/check.ts
6
+ const IGNORED_DIRS = /* @__PURE__ */ new Set([
7
+ "node_modules",
8
+ "build",
9
+ "dist",
10
+ ".git",
11
+ ".react-router",
12
+ ".turbo",
13
+ ".next",
14
+ ".dynamite"
15
+ ]);
16
+ function sourceFiles(dir, result = []) {
17
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) if (entry.isDirectory()) {
18
+ if (!IGNORED_DIRS.has(entry.name)) sourceFiles(path.join(dir, entry.name), result);
19
+ } else if (/\.(ts|tsx|mts|cts)$/.test(entry.name) && !entry.name.endsWith(".d.ts")) result.push(path.join(dir, entry.name));
20
+ return result;
21
+ }
22
+ async function loadTypeScript() {
23
+ try {
24
+ const ts = await import("typescript");
25
+ return ts.default || ts;
26
+ } catch {
27
+ throw new Error("dynamite check needs the typescript package installed in the project.");
28
+ }
29
+ }
30
+ function findDocuments(ts, file) {
31
+ const source = ts.createSourceFile(file, fs.readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true);
32
+ const result = [];
33
+ const visitNode = (node) => {
34
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "graphql") {
35
+ const arg = node.arguments[0];
36
+ if (arg && (ts.isNoSubstitutionTemplateLiteral(arg) || ts.isStringLiteral(arg))) {
37
+ const start = source.getLineAndCharacterOfPosition(arg.getStart(source) + 1);
38
+ result.push({
39
+ file,
40
+ line: start.line,
41
+ column: start.character,
42
+ text: arg.text
43
+ });
44
+ } else if (arg) {
45
+ const start = source.getLineAndCharacterOfPosition(arg.getStart(source));
46
+ result.push({
47
+ file,
48
+ line: start.line,
49
+ column: start.character,
50
+ text: ""
51
+ });
52
+ }
53
+ }
54
+ ts.forEachChild(node, visitNode);
55
+ };
56
+ visitNode(source);
57
+ return result;
58
+ }
59
+ function usedFragments(root, all) {
60
+ const used = /* @__PURE__ */ new Map();
61
+ const collect = (node) => {
62
+ visit(node, { FragmentSpread(spread) {
63
+ const fragment = all.get(spread.name.value);
64
+ if (fragment && !used.has(spread.name.value)) {
65
+ used.set(spread.name.value, fragment);
66
+ collect(fragment);
67
+ }
68
+ } });
69
+ };
70
+ collect(root);
71
+ return used;
72
+ }
73
+ function problemOf(doc, error) {
74
+ const location = error.locations?.[0];
75
+ return {
76
+ file: doc.file,
77
+ line: doc.line + (location?.line ?? 1),
78
+ column: location?.line === 1 ? doc.column + location.column : location?.column ?? 1,
79
+ message: error.message
80
+ };
81
+ }
82
+ /**
83
+ * Validates every `graphql()` document of a project (fragments are resolved by name across
84
+ * the project; a document that isn't a plain string can't be checked and is reported).
85
+ */
86
+ async function check(options = {}) {
87
+ const cwd = path.resolve(options.cwd || process.cwd());
88
+ const schemaFile = path.resolve(cwd, options.schemaFile || ".dynamite/schema.graphql");
89
+ const ts = await loadTypeScript();
90
+ const documents = sourceFiles(cwd).flatMap((file) => findDocuments(ts, file));
91
+ if (!documents.length) return {
92
+ documents: 0,
93
+ problems: []
94
+ };
95
+ if (!options.schema && !fs.existsSync(schemaFile)) throw new Error("Missing " + path.relative(cwd, schemaFile) + ": run `dynamite codegen` first.");
96
+ const schema = options.schema || buildSchema(fs.readFileSync(schemaFile, "utf8"));
97
+ const problems = [];
98
+ const parsed = [];
99
+ const fragments = /* @__PURE__ */ new Map();
100
+ const fragmentOwners = /* @__PURE__ */ new Map();
101
+ for (const doc of documents) {
102
+ if (!doc.text) {
103
+ problems.push({
104
+ file: doc.file,
105
+ line: doc.line + 1,
106
+ column: doc.column + 1,
107
+ message: "graphql() needs a plain string document to be checked (no ${} interpolation)."
108
+ });
109
+ continue;
110
+ }
111
+ try {
112
+ const ast = parse(doc.text);
113
+ parsed.push({
114
+ doc,
115
+ ast
116
+ });
117
+ for (const definition of ast.definitions) {
118
+ if (definition.kind !== Kind.FRAGMENT_DEFINITION) continue;
119
+ const name = definition.name.value;
120
+ if (fragmentOwners.has(name)) problems.push({
121
+ file: doc.file,
122
+ line: doc.line + (definition.loc?.startToken.line ?? 1),
123
+ column: 1,
124
+ message: `Duplicated fragment "${name}" (also in ${path.relative(cwd, fragmentOwners.get(name).file)}).`
125
+ });
126
+ fragments.set(name, definition);
127
+ fragmentOwners.set(name, doc);
128
+ }
129
+ } catch (err) {
130
+ problems.push(problemOf(doc, err));
131
+ }
132
+ }
133
+ const rules = specifiedRules.filter((rule) => rule !== NoUnusedFragmentsRule);
134
+ for (const { doc, ast } of parsed) {
135
+ const own = new Set(ast.definitions.flatMap((d) => d.kind === Kind.FRAGMENT_DEFINITION ? [d.name.value] : []));
136
+ const imported = [...usedFragments(ast, fragments).entries()].filter(([name]) => !own.has(name)).map(([, fragment]) => fragment);
137
+ const errors = validate(schema, {
138
+ ...ast,
139
+ definitions: [...ast.definitions, ...imported]
140
+ }, rules);
141
+ for (const error of errors) {
142
+ if (imported.some((f) => error.nodes?.some((n) => isInside(n, f)))) continue;
143
+ problems.push(problemOf(doc, error));
144
+ }
145
+ }
146
+ problems.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
147
+ return {
148
+ documents: documents.length,
149
+ problems
150
+ };
151
+ }
152
+ function isInside(node, fragment) {
153
+ return !!node.loc && node.loc.source === fragment.loc?.source;
154
+ }
155
+ export { check };
156
+
157
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","names":[],"sources":["../../src/cli/check.ts"],"sourcesContent":["// `dynamite check`: validates the graphql() documents of a project against the codegen schema.\n//\n// Why not gql.tada's `gql-tada check`: it isn't a dependency (@gql.tada/cli-utils), and it finds the schema in a\n// tsconfig plugin entry named after gql.tada/graphqlsp, while the projects here use pocketbomb-dynamite/ts-plugin.\n// The check itself is small: the TypeScript parser finds the documents and graphql-js validates them.\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport {\n buildSchema,\n GraphQLError,\n Kind,\n NoUnusedFragmentsRule,\n parse,\n specifiedRules,\n validate,\n visit,\n type DocumentNode,\n type FragmentDefinitionNode,\n type GraphQLSchema,\n} from \"graphql\";\nimport { DEFAULT_SCHEMA_FILE } from \"./codegen\";\n\nconst IGNORED_DIRS = new Set([\n \"node_modules\",\n \"build\",\n \"dist\",\n \".git\",\n \".react-router\",\n \".turbo\",\n \".next\",\n \".dynamite\",\n]);\n\nexport interface CheckProblem {\n file: string;\n line: number;\n column: number;\n message: string;\n}\n\nexport interface CheckResult {\n documents: number;\n problems: Array<CheckProblem>;\n}\n\ninterface FoundDocument {\n file: string;\n /** The zero based position of the document text start in its file. */\n line: number;\n column: number;\n text: string;\n}\n\nfunction sourceFiles(dir: string, result: Array<string> = []): Array<string> {\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n if (entry.isDirectory()) {\n if (!IGNORED_DIRS.has(entry.name)) sourceFiles(path.join(dir, entry.name), result);\n } else if (/\\.(ts|tsx|mts|cts)$/.test(entry.name) && !entry.name.endsWith(\".d.ts\")) {\n result.push(path.join(dir, entry.name));\n }\n }\n return result;\n}\n\nasync function loadTypeScript(): Promise<typeof import(\"typescript\")> {\n try {\n const ts: any = await import(\"typescript\");\n return ts.default || ts;\n } catch {\n throw new Error(\"dynamite check needs the typescript package installed in the project.\");\n }\n}\n\nfunction findDocuments(ts: typeof import(\"typescript\"), file: string): Array<FoundDocument> {\n const source = ts.createSourceFile(\n file,\n fs.readFileSync(file, \"utf8\"),\n ts.ScriptTarget.Latest,\n true,\n );\n const result: Array<FoundDocument> = [];\n\n const visitNode = (node: import(\"typescript\").Node) => {\n if (\n ts.isCallExpression(node) &&\n ts.isIdentifier(node.expression) &&\n node.expression.text === \"graphql\"\n ) {\n const arg = node.arguments[0];\n if (arg && (ts.isNoSubstitutionTemplateLiteral(arg) || ts.isStringLiteral(arg))) {\n // +1: the opening quote\n const start = source.getLineAndCharacterOfPosition(arg.getStart(source) + 1);\n result.push({ file, line: start.line, column: start.character, text: arg.text });\n } else if (arg) {\n const start = source.getLineAndCharacterOfPosition(arg.getStart(source));\n result.push({ file, line: start.line, column: start.character, text: \"\" });\n }\n }\n ts.forEachChild(node, visitNode);\n };\n visitNode(source);\n\n return result;\n}\n\nfunction usedFragments(root: DocumentNode, all: Map<string, FragmentDefinitionNode>) {\n const used = new Map<string, FragmentDefinitionNode>();\n const collect = (node: DocumentNode | FragmentDefinitionNode) => {\n visit(node, {\n FragmentSpread(spread) {\n const fragment = all.get(spread.name.value);\n if (fragment && !used.has(spread.name.value)) {\n used.set(spread.name.value, fragment);\n collect(fragment);\n }\n },\n });\n };\n collect(root);\n return used;\n}\n\nfunction problemOf(doc: FoundDocument, error: GraphQLError): CheckProblem {\n const location = error.locations?.[0];\n return {\n file: doc.file,\n // the first line of the text continues the source line of the opening quote\n line: doc.line + (location?.line ?? 1),\n column: location?.line === 1 ? doc.column + location.column : (location?.column ?? 1),\n message: error.message,\n };\n}\n\n/**\n * Validates every `graphql()` document of a project (fragments are resolved by name across\n * the project; a document that isn't a plain string can't be checked and is reported).\n */\nexport async function check(\n options: { cwd?: string; schemaFile?: string; schema?: GraphQLSchema } = {},\n): Promise<CheckResult> {\n const cwd = path.resolve(options.cwd || process.cwd());\n const schemaFile = path.resolve(cwd, options.schemaFile || DEFAULT_SCHEMA_FILE);\n const ts = await loadTypeScript();\n\n const documents = sourceFiles(cwd).flatMap((file) => findDocuments(ts, file));\n if (!documents.length) {\n return { documents: 0, problems: [] };\n }\n\n if (!options.schema && !fs.existsSync(schemaFile)) {\n throw new Error(\n \"Missing \" + path.relative(cwd, schemaFile) + \": run `dynamite codegen` first.\",\n );\n }\n const schema = options.schema || buildSchema(fs.readFileSync(schemaFile, \"utf8\"));\n const problems: Array<CheckProblem> = [];\n const parsed: Array<{ doc: FoundDocument; ast: DocumentNode }> = [];\n const fragments = new Map<string, FragmentDefinitionNode>();\n const fragmentOwners = new Map<string, FoundDocument>();\n\n for (const doc of documents) {\n if (!doc.text) {\n problems.push({\n file: doc.file,\n line: doc.line + 1,\n column: doc.column + 1,\n message: \"graphql() needs a plain string document to be checked (no ${} interpolation).\",\n });\n continue;\n }\n try {\n const ast = parse(doc.text);\n parsed.push({ doc, ast });\n for (const definition of ast.definitions) {\n if (definition.kind !== Kind.FRAGMENT_DEFINITION) continue;\n const name = definition.name.value;\n if (fragmentOwners.has(name)) {\n problems.push({\n file: doc.file,\n line: doc.line + (definition.loc?.startToken.line ?? 1),\n column: 1,\n message: `Duplicated fragment \"${name}\" (also in ${path.relative(cwd, fragmentOwners.get(name)!.file)}).`,\n });\n }\n fragments.set(name, definition);\n fragmentOwners.set(name, doc);\n }\n } catch (err) {\n problems.push(problemOf(doc, err as GraphQLError));\n }\n }\n\n // a document only holding fragments is valid on its own\n const rules = specifiedRules.filter((rule) => rule !== NoUnusedFragmentsRule);\n\n for (const { doc, ast } of parsed) {\n const own = new Set(\n ast.definitions.flatMap((d) => (d.kind === Kind.FRAGMENT_DEFINITION ? [d.name.value] : [])),\n );\n const imported = [...usedFragments(ast, fragments).entries()]\n .filter(([name]) => !own.has(name))\n .map(([, fragment]) => fragment);\n\n const errors = validate(\n schema,\n { ...ast, definitions: [...ast.definitions, ...imported] },\n rules,\n );\n for (const error of errors) {\n // an error inside an imported fragment is reported once, by the document that owns it\n if (imported.some((f) => error.nodes?.some((n) => isInside(n, f)))) continue;\n problems.push(problemOf(doc, error));\n }\n }\n\n problems.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);\n\n return { documents: documents.length, problems };\n}\n\nfunction isInside(node: { loc?: { source: unknown } }, fragment: FragmentDefinitionNode): boolean {\n return !!node.loc && node.loc.source === fragment.loc?.source;\n}\n"],"mappings":"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"}
@@ -49,7 +49,8 @@ function withoutEmptyDescriptions(value) {
49
49
  * Returns the GraphQL SDL of an introspection.
50
50
  */
51
51
  function introspectionToSDL(introspection) {
52
- return (0, graphql.printSchema)((0, graphql.buildClientSchema)(withoutEmptyDescriptions(introspection))) + "\n";
52
+ const schema = (0, graphql.buildClientSchema)(withoutEmptyDescriptions(introspection));
53
+ return (0, graphql.printSchema)((0, graphql.lexicographicSortSchema)(schema)) + "\n";
53
54
  }
54
55
  /**
55
56
  * Returns the `pocketbomb-env.d.ts` of an introspection: the gql.tada introspection type
@@ -1 +1 @@
1
- {"version":3,"file":"codegen.cjs","names":["getIntrospectionQuery","printSchema","buildClientSchema","preprocessIntrospection","minifyIntrospection","fs","path"],"sources":["../../src/cli/codegen.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { minifyIntrospection, preprocessIntrospection } from \"@gql.tada/internal\";\nimport {\n buildClientSchema,\n getIntrospectionQuery,\n printSchema,\n type IntrospectionQuery,\n} from \"graphql\";\n\nimport { DEFAULT_ENV_FILE, DEFAULT_SCHEMA_FILE } from \"../shared/constants\";\n\nexport { DEFAULT_ENV_FILE, DEFAULT_SCHEMA_FILE };\n\nexport interface IntrospectOptions {\n /** The PocketBomb URL. */\n url: string;\n /** The public API key (the GraphQL API requires client credentials). */\n publicKey?: string;\n fetch?: typeof fetch;\n}\n\n/**\n * Introspects the PocketBomb GraphQL API (`POST /api/graphql`; the introspection\n * fields are not counted in the query limits).\n *\n * @throws {Error} for the HTTP and GraphQL errors.\n */\nexport async function introspect(options: IntrospectOptions): Promise<IntrospectionQuery> {\n const headers: Record<string, string> = { \"content-type\": \"application/json\" };\n if (options.publicKey) {\n headers[\"X-PB-Public-Key\"] = options.publicKey;\n }\n\n const endpoint = options.url.replace(/\\/+$/, \"\") + \"/api/graphql\";\n const response = await (options.fetch || fetch)(endpoint, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ query: getIntrospectionQuery({ descriptions: true }) }),\n });\n\n let body: any = null;\n try {\n body = await response.json();\n } catch {\n body = null;\n }\n\n if (!response.ok) {\n throw new Error(\n `The GraphQL introspection failed (${response.status}): ${body?.message || JSON.stringify(body)}`,\n );\n }\n if (body?.errors?.length || !body?.data?.__schema) {\n throw new Error(\n \"The GraphQL introspection failed: \" +\n (body?.errors?.map((e: any) => e.message).join(\"; \") || \"no schema\"),\n );\n }\n\n return body.data as IntrospectionQuery;\n}\n\n/**\n * Returns a copy of the introspection without the empty descriptions\n * (the server reports \"\" for the undocumented fields).\n */\nexport function withoutEmptyDescriptions<T>(value: T): T {\n if (Array.isArray(value)) {\n return value.map(withoutEmptyDescriptions) as T;\n }\n\n if (value && typeof value === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(value)) {\n result[key] = key === \"description\" && item === \"\" ? null : withoutEmptyDescriptions(item);\n }\n return result as T;\n }\n\n return value;\n}\n\n/**\n * Returns the GraphQL SDL of an introspection.\n */\nexport function introspectionToSDL(introspection: IntrospectionQuery): string {\n return printSchema(buildClientSchema(withoutEmptyDescriptions(introspection))) + \"\\n\";\n}\n\n/**\n * Returns the `pocketbomb-env.d.ts` of an introspection: the gql.tada introspection type\n * and the `pocketbomb-dynamite/graphql` schema augmentation used by `graphql()`.\n */\nexport function introspectionToEnv(introspection: IntrospectionQuery): string {\n const type = preprocessIntrospection(minifyIntrospection(introspection));\n\n return (\n \"/* eslint-disable */\\n\" +\n \"/* prettier-ignore */\\n\" +\n \"// Generated by `dynamite codegen` from the PocketBomb GraphQL API. Do not edit.\\n\\n\" +\n \"/** The gql.tada introspection of the PocketBomb GraphQL schema. */\\n\" +\n \"export type introspection = \" +\n type +\n \";\\n\\n\" +\n 'declare module \"pocketbomb-dynamite/graphql\" {\\n' +\n \" interface PocketBombSchema {\\n\" +\n \" introspection: introspection;\\n\" +\n \" }\\n\" +\n \"}\\n\"\n );\n}\n\nexport interface GenerateOptions {\n /** The project root (default to the current working dir). */\n cwd?: string;\n /** The SDL output (default to `.dynamite/schema.graphql`). */\n schemaFile?: string;\n /** The gql.tada env output (default to `pocketbomb-env.d.ts`). */\n envFile?: string;\n}\n\nexport interface GenerateResult {\n schemaFile: string;\n envFile: string;\n /** Whether a file was (re)written. */\n changed: boolean;\n}\n\nfunction writeIfChanged(file: string, content: string): boolean {\n if (fs.existsSync(file) && fs.readFileSync(file, \"utf8\") === content) {\n return false;\n }\n\n fs.mkdirSync(path.dirname(file), { recursive: true });\n fs.writeFileSync(file, content);\n\n return true;\n}\n\n/**\n * Writes the schema SDL and the gql.tada env of an introspection (only if they changed).\n */\nexport function generate(\n introspection: IntrospectionQuery,\n options: GenerateOptions = {},\n): GenerateResult {\n const cwd = options.cwd || process.cwd();\n const schemaFile = path.resolve(cwd, options.schemaFile || DEFAULT_SCHEMA_FILE);\n const envFile = path.resolve(cwd, options.envFile || DEFAULT_ENV_FILE);\n\n const schemaChanged = writeIfChanged(schemaFile, introspectionToSDL(introspection));\n const envChanged = writeIfChanged(envFile, introspectionToEnv(introspection));\n\n return { schemaFile, envFile, changed: schemaChanged || envChanged };\n}\n\nexport interface CodegenOptions extends IntrospectOptions, GenerateOptions {}\n\n/**\n * Introspects the API and writes the schema and the env (see {@link generate}).\n */\nexport async function codegen(options: CodegenOptions): Promise<GenerateResult> {\n return generate(await introspect(options), options);\n}\n\nexport interface WatchOptions extends CodegenOptions {\n /** The schema poll interval in ms (default to 2000). */\n interval?: number;\n /** Called after every generation that changed a file. */\n onChange?: (result: GenerateResult) => void;\n /** Called for the failed polls (the watcher keeps running). */\n onError?: (err: unknown) => void;\n}\n\n/**\n * Regenerates the schema and the env when the API schema changes (polling).\n * Returns a function that stops the watcher.\n */\nexport function watchCodegen(options: WatchOptions): () => void {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const tick = async () => {\n try {\n const result = await codegen(options);\n if (result.changed) {\n options.onChange?.(result);\n }\n } catch (err) {\n options.onError?.(err);\n }\n\n if (!stopped) {\n timer = setTimeout(tick, options.interval ?? 2000);\n }\n };\n\n void tick();\n\n return () => {\n stopped = true;\n clearTimeout(timer);\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"codegen.cjs","names":["getIntrospectionQuery","buildClientSchema","printSchema","lexicographicSortSchema","preprocessIntrospection","minifyIntrospection","fs","path"],"sources":["../../src/cli/codegen.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { minifyIntrospection, preprocessIntrospection } from \"@gql.tada/internal\";\nimport {\n buildClientSchema,\n getIntrospectionQuery,\n printSchema,\n type IntrospectionQuery,\n lexicographicSortSchema,\n} from \"graphql\";\n\nimport { DEFAULT_ENV_FILE, DEFAULT_SCHEMA_FILE } from \"../shared/constants\";\n\nexport { DEFAULT_ENV_FILE, DEFAULT_SCHEMA_FILE };\n\nexport interface IntrospectOptions {\n /** The PocketBomb URL. */\n url: string;\n /** The public API key (the GraphQL API requires client credentials). */\n publicKey?: string;\n fetch?: typeof fetch;\n}\n\n/**\n * Introspects the PocketBomb GraphQL API (`POST /api/graphql`; the introspection\n * fields are not counted in the query limits).\n *\n * @throws {Error} for the HTTP and GraphQL errors.\n */\nexport async function introspect(options: IntrospectOptions): Promise<IntrospectionQuery> {\n const headers: Record<string, string> = { \"content-type\": \"application/json\" };\n if (options.publicKey) {\n headers[\"X-PB-Public-Key\"] = options.publicKey;\n }\n\n const endpoint = options.url.replace(/\\/+$/, \"\") + \"/api/graphql\";\n const response = await (options.fetch || fetch)(endpoint, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ query: getIntrospectionQuery({ descriptions: true }) }),\n });\n\n let body: any = null;\n try {\n body = await response.json();\n } catch {\n body = null;\n }\n\n if (!response.ok) {\n throw new Error(\n `The GraphQL introspection failed (${response.status}): ${body?.message || JSON.stringify(body)}`,\n );\n }\n if (body?.errors?.length || !body?.data?.__schema) {\n throw new Error(\n \"The GraphQL introspection failed: \" +\n (body?.errors?.map((e: any) => e.message).join(\"; \") || \"no schema\"),\n );\n }\n\n return body.data as IntrospectionQuery;\n}\n\n/**\n * Returns a copy of the introspection without the empty descriptions\n * (the server reports \"\" for the undocumented fields).\n */\nexport function withoutEmptyDescriptions<T>(value: T): T {\n if (Array.isArray(value)) {\n return value.map(withoutEmptyDescriptions) as T;\n }\n\n if (value && typeof value === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(value)) {\n result[key] = key === \"description\" && item === \"\" ? null : withoutEmptyDescriptions(item);\n }\n return result as T;\n }\n\n return value;\n}\n\n/**\n * Returns the GraphQL SDL of an introspection.\n */\nexport function introspectionToSDL(introspection: IntrospectionQuery): string {\n // sorted: the server doesn't guarantee the order of the types between two introspections\n const schema = buildClientSchema(withoutEmptyDescriptions(introspection));\n return printSchema(lexicographicSortSchema(schema)) + \"\\n\";\n}\n\n/**\n * Returns the `pocketbomb-env.d.ts` of an introspection: the gql.tada introspection type\n * and the `pocketbomb-dynamite/graphql` schema augmentation used by `graphql()`.\n */\nexport function introspectionToEnv(introspection: IntrospectionQuery): string {\n const type = preprocessIntrospection(minifyIntrospection(introspection));\n\n return (\n \"/* eslint-disable */\\n\" +\n \"/* prettier-ignore */\\n\" +\n \"// Generated by `dynamite codegen` from the PocketBomb GraphQL API. Do not edit.\\n\\n\" +\n \"/** The gql.tada introspection of the PocketBomb GraphQL schema. */\\n\" +\n \"export type introspection = \" +\n type +\n \";\\n\\n\" +\n 'declare module \"pocketbomb-dynamite/graphql\" {\\n' +\n \" interface PocketBombSchema {\\n\" +\n \" introspection: introspection;\\n\" +\n \" }\\n\" +\n \"}\\n\"\n );\n}\n\nexport interface GenerateOptions {\n /** The project root (default to the current working dir). */\n cwd?: string;\n /** The SDL output (default to `.dynamite/schema.graphql`). */\n schemaFile?: string;\n /** The gql.tada env output (default to `pocketbomb-env.d.ts`). */\n envFile?: string;\n}\n\nexport interface GenerateResult {\n schemaFile: string;\n envFile: string;\n /** Whether a file was (re)written. */\n changed: boolean;\n}\n\nfunction writeIfChanged(file: string, content: string): boolean {\n if (fs.existsSync(file) && fs.readFileSync(file, \"utf8\") === content) {\n return false;\n }\n\n fs.mkdirSync(path.dirname(file), { recursive: true });\n fs.writeFileSync(file, content);\n\n return true;\n}\n\n/**\n * Writes the schema SDL and the gql.tada env of an introspection (only if they changed).\n */\nexport function generate(\n introspection: IntrospectionQuery,\n options: GenerateOptions = {},\n): GenerateResult {\n const cwd = options.cwd || process.cwd();\n const schemaFile = path.resolve(cwd, options.schemaFile || DEFAULT_SCHEMA_FILE);\n const envFile = path.resolve(cwd, options.envFile || DEFAULT_ENV_FILE);\n\n const schemaChanged = writeIfChanged(schemaFile, introspectionToSDL(introspection));\n const envChanged = writeIfChanged(envFile, introspectionToEnv(introspection));\n\n return { schemaFile, envFile, changed: schemaChanged || envChanged };\n}\n\nexport interface CodegenOptions extends IntrospectOptions, GenerateOptions {}\n\n/**\n * Introspects the API and writes the schema and the env (see {@link generate}).\n */\nexport async function codegen(options: CodegenOptions): Promise<GenerateResult> {\n return generate(await introspect(options), options);\n}\n\nexport interface WatchOptions extends CodegenOptions {\n /** The schema poll interval in ms (default to 2000). */\n interval?: number;\n /** Called after every generation that changed a file. */\n onChange?: (result: GenerateResult) => void;\n /** Called for the failed polls (the watcher keeps running). */\n onError?: (err: unknown) => void;\n}\n\n/**\n * Regenerates the schema and the env when the API schema changes (polling).\n * Returns a function that stops the watcher.\n */\nexport function watchCodegen(options: WatchOptions): () => void {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const tick = async () => {\n try {\n const result = await codegen(options);\n if (result.changed) {\n options.onChange?.(result);\n }\n } catch (err) {\n options.onError?.(err);\n }\n\n if (!stopped) {\n timer = setTimeout(tick, options.interval ?? 2000);\n }\n };\n\n void tick();\n\n return () => {\n stopped = true;\n clearTimeout(timer);\n };\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import { DEFAULT_ENV_FILE, DEFAULT_SCHEMA_FILE } from "../shared/constants.js";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { minifyIntrospection, preprocessIntrospection } from "@gql.tada/internal";
5
- import { buildClientSchema, getIntrospectionQuery, printSchema } from "graphql";
5
+ import { buildClientSchema, getIntrospectionQuery, lexicographicSortSchema, printSchema } from "graphql";
6
6
  // src/cli/codegen.ts
7
7
  /**
8
8
  * Introspects the PocketBomb GraphQL API (`POST /api/graphql`; the introspection
@@ -46,7 +46,8 @@ function withoutEmptyDescriptions(value) {
46
46
  * Returns the GraphQL SDL of an introspection.
47
47
  */
48
48
  function introspectionToSDL(introspection) {
49
- return printSchema(buildClientSchema(withoutEmptyDescriptions(introspection))) + "\n";
49
+ const schema = buildClientSchema(withoutEmptyDescriptions(introspection));
50
+ return printSchema(lexicographicSortSchema(schema)) + "\n";
50
51
  }
51
52
  /**
52
53
  * Returns the `pocketbomb-env.d.ts` of an introspection: the gql.tada introspection type
@@ -1 +1 @@
1
- {"version":3,"file":"codegen.js","names":[],"sources":["../../src/cli/codegen.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { minifyIntrospection, preprocessIntrospection } from \"@gql.tada/internal\";\nimport {\n buildClientSchema,\n getIntrospectionQuery,\n printSchema,\n type IntrospectionQuery,\n} from \"graphql\";\n\nimport { DEFAULT_ENV_FILE, DEFAULT_SCHEMA_FILE } from \"../shared/constants\";\n\nexport { DEFAULT_ENV_FILE, DEFAULT_SCHEMA_FILE };\n\nexport interface IntrospectOptions {\n /** The PocketBomb URL. */\n url: string;\n /** The public API key (the GraphQL API requires client credentials). */\n publicKey?: string;\n fetch?: typeof fetch;\n}\n\n/**\n * Introspects the PocketBomb GraphQL API (`POST /api/graphql`; the introspection\n * fields are not counted in the query limits).\n *\n * @throws {Error} for the HTTP and GraphQL errors.\n */\nexport async function introspect(options: IntrospectOptions): Promise<IntrospectionQuery> {\n const headers: Record<string, string> = { \"content-type\": \"application/json\" };\n if (options.publicKey) {\n headers[\"X-PB-Public-Key\"] = options.publicKey;\n }\n\n const endpoint = options.url.replace(/\\/+$/, \"\") + \"/api/graphql\";\n const response = await (options.fetch || fetch)(endpoint, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ query: getIntrospectionQuery({ descriptions: true }) }),\n });\n\n let body: any = null;\n try {\n body = await response.json();\n } catch {\n body = null;\n }\n\n if (!response.ok) {\n throw new Error(\n `The GraphQL introspection failed (${response.status}): ${body?.message || JSON.stringify(body)}`,\n );\n }\n if (body?.errors?.length || !body?.data?.__schema) {\n throw new Error(\n \"The GraphQL introspection failed: \" +\n (body?.errors?.map((e: any) => e.message).join(\"; \") || \"no schema\"),\n );\n }\n\n return body.data as IntrospectionQuery;\n}\n\n/**\n * Returns a copy of the introspection without the empty descriptions\n * (the server reports \"\" for the undocumented fields).\n */\nexport function withoutEmptyDescriptions<T>(value: T): T {\n if (Array.isArray(value)) {\n return value.map(withoutEmptyDescriptions) as T;\n }\n\n if (value && typeof value === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(value)) {\n result[key] = key === \"description\" && item === \"\" ? null : withoutEmptyDescriptions(item);\n }\n return result as T;\n }\n\n return value;\n}\n\n/**\n * Returns the GraphQL SDL of an introspection.\n */\nexport function introspectionToSDL(introspection: IntrospectionQuery): string {\n return printSchema(buildClientSchema(withoutEmptyDescriptions(introspection))) + \"\\n\";\n}\n\n/**\n * Returns the `pocketbomb-env.d.ts` of an introspection: the gql.tada introspection type\n * and the `pocketbomb-dynamite/graphql` schema augmentation used by `graphql()`.\n */\nexport function introspectionToEnv(introspection: IntrospectionQuery): string {\n const type = preprocessIntrospection(minifyIntrospection(introspection));\n\n return (\n \"/* eslint-disable */\\n\" +\n \"/* prettier-ignore */\\n\" +\n \"// Generated by `dynamite codegen` from the PocketBomb GraphQL API. Do not edit.\\n\\n\" +\n \"/** The gql.tada introspection of the PocketBomb GraphQL schema. */\\n\" +\n \"export type introspection = \" +\n type +\n \";\\n\\n\" +\n 'declare module \"pocketbomb-dynamite/graphql\" {\\n' +\n \" interface PocketBombSchema {\\n\" +\n \" introspection: introspection;\\n\" +\n \" }\\n\" +\n \"}\\n\"\n );\n}\n\nexport interface GenerateOptions {\n /** The project root (default to the current working dir). */\n cwd?: string;\n /** The SDL output (default to `.dynamite/schema.graphql`). */\n schemaFile?: string;\n /** The gql.tada env output (default to `pocketbomb-env.d.ts`). */\n envFile?: string;\n}\n\nexport interface GenerateResult {\n schemaFile: string;\n envFile: string;\n /** Whether a file was (re)written. */\n changed: boolean;\n}\n\nfunction writeIfChanged(file: string, content: string): boolean {\n if (fs.existsSync(file) && fs.readFileSync(file, \"utf8\") === content) {\n return false;\n }\n\n fs.mkdirSync(path.dirname(file), { recursive: true });\n fs.writeFileSync(file, content);\n\n return true;\n}\n\n/**\n * Writes the schema SDL and the gql.tada env of an introspection (only if they changed).\n */\nexport function generate(\n introspection: IntrospectionQuery,\n options: GenerateOptions = {},\n): GenerateResult {\n const cwd = options.cwd || process.cwd();\n const schemaFile = path.resolve(cwd, options.schemaFile || DEFAULT_SCHEMA_FILE);\n const envFile = path.resolve(cwd, options.envFile || DEFAULT_ENV_FILE);\n\n const schemaChanged = writeIfChanged(schemaFile, introspectionToSDL(introspection));\n const envChanged = writeIfChanged(envFile, introspectionToEnv(introspection));\n\n return { schemaFile, envFile, changed: schemaChanged || envChanged };\n}\n\nexport interface CodegenOptions extends IntrospectOptions, GenerateOptions {}\n\n/**\n * Introspects the API and writes the schema and the env (see {@link generate}).\n */\nexport async function codegen(options: CodegenOptions): Promise<GenerateResult> {\n return generate(await introspect(options), options);\n}\n\nexport interface WatchOptions extends CodegenOptions {\n /** The schema poll interval in ms (default to 2000). */\n interval?: number;\n /** Called after every generation that changed a file. */\n onChange?: (result: GenerateResult) => void;\n /** Called for the failed polls (the watcher keeps running). */\n onError?: (err: unknown) => void;\n}\n\n/**\n * Regenerates the schema and the env when the API schema changes (polling).\n * Returns a function that stops the watcher.\n */\nexport function watchCodegen(options: WatchOptions): () => void {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const tick = async () => {\n try {\n const result = await codegen(options);\n if (result.changed) {\n options.onChange?.(result);\n }\n } catch (err) {\n options.onError?.(err);\n }\n\n if (!stopped) {\n timer = setTimeout(tick, options.interval ?? 2000);\n }\n };\n\n void tick();\n\n return () => {\n stopped = true;\n clearTimeout(timer);\n };\n}\n"],"mappings":";;;;;;;;;;;;AA4BA,eAAsB,WAAW,SAAyD;CACxF,MAAM,UAAkC,EAAE,gBAAgB,mBAAmB;CAC7E,IAAI,QAAQ,WACV,QAAQ,qBAAqB,QAAQ;CAGvC,MAAM,WAAW,QAAQ,IAAI,QAAQ,QAAQ,EAAE,IAAI;CACnD,MAAM,WAAW,OAAO,QAAQ,SAAS,MAAA,CAAO,UAAU;EACxD,QAAQ;EACR;EACA,MAAM,KAAK,UAAU,EAAE,OAAO,sBAAsB,EAAE,cAAc,KAAK,CAAC,EAAE,CAAC;CAC/E,CAAC;CAED,IAAI,OAAY;CAChB,IAAI;EACF,OAAO,MAAM,SAAS,KAAK;CAC7B,QAAQ;EACN,OAAO;CACT;CAEA,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MACR,qCAAqC,SAAS,OAAO,KAAK,MAAM,WAAW,KAAK,UAAU,IAAI,GAChG;CAEF,IAAI,MAAM,QAAQ,UAAU,CAAC,MAAM,MAAM,UACvC,MAAM,IAAI,MACR,wCACG,MAAM,QAAQ,KAAK,MAAW,EAAE,OAAO,CAAC,CAAC,KAAK,IAAI,KAAK,YAC5D;CAGF,OAAO,KAAK;AACd;;;;;AAMA,SAAgB,yBAA4B,OAAa;CACvD,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,IAAI,wBAAwB;CAG3C,IAAI,SAAS,OAAO,UAAU,UAAU;EACtC,MAAM,SAAkC,CAAC;EACzC,KAAK,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,KAAK,GAC5C,OAAO,OAAO,QAAQ,iBAAiB,SAAS,KAAK,OAAO,yBAAyB,IAAI;EAE3F,OAAO;CACT;CAEA,OAAO;AACT;;;;AAKA,SAAgB,mBAAmB,eAA2C;CAC5E,OAAO,YAAY,kBAAkB,yBAAyB,aAAa,CAAC,CAAC,IAAI;AACnF;;;;;AAMA,SAAgB,mBAAmB,eAA2C;CAG5E,OACE,uOAHW,wBAAwB,oBAAoB,aAAa,CAQjE,IACH;AAOJ;AAkBA,SAAS,eAAe,MAAc,SAA0B;CAC9D,IAAI,GAAG,WAAW,IAAI,KAAK,GAAG,aAAa,MAAM,MAAM,MAAM,SAC3D,OAAO;CAGT,GAAG,UAAU,KAAK,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;CACpD,GAAG,cAAc,MAAM,OAAO;CAE9B,OAAO;AACT;;;;AAKA,SAAgB,SACd,eACA,UAA2B,CAAC,GACZ;CAChB,MAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;CACvC,MAAM,aAAa,KAAK,QAAQ,KAAK,QAAQ,cAAA,0BAAiC;CAC9E,MAAM,UAAU,KAAK,QAAQ,KAAK,QAAQ,WAAA,qBAA2B;CAErE,MAAM,gBAAgB,eAAe,YAAY,mBAAmB,aAAa,CAAC;CAClF,MAAM,aAAa,eAAe,SAAS,mBAAmB,aAAa,CAAC;CAE5E,OAAO;EAAE;EAAY;EAAS,SAAS,iBAAiB;CAAW;AACrE;;;;AAOA,eAAsB,QAAQ,SAAkD;CAC9E,OAAO,SAAS,MAAM,WAAW,OAAO,GAAG,OAAO;AACpD;;;;;AAeA,SAAgB,aAAa,SAAmC;CAC9D,IAAI,UAAU;CACd,IAAI;CAEJ,MAAM,OAAO,YAAY;EACvB,IAAI;GACF,MAAM,SAAS,MAAM,QAAQ,OAAO;GACpC,IAAI,OAAO,SACT,QAAQ,WAAW,MAAM;EAE7B,SAAS,KAAK;GACZ,QAAQ,UAAU,GAAG;EACvB;EAEA,IAAI,CAAC,SACH,QAAQ,WAAW,MAAM,QAAQ,YAAY,GAAI;CAErD;CAEA,KAAU;CAEV,aAAa;EACX,UAAU;EACV,aAAa,KAAK;CACpB;AACF"}
1
+ {"version":3,"file":"codegen.js","names":[],"sources":["../../src/cli/codegen.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { minifyIntrospection, preprocessIntrospection } from \"@gql.tada/internal\";\nimport {\n buildClientSchema,\n getIntrospectionQuery,\n printSchema,\n type IntrospectionQuery,\n lexicographicSortSchema,\n} from \"graphql\";\n\nimport { DEFAULT_ENV_FILE, DEFAULT_SCHEMA_FILE } from \"../shared/constants\";\n\nexport { DEFAULT_ENV_FILE, DEFAULT_SCHEMA_FILE };\n\nexport interface IntrospectOptions {\n /** The PocketBomb URL. */\n url: string;\n /** The public API key (the GraphQL API requires client credentials). */\n publicKey?: string;\n fetch?: typeof fetch;\n}\n\n/**\n * Introspects the PocketBomb GraphQL API (`POST /api/graphql`; the introspection\n * fields are not counted in the query limits).\n *\n * @throws {Error} for the HTTP and GraphQL errors.\n */\nexport async function introspect(options: IntrospectOptions): Promise<IntrospectionQuery> {\n const headers: Record<string, string> = { \"content-type\": \"application/json\" };\n if (options.publicKey) {\n headers[\"X-PB-Public-Key\"] = options.publicKey;\n }\n\n const endpoint = options.url.replace(/\\/+$/, \"\") + \"/api/graphql\";\n const response = await (options.fetch || fetch)(endpoint, {\n method: \"POST\",\n headers,\n body: JSON.stringify({ query: getIntrospectionQuery({ descriptions: true }) }),\n });\n\n let body: any = null;\n try {\n body = await response.json();\n } catch {\n body = null;\n }\n\n if (!response.ok) {\n throw new Error(\n `The GraphQL introspection failed (${response.status}): ${body?.message || JSON.stringify(body)}`,\n );\n }\n if (body?.errors?.length || !body?.data?.__schema) {\n throw new Error(\n \"The GraphQL introspection failed: \" +\n (body?.errors?.map((e: any) => e.message).join(\"; \") || \"no schema\"),\n );\n }\n\n return body.data as IntrospectionQuery;\n}\n\n/**\n * Returns a copy of the introspection without the empty descriptions\n * (the server reports \"\" for the undocumented fields).\n */\nexport function withoutEmptyDescriptions<T>(value: T): T {\n if (Array.isArray(value)) {\n return value.map(withoutEmptyDescriptions) as T;\n }\n\n if (value && typeof value === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(value)) {\n result[key] = key === \"description\" && item === \"\" ? null : withoutEmptyDescriptions(item);\n }\n return result as T;\n }\n\n return value;\n}\n\n/**\n * Returns the GraphQL SDL of an introspection.\n */\nexport function introspectionToSDL(introspection: IntrospectionQuery): string {\n // sorted: the server doesn't guarantee the order of the types between two introspections\n const schema = buildClientSchema(withoutEmptyDescriptions(introspection));\n return printSchema(lexicographicSortSchema(schema)) + \"\\n\";\n}\n\n/**\n * Returns the `pocketbomb-env.d.ts` of an introspection: the gql.tada introspection type\n * and the `pocketbomb-dynamite/graphql` schema augmentation used by `graphql()`.\n */\nexport function introspectionToEnv(introspection: IntrospectionQuery): string {\n const type = preprocessIntrospection(minifyIntrospection(introspection));\n\n return (\n \"/* eslint-disable */\\n\" +\n \"/* prettier-ignore */\\n\" +\n \"// Generated by `dynamite codegen` from the PocketBomb GraphQL API. Do not edit.\\n\\n\" +\n \"/** The gql.tada introspection of the PocketBomb GraphQL schema. */\\n\" +\n \"export type introspection = \" +\n type +\n \";\\n\\n\" +\n 'declare module \"pocketbomb-dynamite/graphql\" {\\n' +\n \" interface PocketBombSchema {\\n\" +\n \" introspection: introspection;\\n\" +\n \" }\\n\" +\n \"}\\n\"\n );\n}\n\nexport interface GenerateOptions {\n /** The project root (default to the current working dir). */\n cwd?: string;\n /** The SDL output (default to `.dynamite/schema.graphql`). */\n schemaFile?: string;\n /** The gql.tada env output (default to `pocketbomb-env.d.ts`). */\n envFile?: string;\n}\n\nexport interface GenerateResult {\n schemaFile: string;\n envFile: string;\n /** Whether a file was (re)written. */\n changed: boolean;\n}\n\nfunction writeIfChanged(file: string, content: string): boolean {\n if (fs.existsSync(file) && fs.readFileSync(file, \"utf8\") === content) {\n return false;\n }\n\n fs.mkdirSync(path.dirname(file), { recursive: true });\n fs.writeFileSync(file, content);\n\n return true;\n}\n\n/**\n * Writes the schema SDL and the gql.tada env of an introspection (only if they changed).\n */\nexport function generate(\n introspection: IntrospectionQuery,\n options: GenerateOptions = {},\n): GenerateResult {\n const cwd = options.cwd || process.cwd();\n const schemaFile = path.resolve(cwd, options.schemaFile || DEFAULT_SCHEMA_FILE);\n const envFile = path.resolve(cwd, options.envFile || DEFAULT_ENV_FILE);\n\n const schemaChanged = writeIfChanged(schemaFile, introspectionToSDL(introspection));\n const envChanged = writeIfChanged(envFile, introspectionToEnv(introspection));\n\n return { schemaFile, envFile, changed: schemaChanged || envChanged };\n}\n\nexport interface CodegenOptions extends IntrospectOptions, GenerateOptions {}\n\n/**\n * Introspects the API and writes the schema and the env (see {@link generate}).\n */\nexport async function codegen(options: CodegenOptions): Promise<GenerateResult> {\n return generate(await introspect(options), options);\n}\n\nexport interface WatchOptions extends CodegenOptions {\n /** The schema poll interval in ms (default to 2000). */\n interval?: number;\n /** Called after every generation that changed a file. */\n onChange?: (result: GenerateResult) => void;\n /** Called for the failed polls (the watcher keeps running). */\n onError?: (err: unknown) => void;\n}\n\n/**\n * Regenerates the schema and the env when the API schema changes (polling).\n * Returns a function that stops the watcher.\n */\nexport function watchCodegen(options: WatchOptions): () => void {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const tick = async () => {\n try {\n const result = await codegen(options);\n if (result.changed) {\n options.onChange?.(result);\n }\n } catch (err) {\n options.onError?.(err);\n }\n\n if (!stopped) {\n timer = setTimeout(tick, options.interval ?? 2000);\n }\n };\n\n void tick();\n\n return () => {\n stopped = true;\n clearTimeout(timer);\n };\n}\n"],"mappings":";;;;;;;;;;;;AA6BA,eAAsB,WAAW,SAAyD;CACxF,MAAM,UAAkC,EAAE,gBAAgB,mBAAmB;CAC7E,IAAI,QAAQ,WACV,QAAQ,qBAAqB,QAAQ;CAGvC,MAAM,WAAW,QAAQ,IAAI,QAAQ,QAAQ,EAAE,IAAI;CACnD,MAAM,WAAW,OAAO,QAAQ,SAAS,MAAA,CAAO,UAAU;EACxD,QAAQ;EACR;EACA,MAAM,KAAK,UAAU,EAAE,OAAO,sBAAsB,EAAE,cAAc,KAAK,CAAC,EAAE,CAAC;CAC/E,CAAC;CAED,IAAI,OAAY;CAChB,IAAI;EACF,OAAO,MAAM,SAAS,KAAK;CAC7B,QAAQ;EACN,OAAO;CACT;CAEA,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MACR,qCAAqC,SAAS,OAAO,KAAK,MAAM,WAAW,KAAK,UAAU,IAAI,GAChG;CAEF,IAAI,MAAM,QAAQ,UAAU,CAAC,MAAM,MAAM,UACvC,MAAM,IAAI,MACR,wCACG,MAAM,QAAQ,KAAK,MAAW,EAAE,OAAO,CAAC,CAAC,KAAK,IAAI,KAAK,YAC5D;CAGF,OAAO,KAAK;AACd;;;;;AAMA,SAAgB,yBAA4B,OAAa;CACvD,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,IAAI,wBAAwB;CAG3C,IAAI,SAAS,OAAO,UAAU,UAAU;EACtC,MAAM,SAAkC,CAAC;EACzC,KAAK,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,KAAK,GAC5C,OAAO,OAAO,QAAQ,iBAAiB,SAAS,KAAK,OAAO,yBAAyB,IAAI;EAE3F,OAAO;CACT;CAEA,OAAO;AACT;;;;AAKA,SAAgB,mBAAmB,eAA2C;CAE5E,MAAM,SAAS,kBAAkB,yBAAyB,aAAa,CAAC;CACxE,OAAO,YAAY,wBAAwB,MAAM,CAAC,IAAI;AACxD;;;;;AAMA,SAAgB,mBAAmB,eAA2C;CAG5E,OACE,uOAHW,wBAAwB,oBAAoB,aAAa,CAQjE,IACH;AAOJ;AAkBA,SAAS,eAAe,MAAc,SAA0B;CAC9D,IAAI,GAAG,WAAW,IAAI,KAAK,GAAG,aAAa,MAAM,MAAM,MAAM,SAC3D,OAAO;CAGT,GAAG,UAAU,KAAK,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;CACpD,GAAG,cAAc,MAAM,OAAO;CAE9B,OAAO;AACT;;;;AAKA,SAAgB,SACd,eACA,UAA2B,CAAC,GACZ;CAChB,MAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;CACvC,MAAM,aAAa,KAAK,QAAQ,KAAK,QAAQ,cAAA,0BAAiC;CAC9E,MAAM,UAAU,KAAK,QAAQ,KAAK,QAAQ,WAAA,qBAA2B;CAErE,MAAM,gBAAgB,eAAe,YAAY,mBAAmB,aAAa,CAAC;CAClF,MAAM,aAAa,eAAe,SAAS,mBAAmB,aAAa,CAAC;CAE5E,OAAO;EAAE;EAAY;EAAS,SAAS,iBAAiB;CAAW;AACrE;;;;AAOA,eAAsB,QAAQ,SAAkD;CAC9E,OAAO,SAAS,MAAM,WAAW,OAAO,GAAG,OAAO;AACpD;;;;;AAeA,SAAgB,aAAa,SAAmC;CAC9D,IAAI,UAAU;CACd,IAAI;CAEJ,MAAM,OAAO,YAAY;EACvB,IAAI;GACF,MAAM,SAAS,MAAM,QAAQ,OAAO;GACpC,IAAI,OAAO,SACT,QAAQ,WAAW,MAAM;EAE7B,SAAS,KAAK;GACZ,QAAQ,UAAU,GAAG;EACvB;EAEA,IAAI,CAAC,SACH,QAAQ,WAAW,MAAM,QAAQ,YAAY,GAAI;CAErD;CAEA,KAAU;CAEV,aAAa;EACX,UAAU;EACV,aAAa,KAAK;CACpB;AACF"}
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
3
3
  const require_constants = require("../shared/constants.cjs");
4
4
  const require_codegen = require("./codegen.cjs");
5
+ const require_check = require("./check.cjs");
5
6
  let node_fs = require("node:fs");
6
7
  node_fs = require_runtime.__toESM(node_fs, 1);
7
8
  let node_path = require("node:path");
@@ -23,6 +24,8 @@ Commands:
23
24
  --seed <module> --data-dir <dir>
24
25
  --entry <file> run a custom server entry with Node.js (default: the Vite dev server)
25
26
  --port <port> the Vite dev server port
27
+ check Validate the graphql() documents of the project against the schema
28
+ --schema <file> (default ${require_constants.DEFAULT_SCHEMA_FILE})
26
29
  info Print the PocketBomb environment and the generated files status
27
30
  `;
28
31
  const defaultIo = {
@@ -155,6 +158,33 @@ async function runDev(argv, io) {
155
158
  await cleanup();
156
159
  return 0;
157
160
  }
161
+ async function runCheck(argv, io) {
162
+ const { values } = (0, node_util.parseArgs)({
163
+ args: argv,
164
+ options: {
165
+ schema: { type: "string" },
166
+ cwd: { type: "string" }
167
+ }
168
+ });
169
+ const cwd = node_path.default.resolve(values.cwd || process.cwd());
170
+ let result;
171
+ try {
172
+ result = await require_check.check({
173
+ cwd,
174
+ schemaFile: values.schema
175
+ });
176
+ } catch (err) {
177
+ io.error("dynamite check: " + err.message);
178
+ return 2;
179
+ }
180
+ for (const p of result.problems) io.error(`${node_path.default.relative(cwd, p.file)}:${p.line}:${p.column} ${p.message}`);
181
+ if (result.problems.length) {
182
+ io.error(`dynamite check: ${result.problems.length} problem(s) in ${result.documents} documents`);
183
+ return 1;
184
+ }
185
+ io.log(`dynamite check: ${result.documents} documents OK`);
186
+ return 0;
187
+ }
158
188
  async function runInfo(io) {
159
189
  const cwd = process.cwd();
160
190
  const mask = (v) => v ? v.substring(0, 6) + "…" : "(not set)";
@@ -184,6 +214,7 @@ async function run(argv, io = defaultIo) {
184
214
  switch (command) {
185
215
  case "codegen": return runCodegen(rest, io);
186
216
  case "dev": return runDev(rest, io);
217
+ case "check": return runCheck(rest, io);
187
218
  case "info": return runInfo(io);
188
219
  case "--version":
189
220
  case "-v":
@@ -202,6 +233,7 @@ async function run(argv, io = defaultIo) {
202
233
  }
203
234
  exports.DEFAULT_ENV_FILE = require_constants.DEFAULT_ENV_FILE;
204
235
  exports.DEFAULT_SCHEMA_FILE = require_constants.DEFAULT_SCHEMA_FILE;
236
+ exports.check = require_check.check;
205
237
  exports.codegen = require_codegen.codegen;
206
238
  exports.generate = require_codegen.generate;
207
239
  exports.introspect = require_codegen.introspect;