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.
- package/README.md +24 -1
- package/dist/cli/check.cjs +160 -0
- package/dist/cli/check.cjs.map +1 -0
- package/dist/cli/check.d.cts +21 -0
- package/dist/cli/check.d.ts +21 -0
- package/dist/cli/check.js +157 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/codegen.cjs +2 -1
- package/dist/cli/codegen.cjs.map +1 -1
- package/dist/cli/codegen.js +3 -2
- package/dist/cli/codegen.js.map +1 -1
- package/dist/cli/index.cjs +32 -0
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts +2 -1
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +32 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/customer-account/customer-account.cjs +55 -0
- package/dist/customer-account/customer-account.cjs.map +1 -1
- package/dist/customer-account/customer-account.d.cts +33 -0
- package/dist/customer-account/customer-account.d.ts +33 -0
- package/dist/customer-account/customer-account.js +52 -1
- package/dist/customer-account/customer-account.js.map +1 -1
- package/dist/customer-account/index.cjs +4 -0
- package/dist/customer-account/index.d.cts +2 -2
- package/dist/customer-account/index.d.ts +2 -2
- package/dist/customer-account/index.js +1 -1
- package/dist/react/hooks.cjs +37 -5
- package/dist/react/hooks.cjs.map +1 -1
- package/dist/react/hooks.d.cts +12 -2
- package/dist/react/hooks.d.ts +12 -2
- package/dist/react/hooks.js +37 -5
- package/dist/react/hooks.js.map +1 -1
- package/dist/react/index.d.cts +2 -2
- package/dist/react/index.d.ts +2 -2
- package/dist/ts-plugin/index.d.cts +2 -1
- package/dist/vite/index.cjs +6 -2
- package/dist/vite/index.cjs.map +1 -1
- package/dist/vite/index.js +6 -2
- package/dist/vite/index.js.map +1 -1
- 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]`,
|
|
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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"}
|
package/dist/cli/codegen.cjs
CHANGED
|
@@ -49,7 +49,8 @@ function withoutEmptyDescriptions(value) {
|
|
|
49
49
|
* Returns the GraphQL SDL of an introspection.
|
|
50
50
|
*/
|
|
51
51
|
function introspectionToSDL(introspection) {
|
|
52
|
-
|
|
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
|
package/dist/cli/codegen.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codegen.cjs","names":["getIntrospectionQuery","printSchema","
|
|
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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"}
|
package/dist/cli/codegen.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/cli/codegen.js.map
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/cli/index.cjs
CHANGED
|
@@ -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;
|