graphql-jerky 1.0.9 → 1.0.11

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 CHANGED
@@ -31,6 +31,9 @@ By default, the working directory is searched recursively for `*.graphql` files.
31
31
  All of which are combined into a single schema and rendered to `schema_gen.ts`
32
32
  in the same directory.
33
33
 
34
+ Jerky reads the schema files and the resolver files in the order of their
35
+ paths. Thus, the output is the same on all platforms and file systems.
36
+
34
37
  A path to a directory or file can be passed as the first parameter to limit to
35
38
  scope of the search.
36
39
 
package/build/lib.cjs CHANGED
@@ -277,6 +277,30 @@ class RequiredPackages {
277
277
  }
278
278
  }
279
279
 
280
+ /**
281
+ * Glob options that make Windows paths work as patterns. On Windows, a
282
+ * backslash is a path separator and not an escape character.
283
+ */
284
+ const globOptions = {
285
+ windowsPathsNoEscape: path.sep === "\\",
286
+ };
287
+ /**
288
+ * Converts native path separators to forward slashes.
289
+ *
290
+ * Glob patterns and module specifiers must use forward slashes, even on
291
+ * Windows.
292
+ */
293
+ function toPosixPath(p) {
294
+ return path.sep === "\\" ? p.replaceAll("\\", "/") : p;
295
+ }
296
+ /**
297
+ * Compares paths by code unit. The result does not change with the locale
298
+ * or with the order in which the file system returns entries.
299
+ */
300
+ function comparePaths(a, b) {
301
+ return a < b ? -1 : a > b ? 1 : 0;
302
+ }
303
+
280
304
  function renderSymbolReference(pkgs, ref, alias) {
281
305
  const ns = ref.module
282
306
  ? jen.jen.id(pkgs.addRequires(ref.module, ref.symbol, alias ?? ref.alias))
@@ -285,10 +309,10 @@ function renderSymbolReference(pkgs, ref, alias) {
285
309
  }
286
310
 
287
311
  function compareEntryKey([a], [b]) {
288
- return a.localeCompare(b);
312
+ return a.localeCompare(b, "en");
289
313
  }
290
314
  function compareName({ name: a }, { name: b }) {
291
- return a.localeCompare(b);
315
+ return a.localeCompare(b, "en");
292
316
  }
293
317
  const PRIMITIVE_TYPES = new Set(["ID", "Int", "Float", "String", "Boolean"]);
294
318
  function isPrimitiveType(type) {
@@ -307,8 +331,9 @@ function renderImports(base, imports, aliases, extMode = "omit") {
307
331
  if (mod.startsWith("file:///")) {
308
332
  mod = node_url.fileURLToPath(mod);
309
333
  }
310
- if (mod.startsWith("/")) {
311
- mod = path.relative(base, mod);
334
+ if (path.isAbsolute(mod)) {
335
+ // NOTE: module specifiers always use forward slashes, even on Windows.
336
+ mod = toPosixPath(path.relative(base, mod));
312
337
  if (!mod.startsWith(".")) {
313
338
  mod = `./${mod}`;
314
339
  }
@@ -324,8 +349,8 @@ function renderImports(base, imports, aliases, extMode = "omit") {
324
349
  }
325
350
  return jen.import
326
351
  .obj(...Array.from(symbols)
327
- .sort((a, b) => a.localeCompare(b) ||
328
- importAlias(a).localeCompare(importAlias(b)))
352
+ .sort((a, b) => a.localeCompare(b, "en") ||
353
+ importAlias(a).localeCompare(importAlias(b), "en"))
329
354
  .map((symbol) => symbol in aliases
330
355
  ? jen.id(aliases[symbol]).as.id(symbol)
331
356
  : jen.id(symbol)))
@@ -1036,7 +1061,10 @@ class GeneratorContext {
1036
1061
  ? [
1037
1062
  jen.prop("subscription", this._renderGraphQLTypeIdentifier(subscriptionType)),
1038
1063
  ]
1039
- : []), jen.prop("types", jen.array(...types.map((type) => this._renderGraphQLTypeIdentifier(type)))), jen.prop("directives", jen.array(...Array.from(schema.getDirectives())
1064
+ : []), jen.prop("types", jen.array(...types.map((type) => this._renderGraphQLTypeIdentifier(type)))),
1065
+ // graphql-js adds the built-in directives only when the
1066
+ // directives list is not given, so include them explicitly.
1067
+ jen.prop("directives", jen.array(jen.spread(this._gql("specifiedDirectives")), ...Array.from(schema.getDirectives())
1040
1068
  .filter((d) => !isBuiltInDirective(d.name))
1041
1069
  .map((d) => this._renderGraphQLTypeIdentifier(d)))))),
1042
1070
  jen.export.default.id("schema"),
@@ -1092,9 +1120,9 @@ class GeneratorContext {
1092
1120
  ];
1093
1121
  }
1094
1122
  renderFile(outfile, renderStatements) {
1095
- const base = outfile.startsWith("/") ? outfile : path.resolve(outfile);
1123
+ const base = path.resolve(outfile);
1096
1124
  const statements = renderStatements.call(this);
1097
- const [localImports, externalImports] = partition(Object.entries(this._pkgs.requires), ([pkg]) => pkg.startsWith("/"));
1125
+ const [localImports, externalImports] = partition(Object.entries(this._pkgs.requires), ([pkg]) => path.isAbsolute(pkg));
1098
1126
  return format(jen
1099
1127
  .statements(renderImports(path.dirname(base), externalImports, this._pkgs.aliases, this._extMode), renderImports(path.dirname(base), localImports, this._pkgs.aliases, this._extMode), ...statements)
1100
1128
  .toString());
@@ -1275,23 +1303,25 @@ async function parseResolverFile(path) {
1275
1303
  async function parseTypeResolvers(typeName, glob$1, ignore) {
1276
1304
  const shouldIgnore = createMatcher(ignore);
1277
1305
  const result = {};
1278
- for await (const entry of glob.globIterate(glob$1, { withFileTypes: true })) {
1279
- const fullpath = entry.fullpath();
1280
- if (!shouldIgnore(fullpath) &&
1281
- entry.isFile() &&
1282
- entry.name.endsWith(".ts")) {
1283
- for (const [name, reference] of Object.entries(await parseResolverFile(fullpath))) {
1284
- let realName = name;
1285
- if (realName === "default" &&
1286
- typeof glob$1 === "string" &&
1287
- glob$1.endsWith("/*.ts")) {
1288
- realName = path.basename(fullpath, path.extname(fullpath));
1289
- }
1290
- if (realName in result) {
1291
- console.warn(`WARNING: Duplicate resolver name: ${typeName ? `${typeName}.` : ""}${name} in ${fullpath} and ${result[name].module}`);
1292
- }
1293
- result[realName] = reference;
1306
+ // NOTE: sort the files so that the result (and the warnings) do not change
1307
+ // with the order in which the file system returns entries.
1308
+ const fullpaths = (await glob.glob(glob$1, { ...globOptions, withFileTypes: true }))
1309
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".ts"))
1310
+ .map((entry) => toPosixPath(entry.fullpath()))
1311
+ .filter((fullpath) => !shouldIgnore(fullpath))
1312
+ .sort(comparePaths);
1313
+ for (const fullpath of fullpaths) {
1314
+ for (const [name, reference] of Object.entries(await parseResolverFile(fullpath))) {
1315
+ let realName = name;
1316
+ if (realName === "default" &&
1317
+ typeof glob$1 === "string" &&
1318
+ toPosixPath(glob$1).endsWith("/*.ts")) {
1319
+ realName = path.basename(fullpath, path.extname(fullpath));
1294
1320
  }
1321
+ if (realName in result) {
1322
+ console.warn(`WARNING: Duplicate resolver name: ${typeName ? `${typeName}.` : ""}${realName} in ${fullpath} and ${result[realName].module}`);
1323
+ }
1324
+ result[realName] = reference;
1295
1325
  }
1296
1326
  }
1297
1327
  return result;
@@ -1303,8 +1333,9 @@ async function parse$2(dirpath, ignore) {
1303
1333
  }
1304
1334
  const out = {};
1305
1335
  const shouldIgnore = createMatcher(ignore);
1306
- for await (const name of await fs.readdir(dirpath)) {
1307
- const file = path.join(dirpath, name);
1336
+ const names = (await fs.readdir(dirpath)).sort(comparePaths);
1337
+ for (const name of names) {
1338
+ const file = path.posix.join(toPosixPath(dirpath), name);
1308
1339
  const entry = await fs.stat(file);
1309
1340
  if (entry.isDirectory()) {
1310
1341
  out[name] = await parseTypeResolvers(name, `${file}/*.ts`, shouldIgnore);
@@ -1614,7 +1645,7 @@ function namedNodesDict(nodes = []) {
1614
1645
  return Object.fromEntries(nodes.map((node) => [node.name.value, node]));
1615
1646
  }
1616
1647
  function sortNamedNodes(nodes) {
1617
- return Array.from(nodes).sort(({ name: { value: a } }, { name: { value: b } }) => a.localeCompare(b));
1648
+ return Array.from(nodes).sort(({ name: { value: a } }, { name: { value: b } }) => a.localeCompare(b, "en"));
1618
1649
  }
1619
1650
  function isIntValueNodeEqual(a, b) {
1620
1651
  return a.value === b.value;
@@ -1948,21 +1979,31 @@ function mergeDocumentNodes(documentNodes) {
1948
1979
  };
1949
1980
  }
1950
1981
 
1951
- async function* findSchemaFiles(glob$1) {
1952
- for await (const path$1 of glob.globIterate(glob$1)) {
1982
+ async function collectSchemaFiles(glob$1, files) {
1983
+ for (const path$1 of await glob.glob(glob$1, globOptions)) {
1953
1984
  const entry = await fs.stat(path$1);
1954
1985
  if (entry.isFile()) {
1955
- yield path$1;
1986
+ files.add(toPosixPath(path$1));
1956
1987
  }
1957
1988
  else if (entry.isDirectory()) {
1958
- yield* findSchemaFiles(path.join(path$1, "**/*.graphql"));
1989
+ await collectSchemaFiles(path.posix.join(toPosixPath(path$1), "**/*.graphql"), files);
1959
1990
  }
1960
1991
  }
1961
1992
  }
1993
+ /**
1994
+ * Finds the schema files and sorts them by path. The order of the files sets
1995
+ * the order of the merged definitions and fields, so it must not change with
1996
+ * the file system or the platform.
1997
+ */
1998
+ async function findSchemaFiles(glob) {
1999
+ const files = new Set();
2000
+ await collectSchemaFiles(glob, files);
2001
+ return Array.from(files).sort(comparePaths);
2002
+ }
1962
2003
  async function parse(glob, ignore) {
1963
2004
  const shouldIgnore = createMatcher(ignore);
1964
2005
  const documentNodes = [];
1965
- for await (const entry of findSchemaFiles(glob)) {
2006
+ for (const entry of await findSchemaFiles(glob)) {
1966
2007
  if (shouldIgnore(entry)) {
1967
2008
  continue;
1968
2009
  }
package/build/lib.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import path, { resolve, basename, extname, join } from 'node:path';
1
+ import path, { resolve, basename, extname, posix } from 'node:path';
2
2
  import { pathToFileURL, fileURLToPath } from 'node:url';
3
3
  import { GraphQLNonNull, GraphQLList, GraphQLScalarType, GraphQLEnumType, GraphQLDirective, Kind, GraphQLObjectType, getNamedType, getNullableType, GraphQLInterfaceType, GraphQLUnionType, GraphQLInputObjectType, DirectiveLocation, specifiedDirectives, isTypeExtensionNode, GraphQLError, isTypeDefinitionNode, parse as parse$3, Source, buildASTSchema, validateSchema } from 'graphql';
4
4
  import jen$1, { jen } from 'jennifer-js';
@@ -7,7 +7,7 @@ import partition from 'just-partition';
7
7
  import pascalCase from 'just-pascal-case';
8
8
  import fs from 'node:fs/promises';
9
9
  import ts, { getLineAndCharacterOfPosition } from 'typescript';
10
- import { globIterate } from 'glob';
10
+ import { glob } from 'glob';
11
11
  import pm from 'picomatch';
12
12
  import zip from 'just-zip-it';
13
13
 
@@ -275,6 +275,30 @@ class RequiredPackages {
275
275
  }
276
276
  }
277
277
 
278
+ /**
279
+ * Glob options that make Windows paths work as patterns. On Windows, a
280
+ * backslash is a path separator and not an escape character.
281
+ */
282
+ const globOptions = {
283
+ windowsPathsNoEscape: path.sep === "\\",
284
+ };
285
+ /**
286
+ * Converts native path separators to forward slashes.
287
+ *
288
+ * Glob patterns and module specifiers must use forward slashes, even on
289
+ * Windows.
290
+ */
291
+ function toPosixPath(p) {
292
+ return path.sep === "\\" ? p.replaceAll("\\", "/") : p;
293
+ }
294
+ /**
295
+ * Compares paths by code unit. The result does not change with the locale
296
+ * or with the order in which the file system returns entries.
297
+ */
298
+ function comparePaths(a, b) {
299
+ return a < b ? -1 : a > b ? 1 : 0;
300
+ }
301
+
278
302
  function renderSymbolReference(pkgs, ref, alias) {
279
303
  const ns = ref.module
280
304
  ? jen.id(pkgs.addRequires(ref.module, ref.symbol, alias ?? ref.alias))
@@ -283,10 +307,10 @@ function renderSymbolReference(pkgs, ref, alias) {
283
307
  }
284
308
 
285
309
  function compareEntryKey([a], [b]) {
286
- return a.localeCompare(b);
310
+ return a.localeCompare(b, "en");
287
311
  }
288
312
  function compareName({ name: a }, { name: b }) {
289
- return a.localeCompare(b);
313
+ return a.localeCompare(b, "en");
290
314
  }
291
315
  const PRIMITIVE_TYPES = new Set(["ID", "Int", "Float", "String", "Boolean"]);
292
316
  function isPrimitiveType(type) {
@@ -305,8 +329,9 @@ function renderImports(base, imports, aliases, extMode = "omit") {
305
329
  if (mod.startsWith("file:///")) {
306
330
  mod = fileURLToPath(mod);
307
331
  }
308
- if (mod.startsWith("/")) {
309
- mod = path.relative(base, mod);
332
+ if (path.isAbsolute(mod)) {
333
+ // NOTE: module specifiers always use forward slashes, even on Windows.
334
+ mod = toPosixPath(path.relative(base, mod));
310
335
  if (!mod.startsWith(".")) {
311
336
  mod = `./${mod}`;
312
337
  }
@@ -322,8 +347,8 @@ function renderImports(base, imports, aliases, extMode = "omit") {
322
347
  }
323
348
  return jen$1.import
324
349
  .obj(...Array.from(symbols)
325
- .sort((a, b) => a.localeCompare(b) ||
326
- importAlias(a).localeCompare(importAlias(b)))
350
+ .sort((a, b) => a.localeCompare(b, "en") ||
351
+ importAlias(a).localeCompare(importAlias(b), "en"))
327
352
  .map((symbol) => symbol in aliases
328
353
  ? jen$1.id(aliases[symbol]).as.id(symbol)
329
354
  : jen$1.id(symbol)))
@@ -1034,7 +1059,10 @@ class GeneratorContext {
1034
1059
  ? [
1035
1060
  jen$1.prop("subscription", this._renderGraphQLTypeIdentifier(subscriptionType)),
1036
1061
  ]
1037
- : []), jen$1.prop("types", jen$1.array(...types.map((type) => this._renderGraphQLTypeIdentifier(type)))), jen$1.prop("directives", jen$1.array(...Array.from(schema.getDirectives())
1062
+ : []), jen$1.prop("types", jen$1.array(...types.map((type) => this._renderGraphQLTypeIdentifier(type)))),
1063
+ // graphql-js adds the built-in directives only when the
1064
+ // directives list is not given, so include them explicitly.
1065
+ jen$1.prop("directives", jen$1.array(jen$1.spread(this._gql("specifiedDirectives")), ...Array.from(schema.getDirectives())
1038
1066
  .filter((d) => !isBuiltInDirective(d.name))
1039
1067
  .map((d) => this._renderGraphQLTypeIdentifier(d)))))),
1040
1068
  jen$1.export.default.id("schema"),
@@ -1090,9 +1118,9 @@ class GeneratorContext {
1090
1118
  ];
1091
1119
  }
1092
1120
  renderFile(outfile, renderStatements) {
1093
- const base = outfile.startsWith("/") ? outfile : path.resolve(outfile);
1121
+ const base = path.resolve(outfile);
1094
1122
  const statements = renderStatements.call(this);
1095
- const [localImports, externalImports] = partition(Object.entries(this._pkgs.requires), ([pkg]) => pkg.startsWith("/"));
1123
+ const [localImports, externalImports] = partition(Object.entries(this._pkgs.requires), ([pkg]) => path.isAbsolute(pkg));
1096
1124
  return format(jen$1
1097
1125
  .statements(renderImports(path.dirname(base), externalImports, this._pkgs.aliases, this._extMode), renderImports(path.dirname(base), localImports, this._pkgs.aliases, this._extMode), ...statements)
1098
1126
  .toString());
@@ -1270,26 +1298,28 @@ async function parseResolverFile(path) {
1270
1298
  return [];
1271
1299
  })));
1272
1300
  }
1273
- async function parseTypeResolvers(typeName, glob, ignore) {
1301
+ async function parseTypeResolvers(typeName, glob$1, ignore) {
1274
1302
  const shouldIgnore = createMatcher(ignore);
1275
1303
  const result = {};
1276
- for await (const entry of globIterate(glob, { withFileTypes: true })) {
1277
- const fullpath = entry.fullpath();
1278
- if (!shouldIgnore(fullpath) &&
1279
- entry.isFile() &&
1280
- entry.name.endsWith(".ts")) {
1281
- for (const [name, reference] of Object.entries(await parseResolverFile(fullpath))) {
1282
- let realName = name;
1283
- if (realName === "default" &&
1284
- typeof glob === "string" &&
1285
- glob.endsWith("/*.ts")) {
1286
- realName = basename(fullpath, extname(fullpath));
1287
- }
1288
- if (realName in result) {
1289
- console.warn(`WARNING: Duplicate resolver name: ${typeName ? `${typeName}.` : ""}${name} in ${fullpath} and ${result[name].module}`);
1290
- }
1291
- result[realName] = reference;
1304
+ // NOTE: sort the files so that the result (and the warnings) do not change
1305
+ // with the order in which the file system returns entries.
1306
+ const fullpaths = (await glob(glob$1, { ...globOptions, withFileTypes: true }))
1307
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".ts"))
1308
+ .map((entry) => toPosixPath(entry.fullpath()))
1309
+ .filter((fullpath) => !shouldIgnore(fullpath))
1310
+ .sort(comparePaths);
1311
+ for (const fullpath of fullpaths) {
1312
+ for (const [name, reference] of Object.entries(await parseResolverFile(fullpath))) {
1313
+ let realName = name;
1314
+ if (realName === "default" &&
1315
+ typeof glob$1 === "string" &&
1316
+ toPosixPath(glob$1).endsWith("/*.ts")) {
1317
+ realName = basename(fullpath, extname(fullpath));
1292
1318
  }
1319
+ if (realName in result) {
1320
+ console.warn(`WARNING: Duplicate resolver name: ${typeName ? `${typeName}.` : ""}${realName} in ${fullpath} and ${result[realName].module}`);
1321
+ }
1322
+ result[realName] = reference;
1293
1323
  }
1294
1324
  }
1295
1325
  return result;
@@ -1301,8 +1331,9 @@ async function parse$2(dirpath, ignore) {
1301
1331
  }
1302
1332
  const out = {};
1303
1333
  const shouldIgnore = createMatcher(ignore);
1304
- for await (const name of await fs.readdir(dirpath)) {
1305
- const file = join(dirpath, name);
1334
+ const names = (await fs.readdir(dirpath)).sort(comparePaths);
1335
+ for (const name of names) {
1336
+ const file = posix.join(toPosixPath(dirpath), name);
1306
1337
  const entry = await fs.stat(file);
1307
1338
  if (entry.isDirectory()) {
1308
1339
  out[name] = await parseTypeResolvers(name, `${file}/*.ts`, shouldIgnore);
@@ -1612,7 +1643,7 @@ function namedNodesDict(nodes = []) {
1612
1643
  return Object.fromEntries(nodes.map((node) => [node.name.value, node]));
1613
1644
  }
1614
1645
  function sortNamedNodes(nodes) {
1615
- return Array.from(nodes).sort(({ name: { value: a } }, { name: { value: b } }) => a.localeCompare(b));
1646
+ return Array.from(nodes).sort(({ name: { value: a } }, { name: { value: b } }) => a.localeCompare(b, "en"));
1616
1647
  }
1617
1648
  function isIntValueNodeEqual(a, b) {
1618
1649
  return a.value === b.value;
@@ -1946,21 +1977,31 @@ function mergeDocumentNodes(documentNodes) {
1946
1977
  };
1947
1978
  }
1948
1979
 
1949
- async function* findSchemaFiles(glob) {
1950
- for await (const path of globIterate(glob)) {
1980
+ async function collectSchemaFiles(glob$1, files) {
1981
+ for (const path of await glob(glob$1, globOptions)) {
1951
1982
  const entry = await fs.stat(path);
1952
1983
  if (entry.isFile()) {
1953
- yield path;
1984
+ files.add(toPosixPath(path));
1954
1985
  }
1955
1986
  else if (entry.isDirectory()) {
1956
- yield* findSchemaFiles(join(path, "**/*.graphql"));
1987
+ await collectSchemaFiles(posix.join(toPosixPath(path), "**/*.graphql"), files);
1957
1988
  }
1958
1989
  }
1959
1990
  }
1991
+ /**
1992
+ * Finds the schema files and sorts them by path. The order of the files sets
1993
+ * the order of the merged definitions and fields, so it must not change with
1994
+ * the file system or the platform.
1995
+ */
1996
+ async function findSchemaFiles(glob) {
1997
+ const files = new Set();
1998
+ await collectSchemaFiles(glob, files);
1999
+ return Array.from(files).sort(comparePaths);
2000
+ }
1960
2001
  async function parse(glob, ignore) {
1961
2002
  const shouldIgnore = createMatcher(ignore);
1962
2003
  const documentNodes = [];
1963
- for await (const entry of findSchemaFiles(glob)) {
2004
+ for (const entry of await findSchemaFiles(glob)) {
1964
2005
  if (shouldIgnore(entry)) {
1965
2006
  continue;
1966
2007
  }
package/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "graphql-jerky",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Jerky is a schema-first static graphql schema lightweight code generator.",
5
5
  "type": "module",
6
- "bin": "./build/bin/jerky.cjs",
6
+ "bin": {
7
+ "graphql-jerky": "build/bin/jerky.cjs"
8
+ },
7
9
  "types": "./build/lib.d.ts",
8
10
  "module": "./build/lib.mjs",
9
11
  "main": "./build/lib.cjs",