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

Commit 9bd30da

Browse files
committed
[Transform] WIP: Add code to transform away the tagged template literals
A very simple example with just using modern transform implemented.
1 parent 6937cdc commit 9bd30da

File tree

11 files changed

+3377
-0
lines changed

11 files changed

+3377
-0
lines changed

transform/package.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "typescript-transform-relay",
3+
"version": "1.0.0",
4+
"description": "A transformer for use with TypeScript when using relay (modern/compat/classic)",
5+
"main": "lib/index.js",
6+
"scripts": {
7+
"test": "jest"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/kastermester/relay-compiler-language-typescript.git"
12+
},
13+
"keywords": [
14+
"relay",
15+
"typescript"
16+
],
17+
"author": "Kaare Hoff Skovgaard <[email protected]>",
18+
"license": "MIT",
19+
"bugs": {
20+
"url": "https://github.com/kastermester/relay-compiler-language-typescript/issues"
21+
},
22+
"homepage": "https://github.com/kastermester/relay-compiler-language-typescript#readme",
23+
"dependencies": {
24+
"graphql": "^0.12.3",
25+
"typescript": "^2.6.2"
26+
},
27+
"devDependencies": {
28+
"@types/jest": "^22.0.1",
29+
"@types/node": "^9.3.0",
30+
"async-file": "^2.0.2",
31+
"jest": "^22.1.4",
32+
"ts-jest": "^22.0.1"
33+
},
34+
"jest": {
35+
"transform": {
36+
"^.+\\.tsx?$": "ts-jest"
37+
},
38+
"testRegex": "test/.+?-test\\.tsx?$",
39+
"moduleFileExtensions": [
40+
"js",
41+
"ts",
42+
"tsx"
43+
]
44+
}
45+
}

transform/src/Options.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
export interface Options {
3+
artifactDirectory?: string;
4+
compat?: boolean;
5+
schema?: string;
6+
isDevVariable?: string;
7+
buildCommand?: string;
8+
isDevelopment?: boolean;
9+
}

transform/src/compileGraphQLTag.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// const createClassicNode = require('./createClassicNode');
2+
// const createCompatNode = require('./createCompatNode');
3+
import { createModernNode } from "./createModernNode";
4+
import { getFragmentNameParts } from "./getFragmentNameParts";
5+
import { DocumentNode, FragmentDefinitionNode, OperationDefinitionNode } from "graphql";
6+
import * as ts from 'typescript';
7+
import { Options } from "./Options";
8+
9+
/**
10+
* Given a graphql`` tagged template literal, replace it with the appropriate
11+
* runtime artifact.
12+
*/
13+
export function compileGraphQLTag(
14+
ctx: ts.TransformationContext,
15+
opts: Options,
16+
node: ts.Node,
17+
ast: DocumentNode,
18+
fileName: string,
19+
): ts.Expression {
20+
const mainDefinition = ast.definitions[0];
21+
22+
if (mainDefinition.kind === 'FragmentDefinition') {
23+
const objPropName = getAssignedObjectPropertyName(node);
24+
if (objPropName) {
25+
if (ast.definitions.length !== 1) {
26+
throw new Error(
27+
'TSTransformRelay: Expected exactly one fragment in the ' +
28+
`graphql tag referenced by the property ${objPropName}.`,
29+
);
30+
}
31+
return createAST(ctx, opts, node, mainDefinition, fileName);
32+
}
33+
34+
const nodeMap: { [key: string]: ts.Expression } = {};
35+
for (const definition of ast.definitions) {
36+
if (definition.kind !== 'FragmentDefinition') {
37+
throw new Error(
38+
'TSTransformRelay: Expected only fragments within this ' +
39+
'graphql tag.',
40+
);
41+
}
42+
43+
const [, propName] = getFragmentNameParts(definition.name.value);
44+
nodeMap[propName] = createAST(ctx, opts, null, definition, fileName);
45+
}
46+
return createObject(nodeMap, node);
47+
}
48+
49+
if (mainDefinition.kind === 'OperationDefinition') {
50+
if (ast.definitions.length !== 1) {
51+
throw new Error(
52+
'TSTransformRelay: Expected exactly one operation ' +
53+
'(query, mutation, or subscription) per graphql tag.',
54+
);
55+
}
56+
return createAST(ctx, opts, node, mainDefinition, fileName);
57+
}
58+
59+
throw new Error(
60+
'TSTransformRelay: Expected a fragment, mutation, query, or ' +
61+
'subscription, got `' +
62+
mainDefinition.kind +
63+
'`.',
64+
);
65+
}
66+
67+
function createAST(ctx: ts.TransformationContext, opts: Options, node: ts.Node | null, graphqlDefinition: FragmentDefinitionNode | OperationDefinitionNode, fileName: string) {
68+
const isCompatMode = Boolean(opts.compat);
69+
const isDevVariable = opts.isDevVariable;
70+
const artifactDirectory = opts.artifactDirectory;
71+
const buildCommand =
72+
(opts.buildCommand) || 'relay-compiler';
73+
74+
// Fallback is 'true'
75+
const isDevelopment =
76+
(process.env.BABEL_ENV || process.env.NODE_ENV) !== 'production';
77+
78+
const modernNode = createModernNode(ctx, opts, graphqlDefinition, fileName);
79+
if (isCompatMode) {
80+
throw new Error('Not implemented');
81+
// return createCompatNode(
82+
// t,
83+
// modernNode,
84+
// createClassicNode(t, path, graphqlDefinition, state),
85+
// );
86+
}
87+
if (node != null) {
88+
ts.setSourceMapRange(modernNode, ts.getSourceMapRange(node));
89+
}
90+
return modernNode;
91+
}
92+
93+
const idRegex = /^[$a-zA-Z_][$a-z0-9A-Z_]*$/;
94+
95+
function createObject(obj: { [propName: string]: ts.Expression }, originalNode: ts.Node) {
96+
const propNames = Object.keys(obj);
97+
98+
const assignments = propNames.map(propName => {
99+
const name = idRegex.test(propName) ? ts.createIdentifier(propName) : ts.createLiteral(propName);
100+
return ts.createPropertyAssignment(name, obj[propName])
101+
});
102+
103+
const objectLiteralNode = ts.createObjectLiteral(assignments, true);
104+
ts.setSourceMapRange(objectLiteralNode, ts.getSourceMapRange(originalNode));
105+
return objectLiteralNode;
106+
}
107+
108+
function getAssignedObjectPropertyName(node: ts.Node): string | undefined {
109+
if (node.parent == null) {
110+
return undefined;
111+
}
112+
113+
if (!ts.isPropertyAssignment(node.parent)) {
114+
return undefined;
115+
}
116+
117+
const propName = node.parent.name;
118+
119+
if (ts.isIdentifier(propName)) {
120+
return propName.text;
121+
}
122+
if (ts.isStringLiteral(propName)) {
123+
return propName.text;
124+
}
125+
return undefined;
126+
}

