@eddeee888/gcg-typescript-resolver-files 0.18.4 → 0.19.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.
@@ -1,4 +1,4 @@
1
- import { type PropertyAssignment, type SourceFile } from 'ts-morph';
1
+ import { type PropertyAssignment, type SourceFile, type VariableStatement } from 'ts-morph';
2
2
  import type { ObjectTypeFile } from './types.js';
3
3
  export type AddedPropertyAssignmentNodes = Record<string, // SourceFile's filename
4
4
  Record<number, // Line number
@@ -10,9 +10,10 @@ Record<number, // Line number
10
10
  /**
11
11
  * Ensure objectTypeResolver files have all the resolvers due to mismatched types
12
12
  */
13
- export declare const addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented: ({ addedPropertyAssignmentNodes, sourceFile, resolverFile, mode, }: {
13
+ export declare const addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented: ({ addedPropertyAssignmentNodes, sourceFile, variableStatement, resolverFile, mode, }: {
14
14
  addedPropertyAssignmentNodes: AddedPropertyAssignmentNodes;
15
15
  sourceFile: SourceFile;
16
+ variableStatement: VariableStatement | undefined;
16
17
  resolverFile: ObjectTypeFile;
17
18
  mode: "smart" | "fast";
18
19
  }) => void;
@@ -1,9 +1,8 @@
1
- import { SyntaxKind } from 'ts-morph';
2
- import { getVariableStatementWithExpectedIdentifier } from './getVariableStatementWithExpectedIdentifier.js';
1
+ import { SyntaxKind, } from 'ts-morph';
3
2
  /**
4
3
  * Ensure objectTypeResolver files have all the resolvers due to mismatched types
5
4
  */
6
- export const addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented = ({ addedPropertyAssignmentNodes, sourceFile, resolverFile, mode, }) => {
5
+ export const addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented = ({ addedPropertyAssignmentNodes, sourceFile, variableStatement, resolverFile, mode, }) => {
7
6
  const sourceFilePath = sourceFile.getFilePath().toString();
8
7
  addedPropertyAssignmentNodes[sourceFilePath] =
9
8
  addedPropertyAssignmentNodes[sourceFilePath] || {};
@@ -11,71 +10,41 @@ export const addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented = ({
11
10
  if (!Object.keys(resolversToGenerate).length) {
12
11
  return;
13
12
  }
14
- const { variableStatement } = getVariableStatementWithExpectedIdentifier(sourceFile, resolverFile);
15
13
  if (!variableStatement) {
16
14
  throw new Error('Missing variableStatement in addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented.');
17
15
  }
18
16
  // 1. Check to see which generated to-be-generated resolvers are implemented
19
17
  const resolversData = { ...resolversToGenerate };
20
18
  /**
21
- * PropertyAssignment
19
+ * Mark a resolver as implemented if the object literal already has a member
20
+ * with that name, in any of these forms (single traversal over the statement):
22
21
  * ```
23
22
  * const name = () => {};
24
23
  * const OutputType = {
25
- * id: () => {},
26
- * name: name,
24
+ * id: () => {}, // PropertyAssignment
25
+ * greet(){}, // MethodDeclaration
26
+ * name, // ShorthandPropertyAssignment
27
27
  * }
28
28
  * ```
29
29
  */
30
- variableStatement
31
- .getDescendantsOfKind(SyntaxKind.PropertyAssignment)
32
- .forEach((propertyAssignment) => {
33
- const resolverName = propertyAssignment.getName();
34
- if (resolversData[resolverName]) {
35
- resolversData[resolverName].implemented = true;
30
+ variableStatement.forEachDescendant((node) => {
31
+ if (node.isKind(SyntaxKind.PropertyAssignment) ||
32
+ node.isKind(SyntaxKind.MethodDeclaration) ||
33
+ node.isKind(SyntaxKind.ShorthandPropertyAssignment)) {
34
+ const resolverName = node.getName();
35
+ if (resolversData[resolverName]) {
36
+ resolversData[resolverName].implemented = true;
37
+ }
36
38
  }
37
39
  });
38
- /**
39
- * MethodDeclaration
40
- * ```
41
- * const OutputType = {
42
- * id(){},
43
- * }
44
- * ```
45
- */
46
- variableStatement
47
- .getDescendantsOfKind(SyntaxKind.MethodDeclaration)
48
- .forEach((methodDeclaration) => {
49
- const resolverName = methodDeclaration.getName();
50
- if (resolversData[resolverName]) {
51
- resolversData[resolverName].implemented = true;
52
- }
53
- });
54
- /**
55
- * ShorthandPropertyAssignment example:
56
- * ```
57
- * const id = () => {};
58
- * const OutputType = {
59
- * id,
60
- * }
61
- * ```
62
- */
63
- variableStatement
64
- .getDescendantsOfKind(SyntaxKind.ShorthandPropertyAssignment)
65
- .forEach((propertyAssignment) => {
66
- const resolverName = propertyAssignment.getName();
67
- if (resolversData[resolverName]) {
68
- resolversData[resolverName].implemented = true;
69
- }
70
- });
71
- // 2. Add missing resolver properties if they haven't been implemented
40
+ // 2. Add missing resolver properties if they haven't been implemented.
41
+ // Resolve the object literal once, not once per added property.
42
+ const objectLiteralExpression = variableStatement.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)[0];
72
43
  Object.values(resolversData).forEach(({ resolverName, resolverDeclaration, implemented }) => {
73
44
  if (implemented) {
74
45
  return;
75
46
  }
76
- const addedNode = variableStatement
77
- .getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)[0]
78
- .addPropertyAssignment({
47
+ const addedNode = objectLiteralExpression.addPropertyAssignment({
79
48
  name: resolverName,
80
49
  initializer: resolverDeclaration,
81
50
  });
@@ -1,3 +1,4 @@
1
+ import { SyntaxKind } from 'ts-morph';
1
2
  import * as path from 'path';
2
3
  import { cwd } from '../utils/index.js';
3
4
  import { getVariableStatementWithExpectedIdentifier } from './getVariableStatementWithExpectedIdentifier.js';
@@ -43,7 +44,7 @@ export const postProcessFiles = ({ config: { tsMorph: { project }, fixObjectType
43
44
  const addedPropertyAssignmentNodes = {};
44
45
  sourceFilesToProcess.forEach(({ sourceFile, resolverFile }) => {
45
46
  const normalizedRelativePath = path.posix.relative(cwd(), sourceFile.getFilePath());
46
- const { addedVariableStatement } = ensureExportedResolver(sourceFile, resolverFile);
47
+ const { addedVariableStatement, variableStatement } = ensureExportedResolver(sourceFile, resolverFile);
47
48
  if (resolverFile.__filetype !== 'scalarResolver' ||
48
49
  // For scalarResolver, only add `import { GraphQLScalarType } ...` if variable statement was added.
49
50
  // This is because user could have used custom scalar. If so, we don't want to add unnecessary import
@@ -56,6 +57,7 @@ export const postProcessFiles = ({ config: { tsMorph: { project }, fixObjectType
56
57
  addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented({
57
58
  addedPropertyAssignmentNodes,
58
59
  sourceFile,
60
+ variableStatement,
59
61
  resolverFile,
60
62
  mode: fixObjectTypeResolvers.object,
61
63
  });
@@ -169,9 +171,13 @@ const ensureExportedResolver = (sourceFile, resolverFile) => {
169
171
  }
170
172
  if (!variableStatement) {
171
173
  // Did not find variable statement with expected identifier, add it to the end with a warning
172
- sourceFile.addStatements(resolverFile.meta.variableStatement);
174
+ const addedStatements = sourceFile.addStatements(resolverFile.meta.variableStatement);
173
175
  resolverFile.filesystem.contentUpdated = true;
174
- return { addedVariableStatement: true };
176
+ const addedVariableStatementNode = addedStatements.find((statement) => statement.isKind(SyntaxKind.VariableStatement));
177
+ return {
178
+ addedVariableStatement: true,
179
+ variableStatement: addedVariableStatementNode,
180
+ };
175
181
  }
176
182
  else if (variableStatement && !isExported) {
177
183
  // If has identifier but not exported
@@ -184,9 +190,9 @@ const ensureExportedResolver = (sourceFile, resolverFile) => {
184
190
  resolverFile.filesystem.contentUpdated = true;
185
191
  }
186
192
  // else, if identifier's been exported do nothing
187
- return { addedVariableStatement: false };
193
+ return { addedVariableStatement: false, variableStatement };
188
194
  }
189
- return { addedVariableStatement: false };
195
+ return { addedVariableStatement: false, variableStatement };
190
196
  };
191
197
  /**
192
198
  * Ensure correctly imported resolves type generated by typescript-resolvers plugin
@@ -0,0 +1,15 @@
1
+ import type { Project, SourceFile } from 'ts-morph';
2
+ import type { TypeMappersMap } from '../parseTypeMappers/index.js';
3
+ import type { GraphQLObjectTypeResolversToGenerate } from './getGraphQLObjectTypeResolversToGenerate.js';
4
+ type Cache = {
5
+ get(key: string): GraphQLObjectTypeResolversToGenerate | undefined;
6
+ set(key: string, value: GraphQLObjectTypeResolversToGenerate): GraphQLObjectTypeResolversToGenerate;
7
+ createCacheKey({ mode, typesSourceFile, typeMappersMap, tsMorphProject, }: {
8
+ mode: string;
9
+ typesSourceFile: SourceFile;
10
+ typeMappersMap: TypeMappersMap;
11
+ tsMorphProject: Project;
12
+ }): string;
13
+ };
14
+ export declare const createCache: () => Cache;
15
+ export {};
@@ -0,0 +1,83 @@
1
+ import { createHash } from 'crypto';
2
+ /**
3
+ * Maximum number of entries kept in the cache. Each entry corresponds to one
4
+ * distinct `(mode + types file + mappers)` state. A long watch session can visit
5
+ * many such states, and separate `generates` targets each contribute their own
6
+ * key, so we keep the N most-recently-used entries and evict the rest to bound
7
+ * memory. N is comfortably larger than the number of concurrent targets a single
8
+ * codegen run has.
9
+ */
10
+ const MAX_CACHE_ENTRIES = 50;
11
+ export const createCache = () => {
12
+ /**
13
+ * Cache of previous runs' results, so the ts-morph type-checker work is skipped
14
+ * when nothing it depends on has changed (e.g. a watch re-run where only a
15
+ * resolver implementation file was edited). The result is a pure function of
16
+ * - `mode`
17
+ * - the generated types file
18
+ * - the mapper file contents
19
+ *
20
+ * so the key is a hash of exactly those. Reused only on an exact key match, so
21
+ * it can never go stale. A `Map` is used because its insertion order gives a
22
+ * cheap LRU: the first key is the least-recently-used.
23
+ */
24
+ const resultCache = new Map();
25
+ return {
26
+ get(key) {
27
+ const result = resultCache.get(key);
28
+ if (result === undefined) {
29
+ return undefined;
30
+ }
31
+ // Mark as most-recently-used by re-inserting at the end.
32
+ resultCache.delete(key);
33
+ resultCache.set(key, result);
34
+ return structuredClone(result);
35
+ },
36
+ /**
37
+ * set
38
+ * Create a structured clone in the cache
39
+ * because downstream can update the value object
40
+ */
41
+ set(key, value) {
42
+ // Re-insert so this key becomes the most-recently-used entry.
43
+ resultCache.delete(key);
44
+ resultCache.set(key, structuredClone(value));
45
+ // Evict least-recently-used entries beyond the cap.
46
+ while (resultCache.size > MAX_CACHE_ENTRIES) {
47
+ const lruKey = resultCache.keys().next().value;
48
+ if (lruKey === undefined) {
49
+ break;
50
+ }
51
+ resultCache.delete(lruKey);
52
+ }
53
+ return value;
54
+ },
55
+ /**
56
+ * Hash of everything the result depends on: the mode, the generated types file
57
+ * text (captures all schema-derived inputs), and each mapper file's contents.
58
+ * Same key => same result.
59
+ */
60
+ createCacheKey({ mode, typesSourceFile, typeMappersMap, tsMorphProject }) {
61
+ const hash = createHash('sha1');
62
+ hash.update(mode);
63
+ hash.update(' ');
64
+ hash.update(typesSourceFile.getFullText());
65
+ // The parsed mapper map (which schema type -> which mapper declaration) depends
66
+ // on config such as mappersSuffix, so hash it too: mapper file text alone would
67
+ // not distinguish two configs that read the same files differently.
68
+ hash.update(' ');
69
+ hash.update(JSON.stringify(Object.entries(typeMappersMap).sort(([a], [b]) => (a < b ? -1 : 1))));
70
+ const mapperFilenames = [
71
+ ...new Set(Object.values(typeMappersMap).map((m) => m.mapper.filename)),
72
+ ].sort();
73
+ for (const filename of mapperFilenames) {
74
+ const sourceFile = tsMorphProject.getSourceFile(filename);
75
+ hash.update(' ');
76
+ hash.update(filename);
77
+ hash.update(' ');
78
+ hash.update(sourceFile ? sourceFile.getFullText() : '');
79
+ }
80
+ return hash.digest('hex');
81
+ },
82
+ };
83
+ };
@@ -1,10 +1,22 @@
1
1
  import { SyntaxKind, Node, } from 'ts-morph';
2
2
  import { getNodePropertyMap, } from './getNodePropertyMap.js';
3
+ import { createCache } from './cache.js';
4
+ const cache = createCache();
3
5
  export const getGraphQLObjectTypeResolversToGenerate = ({ mode, tsMorphProject, typesSourceFile, typeMappersMap, userDefinedSchemaObjectTypeMap, generatedTypesFileMeta, }) => {
4
6
  const typeMappersEntries = Object.entries(typeMappersMap);
5
7
  if (typeMappersEntries.length === 0) {
6
8
  return {};
7
9
  }
10
+ const cacheKey = cache.createCacheKey({
11
+ mode,
12
+ typesSourceFile,
13
+ typeMappersMap,
14
+ tsMorphProject,
15
+ });
16
+ const cachedResult = cache.get(cacheKey);
17
+ if (cachedResult) {
18
+ return cachedResult;
19
+ }
8
20
  /**
9
21
  * `generatedTypesFileMeta.generatedResolverTypes.userDefined` is `schemaType` -> `generatedResolverTypes`
10
22
  * We will be parsing `types.generated.ts` file for the `generatedResolverTypes`
@@ -27,6 +39,7 @@ export const getGraphQLObjectTypeResolversToGenerate = ({ mode, tsMorphProject,
27
39
  res[generatedSchemaTypeName.name] = schemaType;
28
40
  return res;
29
41
  }, {});
42
+ // "Fast" mode
30
43
  if (mode === 'fast') {
31
44
  // 1. Get property map of all schema types
32
45
  const resolverTypesMap = {};
@@ -105,8 +118,10 @@ export const getGraphQLObjectTypeResolversToGenerate = ({ mode, tsMorphProject,
105
118
  };
106
119
  });
107
120
  });
108
- return result;
121
+ const newResult = cache.set(cacheKey, result);
122
+ return newResult;
109
123
  }
124
+ // "Smart" mode
110
125
  // 1. Get property map of all schema types
111
126
  const schemaResolversTypePropertyMap = {};
112
127
  const populateSchemaTypeResolversPropertyMap = (node) => {
@@ -181,7 +196,8 @@ export const getGraphQLObjectTypeResolversToGenerate = ({ mode, tsMorphProject,
181
196
  return;
182
197
  });
183
198
  });
184
- return result;
199
+ const newResult = cache.set(cacheKey, result);
200
+ return newResult;
185
201
  };
186
202
  const mustGetMapperOriginalDeclarationNode = ({ tsMorphProject, mapper, }) => {
187
203
  const typeMapperFile = tsMorphProject.getSourceFile(mapper.filename);
package/dist/preset.js CHANGED
@@ -151,6 +151,10 @@ export const preset = {
151
151
  plugins: [{ add: { content: meta.content } }],
152
152
  config: {},
153
153
  schema,
154
+ // Pass schemaAst so codegen-core reuses the prebuilt schema instead of
155
+ // rebuilding one per output (O(types) each -> O(types^2) overall). This
156
+ // is an `add`-only output, so the schema doesn't affect its content.
157
+ schemaAst,
154
158
  documents: [],
155
159
  };
156
160
  generatesSection.push(typeDefsFile);
@@ -214,7 +218,17 @@ export const preset = {
214
218
  plugins: [{ add: { content } }],
215
219
  config: {},
216
220
  schema,
221
+ // Pass schemaAst so codegen-core reuses the prebuilt schema instead of
222
+ // rebuilding one per output (O(types) each -> O(types^2) overall). This
223
+ // is an `add`-only output, so the schema doesn't affect its content.
224
+ schemaAst,
217
225
  documents: [],
226
+ // Forces Codegen to compare generated content vs on disk, instead of in-memory cache
227
+ // This avoids situation where resolver files are content on disk are updated,
228
+ // and goes out of sync with the in-memory cache version, and can lead to generated
229
+ // content not getting written to disk
230
+ // https://github.com/dotansimha/graphql-code-generator/pull/10928
231
+ contentComparison: 'disk',
218
232
  };
219
233
  });
220
234
  logger.debug(`Applying changes to ${resolverFilesGenerateOptions.length}/${resultFilesArray.length} files. (${resultFilesArray.length - resolverFilesGenerateOptions.length} skipped files because they are already on filesystem and are not updated)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eddeee888/gcg-typescript-resolver-files",
3
- "version": "0.18.4",
3
+ "version": "0.19.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/eddeee888/graphql-code-generator-plugins.git",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@graphql-codegen/add": "^7.1.0",
44
- "@graphql-codegen/plugin-helpers": "^7.2.1",
44
+ "@graphql-codegen/plugin-helpers": "^7.3.0",
45
45
  "@graphql-codegen/schema-ast": "^6.1.0",
46
46
  "@graphql-codegen/typescript": "^6.1.0",
47
47
  "@graphql-codegen/typescript-resolvers": "^6.1.0",