pocketbomb-dynamite 0.0.0-stage → 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 (113) hide show
  1. package/LICENSE.md +17 -0
  2. package/README.md +61 -2
  3. package/bin/dynamite.js +10 -0
  4. package/dist/_virtual/_rolldown/runtime.cjs +22 -0
  5. package/dist/cli/check.cjs +160 -0
  6. package/dist/cli/check.cjs.map +1 -0
  7. package/dist/cli/check.d.cts +21 -0
  8. package/dist/cli/check.d.ts +21 -0
  9. package/dist/cli/check.js +157 -0
  10. package/dist/cli/check.js.map +1 -0
  11. package/dist/cli/codegen.cjs +121 -0
  12. package/dist/cli/codegen.cjs.map +1 -0
  13. package/dist/cli/codegen.d.cts +68 -0
  14. package/dist/cli/codegen.d.ts +68 -0
  15. package/dist/cli/codegen.js +110 -0
  16. package/dist/cli/codegen.js.map +1 -0
  17. package/dist/cli/index.cjs +246 -0
  18. package/dist/cli/index.cjs.map +1 -0
  19. package/dist/cli/index.d.cts +13 -0
  20. package/dist/cli/index.d.ts +13 -0
  21. package/dist/cli/index.js +232 -0
  22. package/dist/cli/index.js.map +1 -0
  23. package/dist/client/index.d.cts +2 -0
  24. package/dist/client/index.d.ts +2 -0
  25. package/dist/client/storefront.cjs +96 -0
  26. package/dist/client/storefront.cjs.map +1 -0
  27. package/dist/client/storefront.d.cts +134 -0
  28. package/dist/client/storefront.d.ts +134 -0
  29. package/dist/client/storefront.js +96 -0
  30. package/dist/client/storefront.js.map +1 -0
  31. package/dist/core/buyer-ip.cjs +35 -0
  32. package/dist/core/buyer-ip.cjs.map +1 -0
  33. package/dist/core/buyer-ip.d.cts +25 -0
  34. package/dist/core/buyer-ip.d.ts +25 -0
  35. package/dist/core/buyer-ip.js +34 -0
  36. package/dist/core/buyer-ip.js.map +1 -0
  37. package/dist/core/cache.cjs +109 -0
  38. package/dist/core/cache.cjs.map +1 -0
  39. package/dist/core/cache.d.cts +91 -0
  40. package/dist/core/cache.d.ts +91 -0
  41. package/dist/core/cache.js +106 -0
  42. package/dist/core/cache.js.map +1 -0
  43. package/dist/core/index.d.cts +4 -0
  44. package/dist/core/index.d.ts +4 -0
  45. package/dist/core/request-context.cjs +48 -0
  46. package/dist/core/request-context.cjs.map +1 -0
  47. package/dist/core/request-context.d.cts +76 -0
  48. package/dist/core/request-context.d.ts +76 -0
  49. package/dist/core/request-context.js +48 -0
  50. package/dist/core/request-context.js.map +1 -0
  51. package/dist/customer-account/customer-account.cjs +162 -0
  52. package/dist/customer-account/customer-account.cjs.map +1 -0
  53. package/dist/customer-account/customer-account.d.cts +147 -0
  54. package/dist/customer-account/customer-account.d.ts +147 -0
  55. package/dist/customer-account/customer-account.js +157 -0
  56. package/dist/customer-account/customer-account.js.map +1 -0
  57. package/dist/customer-account/index.cjs +8 -0
  58. package/dist/customer-account/index.d.cts +2 -0
  59. package/dist/customer-account/index.d.ts +2 -0
  60. package/dist/customer-account/index.js +1 -0
  61. package/dist/graphql/graphql.cjs +160 -0
  62. package/dist/graphql/graphql.cjs.map +1 -0
  63. package/dist/graphql/graphql.d.cts +164 -0
  64. package/dist/graphql/graphql.d.ts +164 -0
  65. package/dist/graphql/graphql.js +144 -0
  66. package/dist/graphql/graphql.js.map +1 -0
  67. package/dist/graphql/index.cjs +20 -0
  68. package/dist/graphql/index.d.cts +2 -0
  69. package/dist/graphql/index.d.ts +2 -0
  70. package/dist/graphql/index.js +1 -0
  71. package/dist/index.cjs +32 -0
  72. package/dist/index.d.cts +9 -0
  73. package/dist/index.d.ts +9 -0
  74. package/dist/index.js +6 -0
  75. package/dist/react/components.cjs +87 -0
  76. package/dist/react/components.cjs.map +1 -0
  77. package/dist/react/components.d.cts +68 -0
  78. package/dist/react/components.d.ts +68 -0
  79. package/dist/react/components.js +82 -0
  80. package/dist/react/components.js.map +1 -0
  81. package/dist/react/context.cjs +71 -0
  82. package/dist/react/context.cjs.map +1 -0
  83. package/dist/react/context.d.cts +58 -0
  84. package/dist/react/context.d.ts +58 -0
  85. package/dist/react/context.js +69 -0
  86. package/dist/react/context.js.map +1 -0
  87. package/dist/react/hooks.cjs +168 -0
  88. package/dist/react/hooks.cjs.map +1 -0
  89. package/dist/react/hooks.d.cts +55 -0
  90. package/dist/react/hooks.d.ts +55 -0
  91. package/dist/react/hooks.js +167 -0
  92. package/dist/react/hooks.js.map +1 -0
  93. package/dist/react/index.cjs +15 -0
  94. package/dist/react/index.d.cts +4 -0
  95. package/dist/react/index.d.ts +4 -0
  96. package/dist/react/index.js +4 -0
  97. package/dist/shared/constants.cjs +9 -0
  98. package/dist/shared/constants.cjs.map +1 -0
  99. package/dist/shared/constants.d.cts +5 -0
  100. package/dist/shared/constants.d.ts +5 -0
  101. package/dist/shared/constants.js +8 -0
  102. package/dist/shared/constants.js.map +1 -0
  103. package/dist/ts-plugin/index.cjs +58 -0
  104. package/dist/ts-plugin/index.cjs.map +1 -0
  105. package/dist/ts-plugin/index.d.cts +4 -0
  106. package/dist/vite/index.cjs +149 -0
  107. package/dist/vite/index.cjs.map +1 -0
  108. package/dist/vite/index.d.cts +35 -0
  109. package/dist/vite/index.d.ts +35 -0
  110. package/dist/vite/index.js +144 -0
  111. package/dist/vite/index.js.map +1 -0
  112. package/package.json +127 -3
  113. package/ts-plugin/package.json +5 -0