transform/src/createModernNode.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import * as crypto from "crypto";
2+
import * as fs from "fs";
3+
import * as path from "path";
4+
import * as ts from 'typescript';
5+
6+
import { print } from "graphql";
7+
8+
const GENERATED = './__generated__/';
9+
10+
import { OperationDefinitionNode, FragmentDefinitionNode } from "graphql";
11+
import { Options } from "./Options";
12+
13+
function createVariableStatement(type: ts.NodeFlags.Const | ts.NodeFlags.Let | undefined, name: ts.Identifier, initializer: ts.Expression): ts.VariableStatement {
14+
return ts.createVariableStatement(
15+
undefined,
16+
ts.createVariableDeclarationList(
17+
[
18+
ts.createVariableDeclaration(
19+
name,
20+
undefined,
21+
initializer
22+
),
23+
],
24+
type,
25+
),
26+
);
27+
}
28+
29+
/**
30+
* Relay Modern creates separate generated files, so Babel transforms graphql
31+
* definitions to lazy require function calls.
32+
*/
33+
export function createModernNode(
34+
ctx: ts.TransformationContext,
35+
opts: Options,
36+
graphqlDefinition: OperationDefinitionNode | FragmentDefinitionNode,
37+
fileName: string,
38+
): ts.Expression {
39+
const definitionName = graphqlDefinition.name && graphqlDefinition.name.value;
40+
if (!definitionName) {
41+
throw new Error('GraphQL operations and fragments must contain names');
42+
}
43+
const requiredFile = definitionName + '.graphql';
44+
const requiredPath = opts.artifactDirectory
45+
? getRelativeImportPath(fileName, opts.artifactDirectory, requiredFile)
46+
: GENERATED + requiredFile;
47+
48+
const hash = crypto
49+
.createHash('md5')
50+
.update(print(graphqlDefinition), 'utf8')
51+
.digest('hex');
52+
53+
const requireGraphQLModule = ts.createPropertyAccess(ts.createCall(ts.createIdentifier('require'), undefined, [
54+
ts.createLiteral(requiredPath),
55+
]), ts.createIdentifier('default'));
56+
57+
const bodyStatements: ts.Statement[] = [ts.createReturn(requireGraphQLModule)];
58+
if (opts.isDevVariable != null || opts.isDevelopment) {
59+
const nodeVariable = ts.createIdentifier('node');
60+
const nodeDotHash = ts.createPropertyAccess(nodeVariable, ts.createIdentifier('hash'));
61+
let checkStatements: ts.Statement[] = [
62+
createVariableStatement(ts.NodeFlags.Const, nodeVariable, requireGraphQLModule),
63+
ts.createIf(
64+
ts.createLogicalAnd(
65+
nodeDotHash,
66+
ts.createStrictInequality(nodeDotHash, ts.createLiteral(hash)),
67+
),
68+
ts.createBlock([
69+
ts.createStatement(
70+
warnNeedsRebuild(definitionName, opts.buildCommand),
71+
),
72+
]),
73+
),
74+
];
75+
if (opts.isDevVariable != null) {
76+
checkStatements = [
77+
ts.createIf(
78+
ts.createIdentifier(opts.isDevVariable),
79+
ts.createBlock(checkStatements),
80+
),
81+
];
82+
}
83+
bodyStatements.unshift(...checkStatements);
84+
}
85+
return ts.createFunctionExpression(undefined, undefined, undefined, undefined, [], undefined, ts.createBlock(bodyStatements));
86+
}
87+
88+
function warnNeedsRebuild(
89+
definitionName: string,
90+
buildCommand?: string,
91+
): ts.Expression {
92+
return ts.createCall(
93+
ts.createPropertyAccess(ts.createIdentifier('console'), ts.createIdentifier('error')),
94+
undefined,
95+
[
96+
ts.createLiteral(
97+
`The definition of '${definitionName}' appears to have changed. Run ` +
98+
'`' +
99+
(buildCommand || 'relay-compiler') +
100+
'` to update the generated files to receive the expected data.',
101+
),
102+
],
103+
);
104+
}
105+
106+
function getRelativeImportPath(
107+
fileName: string,
108+
artifactDirectory: string,
109+
fileToRequire: string,
110+
): string {
111+
112+
const relative = path.relative(
113+
path.dirname(fileName),
114+
path.resolve(artifactDirectory),
115+
);
116+
117+
const relativeReference = relative.length === 0 ? './' : '';
118+
119+
return relativeReference + path.join(relative, fileToRequire);
120+
}

