Skip to content
This repository was archived by the owner on Sep 27, 2023. It is now read-only.

Commit 94ad992

Browse files
committed
First test passed at transforming Relay Classic nodes
1 parent a240803 commit 94ad992

12 files changed

+1086
-12
lines changed

transform/src/BindingsAtNode.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import * as ts from 'typescript';
2+
export type Bindings = Map<string, BindingInfo>;
3+
export const enum BindingKind {
4+
Local,
5+
Module,
6+
Import,
7+
Require,
8+
Export
9+
}
10+
export interface BindingInfo {
11+
kind: BindingKind;
12+
}
13+
export function bindingsAtNode(node: ts.Node): Bindings {
14+
const bindings = new Map<string, BindingInfo>();
15+
16+
function findInitializerBindings(initializer: ts.ForInitializer) {
17+
if (ts.isVariableDeclarationList(initializer)) {
18+
initializer.declarations.forEach(decl => {
19+
processBinding(decl.name, BindingKind.Local);
20+
});
21+
}
22+
}
23+
24+
function determineBindingKindFromInitializer(initializer: ts.Expression | undefined): BindingKind {
25+
if (initializer == null) {
26+
return BindingKind.Local;
27+
}
28+
if (ts.isCallExpression(initializer) && ts.isIdentifier(initializer.expression) && initializer.expression.text === 'require') {
29+
return BindingKind.Require;
30+
}
31+
return BindingKind.Local;
32+
}
33+
34+
function findStatementsBindings(block: ts.Block | ts.SourceFile) {
35+
block.statements.forEach(stmt => {
36+
if (ts.isClassDeclaration(stmt)) {
37+
if (stmt.name) {
38+
addBinding(stmt.name.text, BindingKind.Local);
39+
}
40+
return;
41+
}
42+
if (ts.isExportDeclaration(stmt)) {
43+
if (stmt.exportClause) {
44+
stmt.exportClause.elements.forEach(element => {
45+
addBinding(element.name.text, BindingKind.Export);
46+
});
47+
}
48+
if (stmt.name) {
49+
addBinding(stmt.name.text, BindingKind.Export);
50+
}
51+
return;
52+
}
53+
if (ts.isImportDeclaration(stmt) && stmt.importClause != null) {
54+
const namedBindings = stmt.importClause.namedBindings;
55+
if (namedBindings == null) {
56+
return;
57+
}
58+
if (ts.isNamespaceImport(namedBindings)) {
59+
addBinding(namedBindings.name.text, BindingKind.Import);
60+
} else {
61+
namedBindings.elements.forEach(element => {
62+
addBinding((element.propertyName || element.name).text, BindingKind.Import);
63+
});
64+
}
65+
return;
66+
}
67+
if (ts.isVariableStatement(stmt)) {
68+
stmt.declarationList.declarations.forEach(decl => {
69+
processBinding(decl.name, determineBindingKindFromInitializer(decl.initializer));
70+
});
71+
return;
72+
}
73+
// One can declare named functions in tons of weird ways that will end up in scope that this won't catch
74+
// the same goes for var declarations deep within the code structure.
75+
if (ts.isExpressionStatement(stmt) && ts.isFunctionExpression(stmt.expression)) {
76+
if (stmt.expression.name != null) {
77+
addBinding(stmt.expression.name.text, BindingKind.Local);
78+
}
79+
return;
80+
}
81+
});
82+
}
83+
84+
function addBinding(name: string, kind: BindingKind) {
85+
if (!bindings.has(name)) {
86+
bindings.set(name, { kind: kind });
87+
}
88+
}
89+
90+
function processBinding(binding: ts.Identifier | ts.ArrayBindingPattern | ts.ObjectBindingPattern, kind: BindingKind) {
91+
if (ts.isIdentifier(binding)) {
92+
addBinding(binding.text, kind);
93+
return;
94+
}
95+
if (ts.isArrayBindingPattern(binding)) {
96+
binding.elements.forEach(element => {
97+
if (ts.isBindingElement(element)) {
98+
processBinding(element.name, kind);
99+
}
100+
});
101+
return;
102+
}
103+
binding.elements.forEach(element => {
104+
if (ts.isBindingElement(element)) {
105+
processBinding(element.name, kind);
106+
}
107+
});
108+
}
109+
110+
while (node.parent != null) {
111+
const parent = node.parent;
112+
if (ts.isBlock(parent) || ts.isSourceFile(parent)) {
113+
findStatementsBindings(parent);
114+
continue;
115+
}
116+
117+
if (ts.isForStatement(parent) && parent.initializer != null) {
118+
findInitializerBindings(parent.initializer);
119+
continue;
120+
}
121+
if (ts.isForOfStatement(parent)) {
122+
findInitializerBindings(parent.initializer);
123+
}
124+
}
125+
return bindings;
126+
}

transform/src/Options.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { GraphQLSchema, buildClientSchema, buildASTSchema, parse } from "graphql";
22
import * as fs from 'fs';
3+
import * as path from 'path';
4+
import { RelayQLTransformer } from "./RelayQLTransformer";
5+
import { SCHEMA_EXTENSION } from "./GraphQLRelayDirective";
36

