├── .gitignore ├── README.md ├── index.js ├── index.ts ├── package.json ├── tsconfig.json └── types.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typescript-relay-plugin # 2 | 3 | A Typescript transformer to find and replace `Relay.QL` tagged template literals with the output 4 | generated by `babel-relay-plugin`. If your source files are written in Typescript, this can save you 5 | a parse and print step through `babel` and generally having it as a dependency. 6 | 7 | ### Usage ### 8 | 9 | ```ts 10 | import * as ts from "typescript" 11 | import {getTransformer, loadSchema} from "typescript-relay-plugin" 12 | 13 | const filePaths = ["..."] 14 | const schemaPath = "/path/to/schema.json" 15 | const program = ts.createProgram(filePaths, ts.getDefaultCompilerOptions()) 16 | 17 | program.emit(undefined, undefined, undefined, undefined, { 18 | before: [ 19 | getTransformer(loadSchema(schemaPath)) 20 | ] 21 | }) 22 | ``` 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | var bt = require("babel-types"); 4 | var chalk = require("chalk"); 5 | var path = require("path"); 6 | var ts = require("typescript"); 7 | var buildClientSchema_1 = require("graphql/utilities/buildClientSchema"); 8 | var RelayQLTransformer = require("babel-relay-plugin/lib/RelayQLTransformer"); 9 | // Internal flag that signifies if the node contains any ES2015 nodes. We need this to quicker find 10 | // tagged template expressions and avoid descending into Typescript nodes since visitEachChild breaks. 11 | var ContainsES2015 = 1 << 7; 12 | function loadSchema(filePath) { 13 | return buildClientSchema_1.buildClientSchema(require(filePath).data); 14 | } 15 | exports.loadSchema = loadSchema; 16 | function getTransformer(schema) { 17 | var relayQlTransformer = new RelayQLTransformer(schema, { 18 | snakeCase: false, 19 | substituteVariables: false 20 | }); 21 | return function transformer(context) { 22 | var sourcePath; 23 | return function (node) { 24 | sourcePath = node.fileName; 25 | var result = ts.updateSourceFileNode(node, ts.visitLexicalEnvironment(node.statements, visitor, context)); 26 | sourcePath = undefined; 27 | return result; 28 | }; 29 | function visitor(node) { 30 | if (node["transformFlags"] & ContainsES2015) { 31 | if (node.kind === ts.SyntaxKind.TaggedTemplateExpression) { 32 | return visitTaggedTemplateExpression(node); 33 | } 34 | else { 35 | return ts.visitEachChild(node, visitor, context); 36 | } 37 | } 38 | else { 39 | return node; 40 | } 41 | } 42 | function visitTaggedTemplateExpression(node) { 43 | if (node.tag.getText() === "Relay.QL") { 44 | try { 45 | // Convert the file's basename to an ok document name 46 | var documentName = getDocumentName(sourcePath); 47 | var template = convertTemplateLiteral(node.template); 48 | var result = relayQlTransformer.transform(bt, template, { 49 | documentName: documentName, 50 | enableValidation: true, 51 | tagName: "Relay.QL" 52 | }); 53 | return convertBabelNode(result); 54 | } 55 | catch (error) { 56 | if (error.stack && !error.stack.includes("validation errors")) { 57 | console.error(chalk.red(error.stack)); 58 | } 59 | else { 60 | console.error(chalk.red(error)); 61 | } 62 | if (error.sourceText && error.validationErrors) { 63 | var source = error.sourceText; 64 | var errorsByLine_1 = {}; 65 | error.validationErrors.forEach(function (error) { 66 | error.locations.forEach(function (location) { 67 | if (!errorsByLine_1[location.line]) { 68 | errorsByLine_1[location.line] = []; 69 | } 70 | errorsByLine_1[location.line].push({ 71 | line: location.line, 72 | column: location.column, 73 | message: error.message 74 | }); 75 | }); 76 | }); 77 | source.split("\n").forEach(function (line, idx) { 78 | var lineNum = idx + 1; 79 | console.log(line); 80 | if (errorsByLine_1[lineNum]) { 81 | errorsByLine_1[lineNum].forEach(function (error) { 82 | var spacer = " ".repeat(error.column - 1); 83 | console.log(spacer + chalk.yellow("^ " + error.message)); 84 | }); 85 | } 86 | }); 87 | } 88 | if (process.env.NODE_ENV === "production") { 89 | throw error; 90 | } 91 | else { 92 | // Generate a runtime throw statement. Because this will get inserted as the return value, 93 | // wrap the throw in an IIFE. Pfft, throw statements, please.. 94 | return ts.createCall(ts.createFunctionExpression(undefined, // modifiers 95 | undefined, // asterisk token 96 | undefined, // name 97 | undefined, // type params 98 | undefined, // arguments 99 | undefined, // type 100 | ts.createBlock([ 101 | ts.createThrow(ts.createNew(ts.createIdentifier("Error"), undefined, // type params 102 | [ts.createLiteral(error.message)])) 103 | ])), undefined, // type params 104 | undefined); 105 | } 106 | } 107 | } 108 | return node; 109 | } 110 | }; 111 | } 112 | exports.getTransformer = getTransformer; 113 | function convertTemplateLiteral(node) { 114 | if (node.kind === ts.SyntaxKind.FirstTemplateToken) { 115 | return { 116 | type: "TemplateLiteral", 117 | start: node.pos - 1, 118 | end: node.end + 1, 119 | loc: null, 120 | quasis: [{ 121 | type: "TemplateElement", 122 | start: node.pos, 123 | end: node.end, 124 | loc: null, 125 | tail: true, 126 | value: { 127 | raw: node.text, 128 | cooked: node.text 129 | } 130 | }], 131 | expressions: [] 132 | }; 133 | } 134 | else { 135 | var expressions = []; 136 | var quasis = [{ 137 | type: "TemplateElement", 138 | start: node.head.pos, 139 | end: node.head.end, 140 | loc: null, 141 | tail: false, 142 | value: { 143 | raw: node.head.text, 144 | cooked: node.head.text 145 | } 146 | }]; 147 | for (var _i = 0, _a = node.templateSpans; _i < _a.length; _i++) { 148 | var span = _a[_i]; 149 | expressions.push({ 150 | type: "ParenthesizedExpression", 151 | start: span.expression.pos, 152 | end: span.expression.end, 153 | loc: null, 154 | expression: span.expression, 155 | __ts_node: true 156 | }); 157 | quasis.push({ 158 | type: "TemplateElement", 159 | start: span.literal.pos, 160 | end: span.literal.end, 161 | loc: null, 162 | tail: false, 163 | value: { 164 | raw: span.literal.text, 165 | cooked: span.literal.text 166 | } 167 | }); 168 | } 169 | quasis[quasis.length - 1].tail = true; 170 | return { 171 | type: "TemplateLiteral", 172 | start: node.pos, 173 | end: node.end, 174 | loc: null, 175 | quasis: quasis, 176 | expressions: expressions 177 | }; 178 | } 179 | } 180 | function convertBabelNode(node) { 181 | if (bt.isArrayExpression(node)) { 182 | return ts.createArrayLiteral(node.elements.map(convertBabelNode)); 183 | } 184 | else if (bt.isCallExpression(node)) { 185 | return ts.createCall(convertBabelNode(node.callee), undefined, // type arguments 186 | node.arguments.map(convertBabelNode)); 187 | } 188 | else if (bt.isFunctionExpression(node)) { 189 | var stmts = node.body.body.map(convertBabelNode); 190 | return ts.createFunctionExpression(undefined, // modifiers 191 | undefined, // asterisk token 192 | undefined, // name 193 | undefined, // type params 194 | node.params.map(function (param) { return ts.createParameter(undefined, // decorators 195 | undefined, // modifiers 196 | undefined, // dotdotdot token 197 | convertBabelNode(param), undefined, // question token, 198 | undefined, // type, 199 | undefined); }), undefined, // type 200 | ts.createBlock(stmts)); 201 | } 202 | else if (bt.isIdentifier(node)) { 203 | return ts.createIdentifier(node.name); 204 | } 205 | else if (bt.isMemberExpression(node)) { 206 | return ts.createPropertyAccess(convertBabelNode(node.object), convertBabelNode(node.property)); 207 | } 208 | else if (bt.isObjectExpression(node)) { 209 | return ts.createObjectLiteral(node.properties.map(convertBabelNode)); 210 | } 211 | else if (bt.isObjectProperty(node)) { 212 | return ts.createPropertyAssignment(convertBabelNode(node.key), convertBabelNode(node.value)); 213 | } 214 | else if (bt.isParenthesizedExpression(node)) { 215 | if (node["__ts_node"]) { 216 | return node.expression; 217 | } 218 | return ts.createParen(convertBabelNode(node.expression)); 219 | } 220 | else if (bt.isReturnStatement(node)) { 221 | return ts.createReturn(convertBabelNode(node.argument)); 222 | } 223 | else if (bt.isStringLiteral(node) || bt.isBooleanLiteral(node) || bt.isNumericLiteral(node)) { 224 | return ts.createLiteral(node.value); 225 | } 226 | throw new Error("Don't know how to convert Babel node \"" + node.type + "\" to Typescript"); 227 | } 228 | function getDocumentName(sourcePath) { 229 | var dir = path.basename(path.dirname(sourcePath)); 230 | var name = path.basename(sourcePath); 231 | return (dir + "_" + name).replace(/\W+/g, "_"); 232 | } 233 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import * as bt from "babel-types" 2 | import * as chalk from "chalk" 3 | import * as path from "path" 4 | import * as ts from "typescript" 5 | import {buildClientSchema} from "graphql/utilities/buildClientSchema" 6 | import RelayQLTransformer = require("babel-relay-plugin/lib/RelayQLTransformer") 7 | import RelayTransformError = require("babel-relay-plugin/lib/RelayTransformError") 8 | 9 | // Internal flag that signifies if the node contains any ES2015 nodes. We need this to quicker find 10 | // tagged template expressions and avoid descending into Typescript nodes since visitEachChild breaks. 11 | const ContainsES2015 = 1 << 7 12 | 13 | export function loadSchema(filePath: string): any { 14 | return buildClientSchema(require(filePath).data) 15 | } 16 | 17 | export function getTransformer(schema): ts.TransformerFactory { 18 | const relayQlTransformer = new RelayQLTransformer(schema, { 19 | snakeCase: false, 20 | substituteVariables: false 21 | }) 22 | 23 | return function transformer(context): ts.Transformer { 24 | let sourcePath: string 25 | 26 | return node => { 27 | sourcePath = node.fileName 28 | const result = ts.updateSourceFileNode(node, ts.visitLexicalEnvironment(node.statements, visitor, context)) 29 | sourcePath = undefined 30 | return result 31 | } 32 | 33 | function visitor(node: ts.Node): ts.Node { 34 | if (node["transformFlags"] & ContainsES2015) { 35 | if (node.kind === ts.SyntaxKind.TaggedTemplateExpression) { 36 | return visitTaggedTemplateExpression(node) 37 | } else { 38 | return ts.visitEachChild(node, visitor, context) 39 | } 40 | } else { 41 | return node 42 | } 43 | } 44 | 45 | function visitTaggedTemplateExpression(node: ts.TaggedTemplateExpression): ts.Node { 46 | if (node.tag.getText() === "Relay.QL") { 47 | 48 | try { 49 | // Convert the file's basename to an ok document name 50 | const documentName = getDocumentName(sourcePath) 51 | const template = convertTemplateLiteral(node.template) 52 | const result = relayQlTransformer.transform(bt, template, { 53 | documentName: documentName, 54 | enableValidation: true, 55 | tagName: "Relay.QL", 56 | }) 57 | 58 | return convertBabelNode(result) 59 | 60 | } catch (error) { 61 | if (error.stack && !error.stack.includes("validation errors")) { 62 | console.error(chalk.red(error.stack)) 63 | } else { 64 | console.error(chalk.red(error)) 65 | } 66 | 67 | if (error.sourceText && error.validationErrors) { 68 | const source: string = error.sourceText 69 | const errorsByLine: { 70 | [line: number]: { 71 | line: number 72 | column: number 73 | message: string 74 | }[] 75 | } = {} 76 | 77 | error.validationErrors.forEach(error => { 78 | error.locations.forEach(location => { 79 | if (!errorsByLine[location.line]) { 80 | errorsByLine[location.line] = [] 81 | } 82 | 83 | errorsByLine[location.line].push({ 84 | line: location.line, 85 | column: location.column, 86 | message: error.message 87 | }) 88 | }) 89 | }) 90 | 91 | source.split("\n").forEach((line, idx) => { 92 | const lineNum = idx + 1 93 | console.log(line) 94 | 95 | if (errorsByLine[lineNum]) { 96 | errorsByLine[lineNum].forEach(error => { 97 | const spacer = " ".repeat(error.column - 1) 98 | console.log(spacer + chalk.yellow("^ " + error.message)) 99 | }) 100 | } 101 | }) 102 | } 103 | 104 | if (process.env.NODE_ENV === "production") { 105 | throw error 106 | } else { 107 | // Generate a runtime throw statement. Because this will get inserted as the return value, 108 | // wrap the throw in an IIFE. Pfft, throw statements, please.. 109 | return ts.createCall( 110 | ts.createFunctionExpression( 111 | undefined, // modifiers 112 | undefined, // asterisk token 113 | undefined, // name 114 | undefined, // type params 115 | undefined, // arguments 116 | undefined, // type 117 | ts.createBlock([ 118 | ts.createThrow( 119 | ts.createNew( 120 | ts.createIdentifier("Error"), 121 | undefined, // type params 122 | [ts.createLiteral(error.message)])) 123 | ])), 124 | undefined, // type params 125 | undefined, // arguments 126 | ) 127 | } 128 | } 129 | } 130 | return node 131 | } 132 | } 133 | } 134 | 135 | function convertTemplateLiteral(node: ts.TemplateLiteral): bt.TemplateLiteral { 136 | if (node.kind === ts.SyntaxKind.FirstTemplateToken) { 137 | return { 138 | type: "TemplateLiteral", 139 | start: node.pos - 1, 140 | end: node.end + 1, 141 | loc: null, 142 | quasis: [{ 143 | type: "TemplateElement", 144 | start: node.pos, 145 | end: node.end, 146 | loc: null, 147 | tail: true, 148 | value: { 149 | raw: node.text, 150 | cooked: node.text 151 | } 152 | }], 153 | expressions: [], 154 | } 155 | } else { 156 | const expressions: bt.Expression[] = [] 157 | const quasis: bt.TemplateElement[] = [{ 158 | type: "TemplateElement", 159 | start: node.head.pos, 160 | end: node.head.end, 161 | loc: null, 162 | tail: false, 163 | value: { 164 | raw: node.head.text, 165 | cooked: node.head.text, 166 | } 167 | }] 168 | 169 | for (const span of node.templateSpans) { 170 | expressions.push({ 171 | type: "ParenthesizedExpression", 172 | start: span.expression.pos, 173 | end: span.expression.end, 174 | loc: null, 175 | expression: span.expression, 176 | __ts_node: true 177 | } as any) 178 | 179 | quasis.push({ 180 | type: "TemplateElement", 181 | start: span.literal.pos, 182 | end: span.literal.end, 183 | loc: null, 184 | tail: false, 185 | value: { 186 | raw: span.literal.text, 187 | cooked: span.literal.text 188 | } 189 | }) 190 | } 191 | 192 | quasis[quasis.length - 1].tail = true 193 | 194 | return { 195 | type: "TemplateLiteral", 196 | start: node.pos, 197 | end: node.end, 198 | loc: null, 199 | quasis, 200 | expressions, 201 | } 202 | } 203 | } 204 | 205 | function convertBabelNode(node) { 206 | if (bt.isArrayExpression(node)) { 207 | return ts.createArrayLiteral(node.elements.map(convertBabelNode)) 208 | } else if (bt.isCallExpression(node)) { 209 | return ts.createCall( 210 | convertBabelNode(node.callee), 211 | undefined, // type arguments 212 | node.arguments.map(convertBabelNode)) 213 | } else if (bt.isFunctionExpression(node)) { 214 | const stmts = node.body.body.map(convertBabelNode) 215 | return ts.createFunctionExpression( 216 | undefined, // modifiers 217 | undefined, // asterisk token 218 | undefined, // name 219 | undefined, // type params 220 | node.params.map(param => ts.createParameter( 221 | undefined, // decorators 222 | undefined, // modifiers 223 | undefined, // dotdotdot token 224 | convertBabelNode(param), 225 | undefined, // question token, 226 | undefined, // type, 227 | undefined, // initializer 228 | )), 229 | undefined, // type 230 | ts.createBlock(stmts)) 231 | } else if (bt.isIdentifier(node)) { 232 | return ts.createIdentifier(node.name) 233 | } else if (bt.isMemberExpression(node)) { 234 | return ts.createPropertyAccess( 235 | convertBabelNode(node.object), 236 | convertBabelNode(node.property)) 237 | } else if (bt.isObjectExpression(node)) { 238 | return ts.createObjectLiteral(node.properties.map(convertBabelNode)) 239 | } else if (bt.isObjectProperty(node)) { 240 | return ts.createPropertyAssignment( 241 | convertBabelNode(node.key), 242 | convertBabelNode(node.value)) 243 | } else if (bt.isParenthesizedExpression(node)) { 244 | if (node["__ts_node"]) { 245 | return node.expression as any 246 | } 247 | return ts.createParen(convertBabelNode(node.expression)) 248 | } else if (bt.isReturnStatement(node)) { 249 | return ts.createReturn(convertBabelNode(node.argument)) 250 | } else if (bt.isStringLiteral(node) || bt.isBooleanLiteral(node) || bt.isNumericLiteral(node)) { 251 | return ts.createLiteral(node.value) 252 | } 253 | 254 | throw new Error(`Don't know how to convert Babel node "${node.type}" to Typescript`) 255 | } 256 | 257 | function getDocumentName(sourcePath: string): string { 258 | const dir = path.basename(path.dirname(sourcePath)) 259 | const name = path.basename(sourcePath) 260 | return (dir + "_" + name).replace(/\W+/g, "_") 261 | } 262 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-relay-plugin", 3 | "version": "0.1.1", 4 | "description": "Relay.QL plugin for Typescript", 5 | "repository": "Pathgather/typescript-relay-plugin", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "Guntars Asmanis-Graham", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "@types/babel-types": "^6.7.14", 14 | "@types/chalk": "^0.4.31", 15 | "@types/node": "^6.0.45" 16 | }, 17 | "dependencies": { 18 | "babel-types": "6.x", 19 | "chalk": "^1.1.3" 20 | }, 21 | "peerDependencies": { 22 | "babel-relay-plugin": "*", 23 | "graphql": "*", 24 | "typescript": "*" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["node"], 4 | "lib": ["es6"] 5 | }, 6 | "compileOnSave": true 7 | } 8 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "babel-relay-plugin/lib/RelayQLTransformer" { 2 | import * as bt from "babel-types" 3 | 4 | interface TransformerOptions { 5 | inputArgumentName?: string, 6 | snakeCase: boolean, 7 | substituteVariables: boolean, 8 | validator?: Function 9 | } 10 | 11 | interface TextTransformOptions { 12 | documentName: string, 13 | enableValidation: boolean, 14 | propName?: string, 15 | tagName: string, 16 | } 17 | 18 | class RelayQLTransformer { 19 | constructor(schema, options: TransformerOptions) 20 | transform(t, node: bt.TemplateLiteral, options: TextTransformOptions): bt.Expression 21 | } 22 | 23 | export = RelayQLTransformer 24 | } 25 | 26 | declare module "babel-relay-plugin/lib/RelayTransformError" { 27 | class RelayTransformError { 28 | message: string 29 | loc: any 30 | stack: string 31 | } 32 | 33 | export = RelayTransformError 34 | } 35 | 36 | declare module "graphql/utilities/buildClientSchema" { 37 | export function buildClientSchema(schema: any): any 38 | } 39 | --------------------------------------------------------------------------------