transform/src/getFragmentNameParts.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const DEFAULT_PROP_NAME = 'data';
2+
3+
/**
4+
* Matches a GraphQL fragment name pattern, extracting the data property key
5+
* from the name.
6+
*/
7+
export function getFragmentNameParts(fragmentName: string): [string, string] {
8+
const match = fragmentName.match(
9+
/^([a-zA-Z][a-zA-Z0-9]*)(?:_([a-zA-Z][_a-zA-Z0-9]*))?$/,
10+
);
11+
if (!match) {
12+
throw new Error(
13+
'TSTransformRelay: Fragments should be named ' +
14+
'`ModuleName_fragmentName`, got `' +
15+
fragmentName +
16+
'`.',
17+
);
18+
}
19+
const module = match[1];
20+
const propName = match[2];
21+
if (propName === DEFAULT_PROP_NAME) {
22+
throw new Error(
23+
'TSTransformRelay: Fragment `' +
24+
fragmentName +
25+
'` should not end in ' +
26+
'`_data` to avoid conflict with a fragment named `' +
27+
module +
28+
'` ' +
29+
'which also provides resulting data via the React prop `data`. Either ' +
30+
'rename this fragment to `' +
31+
module +
32+
'` or choose a different ' +
33+
'prop name.',
34+
);
35+
}
36+
return [module, propName || DEFAULT_PROP_NAME];
37+
}

transform/src/getValidGraphQLTag.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import * as ts from 'typescript';
2+
import { DocumentNode, parse } from 'graphql';
3+
4+
export function getValidGraphQLTag(node: ts.TaggedTemplateExpression): DocumentNode | null {
5+
if (!isGraphQLTag(node)) {
6+
return null;
7+
}
8+
9+
if (!ts.isNoSubstitutionTemplateLiteral(node.template)) {
10+
throw new Error("TSTransformerRelay: Substitutions are not allowed in graphql fragments. " +
11+
"Included fragments should be referenced as `...MyModule_propName`.",
12+
);
13+
}
14+
15+
const text = node.template.text;
16+
17+
const ast = parse(text);
18+
19+
if (ast.definitions.length === 0) {
20+
throw new Error("TSTransformerRelay: Unexpected empty graphql tag.");
21+
}
22+
23+
return ast;
24+
}
25+
26+
function isGraphQLTag(node: ts.TaggedTemplateExpression): boolean {
27+
const tag = node.tag;
28+
29+
if (!ts.isIdentifier(tag)) {
30+
return false;
31+
}
32+
33+
if (tag.text !== 'graphql') {
34+
return false;
35+
}
36+
37+
return true;
38+
}

0 commit comments

Comments
 (0)