package/LICENSE.md ADDED
@@ -0,0 +1,17 @@
1
+ The MIT License (MIT)
2
+ Copyright (c) 2026 - present, the PocketBomb contributors
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software
5
+ and associated documentation files (the "Software"), to deal in the Software without restriction,
6
+ including without limitation the rights to use, copy, modify, merge, publish, distribute,
7
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
8
+ is furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all copies or
11
+ substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
14
+ BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,62 @@
1
- # Temporary Holding Version
1
+ # pocketbomb-dynamite
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The PocketBomb storefront framework (similar to Hydrogen).
4
+
5
+ ```ts
6
+ import { createRequestContext, createStorefrontClient, gql, MemoryCache } from "pocketbomb-dynamite";
7
+ import { createCustomerAccountClient } from "pocketbomb-dynamite/customer-account";
8
+
9
+ const cache = new MemoryCache();
10
+
11
+ export async function handle(request: Request) {
12
+ const context = createRequestContext({ request, cache }); // buyer IP, cookie session, cache
13
+ const storefront = createStorefrontClient({ url, privateKey, requestContext: context });
14
+ const customer = createCustomerAccountClient({
15
+ url, collection: "users", clientId, redirectUri, requestContext: context,
16
+ });
17
+
18
+ const path = new URL(request.url).pathname;
19
+ if (path === "/account/login") return customer.loginRedirect({ returnTo: "/account" });
20
+ if (path === "/account/callback") return customer.callbackRedirect();
21
+ if (path === "/account/logout") return customer.logoutRedirect();
22
+
23
+ const { products } = await storefront.query(PRODUCTS, { variables: { first: 10 } });
24
+ const response = new Response(render(products, customer.isLoggedIn()));
25
+ context.applyResponseHeaders(response.headers); // Set-Cookie + private caching
26
+ return response;
27
+ }
28
+
29
+ const PRODUCTS = gql<{ products: { nodes: Array<{ id: string; title: string }> } }, { first: number }>(
30
+ `query Products($first: Int) { products(first: $first) { nodes { id title } } }`,
31
+ );
32
+ ```
33
+
34
+ Entries: `pocketbomb-dynamite` (core + storefront client + GraphQL), `pocketbomb-dynamite/customer-account`,
35
+ `pocketbomb-dynamite/graphql` (gql.tada `graphql()` typed by `dynamite codegen`), `pocketbomb-dynamite/react`
36
+ (DynamiteProvider, useStorefront, useCustomer, useStorefrontQuery, Pagination, FileImage), `pocketbomb-dynamite/vite`
37
+ (env + private key guard, dev proxy, codegen, fuse) and `pocketbomb-dynamite/ts-plugin` (graphqlsp for the editor).
38
+
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,10 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "../dist/cli/index.js";
3
+
4
+ run(process.argv.slice(2)).then(
5
+ (code) => process.exit(code),
6
+ (err) => {
7
+ console.error(err);
8
+ process.exit(1);
9
+ },
10
+ );
@@ -0,0 +1,22 @@
1
+ // \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ exports.__toESM = __toESM;
@@ -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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"}
@@ -0,0 +1,121 @@
1
+ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
2
+ const require_constants = 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 _gql_tada_internal = require("@gql.tada/internal");
8
+ let graphql = require("graphql");
9
+ // src/cli/codegen.ts
10
+ /**
11
+ * Introspects the PocketBomb GraphQL API (`POST /api/graphql`; the introspection
12
+ * fields are not counted in the query limits).
13
+ *
14
+ * @throws {Error} for the HTTP and GraphQL errors.
15
+ */
16
+ async function introspect(options) {
17
+ const headers = { "content-type": "application/json" };
18
+ if (options.publicKey) headers["X-PB-Public-Key"] = options.publicKey;
19
+ const endpoint = options.url.replace(/\/+$/, "") + "/api/graphql";
20
+ const response = await (options.fetch || fetch)(endpoint, {
21
+ method: "POST",
22
+ headers,
23
+ body: JSON.stringify({ query: (0, graphql.getIntrospectionQuery)({ descriptions: true }) })
24
+ });
25
+ let body = null;
26
+ try {
27
+ body = await response.json();
28
+ } catch {
29
+ body = null;
30
+ }
31
+ if (!response.ok) throw new Error(`The GraphQL introspection failed (${response.status}): ${body?.message || JSON.stringify(body)}`);
32
+ if (body?.errors?.length || !body?.data?.__schema) throw new Error("The GraphQL introspection failed: " + (body?.errors?.map((e) => e.message).join("; ") || "no schema"));
33
+ return body.data;
34
+ }
35
+ /**
36
+ * Returns a copy of the introspection without the empty descriptions
37
+ * (the server reports "" for the undocumented fields).
38
+ */
39
+ function withoutEmptyDescriptions(value) {
40
+ if (Array.isArray(value)) return value.map(withoutEmptyDescriptions);
41
+ if (value && typeof value === "object") {
42
+ const result = {};
43
+ for (const [key, item] of Object.entries(value)) result[key] = key === "description" && item === "" ? null : withoutEmptyDescriptions(item);
44
+ return result;
45
+ }
46
+ return value;
47
+ }
48
+ /**
49
+ * Returns the GraphQL SDL of an introspection.
50
+ */
51
+ function introspectionToSDL(introspection) {
52
+ const schema = (0, graphql.buildClientSchema)(withoutEmptyDescriptions(introspection));
53
+ return (0, graphql.printSchema)((0, graphql.lexicographicSortSchema)(schema)) + "\n";
54
+ }
55
+ /**
56
+ * Returns the `pocketbomb-env.d.ts` of an introspection: the gql.tada introspection type
57
+ * and the `pocketbomb-dynamite/graphql` schema augmentation used by `graphql()`.
58
+ */
59
+ function introspectionToEnv(introspection) {
60
+ return "/* eslint-disable */\n/* prettier-ignore */\n// Generated by `dynamite codegen` from the PocketBomb GraphQL API. Do not edit.\n\n/** The gql.tada introspection of the PocketBomb GraphQL schema. */\nexport type introspection = " + (0, _gql_tada_internal.preprocessIntrospection)((0, _gql_tada_internal.minifyIntrospection)(introspection)) + ";\n\ndeclare module \"pocketbomb-dynamite/graphql\" {\n interface PocketBombSchema {\n introspection: introspection;\n }\n}\n";
61
+ }
62
+ function writeIfChanged(file, content) {
63
+ if (node_fs.default.existsSync(file) && node_fs.default.readFileSync(file, "utf8") === content) return false;
64
+ node_fs.default.mkdirSync(node_path.default.dirname(file), { recursive: true });
65
+ node_fs.default.writeFileSync(file, content);
66
+ return true;
67
+ }
68
+ /**
69
+ * Writes the schema SDL and the gql.tada env of an introspection (only if they changed).
70
+ */
71
+ function generate(introspection, options = {}) {
72
+ const cwd = options.cwd || process.cwd();
73
+ const schemaFile = node_path.default.resolve(cwd, options.schemaFile || ".dynamite/schema.graphql");
74
+ const envFile = node_path.default.resolve(cwd, options.envFile || "pocketbomb-env.d.ts");
75
+ const schemaChanged = writeIfChanged(schemaFile, introspectionToSDL(introspection));
76
+ const envChanged = writeIfChanged(envFile, introspectionToEnv(introspection));
77
+ return {
78
+ schemaFile,
79
+ envFile,
80
+ changed: schemaChanged || envChanged
81
+ };
82
+ }
83
+ /**
84
+ * Introspects the API and writes the schema and the env (see {@link generate}).
85
+ */
86
+ async function codegen(options) {
87
+ return generate(await introspect(options), options);
88
+ }
89
+ /**
90
+ * Regenerates the schema and the env when the API schema changes (polling).
91
+ * Returns a function that stops the watcher.
92
+ */
93
+ function watchCodegen(options) {
94
+ let stopped = false;
95
+ let timer;
96
+ const tick = async () => {
97
+ try {
98
+ const result = await codegen(options);
99
+ if (result.changed) options.onChange?.(result);
100
+ } catch (err) {
101
+ options.onError?.(err);
102
+ }
103
+ if (!stopped) timer = setTimeout(tick, options.interval ?? 2e3);
104
+ };
105
+ tick();
106
+ return () => {
107
+ stopped = true;
108
+ clearTimeout(timer);
109
+ };
110
+ }
111
+ exports.DEFAULT_ENV_FILE = require_constants.DEFAULT_ENV_FILE;
112
+ exports.DEFAULT_SCHEMA_FILE = require_constants.DEFAULT_SCHEMA_FILE;
113
+ exports.codegen = codegen;
114
+ exports.generate = generate;
115
+ exports.introspect = introspect;
116
+ exports.introspectionToEnv = introspectionToEnv;
117
+ exports.introspectionToSDL = introspectionToSDL;
118
+ exports.watchCodegen = watchCodegen;
119
+ exports.withoutEmptyDescriptions = withoutEmptyDescriptions;
120
+
121
+ //# sourceMappingURL=codegen.cjs.map