47
export interface Options {
58
artifactDirectory?: string;
@@ -13,7 +16,7 @@ export interface Options {
1316
export interface NormalizedOptions {
1417
artifactDirectory?: string;
1518
compat?: boolean;
16-
schema?: GraphQLSchema;
19+
relayQLTransformer?: RelayQLTransformer;
1720
isDevVariable?: string;
1821
buildCommand?: string;
1922
isDevelopment?: boolean;
@@ -23,7 +26,7 @@ const dotJsonLength = '.json'.length;
2326
const dotGraphQLLength = '.graphql'.length;
2427

2528
export function readGraphQLSchema(schemaPath: string): GraphQLSchema {
26-
const contents = fs.readFileSync(schemaPath, { encoding: 'utf8' });
29+
const contents = fs.readFileSync(path.resolve(schemaPath), { encoding: 'utf8' });
2730
if (schemaPath.substring(schemaPath.length - dotJsonLength) === '.json') {
2831
const json = JSON.parse(contents);
2932
if (json.__schema) {
@@ -34,14 +37,20 @@ export function readGraphQLSchema(schemaPath: string): GraphQLSchema {
3437
}
3538
throw new Error('Expected data file to contain a JSON encoded GraphQLSchema');
3639
} else if (schemaPath.substring(schemaPath.length - dotGraphQLLength) === '.graphql') {
37-
return buildASTSchema(parse(contents));
40+
return buildASTSchema(parse(SCHEMA_EXTENSION + "\n" + contents));
3841
}
3942
throw new Error('Unsupported file. schema option only supports json and graphql file extensions');
4043
}
4144

4245
export function normalizeOptions(options: Options): NormalizedOptions {
46+
const { schema, ...opts } = options;
4347
return {
44-
...options,
45-
schema: options.schema ? readGraphQLSchema(options.schema) : undefined,
48+
...opts,
49+
relayQLTransformer: options.schema ? new RelayQLTransformer(readGraphQLSchema(options.schema), {
50+
inputArgumentName: 'input',
51+
snakeCase: false,
52+
substituteVariables: true,
53+
validator: null,
54+
}) : undefined,
4655
};
4756
}

transform/src/RelayQLTransformer.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { RelayQLPrinter } from "./RelayQLPrinter";
22

33
import * as util from "util";
4+
import * as path from "path";
45
import * as ts from "typescript";
56

67
import {
@@ -53,6 +54,15 @@ type TextTransformOptions = {
5354
tagName: string,
5455
};
5556

57+
function sanitizeDocumentName(str: string): string {
58+
const basename = path.basename(str);
59+
const firstDot = basename.indexOf('.');
60+
if (firstDot < 0) {
61+
return basename;
62+
}
63+
return basename.substr(0, firstDot);
64+
}
65+
5666
/**
5767
* Transforms a TemplateLiteral node into a RelayQLDefinition, which is then
5868
* transformed into a TS AST via RelayQLPrinter.
@@ -71,10 +81,10 @@ export class RelayQLTransformer {
7181
options: TextTransformOptions,
7282
): Printable {
7383
const {
74-
substitutions,
84+
substitutions,
7585
templateText,
7686
variableNames,
77-
} = this.processTemplateLiteral(node, options.documentName);
87+
} = this.processTemplateLiteral(node, options.documentName);
7888
const documentText = this.processTemplateText(templateText, options);
7989
const definition = this.processDocumentText(documentText, options);
8090

@@ -143,6 +153,7 @@ export class RelayQLTransformer {
143153
): string {
144154
const pattern = /^(fragment|mutation|query|subscription)\s*(\w*)?([\s\S]*)/;
145155
const matches = pattern.exec(templateText);
156+
const sanitizedDocumentName = sanitizeDocumentName(documentName);
146157
if (!matches) {
147158
throw new Error(util.format(
148159
"You supplied a GraphQL document named `%s` with invalid syntax. It " +
@@ -156,7 +167,7 @@ export class RelayQLTransformer {
156167
// Allow `fragment on Type {...}`.
157168
if (type === 'fragment' && name === 'on') {
158169
name =
159-
documentName + (propName ? '_' + capitalize(propName) : '') + 'RelayQL';
170+
sanitizedDocumentName + (propName ? '_' + capitalize(propName) : '') + 'RelayQL';
160171
rest = 'on' + rest;
161172
}
162173
const definitionName = capitalize(name);
@@ -170,6 +181,7 @@ export class RelayQLTransformer {
170181
documentText: string,
171182
{ documentName, enableValidation }: TextTransformOptions,
172183
): RelayQLDefinition<any> {
184+
console.log(documentText);
173185
const document = parse(new Source(documentText, documentName));
174186
const validationErrors = enableValidation
175187
? this.validateDocument(document, documentName)

transform/src/compileRelayQLTag.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { createTransformError } from "./createTransformError";
2+
import * as ts from "typescript";
3+
import { NormalizedOptions } from "./Options";
4+
import { RelayQLTransformer } from "./RelayQLTransformer";
5+
6+
/**
7+
* Given all the metadata about a found RelayQL tag, compile it and return
8+
* the resulting TS AST.
9+
*/
10+
export function compileRelayQLTag(
11+
ctx: ts.TransformationContext,
12+
options: NormalizedOptions,
13+
transformer: RelayQLTransformer,
14+
node: ts.TaggedTemplateExpression,
15+
documentName: string,
16+
propName: string | null,
17+
tagName: string,
18+
enableValidation: boolean,
19+
): ts.Expression {
20+
const result = transformer.transform(node, {
21+
documentName,
22+
propName,
23+
tagName,
24+
enableValidation,
25+
});
26+
ts.setSourceMapRange(result, ts.getSourceMapRange(node));
27+
return result;
28+
}

0 commit comments

Comments
 (0)