Comparing version 0.0.0-main-bbba151a to 0.0.0-main-bfb182a3
{ | ||
"name": "grats", | ||
"version": "0.0.2", | ||
"version": "0.0.8", | ||
"main": "dist/src/index.js", | ||
@@ -12,6 +12,7 @@ "bin": "dist/src/cli.js", | ||
"scripts": { | ||
"test": "ts-node --esm src/tests/test.ts", | ||
"test": "ts-node src/tests/test.ts", | ||
"integration-tests": "node src/tests/integration.mjs", | ||
"build": "tsc --build", | ||
"lint": "eslint src/**/*.ts" | ||
"format": "prettier . --write", | ||
"lint": "eslint src/**/*.ts && prettier . --check" | ||
}, | ||
@@ -33,7 +34,13 @@ "dependencies": { | ||
"process": "^0.11.10", | ||
"ts-node": "^10.9.1" | ||
"ts-node": "^10.9.1", | ||
"prettier": "^2.8.7" | ||
}, | ||
"prettier": { | ||
"trailingComma": "all" | ||
}, | ||
"packageManager": "pnpm@8.1.1", | ||
"engines": { | ||
"node": ">=16 <=21", | ||
"pnpm": "^8" | ||
} | ||
} |
#!/usr/bin/env node | ||
export {}; | ||
import { Location } from "graphql"; | ||
export declare function formatLoc(loc: Location): string; |
@@ -40,6 +40,6 @@ #!/usr/bin/env node | ||
exports.__esModule = true; | ||
exports.formatLoc = void 0; | ||
var graphql_1 = require("graphql"); | ||
var _1 = require("./"); | ||
var lib_1 = require("./lib"); | ||
var utils_1 = require("@graphql-tools/utils"); | ||
var commander_1 = require("commander"); | ||
@@ -49,2 +49,5 @@ var fs_1 = require("fs"); | ||
var package_json_1 = require("../package.json"); | ||
var Locate_1 = require("./Locate"); | ||
var printSchema_1 = require("./printSchema"); | ||
var ts = require("typescript"); | ||
var program = new commander_1.Command(); | ||
@@ -66,18 +69,43 @@ program | ||
}); | ||
program | ||
.command("locate") | ||
.argument("<ENTITY>", "GraphQL entity to locate. E.g. `User` or `User.id`") | ||
.option("--tsconfig <TSCONFIG>", "Path to tsconfig.json. Defaults to auto-detecting based on the current working directory") | ||
.action(function (entity, _a) { | ||
var tsconfig = _a.tsconfig; | ||
var optionsResult = (0, _1.getParsedTsConfig)(tsconfig); | ||
if (optionsResult.kind === "ERROR") { | ||
throw new Error("TODO"); | ||
} | ||
var options = optionsResult.value; | ||
var schema = buildSchema(options); | ||
var loc = (0, Locate_1.locate)(schema, entity); | ||
if (loc.kind === "ERROR") { | ||
console.error(loc.err); | ||
process.exit(1); | ||
} | ||
console.log(formatLoc(loc.value)); | ||
}); | ||
program.parse(); | ||
function build(output, tsconfig) { | ||
if (tsconfig && !(0, fs_1.existsSync)(tsconfig)) { | ||
console.error("Grats: Could not find tsconfig.json at `".concat(tsconfig, "`.")); | ||
process.exit(1); | ||
var configFile = tsconfig || ts.findConfigFile(process.cwd(), ts.sys.fileExists); | ||
if (configFile == null) { | ||
throw new Error("Grats: Could not find tsconfig.json"); | ||
} | ||
var parsed = (0, _1.getParsedTsConfig)(tsconfig); | ||
// FIXME: Validate config! | ||
// https://github.com/tsconfig/bases | ||
var schemaResult = (0, lib_1.buildSchemaResult)(parsed); | ||
if (schemaResult.kind === "ERROR") { | ||
console.error(schemaResult.err.formatDiagnosticsWithColorAndContext()); | ||
var optionsResult = (0, _1.getParsedTsConfig)(configFile); | ||
if (optionsResult.kind === "ERROR") { | ||
console.error(optionsResult.err.formatDiagnosticsWithColorAndContext()); | ||
process.exit(1); | ||
} | ||
var schema = (0, graphql_1.lexicographicSortSchema)(schemaResult.value); | ||
var schemaStr = (0, utils_1.printSchemaWithDirectives)(schema, { assumeValid: true }); | ||
var options = optionsResult.value; | ||
var config = options.raw.grats; | ||
var schema = buildSchema(options); | ||
if (config.EXPERIMENTAL_codegenPath) { | ||
var dest = (0, path_1.resolve)((0, path_1.dirname)(configFile), config.EXPERIMENTAL_codegenPath); | ||
var code = (0, printSchema_1.printExecutableSchema)(schema, config, dest); | ||
(0, fs_1.writeFileSync)(dest, code); | ||
console.error("Grats: Wrote TypeScript schema to `".concat(dest, "`.")); | ||
} | ||
var sortedSchema = (0, graphql_1.lexicographicSortSchema)(schema); | ||
var schemaStr = (0, printSchema_1.printGratsSDL)(sortedSchema, config); | ||
if (output) { | ||
@@ -92,1 +120,15 @@ var absOutput = (0, path_1.resolve)(process.cwd(), output); | ||
} | ||
function buildSchema(options) { | ||
var schemaResult = (0, lib_1.buildSchemaResult)(options); | ||
if (schemaResult.kind === "ERROR") { | ||
console.error(schemaResult.err.formatDiagnosticsWithColorAndContext()); | ||
process.exit(1); | ||
} | ||
return schemaResult.value; | ||
} | ||
// Format a location for printing to the console. Tools like VS Code and iTerm | ||
// will automatically turn this into a clickable link. | ||
function formatLoc(loc) { | ||
return "".concat(loc.source.name, ":").concat(loc.startToken.line + 1, ":").concat(loc.startToken.column + 1); | ||
} | ||
exports.formatLoc = formatLoc; |
@@ -1,6 +0,20 @@ | ||
import { DefinitionNode, FieldDefinitionNode, InputValueDefinitionNode, ListTypeNode, NamedTypeNode, Location as GraphQLLocation, NameNode, Token, TypeNode, NonNullTypeNode, StringValueNode, ConstValueNode, ConstDirectiveNode, ConstArgumentNode, EnumValueDefinitionNode, ConstObjectFieldNode, ConstObjectValueNode, ConstListValueNode } from "graphql"; | ||
import { FieldDefinitionNode, InputValueDefinitionNode, NamedTypeNode, NameNode, TypeNode, StringValueNode, ConstValueNode, ConstDirectiveNode, EnumValueDefinitionNode, ConstObjectFieldNode, ConstObjectValueNode, ConstListValueNode } from "graphql"; | ||
import { DiagnosticsResult } from "./utils/DiagnosticError"; | ||
import * as ts from "typescript"; | ||
import { TypeContext } from "./TypeContext"; | ||
import { GratsDefinitionNode, TypeContext } from "./TypeContext"; | ||
import { ConfigOptions } from "./lib"; | ||
import { GraphQLConstructor } from "./GraphQLConstructor"; | ||
export declare const LIBRARY_IMPORT_NAME = "grats"; | ||
export declare const LIBRARY_NAME = "Grats"; | ||
export declare const ISSUE_URL = "https://github.com/captbaritone/grats/issues"; | ||
export declare const TYPE_TAG = "gqlType"; | ||
export declare const FIELD_TAG = "gqlField"; | ||
export declare const SCALAR_TAG = "gqlScalar"; | ||
export declare const INTERFACE_TAG = "gqlInterface"; | ||
export declare const ENUM_TAG = "gqlEnum"; | ||
export declare const UNION_TAG = "gqlUnion"; | ||
export declare const INPUT_TAG = "gqlInput"; | ||
export declare const IMPLEMENTS_TAG_DEPRECATED = "gqlImplements"; | ||
export declare const KILLS_PARENT_ON_EXCEPTION_TAG = "killsParentOnException"; | ||
export declare const ALL_TAGS: string[]; | ||
type ArgDefaults = Map<string, ts.Expression>; | ||
@@ -18,3 +32,3 @@ /** | ||
export declare class Extractor { | ||
definitions: DefinitionNode[]; | ||
definitions: GratsDefinitionNode[]; | ||
sourceFile: ts.SourceFile; | ||
@@ -24,4 +38,5 @@ ctx: TypeContext; | ||
errors: ts.Diagnostic[]; | ||
gql: GraphQLConstructor; | ||
constructor(sourceFile: ts.SourceFile, ctx: TypeContext, buildOptions: ConfigOptions); | ||
extract(): DiagnosticsResult<DefinitionNode[]>; | ||
extract(): DiagnosticsResult<GratsDefinitionNode[]>; | ||
extractType(node: ts.Node, tag: ts.JSDocTag): void; | ||
@@ -34,4 +49,5 @@ extractScalar(node: ts.Node, tag: ts.JSDocTag): void; | ||
/** Error handling and location juggling */ | ||
report(node: ts.Node, message: string): null; | ||
reportUnhandled(node: ts.Node, message: string): null; | ||
report(node: ts.Node, message: string, relatedInformation?: ts.DiagnosticRelatedInformation[]): null; | ||
reportUnhandled(node: ts.Node, positionKind: "type" | "field" | "field type" | "input" | "input field" | "union member" | "constant value" | "union" | "enum value", message: string, relatedInformation?: ts.DiagnosticRelatedInformation[]): null; | ||
related(node: ts.Node, message: string): ts.DiagnosticRelatedInformation; | ||
diagnosticAnnotatedLocation(node: ts.Node): { | ||
@@ -42,4 +58,2 @@ start: number; | ||
}; | ||
loc(node: ts.Node): GraphQLLocation; | ||
gqlDummyToken(pos: number): Token; | ||
/** TypeScript traversals */ | ||
@@ -55,2 +69,3 @@ unionTypeAliasDeclaration(node: ts.TypeAliasDeclaration, tag: ts.JSDocTag): null | undefined; | ||
typeClassDeclaration(node: ts.ClassDeclaration, tag: ts.JSDocTag): null | undefined; | ||
validateOperationTypes(node: ts.Node, name: string): void; | ||
typeInterfaceDeclaration(node: ts.InterfaceDeclaration, tag: ts.JSDocTag): null | undefined; | ||
@@ -63,5 +78,10 @@ typeTypeAliasDeclaration(node: ts.TypeAliasDeclaration, tag: ts.JSDocTag): null | undefined; | ||
isValidTypenamePropertyType(node: ts.TypeNode, expectedName: string): boolean; | ||
collectInterfaces(node: ts.ClassDeclaration | ts.InterfaceDeclaration): Array<NamedTypeNode> | null; | ||
interfaceInterfaceDeclaration(node: ts.InterfaceDeclaration, tag: ts.JSDocTag): void; | ||
collectInterfaces(node: ts.ClassDeclaration | ts.InterfaceDeclaration | ts.TypeAliasDeclaration): Array<NamedTypeNode> | null; | ||
reportTagInterfaces(node: ts.TypeAliasDeclaration | ts.ClassDeclaration | ts.InterfaceDeclaration): null | undefined; | ||
collectHeritageInterfaces(node: ts.ClassDeclaration | ts.InterfaceDeclaration): Array<NamedTypeNode> | null; | ||
symbolHasGqlTag(node: ts.Node): boolean; | ||
hasGqlTag(node: ts.Node): boolean; | ||
interfaceInterfaceDeclaration(node: ts.InterfaceDeclaration, tag: ts.JSDocTag): null | undefined; | ||
collectFields(node: ts.ClassDeclaration | ts.InterfaceDeclaration | ts.TypeLiteralNode): Array<FieldDefinitionNode>; | ||
constructorParam(node: ts.ParameterDeclaration): FieldDefinitionNode | null; | ||
collectArgs(argsParam: ts.ParameterDeclaration): ReadonlyArray<InputValueDefinitionNode> | null; | ||
@@ -71,3 +91,3 @@ collectArgDefaults(node: ts.ObjectBindingPattern): ArgDefaults; | ||
collectArrayLiteral(node: ts.ArrayLiteralExpression): ConstListValueNode | null; | ||
cellectObjectLiteral(node: ts.ObjectLiteralExpression): ConstObjectValueNode | null; | ||
collectObjectLiteral(node: ts.ObjectLiteralExpression): ConstObjectValueNode | null; | ||
collectObjectField(node: ts.ObjectLiteralElementLike): ConstObjectFieldNode | null; | ||
@@ -79,7 +99,11 @@ collectArg(node: ts.TypeElement, defaults?: Map<string, ts.Expression> | null): InputValueDefinitionNode | null; | ||
collectEnumValues(node: ts.EnumDeclaration): ReadonlyArray<EnumValueDefinitionNode>; | ||
entityName(node: ts.ClassDeclaration | ts.MethodDeclaration | ts.MethodSignature | ts.PropertyDeclaration | ts.InterfaceDeclaration | ts.PropertySignature | ts.EnumDeclaration | ts.TypeAliasDeclaration | ts.FunctionDeclaration, tag: ts.JSDocTag): NameNode | null; | ||
entityName(node: ts.ClassDeclaration | ts.MethodDeclaration | ts.MethodSignature | ts.PropertyDeclaration | ts.InterfaceDeclaration | ts.PropertySignature | ts.EnumDeclaration | ts.TypeAliasDeclaration | ts.FunctionDeclaration | ts.ParameterDeclaration, tag: ts.JSDocTag): NameNode | null; | ||
validateContextParameter(node: ts.ParameterDeclaration): null | undefined; | ||
methodDeclaration(node: ts.MethodDeclaration | ts.MethodSignature): FieldDefinitionNode | null; | ||
collectMethodType(node: ts.TypeNode): TypeNode | null; | ||
collectReturnType(node: ts.TypeNode): { | ||
type: TypeNode; | ||
isStream: boolean; | ||
} | null; | ||
collectPropertyType(node: ts.TypeNode): TypeNode | null; | ||
maybeUnwrapePromise(node: ts.TypeNode): ts.TypeNode | null; | ||
maybeUnwrapPromise(node: ts.TypeNode): ts.TypeNode | null; | ||
collectDescription(node: ts.Node): StringValueNode | null; | ||
@@ -94,14 +118,5 @@ collectDeprecated(node: ts.Node): ConstDirectiveNode | null; | ||
handleErrorBubbling(parentNode: ts.Node, type: TypeNode): TypeNode; | ||
methodNameDirective(nameNode: ts.Node, name: string): ConstDirectiveNode; | ||
exportDirective(nameNode: ts.Node, filename: string, functionName: string): ConstDirectiveNode; | ||
/** GraphQL AST node helper methods */ | ||
gqlName(node: ts.Node, value: string): NameNode; | ||
gqlNamedType(node: ts.Node, value: string): NamedTypeNode; | ||
gqlNonNullType(node: ts.Node, type: TypeNode): NonNullTypeNode; | ||
gqlNullableType(type: TypeNode): NamedTypeNode | ListTypeNode; | ||
gqlListType(node: ts.Node, type: TypeNode): ListTypeNode; | ||
gqlConstArgument(node: ts.Node, name: NameNode, value: ConstValueNode): ConstArgumentNode; | ||
gqlConstDirective(node: ts.Node, name: NameNode, args: ReadonlyArray<ConstArgumentNode>): ConstDirectiveNode; | ||
gqlString(node: ts.Node, value: string): StringValueNode; | ||
exportDirective(nameNode: ts.Node, jsModulePath: string, tsModulePath: string, functionName: string, argCount: number): ConstDirectiveNode; | ||
fieldNameDirective(nameNode: ts.Node, name: string): ConstDirectiveNode; | ||
} | ||
export {}; |
@@ -13,4 +13,20 @@ "use strict"; | ||
}; | ||
var __read = (this && this.__read) || function (o, n) { | ||
var m = typeof Symbol === "function" && o[Symbol.iterator]; | ||
if (!m) return o; | ||
var i = m.call(o), r, ar = [], e; | ||
try { | ||
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); | ||
} | ||
catch (error) { e = { error: error }; } | ||
finally { | ||
try { | ||
if (r && !r.done && (m = i["return"])) m.call(i); | ||
} | ||
finally { if (e) throw e.error; } | ||
} | ||
return ar; | ||
}; | ||
exports.__esModule = true; | ||
exports.Extractor = void 0; | ||
exports.Extractor = exports.ALL_TAGS = exports.KILLS_PARENT_ON_EXCEPTION_TAG = exports.IMPLEMENTS_TAG_DEPRECATED = exports.INPUT_TAG = exports.UNION_TAG = exports.ENUM_TAG = exports.INTERFACE_TAG = exports.SCALAR_TAG = exports.FIELD_TAG = exports.TYPE_TAG = exports.ISSUE_URL = exports.LIBRARY_NAME = exports.LIBRARY_IMPORT_NAME = void 0; | ||
var graphql_1 = require("graphql"); | ||
@@ -20,24 +36,30 @@ var DiagnosticError_1 = require("./utils/DiagnosticError"); | ||
var TypeContext_1 = require("./TypeContext"); | ||
var E = require("./Errors"); | ||
var JSDoc_1 = require("./utils/JSDoc"); | ||
var GraphQLConstructor_1 = require("./GraphQLConstructor"); | ||
var serverDirectives_1 = require("./serverDirectives"); | ||
var LIBRARY_IMPORT_NAME = "grats"; | ||
var LIBRARY_NAME = "Grats"; | ||
var ISSUE_URL = "https://github.com/captbaritone/grats/issues"; | ||
var TYPE_TAG = "gqlType"; | ||
var FIELD_TAG = "gqlField"; | ||
var SCALAR_TAG = "gqlScalar"; | ||
var INTERFACE_TAG = "gqlInterface"; | ||
var ENUM_TAG = "gqlEnum"; | ||
var UNION_TAG = "gqlUnion"; | ||
var INPUT_TAG = "gqlInput"; | ||
var KILLS_PARENT_ON_EXCEPTION_TAG = "killsParentOnException"; | ||
var ALL_TAGS = [ | ||
TYPE_TAG, | ||
FIELD_TAG, | ||
SCALAR_TAG, | ||
INTERFACE_TAG, | ||
ENUM_TAG, | ||
UNION_TAG, | ||
INPUT_TAG, | ||
exports.LIBRARY_IMPORT_NAME = "grats"; | ||
exports.LIBRARY_NAME = "Grats"; | ||
exports.ISSUE_URL = "https://github.com/captbaritone/grats/issues"; | ||
exports.TYPE_TAG = "gqlType"; | ||
exports.FIELD_TAG = "gqlField"; | ||
exports.SCALAR_TAG = "gqlScalar"; | ||
exports.INTERFACE_TAG = "gqlInterface"; | ||
exports.ENUM_TAG = "gqlEnum"; | ||
exports.UNION_TAG = "gqlUnion"; | ||
exports.INPUT_TAG = "gqlInput"; | ||
exports.IMPLEMENTS_TAG_DEPRECATED = "gqlImplements"; | ||
exports.KILLS_PARENT_ON_EXCEPTION_TAG = "killsParentOnException"; | ||
// All the tags that start with gql | ||
exports.ALL_TAGS = [ | ||
exports.TYPE_TAG, | ||
exports.FIELD_TAG, | ||
exports.SCALAR_TAG, | ||
exports.INTERFACE_TAG, | ||
exports.ENUM_TAG, | ||
exports.UNION_TAG, | ||
exports.INPUT_TAG, | ||
]; | ||
var DEPRECATED_TAG = "deprecated"; | ||
var OPERATION_TYPES = new Set(["Query", "Mutation", "Subscription"]); | ||
/** | ||
@@ -60,2 +82,3 @@ * Extracts GraphQL definitions from TypeScript source code. | ||
this.configOptions = buildOptions; | ||
this.gql = new GraphQLConstructor_1.GraphQLConstructor(sourceFile); | ||
} | ||
@@ -68,79 +91,71 @@ // Traverse all nodes, checking each one for its JSDoc tags. | ||
var _this = this; | ||
ts.forEachChild(this.sourceFile, function (node) { | ||
var e_1, _a, e_2, _b; | ||
try { | ||
for (var _c = __values(ts.getJSDocTags(node)), _d = _c.next(); !_d.done; _d = _c.next()) { | ||
var tag = _d.value; | ||
switch (tag.tagName.text) { | ||
case TYPE_TAG: | ||
_this.extractType(node, tag); | ||
break; | ||
case SCALAR_TAG: | ||
_this.extractScalar(node, tag); | ||
break; | ||
case INTERFACE_TAG: | ||
_this.extractInterface(node, tag); | ||
break; | ||
case ENUM_TAG: | ||
_this.extractEnum(node, tag); | ||
break; | ||
case INPUT_TAG: | ||
_this.extractInput(node, tag); | ||
break; | ||
case UNION_TAG: | ||
_this.extractUnion(node, tag); | ||
break; | ||
case FIELD_TAG: | ||
if (ts.isFunctionDeclaration(node)) { | ||
_this.functionDeclarationExtendType(node, tag); | ||
(0, JSDoc_1.traverseJSDocTags)(this.sourceFile, function (node, tag) { | ||
var e_1, _a; | ||
switch (tag.tagName.text) { | ||
case exports.TYPE_TAG: | ||
_this.extractType(node, tag); | ||
break; | ||
case exports.SCALAR_TAG: | ||
_this.extractScalar(node, tag); | ||
break; | ||
case exports.INTERFACE_TAG: | ||
_this.extractInterface(node, tag); | ||
break; | ||
case exports.ENUM_TAG: | ||
_this.extractEnum(node, tag); | ||
break; | ||
case exports.INPUT_TAG: | ||
_this.extractInput(node, tag); | ||
break; | ||
case exports.UNION_TAG: | ||
_this.extractUnion(node, tag); | ||
break; | ||
case exports.FIELD_TAG: | ||
if (ts.isFunctionDeclaration(node)) { | ||
_this.functionDeclarationExtendType(node, tag); | ||
} | ||
else if (!(ts.isParameter(node) || | ||
ts.isMethodDeclaration(node) || | ||
ts.isPropertyDeclaration(node) || | ||
ts.isMethodSignature(node) || | ||
ts.isPropertySignature(node))) { | ||
// Right now this happens via deep traversal | ||
// Note: Keep this in sync with `collectFields` | ||
_this.reportUnhandled(node, "field", E.fieldTagOnWrongNode()); | ||
} | ||
break; | ||
case exports.KILLS_PARENT_ON_EXCEPTION_TAG: { | ||
var hasFieldTag = ts.getJSDocTags(node).some(function (t) { | ||
return t.tagName.text === exports.FIELD_TAG; | ||
}); | ||
if (!hasFieldTag) { | ||
_this.report(tag.tagName, E.killsParentOnExceptionOnWrongNode()); | ||
} | ||
break; | ||
} | ||
default: | ||
{ | ||
var lowerCaseTag = tag.tagName.text.toLowerCase(); | ||
if (lowerCaseTag.startsWith("gql")) { | ||
try { | ||
for (var ALL_TAGS_1 = __values(exports.ALL_TAGS), ALL_TAGS_1_1 = ALL_TAGS_1.next(); !ALL_TAGS_1_1.done; ALL_TAGS_1_1 = ALL_TAGS_1.next()) { | ||
var t = ALL_TAGS_1_1.value; | ||
if (t.toLowerCase() === lowerCaseTag) { | ||
_this.report(tag.tagName, E.wrongCasingForGratsTag(tag.tagName.text, t)); | ||
break; | ||
} | ||
} | ||
} | ||
else if (!(ts.isMethodDeclaration(node) || | ||
ts.isPropertyDeclaration(node) || | ||
ts.isMethodSignature(node) || | ||
ts.isPropertySignature(node))) { | ||
// Right now this happens via deep traversal | ||
// Note: Keep this in sync with `collectFields` | ||
_this.reportUnhandled(node, "`@".concat(FIELD_TAG, "` can only be used on method/property declarations or signatures.")); | ||
} | ||
break; | ||
case KILLS_PARENT_ON_EXCEPTION_TAG: | ||
var hasFieldTag = ts.getJSDocTags(node).some(function (t) { | ||
return t.tagName.text === FIELD_TAG; | ||
}); | ||
if (!hasFieldTag) { | ||
_this.report(tag.tagName, "Unexpected `@".concat(KILLS_PARENT_ON_EXCEPTION_TAG, "`. `@").concat(KILLS_PARENT_ON_EXCEPTION_TAG, "` can only be used in field annotation docblocks. Perhaps you are missing a `@").concat(FIELD_TAG, "` tag?")); | ||
} | ||
break; | ||
default: | ||
var lowerCaseTag = tag.tagName.text.toLowerCase(); | ||
if (lowerCaseTag.startsWith("gql")) { | ||
catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
finally { | ||
try { | ||
for (var ALL_TAGS_1 = (e_2 = void 0, __values(ALL_TAGS)), ALL_TAGS_1_1 = ALL_TAGS_1.next(); !ALL_TAGS_1_1.done; ALL_TAGS_1_1 = ALL_TAGS_1.next()) { | ||
var t = ALL_TAGS_1_1.value; | ||
if (t.toLowerCase() === lowerCaseTag) { | ||
_this.report(tag.tagName, "Incorrect casing for Grats tag `@".concat(tag.tagName.text, "`. Use `@").concat(t, "` instead.")); | ||
break; | ||
} | ||
} | ||
if (ALL_TAGS_1_1 && !ALL_TAGS_1_1.done && (_a = ALL_TAGS_1["return"])) _a.call(ALL_TAGS_1); | ||
} | ||
catch (e_2_1) { e_2 = { error: e_2_1 }; } | ||
finally { | ||
try { | ||
if (ALL_TAGS_1_1 && !ALL_TAGS_1_1.done && (_b = ALL_TAGS_1["return"])) _b.call(ALL_TAGS_1); | ||
} | ||
finally { if (e_2) throw e_2.error; } | ||
} | ||
_this.report(tag.tagName, "`@".concat(tag.tagName.text, "` is not a valid Grats tag. Valid tags are: ").concat(ALL_TAGS.map(function (t) { return "`@".concat(t, "`"); }).join(", "), ".")); | ||
finally { if (e_1) throw e_1.error; } | ||
} | ||
break; | ||
_this.report(tag.tagName, E.invalidGratsTag(tag.tagName.text)); | ||
} | ||
} | ||
} | ||
break; | ||
} | ||
catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
finally { | ||
try { | ||
if (_d && !_d.done && (_a = _c["return"])) _a.call(_c); | ||
} | ||
finally { if (e_1) throw e_1.error; } | ||
} | ||
}); | ||
@@ -163,3 +178,3 @@ if (this.errors.length > 0) { | ||
else { | ||
this.report(tag, "`@".concat(TYPE_TAG, "` can only be used on class or interface declarations.")); | ||
this.report(tag, E.invalidTypeTagUsage()); | ||
} | ||
@@ -172,3 +187,3 @@ }; | ||
else { | ||
this.report(tag, "`@".concat(SCALAR_TAG, "` can only be used on type alias declarations.")); | ||
this.report(tag, E.invalidScalarTagUsage()); | ||
} | ||
@@ -181,3 +196,3 @@ }; | ||
else { | ||
this.report(tag, "`@".concat(INTERFACE_TAG, "` can only be used on interface declarations.")); | ||
this.report(tag, E.invalidInterfaceTagUsage()); | ||
} | ||
@@ -193,3 +208,3 @@ }; | ||
else { | ||
this.report(tag, "`@".concat(ENUM_TAG, "` can only be used on enum declarations or TypeScript unions.")); | ||
this.report(tag, E.invalidEnumTagUsage()); | ||
} | ||
@@ -202,3 +217,3 @@ }; | ||
else { | ||
this.report(tag, "`@".concat(INPUT_TAG, "` can only be used on type alias declarations.")); | ||
this.report(tag, E.invalidInputTagUsage()); | ||
} | ||
@@ -211,7 +226,7 @@ }; | ||
else { | ||
this.report(tag, "`@".concat(UNION_TAG, "` can only be used on type alias declarations.")); | ||
this.report(tag, E.invalidUnionTagUsage()); | ||
} | ||
}; | ||
/** Error handling and location juggling */ | ||
Extractor.prototype.report = function (node, message) { | ||
Extractor.prototype.report = function (node, message, relatedInformation) { | ||
var start = node.getStart(); | ||
@@ -225,3 +240,4 @@ var length = node.getEnd() - start; | ||
start: start, | ||
length: length | ||
length: length, | ||
relatedInformation: relatedInformation | ||
}); | ||
@@ -232,8 +248,17 @@ return null; | ||
// Gives the user a path forward if they think we should be able to infer this type. | ||
Extractor.prototype.reportUnhandled = function (node, message) { | ||
var suggestion = "If you think ".concat(LIBRARY_NAME, " should be able to infer this type, please report an issue at ").concat(ISSUE_URL, "."); | ||
Extractor.prototype.reportUnhandled = function (node, positionKind, message, relatedInformation) { | ||
var suggestion = "If you think ".concat(exports.LIBRARY_NAME, " should be able to infer this ").concat(positionKind, ", please report an issue at ").concat(exports.ISSUE_URL, "."); | ||
var completedMessage = "".concat(message, "\n\n").concat(suggestion); | ||
this.report(node, completedMessage); | ||
return null; | ||
return this.report(node, completedMessage, relatedInformation); | ||
}; | ||
Extractor.prototype.related = function (node, message) { | ||
return { | ||
category: ts.DiagnosticCategory.Message, | ||
code: 0, | ||
file: node.getSourceFile(), | ||
start: node.getStart(), | ||
length: node.getWidth(), | ||
messageText: message | ||
}; | ||
}; | ||
Extractor.prototype.diagnosticAnnotatedLocation = function (node) { | ||
@@ -244,18 +269,5 @@ var start = node.getStart(); | ||
}; | ||
// TODO: This is potentially quite expensive, and we only need it if we report | ||
// an error at one of these locations. We could consider some trick to return a | ||
// proxy object that would lazily compute the line/column info. | ||
Extractor.prototype.loc = function (node) { | ||
var source = new graphql_1.Source(this.sourceFile.text, this.sourceFile.fileName); | ||
var startToken = this.gqlDummyToken(node.getStart()); | ||
var endToken = this.gqlDummyToken(node.getEnd()); | ||
return new graphql_1.Location(startToken, endToken, source); | ||
}; | ||
Extractor.prototype.gqlDummyToken = function (pos) { | ||
var _a = this.sourceFile.getLineAndCharacterOfPosition(pos), line = _a.line, character = _a.character; | ||
return new graphql_1.Token(graphql_1.TokenKind.SOF, pos, pos, line, character, undefined); | ||
}; | ||
/** TypeScript traversals */ | ||
Extractor.prototype.unionTypeAliasDeclaration = function (node, tag) { | ||
var e_3, _a; | ||
var e_2, _a; | ||
var name = this.entityName(node, tag); | ||
@@ -265,3 +277,3 @@ if (name == null) | ||
if (!ts.isUnionTypeNode(node.type)) { | ||
return this.report(node, "Expected a TypeScript union. `@".concat(UNION_TAG, "` can only be used on TypeScript unions.")); | ||
return this.report(node, E.expectedUnionTypeNode()); | ||
} | ||
@@ -274,5 +286,5 @@ var description = this.collectDescription(node.name); | ||
if (!ts.isTypeReferenceNode(member)) { | ||
return this.reportUnhandled(member, "Expected `@".concat(UNION_TAG, "` union members to be type references.")); | ||
return this.reportUnhandled(member, "union member", E.expectedUnionTypeReference()); | ||
} | ||
var namedType = this.gqlNamedType(member.typeName, TypeContext_1.UNRESOLVED_REFERENCE_NAME); | ||
var namedType = this.gql.namedType(member.typeName, TypeContext_1.UNRESOLVED_REFERENCE_NAME); | ||
this.ctx.markUnresolvedType(member.typeName, namedType.name); | ||
@@ -282,3 +294,3 @@ types.push(namedType); | ||
} | ||
catch (e_3_1) { e_3 = { error: e_3_1 }; } | ||
catch (e_2_1) { e_2 = { error: e_2_1 }; } | ||
finally { | ||
@@ -288,12 +300,6 @@ try { | ||
} | ||
finally { if (e_3) throw e_3.error; } | ||
finally { if (e_2) throw e_2.error; } | ||
} | ||
this.ctx.recordTypeName(node.name, name.value); | ||
this.definitions.push({ | ||
kind: graphql_1.Kind.UNION_TYPE_DEFINITION, | ||
loc: this.loc(node), | ||
description: description !== null && description !== void 0 ? description : undefined, | ||
name: name, | ||
types: types | ||
}); | ||
this.ctx.recordTypeName(node.name, name, "UNION"); | ||
this.definitions.push(this.gql.unionTypeDefinition(node, name, types, description)); | ||
}; | ||
@@ -306,3 +312,3 @@ Extractor.prototype.functionDeclarationExtendType = function (node, tag) { | ||
if (typeParam == null) { | ||
return this.report(funcName, "Expected `@".concat(FIELD_TAG, "` function to have a first argument representing the type to extend.")); | ||
return this.report(funcName, E.invalidParentArgForFunctionField()); | ||
} | ||
@@ -316,7 +322,8 @@ var typeName = this.typeReferenceFromParam(typeParam); | ||
if (node.type == null) { | ||
return this.report(funcName, "Expected GraphQL field to have an explicit return type."); | ||
return this.report(funcName, E.invalidReturnTypeForFunctionField()); | ||
} | ||
var type = this.collectMethodType(node.type); | ||
if (type == null) | ||
var returnType = this.collectReturnType(node.type); | ||
if (returnType == null) | ||
return null; | ||
var type = returnType.type, isStream = returnType.isStream; | ||
var args = null; | ||
@@ -327,11 +334,17 @@ var argsParam = node.parameters[1]; | ||
} | ||
var context = node.parameters[2]; | ||
if (context != null) { | ||
this.validateContextParameter(context); | ||
} | ||
var description = this.collectDescription(funcName); | ||
if (!ts.isSourceFile(node.parent)) { | ||
return this.report(node, "Expected `@".concat(FIELD_TAG, "` function to be a top-level declaration.")); | ||
return this.report(node, E.functionFieldNotTopLevel()); | ||
} | ||
// TODO: Does this work in the browser? | ||
var filename = this.ctx.getDestFilePath(node.parent); | ||
var directives = [this.exportDirective(funcName, filename, funcName.text)]; | ||
if (funcName.text !== name.value) { | ||
directives.push(this.methodNameDirective(funcName, funcName.text)); | ||
var _a = this.ctx.getDestFilePath(node.parent), jsModulePath = _a.jsModulePath, tsModulePath = _a.tsModulePath; | ||
var directives = [ | ||
this.exportDirective(funcName, jsModulePath, tsModulePath, funcName.text, node.parameters.length), | ||
]; | ||
if (isStream) { | ||
directives.push(this.gql.constDirective(node.type, this.gql.name(node.type, serverDirectives_1.ASYNC_ITERABLE_TYPE_DIRECTIVE), null)); | ||
} | ||
@@ -342,28 +355,14 @@ var deprecated = this.collectDeprecated(node); | ||
} | ||
this.definitions.push({ | ||
kind: graphql_1.Kind.OBJECT_TYPE_EXTENSION, | ||
loc: this.loc(node), | ||
name: typeName, | ||
fields: [ | ||
{ | ||
kind: graphql_1.Kind.FIELD_DEFINITION, | ||
loc: this.loc(node), | ||
description: description || undefined, | ||
name: name, | ||
arguments: args || undefined, | ||
type: this.handleErrorBubbling(node, type), | ||
directives: directives.length === 0 ? undefined : directives | ||
}, | ||
] | ||
}); | ||
var field = this.gql.fieldDefinition(node, name, this.handleErrorBubbling(node, type), args, directives, description); | ||
this.definitions.push(this.gql.abstractFieldDefinition(node, typeName, field)); | ||
}; | ||
Extractor.prototype.typeReferenceFromParam = function (typeParam) { | ||
if (typeParam.type == null) { | ||
return this.report(typeParam, "Expected first argument of a `@".concat(FIELD_TAG, "` function to have an explicit type annotation.")); | ||
return this.report(typeParam, E.functionFieldParentTypeMissing()); | ||
} | ||
if (!ts.isTypeReferenceNode(typeParam.type)) { | ||
return this.report(typeParam.type, "Expected first argument of a `@".concat(FIELD_TAG, "` function to be typed as a `@").concat(TYPE_TAG, "` type.")); | ||
return this.report(typeParam.type, E.functionFieldParentTypeNotValid()); | ||
} | ||
var nameNode = typeParam.type.typeName; | ||
var typeName = this.gqlName(nameNode, TypeContext_1.UNRESOLVED_REFERENCE_NAME); | ||
var typeName = this.gql.name(nameNode, TypeContext_1.UNRESOLVED_REFERENCE_NAME); | ||
this.ctx.markUnresolvedType(nameNode, typeName); | ||
@@ -375,3 +374,3 @@ return typeName; | ||
if (node.name == null) { | ||
return this.report(node, "Expected a `@".concat(FIELD_TAG, "` function to be a named function.")); | ||
return this.report(node, E.functionFieldNotNamed()); | ||
} | ||
@@ -386,6 +385,6 @@ var exportKeyword = (_a = node.modifiers) === null || _a === void 0 ? void 0 : _a.some(function (modifier) { | ||
// TODO: We could support this | ||
return this.report(defaultKeyword, "Expected a `@".concat(FIELD_TAG, "` function to be a named export, not a default export.")); | ||
return this.report(defaultKeyword, E.functionFieldDefaultExport()); | ||
} | ||
if (exportKeyword == null) { | ||
return this.report(node.name, "Expected a `@".concat(FIELD_TAG, "` function to be a named export.")); | ||
return this.report(node.name, E.functionFieldNotNamedExport()); | ||
} | ||
@@ -399,9 +398,4 @@ return node.name; | ||
var description = this.collectDescription(node.name); | ||
this.ctx.recordTypeName(node.name, name.value); | ||
this.definitions.push({ | ||
kind: graphql_1.Kind.SCALAR_TYPE_DEFINITION, | ||
loc: this.loc(node), | ||
description: description !== null && description !== void 0 ? description : undefined, | ||
name: name | ||
}); | ||
this.ctx.recordTypeName(node.name, name, "SCALAR"); | ||
this.definitions.push(this.gql.scalarTypeDefinition(node, name, description)); | ||
}; | ||
@@ -413,17 +407,12 @@ Extractor.prototype.inputTypeAliasDeclaration = function (node, tag) { | ||
var description = this.collectDescription(node.name); | ||
this.ctx.recordTypeName(node.name, name.value); | ||
this.ctx.recordTypeName(node.name, name, "INPUT_OBJECT"); | ||
var fields = this.collectInputFields(node); | ||
this.definitions.push({ | ||
kind: graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION, | ||
loc: this.loc(node), | ||
description: description !== null && description !== void 0 ? description : undefined, | ||
name: name, | ||
fields: fields !== null && fields !== void 0 ? fields : undefined | ||
}); | ||
var deprecatedDirective = this.collectDeprecated(node); | ||
this.definitions.push(this.gql.inputObjectTypeDefinition(node, name, fields, deprecatedDirective == null ? null : [deprecatedDirective], description)); | ||
}; | ||
Extractor.prototype.collectInputFields = function (node) { | ||
var e_4, _a; | ||
var e_3, _a; | ||
var fields = []; | ||
if (!ts.isTypeLiteralNode(node.type)) { | ||
return this.reportUnhandled(node, "`@".concat(INPUT_TAG, "` can only be used on type literals.")); | ||
return this.reportUnhandled(node, "input", E.inputTypeNotLiteral()); | ||
} | ||
@@ -434,3 +423,3 @@ try { | ||
if (!ts.isPropertySignature(member)) { | ||
this.reportUnhandled(member, "`@".concat(INPUT_TAG, "` types only support property signature members.")); | ||
this.reportUnhandled(member, "input field", E.inputTypeFieldNotProperty()); | ||
continue; | ||
@@ -443,3 +432,3 @@ } | ||
} | ||
catch (e_4_1) { e_4 = { error: e_4_1 }; } | ||
catch (e_3_1) { e_3 = { error: e_3_1 }; } | ||
finally { | ||
@@ -449,3 +438,3 @@ try { | ||
} | ||
finally { if (e_4) throw e_4.error; } | ||
finally { if (e_3) throw e_3.error; } | ||
} | ||
@@ -459,3 +448,3 @@ return fields.length === 0 ? null : fields; | ||
if (node.type == null) { | ||
return this.report(node, "Input field must have a type annotation."); | ||
return this.report(node, E.inputFieldUntyped()); | ||
} | ||
@@ -465,17 +454,10 @@ var inner = this.collectType(node.type); | ||
return null; | ||
var type = node.questionToken == null ? inner : this.gqlNullableType(inner); | ||
var type = node.questionToken == null ? inner : this.gql.nullableType(inner); | ||
var description = this.collectDescription(node.name); | ||
return { | ||
kind: graphql_1.Kind.INPUT_VALUE_DEFINITION, | ||
loc: this.loc(node), | ||
description: description !== null && description !== void 0 ? description : undefined, | ||
name: this.gqlName(id, id.text), | ||
type: type, | ||
defaultValue: undefined, | ||
directives: undefined | ||
}; | ||
var deprecatedDirective = this.collectDeprecated(node); | ||
return this.gql.inputValueDefinition(node, this.gql.name(id, id.text), type, deprecatedDirective == null ? null : [deprecatedDirective], null, description); | ||
}; | ||
Extractor.prototype.typeClassDeclaration = function (node, tag) { | ||
if (node.name == null) { | ||
return this.report(node, "Unexpected `@".concat(TYPE_TAG, "` annotation on unnamed class declaration.")); | ||
return this.report(node, E.typeTagOnUnnamedClass()); | ||
} | ||
@@ -485,17 +467,17 @@ var name = this.entityName(node, tag); | ||
return null; | ||
this.validateOperationTypes(node.name, name.value); | ||
var description = this.collectDescription(node.name); | ||
var fields = this.collectFields(node); | ||
var interfaces = this.collectInterfaces(node); | ||
this.ctx.recordTypeName(node.name, name.value); | ||
this.ctx.recordTypeName(node.name, name, "TYPE"); | ||
this.checkForTypenameProperty(node, name.value); | ||
this.definitions.push({ | ||
kind: graphql_1.Kind.OBJECT_TYPE_DEFINITION, | ||
loc: this.loc(node), | ||
description: description !== null && description !== void 0 ? description : undefined, | ||
directives: undefined, | ||
name: name, | ||
fields: fields, | ||
interfaces: interfaces !== null && interfaces !== void 0 ? interfaces : undefined | ||
}); | ||
this.definitions.push(this.gql.objectTypeDefinition(node, name, fields, interfaces, description)); | ||
}; | ||
Extractor.prototype.validateOperationTypes = function (node, name) { | ||
// TODO: If we start supporting defining operation types using | ||
// non-standard names, we will need to update this logic. | ||
if (OPERATION_TYPES.has(name)) { | ||
this.report(node, E.operationTypeNotUnknown()); | ||
} | ||
}; | ||
Extractor.prototype.typeInterfaceDeclaration = function (node, tag) { | ||
@@ -505,16 +487,9 @@ var name = this.entityName(node, tag); | ||
return null; | ||
this.validateOperationTypes(node.name, name.value); | ||
var description = this.collectDescription(node.name); | ||
var fields = this.collectFields(node); | ||
var interfaces = this.collectInterfaces(node); | ||
this.ctx.recordTypeName(node.name, name.value); | ||
this.ctx.recordTypeName(node.name, name, "INTERFACE"); | ||
this.checkForTypenameProperty(node, name.value); | ||
this.definitions.push({ | ||
kind: graphql_1.Kind.OBJECT_TYPE_DEFINITION, | ||
loc: this.loc(node), | ||
description: description !== null && description !== void 0 ? description : undefined, | ||
directives: undefined, | ||
name: name, | ||
fields: fields, | ||
interfaces: interfaces !== null && interfaces !== void 0 ? interfaces : undefined | ||
}); | ||
this.definitions.push(this.gql.objectTypeDefinition(node, name, fields, interfaces, description)); | ||
}; | ||
@@ -525,21 +500,21 @@ Extractor.prototype.typeTypeAliasDeclaration = function (node, tag) { | ||
return null; | ||
if (!ts.isTypeLiteralNode(node.type)) { | ||
this.reportUnhandled(node.type, "Expected `@".concat(TYPE_TAG, "` type to be a type literal. For example: `type Foo = { bar: string }`")); | ||
return; | ||
var fields = []; | ||
var interfaces = null; | ||
if (ts.isTypeLiteralNode(node.type)) { | ||
this.validateOperationTypes(node.type, name.value); | ||
fields = this.collectFields(node.type); | ||
interfaces = this.collectInterfaces(node); | ||
this.checkForTypenameProperty(node.type, name.value); | ||
} | ||
else if (node.type.kind === ts.SyntaxKind.UnknownKeyword) { | ||
// This is fine, we just don't know what it is. This should be the expected | ||
// case for operation types such as `Query`, `Mutation`, and `Subscription` | ||
// where there is not strong convention around. | ||
} | ||
else { | ||
return this.report(node.type, E.typeTagOnAliasOfNonObjectOrUnknown()); | ||
} | ||
var description = this.collectDescription(node.name); | ||
var fields = this.collectFields(node.type); | ||
this.ctx.recordTypeName(node.name, name.value); | ||
this.checkForTypenameProperty(node.type, name.value); | ||
this.definitions.push({ | ||
kind: graphql_1.Kind.OBJECT_TYPE_DEFINITION, | ||
loc: this.loc(node), | ||
description: description !== null && description !== void 0 ? description : undefined, | ||
directives: undefined, | ||
name: name, | ||
fields: fields, | ||
// I don't believe there is a reasonable way to specify that a type | ||
// implements an interface. | ||
interfaces: undefined | ||
}); | ||
this.ctx.recordTypeName(node.name, name, "TYPE"); | ||
this.definitions.push(this.gql.objectTypeDefinition(node, name, fields, interfaces, description)); | ||
}; | ||
@@ -567,5 +542,4 @@ Extractor.prototype.checkForTypenameProperty = function (node, expectedName) { | ||
} | ||
this.report(member.name, | ||
// TODO: Could show what kind we found, but TS AST does not have node names. | ||
"Expected `__typename` to be a property declaration."); | ||
this.report(member.name, E.typeNameNotDeclaration()); | ||
return false; | ||
@@ -581,11 +555,11 @@ }; | ||
if (node.initializer == null) { | ||
this.report(node.name, "Expected `__typename` property to have an initializer or a string literal type. For example: `__typename = \"MyType\"` or `__typename: \"MyType\";`."); | ||
this.report(node.name, E.typeNameMissingInitializer()); | ||
return false; | ||
} | ||
if (!ts.isStringLiteral(node.initializer)) { | ||
this.report(node.initializer, "Expected `__typename` property initializer to be a string literal. For example: `__typename = \"MyType\"` or `__typename: \"MyType\";`."); | ||
this.report(node.initializer, E.typeNameInitializeNotString()); | ||
return false; | ||
} | ||
if (node.initializer.text !== expectedName) { | ||
this.report(node.initializer, "Expected `__typename` property initializer to be `\"".concat(expectedName, "\"`, found `\"").concat(node.initializer.text, "\"`.")); | ||
this.report(node.initializer, E.typeNameInitializerWrong(expectedName, node.initializer.text)); | ||
return false; | ||
@@ -597,3 +571,3 @@ } | ||
if (node.type == null) { | ||
this.report(node, "Expected `__typename` property signature to specify the typename as a string literal string type. For example `__typename: \"".concat(expectedName, "\";`")); | ||
this.report(node, E.typeNameMissingTypeAnnotation(expectedName)); | ||
return false; | ||
@@ -605,7 +579,7 @@ } | ||
if (!ts.isLiteralTypeNode(node) || !ts.isStringLiteral(node.literal)) { | ||
this.report(node, "Expected `__typename` property signature to specify the typename as a string literal string type. For example `__typename: \"".concat(expectedName, "\";`")); | ||
this.report(node, E.typeNameTypeNotStringLiteral(expectedName)); | ||
return false; | ||
} | ||
if (node.literal.text !== expectedName) { | ||
this.report(node, "Expected `__typename` property to be `\"".concat(expectedName, "\"`")); | ||
this.report(node, E.typeNameDoesNotMatchExpected(expectedName)); | ||
return false; | ||
@@ -616,15 +590,41 @@ } | ||
Extractor.prototype.collectInterfaces = function (node) { | ||
this.reportTagInterfaces(node); | ||
return ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) | ||
? this.collectHeritageInterfaces(node) | ||
: null; | ||
}; | ||
Extractor.prototype.reportTagInterfaces = function (node) { | ||
var tag = this.findTag(node, exports.IMPLEMENTS_TAG_DEPRECATED); | ||
if (tag == null) | ||
return null; | ||
if (node.kind === ts.SyntaxKind.ClassDeclaration) { | ||
this.report(tag, E.implementsTagOnClass()); | ||
} | ||
if (node.kind === ts.SyntaxKind.InterfaceDeclaration) { | ||
this.report(tag, E.implementsTagOnInterface()); | ||
} | ||
if (node.kind === ts.SyntaxKind.TypeAliasDeclaration) { | ||
this.report(tag, E.implementsTagOnTypeAlias()); | ||
} | ||
}; | ||
Extractor.prototype.collectHeritageInterfaces = function (node) { | ||
var _this = this; | ||
if (node.heritageClauses == null) | ||
return null; | ||
var maybeInterfaces = node.heritageClauses.flatMap(function (clause) { | ||
if (clause.token !== ts.SyntaxKind.ImplementsKeyword) | ||
return []; | ||
return clause.types.map(function (type) { | ||
if (!ts.isIdentifier(type.expression)) { | ||
// TODO: Are there valid cases we want to cover here? | ||
return null; | ||
} | ||
var namedType = _this.gqlNamedType(type.expression, TypeContext_1.UNRESOLVED_REFERENCE_NAME); | ||
_this.ctx.markUnresolvedType(type.expression, namedType.name); | ||
var maybeInterfaces = node.heritageClauses | ||
.filter(function (clause) { | ||
if (node.kind === ts.SyntaxKind.ClassDeclaration) { | ||
return clause.token === ts.SyntaxKind.ImplementsKeyword; | ||
} | ||
// Interfaces can only have extends clauses, and those are allowed. | ||
return true; | ||
}) | ||
.flatMap(function (clause) { | ||
return clause.types | ||
.map(function (type) { return type.expression; }) | ||
.filter(function (expression) { return ts.isIdentifier(expression); }) | ||
.filter(function (expression) { return _this.symbolHasGqlTag(expression); }) | ||
.map(function (expression) { | ||
var namedType = _this.gql.namedType(expression, TypeContext_1.UNRESOLVED_REFERENCE_NAME); | ||
_this.ctx.markUnresolvedType(expression, namedType.name); | ||
return namedType; | ||
@@ -639,3 +639,18 @@ }); | ||
}; | ||
Extractor.prototype.symbolHasGqlTag = function (node) { | ||
var symbol = this.ctx.checker.getSymbolAtLocation(node); | ||
if (symbol == null) | ||
return false; | ||
var declaration = this.ctx.findSymbolDeclaration(symbol); | ||
if (declaration == null) | ||
return false; | ||
return this.hasGqlTag(declaration); | ||
}; | ||
Extractor.prototype.hasGqlTag = function (node) { | ||
return ts.getJSDocTags(node).some(function (tag) { | ||
return exports.ALL_TAGS.includes(tag.tagName.text); | ||
}); | ||
}; | ||
Extractor.prototype.interfaceInterfaceDeclaration = function (node, tag) { | ||
var _this = this; | ||
var name = this.entityName(node, tag); | ||
@@ -645,16 +660,24 @@ if (name == null || name.value == null) { | ||
} | ||
// Prevent using merged interfaces as GraphQL interfaces. | ||
// https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces | ||
var symbol = this.ctx.checker.getSymbolAtLocation(node.name); | ||
if (symbol != null && | ||
symbol.declarations != null && | ||
symbol.declarations.length > 1) { | ||
var otherLocations = symbol.declarations | ||
.filter(function (d) { return d !== node && ts.isInterfaceDeclaration(d); }) | ||
.map(function (d) { | ||
var _a; | ||
var locNode = (_a = ts.getNameOfDeclaration(d)) !== null && _a !== void 0 ? _a : d; | ||
return _this.related(locNode, "Other declaration"); | ||
}); | ||
if (otherLocations.length > 0) { | ||
return this.report(node.name, E.mergedInterfaces(name.value), otherLocations); | ||
} | ||
} | ||
var description = this.collectDescription(node.name); | ||
var interfaces = this.collectInterfaces(node); | ||
var fields = this.collectFields(node); | ||
this.ctx.recordTypeName(node.name, name.value); | ||
// While GraphQL supports interfaces that extend other interfaces, | ||
// TypeScript does not. So we can't support that here either. | ||
// In the future we could support classes that act as interfaces through | ||
// inheritance. | ||
this.definitions.push({ | ||
kind: graphql_1.Kind.INTERFACE_TYPE_DEFINITION, | ||
loc: this.loc(node), | ||
description: description || undefined, | ||
name: name, | ||
fields: fields | ||
}); | ||
this.ctx.recordTypeName(node.name, name, "INTERFACE"); | ||
this.definitions.push(this.gql.interfaceTypeDefinition(node, name, fields, interfaces, description)); | ||
}; | ||
@@ -665,5 +688,26 @@ Extractor.prototype.collectFields = function (node) { | ||
ts.forEachChild(node, function (node) { | ||
var e_4, _a; | ||
if (ts.isConstructorDeclaration(node)) { | ||
try { | ||
// Handle parameter properties | ||
// https://www.typescriptlang.org/docs/handbook/2/classes.html#parameter-properties | ||
for (var _b = __values(node.parameters), _c = _b.next(); !_c.done; _c = _b.next()) { | ||
var param = _c.value; | ||
var field = _this.constructorParam(param); | ||
if (field != null) { | ||
fields.push(field); | ||
} | ||
} | ||
} | ||
catch (e_4_1) { e_4 = { error: e_4_1 }; } | ||
finally { | ||
try { | ||
if (_c && !_c.done && (_a = _b["return"])) _a.call(_b); | ||
} | ||
finally { if (e_4) throw e_4.error; } | ||
} | ||
} | ||
if (ts.isMethodDeclaration(node) || ts.isMethodSignature(node)) { | ||
var field = _this.methodDeclaration(node); | ||
if (field) { | ||
if (field != null) { | ||
fields.push(field); | ||
@@ -682,2 +726,52 @@ } | ||
}; | ||
Extractor.prototype.constructorParam = function (node) { | ||
var tag = this.findTag(node, exports.FIELD_TAG); | ||
if (tag == null) | ||
return null; | ||
if (node.modifiers == null) { | ||
return this.report(node, E.parameterWithoutModifiers()); | ||
} | ||
var isParameterProperty = node.modifiers.some(function (modifier) { | ||
return modifier.kind === ts.SyntaxKind.PublicKeyword || | ||
modifier.kind === ts.SyntaxKind.PrivateKeyword || | ||
modifier.kind === ts.SyntaxKind.ProtectedKeyword || | ||
modifier.kind === ts.SyntaxKind.ReadonlyKeyword; | ||
}); | ||
if (!isParameterProperty) { | ||
return this.report(node, E.parameterWithoutModifiers()); | ||
} | ||
var notPublic = node.modifiers.find(function (modifier) { | ||
return modifier.kind === ts.SyntaxKind.PrivateKeyword || | ||
modifier.kind === ts.SyntaxKind.ProtectedKeyword; | ||
}); | ||
if (notPublic != null) { | ||
return this.report(notPublic, E.parameterPropertyNotPublic()); | ||
} | ||
var name = this.entityName(node, tag); | ||
if (name == null) | ||
return null; | ||
if (node.type == null) { | ||
return this.report(node, E.parameterPropertyMissingType()); | ||
} | ||
var id = node.name; | ||
if (ts.isArrayBindingPattern(id) || ts.isObjectBindingPattern(id)) { | ||
// TypeScript triggers an error if a binding pattern is used for a | ||
// parameter property, so we don't need to report them. | ||
// https://www.typescriptlang.org/play?#code/MYGwhgzhAEBiD29oG8BQ1rHgOwgFwCcBXYPeAgCgAciAjEAS2BQDNEBfAShXdXaA | ||
return null; | ||
} | ||
var directives = []; | ||
if (id.text !== name.value) { | ||
directives = [this.fieldNameDirective(node.name, id.text)]; | ||
} | ||
var type = this.collectType(node.type); | ||
if (type == null) | ||
return null; | ||
var deprecated = this.collectDeprecated(node); | ||
if (deprecated != null) { | ||
directives.push(deprecated); | ||
} | ||
var description = this.collectDescription(node.name); | ||
return this.gql.fieldDefinition(node, name, this.handleErrorBubbling(node, type), null, directives, description); | ||
}; | ||
Extractor.prototype.collectArgs = function (argsParam) { | ||
@@ -688,9 +782,9 @@ var e_5, _a; | ||
if (argsType == null) { | ||
return this.report(argsParam, "Expected GraphQL field arguments to have a TypeScript type. If there are no arguments, you can use `args: never`."); | ||
return this.report(argsParam, E.argumentParamIsMissingType()); | ||
} | ||
if (argsType.kind === ts.SyntaxKind.NeverKeyword) { | ||
if (argsType.kind === ts.SyntaxKind.UnknownKeyword) { | ||
return []; | ||
} | ||
if (!ts.isTypeLiteralNode(argsType)) { | ||
return this.report(argsType, "Expected GraphQL field arguments to be typed using a literal object: `{someField: string}`."); | ||
return this.report(argsType, E.argumentParamIsNotObject()); | ||
} | ||
@@ -743,19 +837,20 @@ var defaults = null; | ||
if (ts.isStringLiteral(node)) { | ||
return { kind: graphql_1.Kind.STRING, loc: this.loc(node), value: node.text }; | ||
return this.gql.string(node, node.text); | ||
} | ||
else if (ts.isNumericLiteral(node)) { | ||
var kind = node.text.includes(".") ? graphql_1.Kind.FLOAT : graphql_1.Kind.INT; | ||
return { kind: kind, loc: this.loc(node), value: node.text }; | ||
return node.text.includes(".") | ||
? this.gql.float(node, node.text) | ||
: this.gql.int(node, node.text); | ||
} | ||
else if (this.isNullish(node)) { | ||
return { kind: graphql_1.Kind.NULL, loc: this.loc(node) }; | ||
return this.gql["null"](node); | ||
} | ||
else if (node.kind === ts.SyntaxKind.TrueKeyword) { | ||
return { kind: graphql_1.Kind.BOOLEAN, loc: this.loc(node), value: true }; | ||
return this.gql.boolean(node, true); | ||
} | ||
else if (node.kind === ts.SyntaxKind.FalseKeyword) { | ||
return { kind: graphql_1.Kind.BOOLEAN, loc: this.loc(node), value: false }; | ||
return this.gql.boolean(node, false); | ||
} | ||
else if (ts.isObjectLiteralExpression(node)) { | ||
return this.cellectObjectLiteral(node); | ||
return this.collectObjectLiteral(node); | ||
} | ||
@@ -765,4 +860,3 @@ else if (ts.isArrayLiteralExpression(node)) { | ||
} | ||
this.reportUnhandled(node, "Expected GraphQL field argument default values to be a literal."); | ||
return null; | ||
return this.reportUnhandled(node, "constant value", E.defaultValueIsNotLiteral()); | ||
}; | ||
@@ -795,9 +889,5 @@ Extractor.prototype.collectArrayLiteral = function (node) { | ||
} | ||
return { | ||
kind: graphql_1.Kind.LIST, | ||
loc: this.loc(node), | ||
values: values | ||
}; | ||
return this.gql.list(node, values); | ||
}; | ||
Extractor.prototype.cellectObjectLiteral = function (node) { | ||
Extractor.prototype.collectObjectLiteral = function (node) { | ||
var e_8, _a; | ||
@@ -828,14 +918,10 @@ var fields = []; | ||
} | ||
return { | ||
kind: graphql_1.Kind.OBJECT, | ||
loc: this.loc(node), | ||
fields: fields | ||
}; | ||
return this.gql.object(node, fields); | ||
}; | ||
Extractor.prototype.collectObjectField = function (node) { | ||
if (!ts.isPropertyAssignment(node)) { | ||
return this.reportUnhandled(node, "Expected object literal property to be a property assignment."); | ||
return this.reportUnhandled(node, "constant value", E.defaultArgElementIsNotAssignment()); | ||
} | ||
if (node.name == null) { | ||
return this.reportUnhandled(node, "Expected object literal property to have a name."); | ||
return this.reportUnhandled(node, "field", E.defaultArgPropertyMissingName()); | ||
} | ||
@@ -847,3 +933,3 @@ var name = this.expectIdentifier(node.name); | ||
if (initialize == null) { | ||
return this.report(node, "Expected object literal property to have an initializer. For example: `{ offset = 10}`."); | ||
return this.report(node, E.defaultArgPropertyMissingInitializer()); | ||
} | ||
@@ -853,8 +939,3 @@ var value = this.collectConstValue(initialize); | ||
return null; | ||
return { | ||
kind: graphql_1.Kind.OBJECT_FIELD, | ||
loc: this.loc(node), | ||
name: this.gqlName(node.name, name.text), | ||
value: value | ||
}; | ||
return this.gql.constObjectField(node, this.gql.name(node.name, name.text), value); | ||
}; | ||
@@ -864,10 +945,10 @@ Extractor.prototype.collectArg = function (node, defaults) { | ||
// TODO: How can I create this error? | ||
return this.report(node, "Expected GraphQL field argument type to be a property signature."); | ||
return this.report(node, E.argIsNotProperty()); | ||
} | ||
if (!ts.isIdentifier(node.name)) { | ||
// TODO: How can I create this error? | ||
return this.report(node.name, "Expected GraphQL field argument names to be a literal."); | ||
return this.report(node.name, E.argNameNotLiteral()); | ||
} | ||
if (node.type == null) { | ||
return this.report(node.name, "Expected GraphQL field argument to have a type."); | ||
return this.report(node.name, E.argNotTyped()); | ||
} | ||
@@ -878,3 +959,9 @@ var type = this.collectType(node.type); | ||
if (node.questionToken) { | ||
type = this.gqlNullableType(type); | ||
/* | ||
// TODO: Don't allow args that are optional but don't accept null | ||
if (type.kind === Kind.NON_NULL_TYPE) { | ||
return this.report(node.questionToken, E.nonNullTypeCannotBeOptional()); | ||
} | ||
*/ | ||
type = this.gql.nullableType(type); | ||
} | ||
@@ -889,11 +976,4 @@ var description = this.collectDescription(node.name); | ||
} | ||
return { | ||
kind: graphql_1.Kind.INPUT_VALUE_DEFINITION, | ||
loc: this.loc(node), | ||
description: description || undefined, | ||
name: this.gqlName(node.name, node.name.text), | ||
type: type, | ||
defaultValue: defaultValue || undefined, | ||
directives: [] | ||
}; | ||
var deprecatedDirective = this.collectDeprecated(node); | ||
return this.gql.inputValueDefinition(node, this.gql.name(node.name, node.name.text), type, deprecatedDirective == null ? null : [deprecatedDirective], defaultValue, description); | ||
}; | ||
@@ -907,10 +987,4 @@ Extractor.prototype.enumEnumDeclaration = function (node, tag) { | ||
var values = this.collectEnumValues(node); | ||
this.ctx.recordTypeName(node.name, name.value); | ||
this.definitions.push({ | ||
kind: graphql_1.Kind.ENUM_TYPE_DEFINITION, | ||
loc: this.loc(node), | ||
description: description || undefined, | ||
name: name, | ||
values: values | ||
}); | ||
this.ctx.recordTypeName(node.name, name, "ENUM"); | ||
this.definitions.push(this.gql.enumTypeDefinition(node, name, values, description)); | ||
}; | ||
@@ -926,13 +1000,8 @@ Extractor.prototype.enumTypeAliasDeclaration = function (node, tag) { | ||
var description = this.collectDescription(node.name); | ||
this.ctx.recordTypeName(node.name, name.value); | ||
this.definitions.push({ | ||
kind: graphql_1.Kind.ENUM_TYPE_DEFINITION, | ||
loc: this.loc(node), | ||
description: description || undefined, | ||
name: name, | ||
values: values | ||
}); | ||
this.ctx.recordTypeName(node.name, name, "ENUM"); | ||
this.definitions.push(this.gql.enumTypeDefinition(node, name, values, description)); | ||
}; | ||
Extractor.prototype.enumTypeAliasVariants = function (node) { | ||
var e_9, _a; | ||
var _b; | ||
// Semantically we only support deriving enums from type aliases that | ||
@@ -946,12 +1015,7 @@ // are unions of string literals. However, in the edge case of a union | ||
return [ | ||
{ | ||
kind: graphql_1.Kind.ENUM_VALUE_DEFINITION, | ||
name: this.gqlName(node.type.literal, node.type.literal.text), | ||
description: undefined, | ||
loc: this.loc(node) | ||
}, | ||
this.gql.enumValueDefinition(node, this.gql.name(node.type.literal, node.type.literal.text), undefined, null), | ||
]; | ||
} | ||
if (!ts.isUnionTypeNode(node.type)) { | ||
this.reportUnhandled(node.type, "Expected `@".concat(ENUM_TAG, "` to be a union type, or a string literal in the edge case of a single value enum.")); | ||
this.reportUnhandled(node.type, "union", E.enumTagOnInvalidNode()); | ||
return null; | ||
@@ -961,7 +1025,26 @@ } | ||
try { | ||
for (var _b = __values(node.type.types), _c = _b.next(); !_c.done; _c = _b.next()) { | ||
var member = _c.value; | ||
for (var _c = __values(node.type.types), _d = _c.next(); !_d.done; _d = _c.next()) { | ||
var member = _d.value; | ||
// TODO: Complete this feature | ||
if (ts.isTypeReferenceNode(member)) { | ||
if (member.typeName.kind === ts.SyntaxKind.Identifier) { | ||
var symbol = this.ctx.checker.getSymbolAtLocation(member.typeName); | ||
if (((_b = symbol === null || symbol === void 0 ? void 0 : symbol.declarations) === null || _b === void 0 ? void 0 : _b.length) === 1) { | ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
var declaration = symbol.declarations[0]; | ||
if (ts.isTypeAliasDeclaration(declaration)) { | ||
if (ts.isLiteralTypeNode(declaration.type) && | ||
ts.isStringLiteral(declaration.type.literal)) { | ||
var deprecatedDirective = this.collectDeprecated(declaration); | ||
var memberDescription = this.collectDescription(declaration.name); | ||
values.push(this.gql.enumValueDefinition(node, this.gql.name(declaration.type.literal, declaration.type.literal.text), deprecatedDirective ? [deprecatedDirective] : [], memberDescription)); | ||
continue; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
if (!ts.isLiteralTypeNode(member) || | ||
!ts.isStringLiteral(member.literal)) { | ||
this.reportUnhandled(member, "Expected `@".concat(ENUM_TAG, "` enum members to be string literal types. For example: `'foo'`.")); | ||
this.reportUnhandled(member, "union member", E.enumVariantNotStringLiteral()); | ||
continue; | ||
@@ -971,8 +1054,3 @@ } | ||
// does not allow comments attached to string literal types. | ||
values.push({ | ||
kind: graphql_1.Kind.ENUM_VALUE_DEFINITION, | ||
name: this.gqlName(member.literal, member.literal.text), | ||
description: undefined, | ||
loc: this.loc(member) | ||
}); | ||
values.push(this.gql.enumValueDefinition(node, this.gql.name(member.literal, member.literal.text), undefined, null)); | ||
} | ||
@@ -983,3 +1061,3 @@ } | ||
try { | ||
if (_c && !_c.done && (_a = _b["return"])) _a.call(_b); | ||
if (_d && !_d.done && (_a = _c["return"])) _a.call(_c); | ||
} | ||
@@ -998,3 +1076,3 @@ finally { if (e_9) throw e_9.error; } | ||
!ts.isStringLiteral(member.initializer)) { | ||
this.reportUnhandled(member, "Expected `@".concat(ENUM_TAG, "` enum members to have string literal initializers. For example: `FOO = 'foo'`.")); | ||
this.reportUnhandled(member, "enum value", E.enumVariantMissingInitializer()); | ||
continue; | ||
@@ -1004,9 +1082,3 @@ } | ||
var deprecated = this.collectDeprecated(member); | ||
values.push({ | ||
kind: graphql_1.Kind.ENUM_VALUE_DEFINITION, | ||
loc: this.loc(member), | ||
description: description || undefined, | ||
name: this.gqlName(member.initializer, member.initializer.text), | ||
directives: deprecated ? [deprecated] : undefined | ||
}); | ||
values.push(this.gql.enumValueDefinition(member, this.gql.name(member.initializer, member.initializer.text), deprecated ? [deprecated] : undefined, description)); | ||
} | ||
@@ -1028,7 +1100,28 @@ } | ||
// FIXME: Use the _value_'s location not the tag's | ||
return this.gqlName(tag, commentName); | ||
var locNode = tag; | ||
// Test for leading newlines using the raw text | ||
var hasLeadingNewlines = /\n/.test(tag.getText().trimEnd()); | ||
var hasInternalWhitespace = /\s/.test(commentName); | ||
var validationMessage = graphQLNameValidationMessage(commentName); | ||
if (hasLeadingNewlines && validationMessage == null) { | ||
// TODO: Offer quick fix. | ||
return this.report(locNode, E.graphQLNameHasLeadingNewlines(commentName, tag.tagName.text)); | ||
} | ||
if (hasLeadingNewlines || hasInternalWhitespace) { | ||
return this.report(locNode, E.graphQLTagNameHasWhitespace(tag.tagName.text)); | ||
} | ||
// No whitespace, but still invalid. We will assume they meant this to | ||
// be a GraphQL name but didn't provide a valid identifier. | ||
// | ||
// NOTE: We can't let GraphQL validation handle this, because it throws rather | ||
// than returning a validation message. Presumably because it expects token | ||
// validation to be done during lexing/parsing. | ||
if (validationMessage !== null) { | ||
return this.report(locNode, validationMessage); | ||
} | ||
return this.gql.name(locNode, commentName); | ||
} | ||
} | ||
if (node.name == null) { | ||
return this.report(node, "Expected GraphQL entity to have a name."); | ||
return this.report(node, E.gqlEntityMissingName()); | ||
} | ||
@@ -1038,6 +1131,45 @@ var id = this.expectIdentifier(node.name); | ||
return null; | ||
return this.gqlName(id, id.text); | ||
return this.gql.name(id, id.text); | ||
}; | ||
// Ensure the type of the ctx param resolves to the declaration | ||
// annotated with `@gqlContext`. | ||
Extractor.prototype.validateContextParameter = function (node) { | ||
if (node.type == null) { | ||
return this.report(node, E.expectedTypeAnnotationOnContext()); | ||
} | ||
if (node.type.kind === ts.SyntaxKind.UnknownKeyword) { | ||
// If the user just needs to define the argument to get to a later parameter, | ||
// they can use `ctx: unknown` to safely avoid triggering a Grats error. | ||
return; | ||
} | ||
if (!ts.isTypeReferenceNode(node.type)) { | ||
return this.report(node.type, E.expectedTypeAnnotationOfReferenceOnContext()); | ||
} | ||
// Check for ... | ||
if (node.dotDotDotToken != null) { | ||
return this.report(node.dotDotDotToken, E.unexpectedParamSpreadForContextParam()); | ||
} | ||
var symbol = this.ctx.checker.getSymbolAtLocation(node.type.typeName); | ||
if (symbol == null) { | ||
return this.report(node.type.typeName, E.expectedTypeAnnotationOnContextToBeResolvable()); | ||
} | ||
var declaration = this.ctx.findSymbolDeclaration(symbol); | ||
if (declaration == null) { | ||
return this.report(node.type.typeName, E.expectedTypeAnnotationOnContextToHaveDeclaration()); | ||
} | ||
if (this.ctx.gqlContext == null) { | ||
// This is the first typed context value we've seen... | ||
this.ctx.gqlContext = { | ||
declaration: declaration, | ||
firstReference: node.type.typeName | ||
}; | ||
} | ||
else if (this.ctx.gqlContext.declaration !== declaration) { | ||
return this.report(node.type.typeName, E.multipleContextTypes(), [ | ||
this.related(this.ctx.gqlContext.firstReference, "A different type reference was used here"), | ||
]); | ||
} | ||
}; | ||
Extractor.prototype.methodDeclaration = function (node) { | ||
var tag = this.findTag(node, FIELD_TAG); | ||
var tag = this.findTag(node, exports.FIELD_TAG); | ||
if (tag == null) | ||
@@ -1049,5 +1181,8 @@ return null; | ||
if (node.type == null) { | ||
return this.report(node.name, "Expected GraphQL field to have a type."); | ||
return this.report(node.name, E.methodMissingType()); | ||
} | ||
var type = this.collectMethodType(node.type); | ||
var returnType = this.collectReturnType(node.type); | ||
if (returnType == null) | ||
return null; | ||
var type = returnType.type, isStream = returnType.isStream; | ||
// We already reported an error | ||
@@ -1061,2 +1196,6 @@ if (type == null) | ||
} | ||
var context = node.parameters[1]; | ||
if (context != null) { | ||
this.validateContextParameter(context); | ||
} | ||
var description = this.collectDescription(node.name); | ||
@@ -1068,4 +1207,7 @@ var id = this.expectIdentifier(node.name); | ||
if (id.text !== name.value) { | ||
directives = [this.methodNameDirective(node.name, id.text)]; | ||
directives = [this.fieldNameDirective(node.name, id.text)]; | ||
} | ||
if (isStream) { | ||
directives.push(this.gql.constDirective(node.type, this.gql.name(node.type, serverDirectives_1.ASYNC_ITERABLE_TYPE_DIRECTIVE), null)); | ||
} | ||
var deprecated = this.collectDeprecated(node); | ||
@@ -1075,21 +1217,31 @@ if (deprecated != null) { | ||
} | ||
return { | ||
kind: graphql_1.Kind.FIELD_DEFINITION, | ||
loc: this.loc(node), | ||
description: description || undefined, | ||
name: name, | ||
arguments: args || undefined, | ||
type: this.handleErrorBubbling(node, type), | ||
directives: directives.length === 0 ? undefined : directives | ||
}; | ||
return this.gql.fieldDefinition(node, name, this.handleErrorBubbling(node, type), args, directives, description); | ||
}; | ||
Extractor.prototype.collectMethodType = function (node) { | ||
var inner = this.maybeUnwrapePromise(node); | ||
Extractor.prototype.collectReturnType = function (node) { | ||
if (ts.isTypeReferenceNode(node)) { | ||
var identifier = this.expectIdentifier(node.typeName); | ||
if (identifier == null) | ||
return null; | ||
if (identifier.text == "AsyncIterable") { | ||
if (node.typeArguments == null || node.typeArguments.length === 0) { | ||
// TODO: Better error? | ||
return this.report(node, E.promiseMissingTypeArg()); | ||
} | ||
var t_1 = this.collectType(node.typeArguments[0]); | ||
if (t_1 == null) | ||
return null; | ||
return { type: t_1, isStream: true }; | ||
} | ||
} | ||
var inner = this.maybeUnwrapPromise(node); | ||
if (inner == null) | ||
return null; | ||
return this.collectType(inner); | ||
var t = this.collectType(inner); | ||
if (t == null) | ||
return null; | ||
return { type: t, isStream: false }; | ||
}; | ||
Extractor.prototype.collectPropertyType = function (node) { | ||
// TODO: Handle function types here. | ||
var inner = this.maybeUnwrapePromise(node); | ||
var inner = this.maybeUnwrapPromise(node); | ||
if (inner == null) | ||
@@ -1099,3 +1251,3 @@ return null; | ||
}; | ||
Extractor.prototype.maybeUnwrapePromise = function (node) { | ||
Extractor.prototype.maybeUnwrapPromise = function (node) { | ||
if (ts.isTypeReferenceNode(node)) { | ||
@@ -1106,4 +1258,4 @@ var identifier = this.expectIdentifier(node.typeName); | ||
if (identifier.text === "Promise") { | ||
if (node.typeArguments == null) { | ||
return this.report(node, "Expected type reference to have type arguments."); | ||
if (node.typeArguments == null || node.typeArguments.length === 0) { | ||
return this.report(node, E.promiseMissingTypeArg()); | ||
} | ||
@@ -1118,3 +1270,3 @@ return node.typeArguments[0]; | ||
if (symbol == null) { | ||
return this.report(node, "Expected TypeScript to be able to resolve this GraphQL entity to a symbol."); | ||
return this.report(node, E.cannotResolveSymbolForDescription()); | ||
} | ||
@@ -1124,8 +1276,3 @@ var doc = symbol.getDocumentationComment(this.ctx.checker); | ||
if (description) { | ||
return { | ||
kind: graphql_1.Kind.STRING, | ||
loc: this.loc(node), | ||
value: description, | ||
block: true | ||
}; | ||
return this.gql.string(node, description.trim(), true); | ||
} | ||
@@ -1143,15 +1290,9 @@ return null; | ||
// FIXME: Use the _value_'s location not the tag's | ||
reason = this.gqlConstArgument(tag, this.gqlName(tag, "reason"), this.gqlString(tag, reasonComment)); | ||
reason = this.gql.constArgument(tag, this.gql.name(tag, "reason"), this.gql.string(tag, reasonComment)); | ||
} | ||
} | ||
var args = reason == null ? undefined : [reason]; | ||
return { | ||
kind: graphql_1.Kind.DIRECTIVE, | ||
loc: this.loc(tag), | ||
name: this.gqlName(tag, DEPRECATED_TAG), | ||
arguments: args | ||
}; | ||
return this.gql.constDirective(tag.tagName, this.gql.name(node, DEPRECATED_TAG), reason == null ? null : [reason]); | ||
}; | ||
Extractor.prototype.property = function (node) { | ||
var tag = this.findTag(node, FIELD_TAG); | ||
var tag = this.findTag(node, exports.FIELD_TAG); | ||
if (tag == null) | ||
@@ -1163,3 +1304,3 @@ return null; | ||
if (node.type == null) { | ||
this.report(node.name, "Expected GraphQL field to have a type."); | ||
this.report(node.name, E.propertyFieldMissingType()); | ||
return null; | ||
@@ -1171,3 +1312,3 @@ } | ||
return null; | ||
var type = node.questionToken == null ? inner : this.gqlNullableType(inner); | ||
var type = node.questionToken == null ? inner : this.gql.nullableType(inner); | ||
var description = this.collectDescription(node.name); | ||
@@ -1183,14 +1324,8 @@ var directives = []; | ||
if (id.text !== name.value) { | ||
directives = [this.methodNameDirective(node.name, id.text)]; | ||
directives = [this.fieldNameDirective(node.name, id.text)]; | ||
} | ||
return { | ||
kind: graphql_1.Kind.FIELD_DEFINITION, | ||
loc: this.loc(node), | ||
description: description || undefined, | ||
name: name, | ||
arguments: undefined, | ||
type: this.handleErrorBubbling(node, type), | ||
directives: directives.length === 0 ? undefined : directives | ||
}; | ||
return this.gql.fieldDefinition(node, name, this.handleErrorBubbling(node, type), null, directives, description); | ||
}; | ||
// TODO: Support separate modes for input and output types | ||
// For input nodes and field may only be optional if `null` is a valid value. | ||
Extractor.prototype.collectType = function (node) { | ||
@@ -1208,9 +1343,8 @@ var _this = this; | ||
return null; | ||
return this.gqlNonNullType(node, this.gqlListType(node, element)); | ||
return this.gql.nonNullType(node, this.gql.listType(node, element)); | ||
} | ||
else if (ts.isUnionTypeNode(node)) { | ||
var types = node.types.filter(function (type) { return !_this.isNullish(type); }); | ||
if (types.length !== 1) { | ||
this.report(node, "Expected exactly one non-nullish type."); | ||
return null; | ||
if (types.length === 0) { | ||
return this.report(node, E.expectedOneNonNullishType()); | ||
} | ||
@@ -1220,6 +1354,15 @@ var type = this.collectType(types[0]); | ||
return null; | ||
if (types.length > 1) { | ||
var _a = __read(types), first = _a[0], rest = _a.slice(1); | ||
// FIXME: If each of `rest` matches `first` this should be okay. | ||
var incompatibleVariants = rest.map(function (tsType) { | ||
return _this.related(tsType, "Other non-nullish type"); | ||
}); | ||
this.report(first, E.expectedOneNonNullishType(), incompatibleVariants); | ||
return null; | ||
} | ||
if (node.types.length > 1) { | ||
return this.gqlNullableType(type); | ||
return this.gql.nullableType(type); | ||
} | ||
return this.gqlNonNullType(node, type); | ||
return this.gql.nonNullType(node, type); | ||
} | ||
@@ -1230,15 +1373,15 @@ else if (ts.isParenthesizedTypeNode(node)) { | ||
else if (node.kind === ts.SyntaxKind.StringKeyword) { | ||
return this.gqlNonNullType(node, this.gqlNamedType(node, "String")); | ||
return this.gql.nonNullType(node, this.gql.namedType(node, "String")); | ||
} | ||
else if (node.kind === ts.SyntaxKind.BooleanKeyword) { | ||
return this.gqlNonNullType(node, this.gqlNamedType(node, "Boolean")); | ||
return this.gql.nonNullType(node, this.gql.namedType(node, "Boolean")); | ||
} | ||
else if (node.kind === ts.SyntaxKind.NumberKeyword) { | ||
return this.report(node, "Unexpected number type. GraphQL supports both Int and Float, making `number` ambiguous. Instead, import the `Int` or `Float` type from `".concat(LIBRARY_IMPORT_NAME, "` and use that. e.g. `import { Int, Float } from \"").concat(LIBRARY_IMPORT_NAME, "\";`.")); | ||
return this.report(node, E.ambiguousNumberType()); | ||
} | ||
else if (ts.isTypeLiteralNode(node)) { | ||
return this.report(node, "Unexpected type literal. You may want to define a named GraphQL type elsewhere and reference it here."); | ||
return this.report(node, E.unsupportedTypeLiteral()); | ||
} | ||
// TODO: Better error message. This is okay if it's a type reference, but everything else is not. | ||
this.reportUnhandled(node, "Unknown GraphQL type."); | ||
this.reportUnhandled(node, "type", E.unknownGraphQLType()); | ||
return null; | ||
@@ -1256,3 +1399,3 @@ }; | ||
if (node.typeArguments == null) { | ||
return this.report(node, "Expected type reference to have type arguments."); | ||
return this.report(node, E.pluralTypeMissingParameter()); | ||
} | ||
@@ -1262,3 +1405,3 @@ var element = this.collectType(node.typeArguments[0]); | ||
return null; | ||
return this.gqlNonNullType(node, this.gqlListType(node, element)); | ||
return this.gql.nonNullType(node, this.gql.listType(node, element)); | ||
} | ||
@@ -1270,5 +1413,5 @@ default: { | ||
// A later pass will resolve the type. | ||
var namedType = this.gqlNamedType(node, TypeContext_1.UNRESOLVED_REFERENCE_NAME); | ||
var namedType = this.gql.namedType(node, TypeContext_1.UNRESOLVED_REFERENCE_NAME); | ||
this.ctx.markUnresolvedType(node.typeName, namedType.name); | ||
return this.gqlNonNullType(node, namedType); | ||
return this.gql.nonNullType(node, namedType); | ||
} | ||
@@ -1293,12 +1436,25 @@ } | ||
} | ||
return this.report(node, "Expected an identifier."); | ||
return this.report(node, E.expectedIdentifier()); | ||
}; | ||
Extractor.prototype.findTag = function (node, tagName) { | ||
var _a; | ||
return ((_a = ts | ||
var _this = this; | ||
var tags = ts | ||
.getJSDocTags(node) | ||
.find(function (tag) { return tag.tagName.escapedText === tagName; })) !== null && _a !== void 0 ? _a : null); | ||
.filter(function (tag) { return tag.tagName.escapedText === tagName; }); | ||
if (tags.length === 0) { | ||
return null; | ||
} | ||
if (tags.length > 1) { | ||
var additionalTags = tags.slice(1).map(function (tag) { | ||
return _this.related(tag, "Additional tag"); | ||
}); | ||
var message = tagName === exports.IMPLEMENTS_TAG_DEPRECATED | ||
? E.duplicateInterfaceTag() | ||
: E.duplicateTag(tagName); | ||
return this.report(tags[0], message, additionalTags); | ||
} | ||
return tags[0]; | ||
}; | ||
// It is a GraphQL best practice to model all fields as nullable. This allows | ||
// the server to handle field level exections by simply returning null for | ||
// the server to handle field level executions by simply returning null for | ||
// that field. | ||
@@ -1308,9 +1464,9 @@ // https://graphql.org/learn/best-practices/#nullability | ||
var tags = ts.getJSDocTags(parentNode); | ||
var killsParentOnExceptions = tags.find(function (tag) { return tag.tagName.text === KILLS_PARENT_ON_EXCEPTION_TAG; }); | ||
var killsParentOnExceptions = tags.find(function (tag) { return tag.tagName.text === exports.KILLS_PARENT_ON_EXCEPTION_TAG; }); | ||
if (killsParentOnExceptions) { | ||
if (!this.configOptions.nullableByDefault) { | ||
this.report(killsParentOnExceptions.tagName, "Unexpected `@".concat(KILLS_PARENT_ON_EXCEPTION_TAG, "` tag. `@").concat(KILLS_PARENT_ON_EXCEPTION_TAG, "` is only supported when the Grats config `nullableByDefault` is enabled.")); | ||
this.report(killsParentOnExceptions.tagName, E.killsParentOnExceptionWithWrongConfig()); | ||
} | ||
if (type.kind !== graphql_1.Kind.NON_NULL_TYPE) { | ||
this.report(killsParentOnExceptions.tagName, "Unexpected `@".concat(KILLS_PARENT_ON_EXCEPTION_TAG, "` tag. `@").concat(KILLS_PARENT_ON_EXCEPTION_TAG, "` is unnessesary on fields that are already nullable.")); | ||
this.report(killsParentOnExceptions.tagName, E.killsParentOnExceptionOnNullable()); | ||
} | ||
@@ -1320,55 +1476,31 @@ return type; | ||
if (this.configOptions.nullableByDefault) { | ||
return this.gqlNullableType(type); | ||
return this.gql.nullableType(type); | ||
} | ||
return type; | ||
}; | ||
Extractor.prototype.methodNameDirective = function (nameNode, name) { | ||
return this.gqlConstDirective(nameNode, this.gqlName(nameNode, serverDirectives_1.METHOD_NAME_DIRECTIVE), [ | ||
this.gqlConstArgument(nameNode, this.gqlName(nameNode, serverDirectives_1.METHOD_NAME_ARG), this.gqlString(nameNode, name)), | ||
/* Grats directives */ | ||
Extractor.prototype.exportDirective = function (nameNode, jsModulePath, tsModulePath, functionName, argCount) { | ||
return this.gql.constDirective(nameNode, this.gql.name(nameNode, serverDirectives_1.EXPORTED_DIRECTIVE), [ | ||
this.gql.constArgument(nameNode, this.gql.name(nameNode, serverDirectives_1.JS_MODULE_PATH_ARG), this.gql.string(nameNode, jsModulePath)), | ||
this.gql.constArgument(nameNode, this.gql.name(nameNode, serverDirectives_1.TS_MODULE_PATH_ARG), this.gql.string(nameNode, tsModulePath)), | ||
this.gql.constArgument(nameNode, this.gql.name(nameNode, serverDirectives_1.EXPORTED_FUNCTION_NAME_ARG), this.gql.string(nameNode, functionName)), | ||
this.gql.constArgument(nameNode, this.gql.name(nameNode, serverDirectives_1.ARG_COUNT), this.gql.int(nameNode, String(argCount))), | ||
]); | ||
}; | ||
Extractor.prototype.exportDirective = function (nameNode, filename, functionName) { | ||
return this.gqlConstDirective(nameNode, this.gqlName(nameNode, serverDirectives_1.EXPORTED_DIRECTIVE), [ | ||
this.gqlConstArgument(nameNode, this.gqlName(nameNode, serverDirectives_1.EXPORTED_FILENAME_ARG), this.gqlString(nameNode, filename)), | ||
this.gqlConstArgument(nameNode, this.gqlName(nameNode, serverDirectives_1.EXPORTED_FUNCTION_NAME_ARG), this.gqlString(nameNode, functionName)), | ||
Extractor.prototype.fieldNameDirective = function (nameNode, name) { | ||
return this.gql.constDirective(nameNode, this.gql.name(nameNode, serverDirectives_1.METHOD_NAME_DIRECTIVE), [ | ||
this.gql.constArgument(nameNode, this.gql.name(nameNode, serverDirectives_1.METHOD_NAME_ARG), this.gql.string(nameNode, name)), | ||
]); | ||
}; | ||
/** GraphQL AST node helper methods */ | ||
Extractor.prototype.gqlName = function (node, value) { | ||
return { kind: graphql_1.Kind.NAME, loc: this.loc(node), value: value }; | ||
}; | ||
Extractor.prototype.gqlNamedType = function (node, value) { | ||
return { | ||
kind: graphql_1.Kind.NAMED_TYPE, | ||
loc: this.loc(node), | ||
name: this.gqlName(node, value) | ||
}; | ||
}; | ||
Extractor.prototype.gqlNonNullType = function (node, type) { | ||
if (type.kind === graphql_1.Kind.NON_NULL_TYPE) { | ||
return type; | ||
} | ||
return { kind: graphql_1.Kind.NON_NULL_TYPE, loc: this.loc(node), type: type }; | ||
}; | ||
Extractor.prototype.gqlNullableType = function (type) { | ||
var inner = type; | ||
while (inner.kind === graphql_1.Kind.NON_NULL_TYPE) { | ||
inner = inner.type; | ||
} | ||
return inner; | ||
}; | ||
Extractor.prototype.gqlListType = function (node, type) { | ||
return { kind: graphql_1.Kind.LIST_TYPE, loc: this.loc(node), type: type }; | ||
}; | ||
Extractor.prototype.gqlConstArgument = function (node, name, value) { | ||
return { kind: graphql_1.Kind.ARGUMENT, loc: this.loc(node), name: name, value: value }; | ||
}; | ||
Extractor.prototype.gqlConstDirective = function (node, name, args) { | ||
return { kind: graphql_1.Kind.DIRECTIVE, loc: this.loc(node), name: name, arguments: args }; | ||
}; | ||
Extractor.prototype.gqlString = function (node, value) { | ||
return { kind: graphql_1.Kind.STRING, loc: this.loc(node), value: value }; | ||
}; | ||
return Extractor; | ||
}()); | ||
exports.Extractor = Extractor; | ||
function graphQLNameValidationMessage(name) { | ||
try { | ||
(0, graphql_1.assertName)(name); | ||
return null; | ||
} | ||
catch (e) { | ||
return e.message; | ||
} | ||
} |
import * as ts from "typescript"; | ||
export declare function getRelativeOutputPath(options: ts.ParsedCommandLine, sourceFile: ts.SourceFile): string; | ||
export declare function getRelativeOutputPath(options: ts.ParsedCommandLine, sourceFile: ts.SourceFile): { | ||
jsModulePath: string; | ||
tsModulePath: string; | ||
}; | ||
export declare function resolveRelativePath(relativePath: string): string; |
@@ -13,3 +13,3 @@ "use strict"; | ||
// step and the runtime can agree on. This path is that thing. | ||
var gratsRoot = __dirname; | ||
var gratsRoot = (0, path_1.join)(__dirname, "../.."); | ||
function getRelativeOutputPath(options, sourceFile) { | ||
@@ -25,3 +25,5 @@ var fileNames = ts.getOutputFileNames(options, sourceFile.fileName, true); | ||
} | ||
return (0, path_1.relative)(gratsRoot, fileNames[0]); | ||
var jsModulePath = (0, path_1.relative)(gratsRoot, fileNames[0]); | ||
var tsModulePath = (0, path_1.relative)(gratsRoot, sourceFile.fileName); | ||
return { jsModulePath: jsModulePath, tsModulePath: tsModulePath }; | ||
} | ||
@@ -28,0 +30,0 @@ exports.getRelativeOutputPath = getRelativeOutputPath; |
import { GraphQLSchema } from "graphql"; | ||
import * as ts from "typescript"; | ||
import { ParsedCommandLineGrats } from "./lib"; | ||
import { ReportableDiagnostics, Result } from "./utils/DiagnosticError"; | ||
export * from "./Types"; | ||
export * from "./lib"; | ||
export { codegen } from "./codegen"; | ||
type RuntimeOptions = { | ||
@@ -10,2 +12,2 @@ emitSchemaFile?: string; | ||
export declare function buildSchemaFromSDL(sdl: string): GraphQLSchema; | ||
export declare function getParsedTsConfig(configPath?: string): ts.ParsedCommandLine; | ||
export declare function getParsedTsConfig(configFile: string): Result<ParsedCommandLineGrats, ReportableDiagnostics>; |
@@ -17,10 +17,13 @@ "use strict"; | ||
exports.__esModule = true; | ||
exports.getParsedTsConfig = exports.buildSchemaFromSDL = exports.extractGratsSchemaAtRuntime = void 0; | ||
exports.getParsedTsConfig = exports.buildSchemaFromSDL = exports.extractGratsSchemaAtRuntime = exports.codegen = void 0; | ||
var graphql_1 = require("graphql"); | ||
var utils_1 = require("@graphql-tools/utils"); | ||
var fs = require("fs"); | ||
var ts = require("typescript"); | ||
var lib_1 = require("./lib"); | ||
var printSchema_1 = require("./printSchema"); | ||
var DiagnosticError_1 = require("./utils/DiagnosticError"); | ||
__exportStar(require("./Types"), exports); | ||
__exportStar(require("./lib"), exports); | ||
var codegen_1 = require("./codegen"); | ||
__createBinding(exports, codegen_1, "codegen"); | ||
// Build an executable schema from a set of files. Note that if extraction | ||
@@ -30,4 +33,12 @@ // fails, this function will exit the process and print a helpful error | ||
function extractGratsSchemaAtRuntime(runtimeOptions) { | ||
var _a; | ||
var parsedTsConfig = getParsedTsConfig(); | ||
var configFile = ts.findConfigFile(process.cwd(), ts.sys.fileExists); | ||
if (configFile == null) { | ||
throw new Error("Grats: Could not find tsconfig.json"); | ||
} | ||
var tsConfigResult = getParsedTsConfig(configFile); | ||
if (tsConfigResult.kind === "ERROR") { | ||
console.error(tsConfigResult.err.formatDiagnosticsWithColorAndContext()); | ||
process.exit(1); | ||
} | ||
var parsedTsConfig = tsConfigResult.value; | ||
var schemaResult = (0, lib_1.buildSchemaResult)(parsedTsConfig); | ||
@@ -41,4 +52,4 @@ if (schemaResult.kind === "ERROR") { | ||
runtimeSchema = (0, graphql_1.lexicographicSortSchema)(runtimeSchema); | ||
var sdl = (0, utils_1.printSchemaWithDirectives)(runtimeSchema, { assumeValid: true }); | ||
var filePath = (_a = runtimeOptions.emitSchemaFile) !== null && _a !== void 0 ? _a : "./schema.graphql"; | ||
var sdl = (0, printSchema_1.printGratsSDL)(runtimeSchema, parsedTsConfig.raw.grats); | ||
var filePath = runtimeOptions.emitSchemaFile; | ||
fs.writeFileSync(filePath, sdl); | ||
@@ -55,4 +66,3 @@ } | ||
// #FIXME: Report diagnostics instead of throwing! | ||
function getParsedTsConfig(configPath) { | ||
var configFile = configPath || ts.findConfigFile(process.cwd(), ts.sys.fileExists); | ||
function getParsedTsConfig(configFile) { | ||
if (!configFile) { | ||
@@ -64,7 +74,10 @@ throw new Error("Grats: Could not find tsconfig.json"); | ||
var parsed = ts.getParsedCommandLineOfConfigFile(configFile, undefined, configFileHost); | ||
if (!parsed || parsed.errors.length > 0) { | ||
throw new Error("Grats: Could not parse tsconfig.json"); | ||
if (!parsed) { | ||
throw new Error("Grats: Could not locate tsconfig.json"); | ||
} | ||
return parsed; | ||
if (parsed.errors.length > 0) { | ||
return (0, DiagnosticError_1.err)(DiagnosticError_1.ReportableDiagnostics.fromDiagnostics(parsed.errors)); | ||
} | ||
return (0, DiagnosticError_1.ok)((0, lib_1.validateGratsOptions)(parsed)); | ||
} | ||
exports.getParsedTsConfig = getParsedTsConfig; |
import { GraphQLSchema } from "graphql"; | ||
import { Result, ReportableDiagnostics } from "./utils/DiagnosticError"; | ||
import * as ts from "typescript"; | ||
import { ParsedCommandLineGrats } from "./gratsConfig"; | ||
export { applyServerDirectives } from "./serverDirectives"; | ||
export type ConfigOptions = { | ||
nullableByDefault?: boolean; | ||
reportTypeScriptTypeErrors?: boolean; | ||
}; | ||
export declare function buildSchemaResult(options: ts.ParsedCommandLine): Result<GraphQLSchema, ReportableDiagnostics>; | ||
export declare function buildSchemaResultWithHost(options: ts.ParsedCommandLine, compilerHost: ts.CompilerHost): Result<GraphQLSchema, ReportableDiagnostics>; | ||
export * from "./gratsConfig"; | ||
export declare function buildSchemaResult(options: ParsedCommandLineGrats): Result<GraphQLSchema, ReportableDiagnostics>; | ||
export declare function buildSchemaResultWithHost(options: ParsedCommandLineGrats, compilerHost: ts.CompilerHost): Result<GraphQLSchema, ReportableDiagnostics>; |
"use strict"; | ||
var __assign = (this && this.__assign) || function () { | ||
__assign = Object.assign || function(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) | ||
t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
return __assign.apply(this, arguments); | ||
}; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
@@ -24,2 +13,5 @@ if (k2 === undefined) k2 = k; | ||
})); | ||
var __exportStar = (this && this.__exportStar) || function(m, exports) { | ||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); | ||
}; | ||
var __values = (this && this.__values) || function(o) { | ||
@@ -45,4 +37,6 @@ var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; | ||
var serverDirectives_1 = require("./serverDirectives"); | ||
var helpers_1 = require("./utils/helpers"); | ||
var serverDirectives_2 = require("./serverDirectives"); | ||
__createBinding(exports, serverDirectives_2, "applyServerDirectives"); | ||
__exportStar(require("./gratsConfig"), exports); | ||
// Construct a schema, using GraphQL schema language | ||
@@ -59,4 +53,3 @@ // Exported for tests that want to intercept diagnostic errors. | ||
function buildSchemaResultWithHost(options, compilerHost) { | ||
var gratsOptions = parseGratsOptions(options); | ||
var schemaResult = extractSchema(options, gratsOptions, compilerHost); | ||
var schemaResult = extractSchema(options, compilerHost); | ||
if (schemaResult.kind === "ERROR") { | ||
@@ -68,22 +61,3 @@ return (0, DiagnosticError_1.err)(new DiagnosticError_1.ReportableDiagnostics(compilerHost, schemaResult.err)); | ||
exports.buildSchemaResultWithHost = buildSchemaResultWithHost; | ||
// TODO: Make this return diagnostics | ||
function parseGratsOptions(options) { | ||
var _a, _b; | ||
var gratsOptions = __assign({}, ((_b = (_a = options.raw) === null || _a === void 0 ? void 0 : _a.grats) !== null && _b !== void 0 ? _b : {})); | ||
if (gratsOptions.nullableByDefault === undefined) { | ||
gratsOptions.nullableByDefault = true; | ||
} | ||
else if (typeof gratsOptions.nullableByDefault !== "boolean") { | ||
throw new Error("Grats: The Grats config option `nullableByDefault` must be a boolean if provided."); | ||
} | ||
if (gratsOptions.reportTypeScriptTypeErrors === undefined) { | ||
gratsOptions.reportTypeScriptTypeErrors = false; | ||
} | ||
else if (typeof gratsOptions.reportTypeScriptTypeErrors !== "boolean") { | ||
throw new Error("Grats: The Grats config option `reportTypeScriptTypeErrors` must be a boolean if provided"); | ||
} | ||
// FIXME: Check for unknown options | ||
return gratsOptions; | ||
} | ||
function extractSchema(options, gratsOptions, host) { | ||
function extractSchema(options, host) { | ||
var e_1, _a, e_2, _b; | ||
@@ -94,2 +68,3 @@ var program = ts.createProgram(options.fileNames, options.options, host); | ||
var definitions = Array.from(serverDirectives_1.DIRECTIVES_AST.definitions); | ||
var errors = []; | ||
try { | ||
@@ -102,7 +77,8 @@ for (var _c = __values(program.getSourceFiles()), _d = _c.next(); !_d.done; _d = _c.next()) { | ||
} | ||
if (gratsOptions.reportTypeScriptTypeErrors) { | ||
if (options.raw.grats.reportTypeScriptTypeErrors) { | ||
// If the user asked for us to report TypeScript errors, then we'll report them. | ||
var typeErrors = ts.getPreEmitDiagnostics(program, sourceFile); | ||
if (typeErrors.length > 0) { | ||
return (0, DiagnosticError_1.err)(typeErrors); | ||
(0, helpers_1.extend)(errors, typeErrors); | ||
continue; | ||
} | ||
@@ -117,9 +93,12 @@ } | ||
// the first one. | ||
return (0, DiagnosticError_1.err)([syntaxErrors[0]]); | ||
errors.push(syntaxErrors[0]); | ||
continue; | ||
} | ||
} | ||
var extractor = new Extractor_1.Extractor(sourceFile, ctx, gratsOptions); | ||
var extractor = new Extractor_1.Extractor(sourceFile, ctx, options.raw.grats); | ||
var extractedResult = extractor.extract(); | ||
if (extractedResult.kind === "ERROR") | ||
return extractedResult; | ||
if (extractedResult.kind === "ERROR") { | ||
(0, helpers_1.extend)(errors, extractedResult.err); | ||
continue; | ||
} | ||
try { | ||
@@ -147,6 +126,23 @@ for (var _e = (e_2 = void 0, __values(extractedResult.value)), _f = _e.next(); !_f.done; _f = _e.next()) { | ||
} | ||
var docResult = ctx.resolveTypes({ kind: graphql_1.Kind.DOCUMENT, definitions: definitions }); | ||
if (errors.length > 0) { | ||
return (0, DiagnosticError_1.err)(errors); | ||
} | ||
// If you define a field on an interface using the functional style, we need to add | ||
// that field to each concrete type as well. This must be done after all types are created, | ||
// but before we validate the schema. | ||
var definitionsResult = ctx.handleAbstractDefinitions(definitions); | ||
if (definitionsResult.kind === "ERROR") { | ||
return definitionsResult; | ||
} | ||
var docResult = ctx.resolveTypes({ | ||
kind: graphql_1.Kind.DOCUMENT, | ||
definitions: definitionsResult.value | ||
}); | ||
if (docResult.kind === "ERROR") | ||
return docResult; | ||
var doc = docResult.value; | ||
var subscriptionsValidationResult = ctx.validateAsyncIterableFields(doc); | ||
if (subscriptionsValidationResult.kind === "ERROR") { | ||
return subscriptionsValidationResult; | ||
} | ||
// TODO: Currently this does not detect definitions that shadow builtins | ||
@@ -153,0 +149,0 @@ // (`String`, `Int`, etc). However, if we pass a second param (extending an |
@@ -5,5 +5,8 @@ import { DocumentNode, GraphQLSchema } from "graphql"; | ||
export declare const EXPORTED_DIRECTIVE = "exported"; | ||
export declare const EXPORTED_FILENAME_ARG = "filename"; | ||
export declare const JS_MODULE_PATH_ARG = "jsModulePath"; | ||
export declare const TS_MODULE_PATH_ARG = "tsModulePath"; | ||
export declare const ARG_COUNT = "argCount"; | ||
export declare const EXPORTED_FUNCTION_NAME_ARG = "functionName"; | ||
export declare const ASYNC_ITERABLE_TYPE_DIRECTIVE = "asyncIterable"; | ||
export declare const DIRECTIVES_AST: DocumentNode; | ||
export declare function applyServerDirectives(schema: GraphQLSchema): GraphQLSchema; |
@@ -50,13 +50,18 @@ "use strict"; | ||
exports.__esModule = true; | ||
exports.applyServerDirectives = exports.DIRECTIVES_AST = exports.EXPORTED_FUNCTION_NAME_ARG = exports.EXPORTED_FILENAME_ARG = exports.EXPORTED_DIRECTIVE = exports.METHOD_NAME_ARG = exports.METHOD_NAME_DIRECTIVE = void 0; | ||
exports.applyServerDirectives = exports.DIRECTIVES_AST = exports.ASYNC_ITERABLE_TYPE_DIRECTIVE = exports.EXPORTED_FUNCTION_NAME_ARG = exports.ARG_COUNT = exports.TS_MODULE_PATH_ARG = exports.JS_MODULE_PATH_ARG = exports.EXPORTED_DIRECTIVE = exports.METHOD_NAME_ARG = exports.METHOD_NAME_DIRECTIVE = void 0; | ||
var utils_1 = require("@graphql-tools/utils"); | ||
var graphql_1 = require("graphql"); | ||
var gratsRoot_1 = require("./gratsRoot"); | ||
// TODO: Rename to be generic since it can apply to properties as well as methods. | ||
exports.METHOD_NAME_DIRECTIVE = "methodName"; | ||
exports.METHOD_NAME_ARG = "name"; | ||
exports.EXPORTED_DIRECTIVE = "exported"; | ||
exports.EXPORTED_FILENAME_ARG = "filename"; | ||
exports.JS_MODULE_PATH_ARG = "jsModulePath"; | ||
exports.TS_MODULE_PATH_ARG = "tsModulePath"; | ||
exports.ARG_COUNT = "argCount"; | ||
exports.EXPORTED_FUNCTION_NAME_ARG = "functionName"; | ||
exports.DIRECTIVES_AST = (0, graphql_1.parse)("\n directive @".concat(exports.METHOD_NAME_DIRECTIVE, "(").concat(exports.METHOD_NAME_ARG, ": String!) on FIELD_DEFINITION\n directive @").concat(exports.EXPORTED_DIRECTIVE, "(\n ").concat(exports.EXPORTED_FILENAME_ARG, ": String!,\n ").concat(exports.EXPORTED_FUNCTION_NAME_ARG, ": String!\n ) on FIELD_DEFINITION\n")); | ||
exports.ASYNC_ITERABLE_TYPE_DIRECTIVE = "asyncIterable"; | ||
exports.DIRECTIVES_AST = (0, graphql_1.parse)("\n directive @".concat(exports.ASYNC_ITERABLE_TYPE_DIRECTIVE, " on FIELD_DEFINITION\n directive @").concat(exports.METHOD_NAME_DIRECTIVE, "(").concat(exports.METHOD_NAME_ARG, ": String!) on FIELD_DEFINITION\n directive @").concat(exports.EXPORTED_DIRECTIVE, "(\n ").concat(exports.JS_MODULE_PATH_ARG, ": String!,\n ").concat(exports.TS_MODULE_PATH_ARG, ": String!,\n ").concat(exports.EXPORTED_FUNCTION_NAME_ARG, ": String!\n ").concat(exports.ARG_COUNT, ": Int!\n ) on FIELD_DEFINITION\n")); | ||
function applyServerDirectives(schema) { | ||
// TODO: Throw if the schema is missing our directives! | ||
var _a; | ||
@@ -67,3 +72,3 @@ // TODO: Do we really need all of mapSchema here or can we create our own | ||
_a[utils_1.MapperKind.OBJECT_FIELD] = function (fieldConfig) { | ||
var _a, _b; | ||
var _a, _b, _c; | ||
var newFieldConfig = fieldConfig; | ||
@@ -78,2 +83,6 @@ var methodNameDirective = (_a = (0, utils_1.getDirective)(schema, fieldConfig, exports.METHOD_NAME_DIRECTIVE)) === null || _a === void 0 ? void 0 : _a[0]; | ||
} | ||
var asyncIterableDirective = (_c = (0, utils_1.getDirective)(schema, fieldConfig, exports.ASYNC_ITERABLE_TYPE_DIRECTIVE)) === null || _c === void 0 ? void 0 : _c[0]; | ||
if (asyncIterableDirective != null) { | ||
newFieldConfig = __assign(__assign({}, newFieldConfig), { subscribe: newFieldConfig.resolve, resolve: function (payload) { return payload; } }); | ||
} | ||
return newFieldConfig; | ||
@@ -109,16 +118,21 @@ }, | ||
// TODO: Does this work in the browser? | ||
var filename = (0, gratsRoot_1.resolveRelativePath)(methodNameDirective[exports.EXPORTED_FILENAME_ARG]); | ||
var jsModulePath = (0, gratsRoot_1.resolveRelativePath)(methodNameDirective[exports.JS_MODULE_PATH_ARG]); | ||
var tsModulePath = (0, gratsRoot_1.resolveRelativePath)(methodNameDirective[exports.TS_MODULE_PATH_ARG]); | ||
var functionName = methodNameDirective[exports.EXPORTED_FUNCTION_NAME_ARG]; | ||
var mod = undefined; | ||
var modPromise = undefined; | ||
return __assign(__assign({}, fieldConfig), { resolve: function (source, args, context, info) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var mod, e_1, resolve; | ||
var e_1, resolve; | ||
return __generator(this, function (_a) { | ||
var _b; | ||
switch (_a.label) { | ||
case 0: | ||
mod = {}; | ||
if (modPromise == null) { | ||
modPromise = importWithFallback(jsModulePath, tsModulePath); | ||
} | ||
if (!(mod == null)) return [3 /*break*/, 4]; | ||
_a.label = 1; | ||
case 1: | ||
_a.trys.push([1, 3, , 4]); | ||
return [4 /*yield*/, (_b = filename, Promise.resolve().then(function () { return require(_b); }))]; | ||
return [4 /*yield*/, modPromise]; | ||
case 2: | ||
@@ -129,3 +143,3 @@ mod = _a.sent(); | ||
e_1 = _a.sent(); | ||
console.error("Grats Error: Failed to import module `".concat(filename, "`. You may need to rerun Grats.")); | ||
console.error(loadModuleErrorMessage(jsModulePath, tsModulePath)); | ||
throw e_1; | ||
@@ -135,3 +149,4 @@ case 4: | ||
if (typeof resolve !== "function") { | ||
throw new Error("Grats Error: Expected `".concat(filename, "` to have a named export `").concat(functionName, "` that is a function, but it was `").concat(typeof resolve, "`. You may need to rerun Grats.")); | ||
// TODO: Better error message that indicates if it was loaded from JS or TS. | ||
throw new Error("Grats Error: Expected `".concat(tsModulePath, "` to have a named export `").concat(functionName, "` that is a function, but it was `").concat(typeof resolve, "`. You may need to rerun Grats or regenerate the JavaScript version of your module by rerunning the TypeScript compiler.")); | ||
} | ||
@@ -144,1 +159,33 @@ return [2 /*return*/, resolve(source, args, context, info)]; | ||
} | ||
// When people use Grats with loaders like `esbuild-register` or `ts-node`, the | ||
// compiled JavaScript version of the file may not exist on disk. In these specific | ||
// cases, esbuild or ts-node can load the file via its TypeScript source, so we try | ||
// falling back to that. | ||
function importWithFallback(jsModulePath, tsModulePath) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var e_2; | ||
return __generator(this, function (_a) { | ||
var _b, _c; | ||
switch (_a.label) { | ||
case 0: | ||
_a.trys.push([0, 2, , 4]); | ||
return [4 /*yield*/, (_b = tsModulePath, Promise.resolve().then(function () { return require(_b); }))]; | ||
case 1: | ||
// We start with the .ts version because if both exist, and can be loaded, the .ts version is | ||
// going to be more up to date. The downside is that this causes some extra work to be done in | ||
// in prod. This should be manageable since we cache the loaded module for each field. | ||
// It's important that we await here so that we catch the error if the module doesn't exist or | ||
// cannot be parsed. | ||
return [2 /*return*/, _a.sent()]; | ||
case 2: | ||
e_2 = _a.sent(); | ||
return [4 /*yield*/, (_c = jsModulePath, Promise.resolve().then(function () { return require(_c); }))]; | ||
case 3: return [2 /*return*/, _a.sent()]; | ||
case 4: return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
} | ||
function loadModuleErrorMessage(jsPath, tsPath) { | ||
return "Grats Error: Failed to import module. Tried loading from two locations:\n* `".concat(jsPath, "`\n* `").concat(tsPath, "`\n\nThis can happen for a few reasons:\n\n* You resolver has moved and you need to rerun Grats to regenerate your schema.\n* Your TypeScript code has changed and you need to rerun `tsc` to generate the JavaScript variant of the file.\n* You compiled your TypeScript with a different TSConfig than what you ran Grats with.\n* The Grats NPM module moved between when you ran Grats and when you ran your server."); | ||
} |
@@ -64,27 +64,39 @@ "use strict"; | ||
var lib_1 = require("../lib"); | ||
var utils_1 = require("@graphql-tools/utils"); | ||
function main() { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var write, filter, filterRegex, failures, testDirs_1, testDirs_1_1, _a, fixturesDir_1, transformer, runner, e_1_1; | ||
var e_1, _b; | ||
return __generator(this, function (_c) { | ||
switch (_c.label) { | ||
var ts = require("typescript"); | ||
var graphql_1 = require("graphql"); | ||
var commander_1 = require("commander"); | ||
var Locate_1 = require("../Locate"); | ||
var DiagnosticError_1 = require("../utils/DiagnosticError"); | ||
var printSchema_1 = require("../printSchema"); | ||
var fs_1 = require("fs"); | ||
var codegen_1 = require("../codegen"); | ||
var program = new commander_1.Command(); | ||
program | ||
.name("grats-tests") | ||
.description("Run Grats' internal tests") | ||
.option("-w, --write", "Write the actual output of the test to the expected output files. Useful for updating tests.") | ||
.option("-f, --filter <FILTER_REGEX>", "A regex to filter the tests to run. Only tests with a file path matching the regex will be run.") | ||
.action(function (_a) { | ||
var filter = _a.filter, write = _a.write; | ||
return __awaiter(void 0, void 0, void 0, function () { | ||
var filterRegex, failures, testDirs_1, testDirs_1_1, _b, fixturesDir_1, transformer, extension, runner, e_1_1; | ||
var e_1, _c; | ||
return __generator(this, function (_d) { | ||
switch (_d.label) { | ||
case 0: | ||
write = process.argv.some(function (arg) { return arg === "--write"; }); | ||
filter = process.argv.find(function (arg) { return arg.startsWith("--filter="); }); | ||
filterRegex = filter != null ? filter.slice(9) : null; | ||
filterRegex = filter !== null && filter !== void 0 ? filter : null; | ||
failures = false; | ||
_c.label = 1; | ||
_d.label = 1; | ||
case 1: | ||
_c.trys.push([1, 6, 7, 8]); | ||
_d.trys.push([1, 6, 7, 8]); | ||
testDirs_1 = __values(testDirs), testDirs_1_1 = testDirs_1.next(); | ||
_c.label = 2; | ||
_d.label = 2; | ||
case 2: | ||
if (!!testDirs_1_1.done) return [3 /*break*/, 5]; | ||
_a = testDirs_1_1.value, fixturesDir_1 = _a.fixturesDir, transformer = _a.transformer; | ||
runner = new TestRunner_1["default"](fixturesDir_1, write, filterRegex, transformer); | ||
_b = testDirs_1_1.value, fixturesDir_1 = _b.fixturesDir, transformer = _b.transformer, extension = _b.extension; | ||
runner = new TestRunner_1["default"](fixturesDir_1, !!write, filterRegex, extension, transformer); | ||
return [4 /*yield*/, runner.run()]; | ||
case 3: | ||
failures = !(_c.sent()) || failures; | ||
_c.label = 4; | ||
failures = !(_d.sent()) || failures; | ||
_d.label = 4; | ||
case 4: | ||
@@ -95,3 +107,3 @@ testDirs_1_1 = testDirs_1.next(); | ||
case 6: | ||
e_1_1 = _c.sent(); | ||
e_1_1 = _d.sent(); | ||
e_1 = { error: e_1_1 }; | ||
@@ -101,3 +113,3 @@ return [3 /*break*/, 8]; | ||
try { | ||
if (testDirs_1_1 && !testDirs_1_1.done && (_b = testDirs_1["return"])) _b.call(testDirs_1); | ||
if (testDirs_1_1 && !testDirs_1_1.done && (_c = testDirs_1["return"])) _c.call(testDirs_1); | ||
} | ||
@@ -114,11 +126,16 @@ finally { if (e_1) throw e_1.error; } | ||
}); | ||
} | ||
}); | ||
var gratsDir = path.join(__dirname, "../.."); | ||
var fixturesDir = path.join(__dirname, "fixtures"); | ||
var integrationFixturesDir = path.join(__dirname, "integrationFixtures"); | ||
var codegenFixturesDir = path.join(__dirname, "codegenFixtures"); | ||
var testDirs = [ | ||
{ | ||
fixturesDir: fixturesDir, | ||
extension: ".ts", | ||
transformer: function (code, fileName) { | ||
var firstLine = code.split("\n")[0]; | ||
var options = { | ||
nullableByDefault: true | ||
nullableByDefault: true, | ||
schemaHeader: null | ||
}; | ||
@@ -131,3 +148,3 @@ if (firstLine.startsWith("// {")) { | ||
var files = ["".concat(fixturesDir, "/").concat(fileName), "src/Types.ts"]; | ||
var parsedOptions = { | ||
var parsedOptions = (0, lib_1.validateGratsOptions)({ | ||
options: {}, | ||
@@ -139,13 +156,94 @@ raw: { | ||
fileNames: files | ||
}; | ||
var schemaResult = (0, lib_1.buildSchemaResult)(parsedOptions); | ||
}); | ||
// https://stackoverflow.com/a/66604532/1263117 | ||
var compilerHost = ts.createCompilerHost(parsedOptions.options, | ||
/* setParentNodes this is needed for finding jsDocs */ | ||
true); | ||
var schemaResult = (0, lib_1.buildSchemaResultWithHost)(parsedOptions, compilerHost); | ||
if (schemaResult.kind === "ERROR") { | ||
return schemaResult.err.formatDiagnosticsWithContext(); | ||
} | ||
return (0, utils_1.printSchemaWithDirectives)(schemaResult.value, { | ||
assumeValid: true | ||
}); | ||
// We run codegen here just ensure that it doesn't throw. | ||
(0, codegen_1.codegen)(schemaResult.value, "".concat(fixturesDir, "/").concat(fileName)); | ||
var LOCATION_REGEX = /^\/\/ Locate: (.*)/; | ||
var locationMatch = code.match(LOCATION_REGEX); | ||
if (locationMatch != null) { | ||
var locResult = (0, Locate_1.locate)(schemaResult.value, locationMatch[1].trim()); | ||
if (locResult.kind === "ERROR") { | ||
return locResult.err; | ||
} | ||
return new DiagnosticError_1.ReportableDiagnostics(compilerHost, [ | ||
(0, DiagnosticError_1.diagnosticAtGraphQLLocation)("Located here", locResult.value), | ||
]).formatDiagnosticsWithContext(); | ||
} | ||
else { | ||
return (0, printSchema_1.printGratsSDL)(schemaResult.value, options); | ||
} | ||
} | ||
}, | ||
{ | ||
fixturesDir: integrationFixturesDir, | ||
extension: ".ts", | ||
transformer: function (code, fileName) { return __awaiter(void 0, void 0, void 0, function () { | ||
var filePath, server, options, files, parsedOptions, schemaResult, schema, data; | ||
return __generator(this, function (_a) { | ||
var _b; | ||
switch (_a.label) { | ||
case 0: | ||
filePath = "".concat(integrationFixturesDir, "/").concat(fileName); | ||
return [4 /*yield*/, (_b = filePath, Promise.resolve().then(function () { return require(_b); }))]; | ||
case 1: | ||
server = _a.sent(); | ||
if (server.query == null || typeof server.query !== "string") { | ||
throw new Error("Expected `".concat(filePath, "` to export a query text as `query`")); | ||
} | ||
options = { | ||
nullableByDefault: true | ||
}; | ||
files = [filePath, "src/Types.ts"]; | ||
parsedOptions = { | ||
options: { | ||
// Required to enable ts-node to locate function exports | ||
rootDir: gratsDir, | ||
outDir: "dist", | ||
configFilePath: "tsconfig.json" | ||
}, | ||
raw: { | ||
grats: options | ||
}, | ||
errors: [], | ||
fileNames: files | ||
}; | ||
schemaResult = (0, lib_1.buildSchemaResult)(parsedOptions); | ||
if (schemaResult.kind === "ERROR") { | ||
throw new Error(schemaResult.err.formatDiagnosticsWithContext()); | ||
} | ||
schema = schemaResult.value; | ||
// We run codegen here just ensure that it doesn't throw. | ||
(0, codegen_1.codegen)(schema, filePath); | ||
return [4 /*yield*/, (0, graphql_1.graphql)({ | ||
schema: schema, | ||
source: server.query | ||
})]; | ||
case 2: | ||
data = _a.sent(); | ||
return [2 /*return*/, JSON.stringify(data, null, 2)]; | ||
} | ||
}); | ||
}); } | ||
}, | ||
{ | ||
fixturesDir: codegenFixturesDir, | ||
extension: ".graphql", | ||
transformer: function (code, fileName) { return __awaiter(void 0, void 0, void 0, function () { | ||
var filePath, sdl, schema; | ||
return __generator(this, function (_a) { | ||
filePath = "".concat(codegenFixturesDir, "/").concat(fileName); | ||
sdl = (0, fs_1.readFileSync)(filePath, "utf8"); | ||
schema = (0, graphql_1.buildSchema)(sdl); | ||
return [2 /*return*/, (0, codegen_1.codegen)(schema, filePath)]; | ||
}); | ||
}); } | ||
}, | ||
]; | ||
main(); | ||
program.parse(); |
@@ -15,3 +15,3 @@ type Transformer = (code: string, filename: string) => Promise<string> | string; | ||
_transformer: Transformer; | ||
constructor(fixturesDir: string, write: boolean, filter: string | null, transformer: Transformer); | ||
constructor(fixturesDir: string, write: boolean, filter: string | null, extension: string, transformer: Transformer); | ||
run(): Promise<boolean>; | ||
@@ -18,0 +18,0 @@ _testFixture(fixture: string): Promise<void>; |
@@ -59,3 +59,3 @@ "use strict"; | ||
var TestRunner = /** @class */ (function () { | ||
function TestRunner(fixturesDir, write, filter, transformer) { | ||
function TestRunner(fixturesDir, write, filter, extension, transformer) { | ||
var e_1, _a; | ||
@@ -73,5 +73,6 @@ this._testFixtures = []; | ||
var fileName = _c.value; | ||
if (fileName.endsWith(".ts")) { | ||
if (fileName.endsWith(extension)) { | ||
this._testFixtures.push(fileName); | ||
if (filterRegex != null && !fileName.match(filterRegex)) { | ||
var filePath = path.join(fixturesDir, fileName); | ||
if (filterRegex != null && !filePath.match(filterRegex)) { | ||
this._skip.add(fileName); | ||
@@ -78,0 +79,0 @@ } |
@@ -1,6 +0,26 @@ | ||
import { DocumentNode, NameNode } from "graphql"; | ||
import { DefinitionNode, DocumentNode, FieldDefinitionNode, InterfaceTypeDefinitionNode, InterfaceTypeExtensionNode, Location, NameNode, ObjectTypeDefinitionNode, ObjectTypeExtensionNode } from "graphql"; | ||
import * as ts from "typescript"; | ||
import { DiagnosticResult, DiagnosticsResult } from "./utils/DiagnosticError"; | ||
import { InterfaceMap } from "./InterfaceGraph"; | ||
export declare const UNRESOLVED_REFERENCE_NAME = "__UNRESOLVED_REFERENCE__"; | ||
type NameDefinition = { | ||
name: NameNode; | ||
kind: "TYPE" | "INTERFACE" | "UNION" | "SCALAR" | "INPUT_OBJECT" | "ENUM"; | ||
}; | ||
export type GratsDefinitionNode = DefinitionNode | AbstractFieldDefinitionNode; | ||
export type AbstractFieldDefinitionNode = { | ||
readonly kind: "AbstractFieldDefinition"; | ||
readonly loc: Location; | ||
readonly onType: NameNode; | ||
readonly field: FieldDefinitionNode; | ||
}; | ||
/** | ||
* Information about the GraphQL context type. We track the first value we see, | ||
* and then validate that any other values we see are the same. | ||
*/ | ||
type GqlContext = { | ||
declaration: ts.Node; | ||
firstReference: ts.Node; | ||
}; | ||
/** | ||
* Used to track TypeScript references. | ||
@@ -21,13 +41,25 @@ * | ||
_options: ts.ParsedCommandLine; | ||
_symbolToName: Map<ts.Symbol, string>; | ||
_symbolToName: Map<ts.Symbol, NameDefinition>; | ||
_unresolvedTypes: Map<NameNode, ts.Symbol>; | ||
gqlContext: GqlContext | null; | ||
hasTypename: Set<string>; | ||
constructor(options: ts.ParsedCommandLine, checker: ts.TypeChecker, host: ts.CompilerHost); | ||
recordTypeName(node: ts.Node, name: string): void; | ||
recordTypeName(node: ts.Node, name: NameNode, kind: NameDefinition["kind"]): void; | ||
recordHasTypenameField(name: string): void; | ||
markUnresolvedType(node: ts.Node, name: NameNode): void; | ||
findSymbolDeclaration(startSymbol: ts.Symbol): ts.Declaration | null; | ||
resolveSymbol(startSymbol: ts.Symbol): ts.Symbol; | ||
resolveTypes(doc: DocumentNode): DiagnosticsResult<DocumentNode>; | ||
validateAsyncIterableFields(doc: DocumentNode): DiagnosticsResult<void>; | ||
validateField(t: ObjectTypeDefinitionNode | ObjectTypeExtensionNode | InterfaceTypeDefinitionNode | InterfaceTypeExtensionNode): ts.Diagnostic | void; | ||
handleAbstractDefinitions(docs: GratsDefinitionNode[]): DiagnosticsResult<DefinitionNode[]>; | ||
addAbstractFieldDefinition(doc: AbstractFieldDefinitionNode, interfaceGraph: InterfaceMap): DiagnosticsResult<DefinitionNode[]>; | ||
resolveNamedType(unresolved: NameNode): DiagnosticResult<NameNode>; | ||
validateInterfaceImplementorsHaveTypenameField(): DiagnosticResult<null>; | ||
getDestFilePath(sourceFile: ts.SourceFile): string; | ||
err(loc: Location, message: string, relatedInformation?: ts.DiagnosticRelatedInformation[]): ts.Diagnostic; | ||
relatedInformation(loc: Location, message: string): ts.DiagnosticRelatedInformation; | ||
getDestFilePath(sourceFile: ts.SourceFile): { | ||
jsModulePath: string; | ||
tsModulePath: string; | ||
}; | ||
} | ||
export {}; |
@@ -13,2 +13,13 @@ "use strict"; | ||
}; | ||
var __values = (this && this.__values) || function(o) { | ||
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; | ||
if (m) return m.call(o); | ||
if (o && typeof o.length === "number") return { | ||
next: function () { | ||
if (o && i >= o.length) o = void 0; | ||
return { value: o && o[i++], done: !o }; | ||
} | ||
}; | ||
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); | ||
}; | ||
exports.__esModule = true; | ||
@@ -20,2 +31,7 @@ exports.TypeContext = exports.UNRESOLVED_REFERENCE_NAME = void 0; | ||
var gratsRoot_1 = require("./gratsRoot"); | ||
var serverDirectives_1 = require("./serverDirectives"); | ||
var Extractor_1 = require("./Extractor"); | ||
var E = require("./Errors"); | ||
var InterfaceGraph_1 = require("./InterfaceGraph"); | ||
var helpers_1 = require("./utils/helpers"); | ||
exports.UNRESOLVED_REFERENCE_NAME = "__UNRESOLVED_REFERENCE__"; | ||
@@ -38,2 +54,5 @@ /** | ||
this._unresolvedTypes = new Map(); | ||
// The resolver context declaration, if it has been encountered. | ||
// Gets mutated by Extractor. | ||
this.gqlContext = null; | ||
this.hasTypename = new Set(); | ||
@@ -44,3 +63,3 @@ this._options = options; | ||
} | ||
TypeContext.prototype.recordTypeName = function (node, name) { | ||
TypeContext.prototype.recordTypeName = function (node, name, kind) { | ||
var symbol = this.checker.getSymbolAtLocation(node); | ||
@@ -55,3 +74,3 @@ if (symbol == null) { | ||
} | ||
this._symbolToName.set(symbol, name); | ||
this._symbolToName.set(symbol, { name: name, kind: kind }); | ||
}; | ||
@@ -67,13 +86,30 @@ TypeContext.prototype.recordHasTypenameField = function (name) { | ||
} | ||
if (symbol.flags & ts.SymbolFlags.Alias) { | ||
// Follow any aliases to get the real type declaration. | ||
this._unresolvedTypes.set(name, this.resolveSymbol(symbol)); | ||
}; | ||
TypeContext.prototype.findSymbolDeclaration = function (startSymbol) { | ||
var _a; | ||
var symbol = this.resolveSymbol(startSymbol); | ||
var declaration = (_a = symbol.declarations) === null || _a === void 0 ? void 0 : _a[0]; | ||
return declaration !== null && declaration !== void 0 ? declaration : null; | ||
}; | ||
// Follow symbol aliases until we find the original symbol. Accounts for | ||
// cyclical aliases. | ||
TypeContext.prototype.resolveSymbol = function (startSymbol) { | ||
var symbol = startSymbol; | ||
var visitedSymbols = new Set(); | ||
while (ts.SymbolFlags.Alias & symbol.flags) { | ||
if (visitedSymbols.has(symbol)) { | ||
throw new Error("Cyclical alias detected. Breaking resolution."); | ||
} | ||
visitedSymbols.add(symbol); | ||
symbol = this.checker.getAliasedSymbol(symbol); | ||
} | ||
this._unresolvedTypes.set(name, symbol); | ||
return symbol; | ||
}; | ||
TypeContext.prototype.resolveTypes = function (doc) { | ||
var _a; | ||
var _this = this; | ||
var errors = []; | ||
var newDoc = (0, graphql_1.visit)(doc, { | ||
Name: function (t) { | ||
var newDoc = (0, graphql_1.visit)(doc, (_a = {}, | ||
_a[graphql_1.Kind.NAME] = function (t) { | ||
var namedTypeResult = _this.resolveNamedType(t); | ||
@@ -85,4 +121,4 @@ if (namedTypeResult.kind === "ERROR") { | ||
return namedTypeResult.value; | ||
} | ||
}); | ||
}, | ||
_a)); | ||
if (errors.length > 0) { | ||
@@ -93,2 +129,200 @@ return (0, DiagnosticError_1.err)(errors); | ||
}; | ||
// Ensure that all fields on `Subscription` return an AsyncIterable, and that no other | ||
// fields do. | ||
TypeContext.prototype.validateAsyncIterableFields = function (doc) { | ||
var _a; | ||
var _this = this; | ||
var errors = []; | ||
var visitNode = function (t) { | ||
var validateFieldsResult = _this.validateField(t); | ||
if (validateFieldsResult != null) { | ||
errors.push(validateFieldsResult); | ||
} | ||
}; | ||
(0, graphql_1.visit)(doc, (_a = {}, | ||
_a[graphql_1.Kind.INTERFACE_TYPE_DEFINITION] = visitNode, | ||
_a[graphql_1.Kind.INTERFACE_TYPE_EXTENSION] = visitNode, | ||
_a[graphql_1.Kind.OBJECT_TYPE_DEFINITION] = visitNode, | ||
_a[graphql_1.Kind.OBJECT_TYPE_EXTENSION] = visitNode, | ||
_a)); | ||
if (errors.length > 0) { | ||
return (0, DiagnosticError_1.err)(errors); | ||
} | ||
return (0, DiagnosticError_1.ok)(undefined); | ||
}; | ||
TypeContext.prototype.validateField = function (t) { | ||
var e_1, _a; | ||
var _b; | ||
if (t.fields == null) | ||
return; | ||
// Note: We assume the default name is used here. When custom operation types are supported | ||
// we'll need to update this. | ||
var isSubscription = t.name.value === "Subscription" && | ||
(t.kind === graphql_1.Kind.OBJECT_TYPE_DEFINITION || | ||
t.kind === graphql_1.Kind.OBJECT_TYPE_EXTENSION); | ||
try { | ||
for (var _c = __values(t.fields), _d = _c.next(); !_d.done; _d = _c.next()) { | ||
var field = _d.value; | ||
var asyncDirective = (_b = field.directives) === null || _b === void 0 ? void 0 : _b.find(function (directive) { return directive.name.value === serverDirectives_1.ASYNC_ITERABLE_TYPE_DIRECTIVE; }); | ||
if (isSubscription && asyncDirective == null) { | ||
if (field.type.loc == null) { | ||
throw new Error("Expected field type to have a location."); | ||
} | ||
return this.err(field.type.loc, E.subscriptionFieldNotAsyncIterable()); | ||
} | ||
if (!isSubscription && asyncDirective != null) { | ||
if (asyncDirective.loc == null) { | ||
throw new Error("Expected asyncDirective to have a location."); | ||
} | ||
return this.err(asyncDirective.loc, // Directive location is the AsyncIterable type. | ||
E.nonSubscriptionFieldAsyncIterable()); | ||
} | ||
} | ||
} | ||
catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
finally { | ||
try { | ||
if (_d && !_d.done && (_a = _c["return"])) _a.call(_c); | ||
} | ||
finally { if (e_1) throw e_1.error; } | ||
} | ||
}; | ||
// TODO: Is this still used? | ||
TypeContext.prototype.handleAbstractDefinitions = function (docs) { | ||
var e_2, _a; | ||
var newDocs = []; | ||
var errors = []; | ||
var interfaceGraphResult = (0, InterfaceGraph_1.computeInterfaceMap)(this, docs); | ||
if (interfaceGraphResult.kind === "ERROR") { | ||
return interfaceGraphResult; | ||
} | ||
var interfaceGraph = interfaceGraphResult.value; | ||
try { | ||
for (var docs_1 = __values(docs), docs_1_1 = docs_1.next(); !docs_1_1.done; docs_1_1 = docs_1.next()) { | ||
var doc = docs_1_1.value; | ||
if (doc.kind === "AbstractFieldDefinition") { | ||
var abstractDocResults = this.addAbstractFieldDefinition(doc, interfaceGraph); | ||
if (abstractDocResults.kind === "ERROR") { | ||
(0, helpers_1.extend)(errors, abstractDocResults.err); | ||
} | ||
else { | ||
(0, helpers_1.extend)(newDocs, abstractDocResults.value); | ||
} | ||
} | ||
else { | ||
newDocs.push(doc); | ||
} | ||
} | ||
} | ||
catch (e_2_1) { e_2 = { error: e_2_1 }; } | ||
finally { | ||
try { | ||
if (docs_1_1 && !docs_1_1.done && (_a = docs_1["return"])) _a.call(docs_1); | ||
} | ||
finally { if (e_2) throw e_2.error; } | ||
} | ||
if (errors.length > 0) { | ||
return (0, DiagnosticError_1.err)(errors); | ||
} | ||
return (0, DiagnosticError_1.ok)(newDocs); | ||
}; | ||
// A field definition may be on a concrete type, or on an interface. If it's on an interface, | ||
// we need to add it to each concrete type that implements the interface. | ||
TypeContext.prototype.addAbstractFieldDefinition = function (doc, interfaceGraph) { | ||
var e_3, _a; | ||
var _b; | ||
var newDocs = []; | ||
var typeNameResult = this.resolveNamedType(doc.onType); | ||
if (typeNameResult.kind === "ERROR") { | ||
return (0, DiagnosticError_1.err)([typeNameResult.err]); | ||
} | ||
var symbol = this._unresolvedTypes.get(doc.onType); | ||
if (symbol == null) { | ||
// This should have already been handled by resolveNamedType | ||
throw new Error("Expected to find unresolved type."); | ||
} | ||
var nameDefinition = this._symbolToName.get(symbol); | ||
if (nameDefinition == null) { | ||
// This should have already been handled by resolveNamedType | ||
throw new Error("Expected to find name definition."); | ||
} | ||
switch (nameDefinition.kind) { | ||
case "TYPE": | ||
// Extending a type, is just adding a field to it. | ||
newDocs.push({ | ||
kind: graphql_1.Kind.OBJECT_TYPE_EXTENSION, | ||
name: doc.onType, | ||
fields: [doc.field], | ||
loc: doc.loc | ||
}); | ||
break; | ||
case "INTERFACE": { | ||
// Extending an interface is a bit more complicated. We need to add the field | ||
// to the interface, and to each type that implements the interface. | ||
// The interface field definition is not executable, so we don't | ||
// need to annotate it with the details of the implementation. | ||
var directives = (_b = doc.field.directives) === null || _b === void 0 ? void 0 : _b.filter(function (directive) { | ||
return directive.name.value !== serverDirectives_1.EXPORTED_DIRECTIVE; | ||
}); | ||
newDocs.push({ | ||
kind: graphql_1.Kind.INTERFACE_TYPE_EXTENSION, | ||
name: doc.onType, | ||
fields: [__assign(__assign({}, doc.field), { directives: directives })] | ||
}); | ||
try { | ||
for (var _c = __values(interfaceGraph.get(nameDefinition.name.value)), _d = _c.next(); !_d.done; _d = _c.next()) { | ||
var implementor = _d.value; | ||
var name = { | ||
kind: graphql_1.Kind.NAME, | ||
value: implementor.name, | ||
loc: doc.loc | ||
}; | ||
switch (implementor.kind) { | ||
case "TYPE": | ||
newDocs.push({ | ||
kind: graphql_1.Kind.OBJECT_TYPE_EXTENSION, | ||
name: name, | ||
fields: [doc.field], | ||
loc: doc.loc | ||
}); | ||
break; | ||
case "INTERFACE": | ||
newDocs.push({ | ||
kind: graphql_1.Kind.INTERFACE_TYPE_EXTENSION, | ||
name: name, | ||
fields: [__assign(__assign({}, doc.field), { directives: directives })], | ||
loc: doc.loc | ||
}); | ||
break; | ||
} | ||
} | ||
} | ||
catch (e_3_1) { e_3 = { error: e_3_1 }; } | ||
finally { | ||
try { | ||
if (_d && !_d.done && (_a = _c["return"])) _a.call(_c); | ||
} | ||
finally { if (e_3) throw e_3.error; } | ||
} | ||
break; | ||
} | ||
default: { | ||
// Extending any other type of definition is not supported. | ||
var loc = doc.onType.loc; | ||
if (loc == null) { | ||
throw new Error("Expected onType to have a location."); | ||
} | ||
var relatedLoc = nameDefinition.name.loc; | ||
if (relatedLoc == null) { | ||
throw new Error("Expected nameDefinition to have a location."); | ||
} | ||
return (0, DiagnosticError_1.err)([ | ||
this.err(loc, E.invalidTypePassedToFieldFunction(), [ | ||
this.relatedInformation(relatedLoc, "This is the type that was passed to `@".concat(Extractor_1.FIELD_TAG, "`.")), | ||
]), | ||
]); | ||
} | ||
} | ||
return (0, DiagnosticError_1.ok)(newDocs); | ||
}; | ||
TypeContext.prototype.resolveNamedType = function (unresolved) { | ||
@@ -103,21 +337,32 @@ var symbol = this._unresolvedTypes.get(unresolved); | ||
} | ||
var name = this._symbolToName.get(symbol); | ||
if (name == null) { | ||
var nameDefinition = this._symbolToName.get(symbol); | ||
if (nameDefinition == null) { | ||
if (unresolved.loc == null) { | ||
throw new Error("Expected namedType to have a location."); | ||
} | ||
return (0, DiagnosticError_1.err)({ | ||
messageText: "This type is not a valid GraphQL type. Did you mean to annotate it's definition with a `/** @gql */` tag such as `/** @gqlType */` or `/** @gqlInput **/`?", | ||
start: unresolved.loc.start, | ||
length: unresolved.loc.end - unresolved.loc.start, | ||
category: ts.DiagnosticCategory.Error, | ||
code: DiagnosticError_1.FAKE_ERROR_CODE, | ||
file: ts.createSourceFile(unresolved.loc.source.name, unresolved.loc.source.body, ts.ScriptTarget.Latest) | ||
}); | ||
return (0, DiagnosticError_1.err)(this.err(unresolved.loc, E.unresolvedTypeReference())); | ||
} | ||
return (0, DiagnosticError_1.ok)(__assign(__assign({}, unresolved), { value: name })); | ||
return (0, DiagnosticError_1.ok)(__assign(__assign({}, unresolved), { value: nameDefinition.name.value })); | ||
}; | ||
TypeContext.prototype.validateInterfaceImplementorsHaveTypenameField = function () { | ||
return (0, DiagnosticError_1.ok)(null); | ||
TypeContext.prototype.err = function (loc, message, relatedInformation) { | ||
return { | ||
messageText: message, | ||
start: loc.start, | ||
length: loc.end - loc.start, | ||
category: ts.DiagnosticCategory.Error, | ||
code: DiagnosticError_1.FAKE_ERROR_CODE, | ||
file: ts.createSourceFile(loc.source.name, loc.source.body, ts.ScriptTarget.Latest), | ||
relatedInformation: relatedInformation | ||
}; | ||
}; | ||
TypeContext.prototype.relatedInformation = function (loc, message) { | ||
return { | ||
category: ts.DiagnosticCategory.Message, | ||
code: DiagnosticError_1.FAKE_ERROR_CODE, | ||
messageText: message, | ||
file: ts.createSourceFile(loc.source.name, loc.source.body, ts.ScriptTarget.Latest), | ||
start: loc.start, | ||
length: loc.end - loc.start | ||
}; | ||
}; | ||
TypeContext.prototype.getDestFilePath = function (sourceFile) { | ||
@@ -124,0 +369,0 @@ return (0, gratsRoot_1.getRelativeOutputPath)(this._options, sourceFile); |
@@ -17,5 +17,6 @@ import { GraphQLError, Location, Source } from "graphql"; | ||
export declare class ReportableDiagnostics { | ||
_host: ts.CompilerHost; | ||
_host: ts.FormatDiagnosticsHost; | ||
_diagnostics: ts.Diagnostic[]; | ||
constructor(host: ts.CompilerHost, diagnostics: ts.Diagnostic[]); | ||
constructor(host: ts.FormatDiagnosticsHost, diagnostics: ts.Diagnostic[]); | ||
static fromDiagnostics(diagnostics: ts.Diagnostic[]): ReportableDiagnostics; | ||
formatDiagnosticsWithColorAndContext(): string; | ||
@@ -22,0 +23,0 @@ formatDiagnosticsWithContext(): string; |
"use strict"; | ||
var __read = (this && this.__read) || function (o, n) { | ||
var m = typeof Symbol === "function" && o[Symbol.iterator]; | ||
if (!m) return o; | ||
var i = m.call(o), r, ar = [], e; | ||
try { | ||
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); | ||
} | ||
catch (error) { e = { error: error }; } | ||
finally { | ||
try { | ||
if (r && !r.done && (m = i["return"])) m.call(i); | ||
} | ||
finally { if (e) throw e.error; } | ||
} | ||
return ar; | ||
}; | ||
var __values = (this && this.__values) || function(o) { | ||
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; | ||
if (m) return m.call(o); | ||
if (o && typeof o.length === "number") return { | ||
next: function () { | ||
if (o && i >= o.length) o = void 0; | ||
return { value: o && o[i++], done: !o }; | ||
} | ||
}; | ||
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); | ||
}; | ||
exports.__esModule = true; | ||
@@ -18,2 +45,12 @@ exports.graphqlSourceToSourceFile = exports.diagnosticAtGraphQLLocation = exports.graphQlErrorToDiagnostic = exports.FAKE_ERROR_CODE = exports.ReportableDiagnostics = exports.err = exports.ok = void 0; | ||
} | ||
// If you don't have a host, for example if you error while parsing the | ||
// tsconfig, you can use this method and one will be created for you. | ||
ReportableDiagnostics.fromDiagnostics = function (diagnostics) { | ||
var formatHost = { | ||
getCanonicalFileName: function (path) { return path; }, | ||
getCurrentDirectory: ts.sys.getCurrentDirectory, | ||
getNewLine: function () { return ts.sys.newLine; } | ||
}; | ||
return new ReportableDiagnostics(formatHost, diagnostics); | ||
}; | ||
ReportableDiagnostics.prototype.formatDiagnosticsWithColorAndContext = function () { | ||
@@ -43,2 +80,3 @@ var formatted = ts.formatDiagnosticsWithColorAndContext(this._diagnostics, this._host); | ||
function graphQlErrorToDiagnostic(error) { | ||
var e_1, _a; | ||
var position = error.positions[0]; | ||
@@ -48,2 +86,41 @@ if (position == null) { | ||
} | ||
// Start with baseline location infromation | ||
var start = position; | ||
var length = 1; | ||
var relatedInformation; | ||
// Nodes have actual ranges (not just a single position), so we we have one | ||
// (or more!) use that instead. | ||
if (error.nodes != null && error.nodes.length > 0) { | ||
var _b = __read(error.nodes), node = _b[0], rest = _b.slice(1); | ||
if (node.loc != null) { | ||
start = node.loc.start; | ||
length = node.loc.end - node.loc.start; | ||
if (rest.length > 0) { | ||
relatedInformation = []; | ||
try { | ||
for (var rest_1 = __values(rest), rest_1_1 = rest_1.next(); !rest_1_1.done; rest_1_1 = rest_1.next()) { | ||
var relatedNode = rest_1_1.value; | ||
if (relatedNode.loc == null) { | ||
continue; | ||
} | ||
relatedInformation.push({ | ||
category: ts.DiagnosticCategory.Message, | ||
code: exports.FAKE_ERROR_CODE, | ||
messageText: "Related location", | ||
file: graphqlSourceToSourceFile(relatedNode.loc.source), | ||
start: relatedNode.loc.start, | ||
length: relatedNode.loc.end - relatedNode.loc.start | ||
}); | ||
} | ||
} | ||
catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
finally { | ||
try { | ||
if (rest_1_1 && !rest_1_1.done && (_a = rest_1["return"])) _a.call(rest_1); | ||
} | ||
finally { if (e_1) throw e_1.error; } | ||
} | ||
} | ||
} | ||
} | ||
var sourceFile; | ||
@@ -58,5 +135,5 @@ if (error.source != null) { | ||
category: ts.DiagnosticCategory.Error, | ||
start: position, | ||
// FIXME: Improve ranges | ||
length: 1 | ||
start: start, | ||
length: length, | ||
relatedInformation: relatedInformation | ||
}; | ||
@@ -63,0 +140,0 @@ } |
{ | ||
"name": "grats", | ||
"version": "0.0.0-main-bbba151a", | ||
"version": "0.0.0-main-bfb182a3", | ||
"main": "dist/src/index.js", | ||
@@ -26,3 +26,4 @@ "bin": "dist/src/cli.js", | ||
"process": "^0.11.10", | ||
"ts-node": "^10.9.1" | ||
"ts-node": "^10.9.1", | ||
"prettier": "^2.8.7" | ||
}, | ||
@@ -32,8 +33,14 @@ "prettier": { | ||
}, | ||
"packageManager": "pnpm@8.1.1", | ||
"engines": { | ||
"node": ">=16 <=21", | ||
"pnpm": "^8" | ||
}, | ||
"scripts": { | ||
"test": "ts-node --esm src/tests/test.ts", | ||
"test": "ts-node src/tests/test.ts", | ||
"integration-tests": "node src/tests/integration.mjs", | ||
"build": "tsc --build", | ||
"lint": "eslint src/**/*.ts" | ||
"format": "prettier . --write", | ||
"lint": "eslint src/**/*.ts && prettier . --check" | ||
} | ||
} |
539
README.md
@@ -1,4 +0,4 @@ | ||
# -=[ EXPERIMENTAL PRE ALPHA ]=- | ||
# -=[ ALPHA SOFTWARE ]=- | ||
**This is currently a proof of concept. It won't yet work on any real projects.** | ||
**Grats is still experimental. Feel free to try it out and give feedback, but the api is still in flux** | ||
@@ -9,478 +9,15 @@ # Grats: Implementation-First GraphQL for TypeScript | ||
Grats is a tool for statically infering GraphQL schema from your vanilla | ||
TypeScript code. | ||
**What if building a GraphQL server were as simple as just writing functions?** | ||
Just write your types and resolvers as regular TypeScript and annotate your | ||
types and fields with simple JSDoc tags. From there, Grats can extract your | ||
GraphQL schema automatically by statically analyzing your code and its types. No | ||
convoluted directive APIs to remember. No need to define your Schema at | ||
runtime with verbose builder APIs. | ||
When you write your GraphQL server in TypeScript, your fields and resolvers | ||
are _already_ annotated with type information. _Grats leverages your existing | ||
type annotations to automatically extract an executable GraphQL schema from your | ||
generic TypeScript resolver code._ | ||
By making your TypeScript implementation the source of truth, you entirely | ||
remove the question of mismatches between your implementation and your GraphQL | ||
schema definition. Your implementation _is_ the schema definition! | ||
By making your TypeScript implementation the source of truth, you never have to | ||
worry about validating that your implementation matches your schema. Your | ||
implementation _is_ your schema! | ||
## Example | ||
## Read the docs: https://grats.capt.dev/ | ||
```ts | ||
/** @gqlType */ | ||
export default class Query { | ||
/** @gqlField */ | ||
me(): UserResolver { | ||
return new UserResolver(); | ||
} | ||
/** | ||
* @gqlField | ||
* @deprecated Please use `me` instead. | ||
*/ | ||
viewer(): UserResolver { | ||
return new UserResolver(); | ||
} | ||
} | ||
/** | ||
* A user in our kick-ass system! | ||
* @gqlType User | ||
*/ | ||
class UserResolver { | ||
/** @gqlField */ | ||
name: string = 'Alice'; | ||
/** @gqlField */ | ||
greeting(args: { salutation: string }): string { | ||
return `${args.salutation}, ${this.name}`; | ||
} | ||
} | ||
``` | ||
Extracts the following GraphQL schema: | ||
```graphql | ||
type Query { | ||
me: User | ||
viewer: User @deprecated(reason: "Please use `me` instead.") | ||
} | ||
"""A user in our kick-ass system!""" | ||
type User { | ||
name: String | ||
greeting(salutation: String!): String | ||
} | ||
``` | ||
**Give it a try in the [online playground](https://capt.dev/grats-sandbox)!** | ||
## Quick Start | ||
For dev mode or small projects, Grats offers a runtime extraction mode. This is | ||
the easiest way to get started with Grats, although you may find that it causes | ||
a slow startup time. For larger projects, you probably want to use the build | ||
mode (documentation to come). | ||
```sh | ||
npm install express express-graphql grats | ||
``` | ||
**Ensure your project has a `tsconfig.json` file.** | ||
```ts | ||
import * as express from "express"; | ||
import { graphqlHTTP } from "express-graphql"; | ||
import { extractGratsSchemaAtRuntime } from "grats"; | ||
/** @gqlType */ | ||
class Query { | ||
/** @gqlField */ | ||
hello(): string { | ||
return "Hello world!"; | ||
} | ||
} | ||
const app = express(); | ||
// At runtime Grats will parse your TypeScript project (including this file!) and | ||
// extract the GraphQL schema. | ||
const schema = extractGratsSchemaAtRuntime({ | ||
emitSchemaFile: "./schema.graphql", | ||
}); | ||
app.use( | ||
"/graphql", | ||
graphqlHTTP({ schema, rootValue: new Query(), graphiql: true }), | ||
); | ||
app.listen(4000); | ||
console.log("Running a GraphQL API server at http://localhost:4000/graphql"); | ||
``` | ||
Try it out on [CodeSandbox](https://capt.dev/grats-sandbox)! | ||
## Configuration | ||
Grats has a few configuration options. They can be set in your project's | ||
`tsconfig.json` file: | ||
```json5 | ||
{ | ||
"grats": { | ||
// Should all fields be typed as nullable in accordance with GraphQL best | ||
// practices? | ||
// https://graphql.org/learn/best-practices/#nullability | ||
// | ||
// Individual fileds can declare themselves as nonnullable by adding the | ||
// docblock tag `@killsParentOnException`. | ||
"nullableByDefault": true, // Default: true | ||
// Should Grats error if it encounters a TypeScript type error? | ||
// Note that Grats will always error if it encounters a TypeScript syntax | ||
// error. | ||
"reportTypeScriptTypeErrors": false, // Default: false | ||
}, | ||
"compilerOptions": { | ||
// ... TypeScript config... | ||
} | ||
} | ||
``` | ||
## API Usage | ||
In order for Grats to extract GraphQL schema from your code, simply mark which | ||
TypeScript structures should be included in the schema by marking them with | ||
special JSDoc tags such as `/** @gqlType */` or `/** @gqlField */`. | ||
Any comment text preceding the JSDoc `@` tag will be used as that element's description. | ||
**Note that JSDocs must being with `/**` (two asterix).** However, they may be consolidated into a single line. | ||
The following JSDoc tags are supported: | ||
* [`@gqlType`](#gqltype) | ||
* [`@gqlInterface`](#gqlinterface) | ||
* [`@gqlField`](#gqlfield) | ||
* [`@gqlUnion`](#gqlunion) | ||
* [`@gqlScalar`](#gqlscalar) | ||
* [`@gqlEnum`](#gqlenum) | ||
* [`@gqlInput`](#gqlinput) | ||
Each tag maps directly to a concept in the GraphQL [Schema Definition | ||
Language](https://graphql.org/learn/schema/) (SDL). The documentation below aims | ||
to be complete, but our hope is that you feel empowered to just slap one of | ||
these docblock tags on the relevent TypeScript class/type/method/etc in your | ||
code, and let Grats' helpful error messages guide you. | ||
### @gqlType | ||
GraphQL types can be defined by placing a `@gqlType` docblock directly before a: | ||
* Class declaration | ||
* Interface declaration | ||
* Type alias of a literal type | ||
```ts | ||
/** | ||
* Here I can write a description of my type that will be included in the schema. | ||
* @gqlType <optional name of the type, if different from class name> | ||
*/ | ||
class MyClass { | ||
/** @gqlField */ | ||
someField: string; | ||
} | ||
``` | ||
```ts | ||
/** @gqlType */ | ||
interface MyType { | ||
/** @gqlField */ | ||
someField: string; | ||
} | ||
``` | ||
```ts | ||
/** @gqlType */ | ||
type MyType = { | ||
/** @gqlField */ | ||
someField: string; | ||
} | ||
``` | ||
Note: If your type implements a GraphQL interface or is a member of a GraphQL | ||
union, Grats will remind you to add a `__typename: "MyType"` property to your | ||
class or interface. | ||
### @gqlInterface | ||
GraphQL interfaces can be defined by placing a `@gqlInterface` docblock directly before an: | ||
* Interface declaration | ||
```ts | ||
/** | ||
* A description of my interface. | ||
* @gqlInterface <optional name of the type, if different from class name> | ||
*/ | ||
interface MyClass { | ||
/** @gqlField */ | ||
someField: string; | ||
} | ||
``` | ||
All `@gqlType` types which implement the interface in TypeScript will | ||
automatically implement it in GraphQL as well. | ||
**Note**: Types declared using type literals `type MyType = { ... }` cannot yet | ||
implement interfaces. For now, you must use a class declarations for types which | ||
implement interfaces. | ||
### @gqlField | ||
You can define GraphQL fields by placing a `@gqlField` directly before a: | ||
* Method declaration | ||
* Method signature | ||
* Property declaration | ||
* Property signature | ||
* Function declaration (with named export) | ||
```ts | ||
/** | ||
* A description of some field. | ||
* @gqlField <optional name of the field, if different from property name> | ||
*/ | ||
someField: string; | ||
/** @gqlField */ | ||
myField(): string { | ||
return "Hello World"; | ||
} | ||
``` | ||
#### Field nullability | ||
**Note**: By default, Grats makes all fields nullable in keeping with [GraphQL | ||
*best practices](https://graphql.org/learn/best-practices/#nullability). This | ||
*behavior can be changed by setting the config option `nullableByDefault` to | ||
`false`. | ||
With `nullableByDefault` _enabled_, you may declare an individual field as | ||
nonnullable adding the docblock tag `@killsParentOnException`. This will cause | ||
the field to be typed as non-nullable, but _it comes at a price_. Should the | ||
resolver throw, the error will bubble up to the first nullable parent. If | ||
`@killsParentOnException` is used too liberally, small errors can take down huge | ||
portions of your query. | ||
Dissabling `nullableByDefault` is equivilent to marking all nonnullable fields | ||
with `@killsParentOnException`. | ||
```ts | ||
/** | ||
* @gqlField | ||
* @killsParentOnException | ||
*/ | ||
myField(): string { | ||
return this.someOtherMethod(); | ||
} | ||
``` | ||
#### Field arguments | ||
If you wish to define arguments for a field, define your argument types inline: | ||
```ts | ||
/** @gqlField */ | ||
myField(args: { greeting: string }): string { | ||
return `${args.greeting} World`; | ||
} | ||
``` | ||
Default values for arguments can be defined by using the `=` operator with destructuring: | ||
```ts | ||
/** @gqlField */ | ||
myField({ greeting = "Hello" }: { greeting: string }): string { | ||
return `${greeting} World`; | ||
} | ||
``` | ||
```ts | ||
/** @gqlField */ | ||
myField({ greeting = { salutation: "Sup" } }: { greeting: GreetingConfig }): string { | ||
return `${greeting.salutation} World`; | ||
} | ||
``` | ||
#### Field descriptions | ||
Arguments can be given descriptions by using the `/**` syntax: | ||
```ts | ||
/** @gqlField */ | ||
myField(args: { | ||
/** A description of the greeting argument */ | ||
greeting: string | ||
}): string { | ||
return `${args.greeting} World`; | ||
} | ||
``` | ||
#### Deprecated fields | ||
To mark a field as deprecated, use the `@deprecated` JSDoc tag: | ||
```ts | ||
/** | ||
* @gqlField | ||
* @deprecated Please use myNewField instead. | ||
*/ | ||
myOldField(): string { | ||
return "Hello World"; | ||
} | ||
``` | ||
#### Functional style fields | ||
Sometimes you want to add a computed field to a non-class type, or extend base | ||
types like `Query` or `Mutation` from another file. Both of these usecases are | ||
enabled by placing a `@gqlField` before an exported function declaration. | ||
In this case, the function should expect an instance of the base type as the | ||
first argument, and an object representing the GraphQL field arguments as the | ||
second argument. The function should return the value of the field. | ||
Extending Query: | ||
```ts | ||
/** @gqlField */ | ||
export function userById(_: Query, args: {id: string}): User { | ||
return DB.getUserById(args.id); | ||
} | ||
``` | ||
Extending Mutation: | ||
```ts | ||
/** @gqlField */ | ||
export function deleteUser(_: Mutation, args: {id: string}): boolean { | ||
return DB.deleteUser(args.id); | ||
} | ||
``` | ||
Note that Grats will use the type of the first argument to determine which type | ||
is being extended. So, as seen in the previous examples, even if you don't need | ||
access to the instance you should still define a typed first argument. | ||
### @gqlUnion | ||
GraphQL unions can be defined by placing a `@gqlUnion` docblock directly before a: | ||
* Type alias of a union of object types | ||
```ts | ||
/** | ||
* A description of my union. | ||
* @gqlUnion <optional name of the union, if different from type name> | ||
*/ | ||
type MyUnion = User | Post; | ||
``` | ||
### @gqlScalar | ||
GraphQL custom sclars can be defined by placing a `@gqlScalar` docblock directly before a: | ||
* Type alias declaration | ||
```ts | ||
/** | ||
* A description of my custom scalar. | ||
* @gqlScalar <optional name of the scalar, if different from type name> | ||
*/ | ||
type MyCustomString = string; | ||
``` | ||
Note: For the built-in GraphQL scalars that don't have a corresponding TypeScript type, Grats ships with type aliases you can import. You may be promted to use one of these by Grat if you try to use `number` in a positon from which Grat needs to infer a GraphQL type. | ||
```ts | ||
import { Float, Int, ID } from "grats"; | ||
/** @gqlType */ | ||
class Math { | ||
id: ID; | ||
/** @gqlField */ | ||
round(args: {float: Float}): Int { | ||
return Math.round(args.float); | ||
} | ||
} | ||
``` | ||
### @gqlEnum | ||
GraphQL enums can be defined by placing a `@gqlEnum` docblock directly before a: | ||
* TypeScript enum declaration | ||
* Type alias of a union of string literals | ||
```ts | ||
/** | ||
* A description of my enum. | ||
* @gqlEnum <optional name of the enum, if different from type name> | ||
*/ | ||
enum MyEnum { | ||
/** A description of my variant */ | ||
OK = "OK", | ||
/** A description of my other variant */ | ||
ERROR = "ERROR" | ||
} | ||
``` | ||
Note that the values of the enum are used as the GraphQL enum values, and must | ||
be string literals. | ||
To mark a variants as deprecated, use the `@deprecated` JSDoc tag directly before it: | ||
```ts | ||
/** @gqlEnum */ | ||
enum MyEnum { | ||
OK = "OK" | ||
/** @deprecated Please use OK instead. */ | ||
OKAY = "OKAY" | ||
ERROR = "ERROR" | ||
} | ||
``` | ||
We also support defining enums using a union of string literals, however there | ||
are some limitations to this approach: | ||
* You cannot add descriptions to enum values | ||
* You cannot mark enum values as deprecated | ||
This is due to the fact that TypeScript does not see JSDoc comments as | ||
"attaching" to string literal types. | ||
```ts | ||
/** @gqlEnum */ | ||
type MyEnum = "OK" | "ERROR"; | ||
``` | ||
### @gqlInput | ||
GraphQL input types can be defined by placing a `@gqlInput` docblock directly before a: | ||
* Type alias declaration | ||
```ts | ||
/** | ||
* Description of my input type | ||
* @gqlInput <optional name of the input, if different from type name> | ||
*/ | ||
type MyInput = { | ||
name: string; | ||
age: number; | ||
}; | ||
``` | ||
## Example | ||
See `examples/express-graphql/` in the repo root for a working example. Here we | ||
run the static analysis at startup time. Nice for development, but not ideal for | ||
production where you would want to cache the schema and write it to disk for | ||
other tools to see. | ||
## Contributing | ||
@@ -490,49 +27,11 @@ | ||
# FAQ | ||
## Why would I _not_ want to use Grats | ||
Because Grats relies on static analysis to infer types, it requires that your | ||
GraphQL fields use types that can be statically analyzed. This means that you | ||
can't use complex derived types in positions where Grats needs to be able to | ||
infer the type. For example, field arguments and return values. | ||
Currently, Grats does not have a great way to handle the case where you want to | ||
expose structures that are not owned by your codebase. For example, if you want | ||
to expose a field that returns a type from a third-party library, or a type that | ||
is generated by some other codegen tool. Today, your best option is to define a | ||
wrapper resolver class. | ||
## Why use comments and not decorators? | ||
Using decorators to signal that a class/method/etc should be included in the | ||
schema would have some advantages: | ||
* The syntax is well defined, so it: | ||
* Can be checked/documented by TypeScript types | ||
* Formatted with tools like Prettier | ||
* Would not require custom parsing/validaiton rules | ||
However, it also has some disadvantages: | ||
* The feature is technically "experimental" in TypeScript and may change in the future. | ||
* Decorators cannot be applied to types, so it would precude the ability to | ||
define GraphQL constructs using types (e.g. interfaces, unions, etc). | ||
* Decorators cannot be applied to parameters, so it would preclude the ability | ||
to define GraphQL constructs using parameters (e.g. field arguments). | ||
* Decorators are a runtime construct, which means they must be imported and give | ||
the impression that they might have some runtime behavior. This is not the | ||
case for Grats, which is purely a static analysis tool. | ||
Given these tradeoffs, we've decided to use comments instead of decorators. | ||
# Acknowledgements | ||
* [@mofeiZ](https://github.com/mofeiZ) and [@alunyov](https://github/alunyov) for their Relay hack-week project exploring a similar idea. | ||
* [@josephsavona](https://github.com/josephsavona) for input on the design of [Relay Resolvers](https://relay.dev/docs/guides/relay-resolvers/) which inspired this project. | ||
* [@bradzacher](https://github.com/bradzacher) for tips on how to handle TypeScript ASTs. | ||
* Everyone who worked on Meta's Hack GraphQL server, the developer experince of which inspired this project. | ||
* A number of other projects which seem to have explored similar ideas in the past: | ||
* [ts2gql](https://github.com/convoyinc/ts2gql) | ||
* [ts2graphql](https://github.com/cevek/ts2graphql) | ||
* [typegraphql-reflection-poc](https://github.com/MichalLytek/typegraphql-reflection-poc) | ||
- [@mofeiZ](https://github.com/mofeiZ) and [@alunyov](https://github/alunyov) for their Relay hack-week project exploring a similar idea. | ||
- [@josephsavona](https://github.com/josephsavona) for input on the design of [Relay Resolvers](https://relay.dev/docs/guides/relay-resolvers/) which inspired this project. | ||
- [@bradzacher](https://github.com/bradzacher) for tips on how to handle TypeScript ASTs. | ||
- Everyone who worked on Meta's Hack GraphQL server, the developer experince of which inspired this project. | ||
- A number of other projects which seem to have explored similar ideas in the past: | ||
- [ts2gql](https://github.com/convoyinc/ts2gql) | ||
- [ts2graphql](https://github.com/cevek/ts2graphql) | ||
- [typegraphql-reflection-poc](https://github.com/MichalLytek/typegraphql-reflection-poc) |
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
253281
57
5196
0
10
36
8
96