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

Commit dc4acba

Browse files
committed
feat: initial pass at generated typed hooks
1 parent a190679 commit dc4acba

File tree

4 files changed

+349
-33
lines changed

4 files changed

+349
-33
lines changed

hooks.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require("./lib/hooks");

package.json

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
],
3030
"main": "lib/index.js",
3131
"files": [
32-
"lib"
32+
"lib",
33+
"hooks.js"
3334
],
3435
"scripts": {
3536
"build": "rm -rf lib && tsc --project tsconfig.build.json",
@@ -83,8 +84,8 @@
8384
"@types/jest": "^26.0.20",
8485
"@types/node": "14.14.28",
8586
"@types/relay-compiler": "^8.0.0",
86-
"@types/relay-runtime": "^10.0.1",
87-
"babel-plugin-relay": "^10.0.0",
87+
"@types/relay-runtime": "^10.1.10",
88+
"babel-plugin-relay": "^11.0.0",
8889
"chokidar-cli": "^2.0.0",
8990
"concurrently": "^5.0.0",
9091
"glob": "^7.1.6",
@@ -94,20 +95,20 @@
9495
"jest-cli": "^26.6.3",
9596
"lint-staged": "^10.5.3",
9697
"prettier": "^2.2.1",
97-
"relay-compiler": "^10.0.1",
98-
"relay-runtime": "^10.0.1",
99-
"relay-test-utils-internal": "^10.0.1",
98+
"relay-compiler": "^11.0.0",
99+
"relay-runtime": "^11.0.0",
100+
"relay-test-utils-internal": "^11.0.0",
100101
"ts-jest": "^26.4.4",
101102
"ts-node": "^9.1.1",
102103
"tslint": "^6.1.3",
103104
"tslint-config-prettier": "^1.18.0",
104105
"typescript": "4.1.5"
105106
},
106107
"peerDependencies": {
107-
"@types/react-relay": ">=7.0.0",
108-
"@types/relay-runtime": ">=6.0.7",
109-
"relay-compiler": ">=9.0.0",
110-
"relay-runtime": ">=9.0.0",
108+
"@types/react-relay": "^11.0.0",
109+
"@types/relay-runtime": ">=10.1.10",
110+
"relay-compiler": ">=11.0.0",
111+
"relay-runtime": ">=11.0.0",
111112
"typescript": ">=3.6.4"
112113
},
113114
"publishConfig": {

src/hooks.ts

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
import { FormatModule, LocalArgumentDefinition } from "relay-compiler";
2+
import addAnyTypeCast from "./addAnyTypeCast";
3+
import relayCompilerLanguageTypescript from "./index";
4+
import { loadCompilerOptions } from "./loadCompilerOptions";
5+
6+
export default function relayHooksTypescriptCompiler() {
7+
const compilerOptions = loadCompilerOptions();
8+
9+
const formatModule: FormatModule = (opts) => {
10+
const {
11+
// moduleName,
12+
documentType,
13+
docText,
14+
concreteText,
15+
typeText,
16+
hash,
17+
sourceHash,
18+
definition,
19+
definition: { name, metadata },
20+
} = opts;
21+
22+
const allHooks: string[] = [];
23+
const typeImports: string[] = [];
24+
const reactImports: string[] = [];
25+
const reactRelayImports: string[] = [];
26+
const relayRuntimeImports: string[] = [];
27+
28+
if (documentType) {
29+
relayRuntimeImports.push(documentType);
30+
}
31+
32+
const meta = (metadata ?? {}) as CompilerMeta;
33+
34+
if (!meta.derivedFrom) {
35+
if (definition.kind === "Fragment") {
36+
if (meta.refetch) {
37+
typeImports.push(
38+
`import { ${meta.refetch.operation} } from "./${meta.refetch.operation}.graphql"`
39+
);
40+
if (meta.connection) {
41+
relayRuntimeImports.push("OperationType");
42+
reactRelayImports.push(
43+
"LoadMoreFn",
44+
"RefetchFnDynamic",
45+
"usePaginationFragment"
46+
);
47+
allHooks.push(
48+
makePaginationFragmentBlock(name, meta.refetch.operation)
49+
);
50+
} else {
51+
reactRelayImports.push(
52+
"useRefetchableFragment",
53+
"RefetchFnDynamic"
54+
);
55+
allHooks.push(
56+
makeRefetchableFragmentBlock(name, meta.refetch.operation)
57+
);
58+
}
59+
} else {
60+
reactRelayImports.push("useFragment");
61+
allHooks.push(makeFragmentBlock(name));
62+
}
63+
} else if (definition.kind === "Request") {
64+
if (definition.root.operation === "query") {
65+
// Common across the query fns
66+
relayRuntimeImports.push(
67+
"VariablesOf",
68+
"FetchPolicy",
69+
"CacheConfig",
70+
"RenderPolicy"
71+
);
72+
relayRuntimeImports.push("IEnvironment");
73+
reactRelayImports.push(
74+
"loadQuery",
75+
"LoadQueryOptions",
76+
"EnvironmentProviderOptions"
77+
);
78+
allHooks.push(
79+
makeLoadBlock(name, definition.root.argumentDefinitions)
80+
);
81+
reactRelayImports.push("useLazyLoadQuery");
82+
allHooks.push(
83+
makeLazyLoadBlock(name, definition.root.argumentDefinitions)
84+
);
85+
86+
reactRelayImports.push("useQueryLoader", "PreloadedQuery");
87+
allHooks.push(makeQueryLoaderBlock(name));
88+
89+
reactRelayImports.push("usePreloadedQuery");
90+
allHooks.push(makePreloadedQueryBlock(name));
91+
} else if (definition.root.operation === "mutation") {
92+
relayRuntimeImports.push(
93+
"MutationConfig",
94+
"IEnvironment",
95+
"Disposable"
96+
);
97+
reactRelayImports.push("useMutation");
98+
allHooks.push(makeMutationBlock(name));
99+
} else if (definition.root.operation === "subscription") {
100+
reactImports.push("useMemo");
101+
relayRuntimeImports.push(
102+
"GraphQLSubscriptionConfig",
103+
"requestSubscription"
104+
);
105+
reactRelayImports.push("useSubscription");
106+
allHooks.push(
107+
makeSubscriptionBlock(name, definition.root.argumentDefinitions)
108+
);
109+
}
110+
}
111+
}
112+
113+
const allImports: string[] = [];
114+
115+
if (relayRuntimeImports.length) {
116+
allImports.push(makeImport(relayRuntimeImports, "relay-runtime"));
117+
}
118+
if (reactRelayImports.length) {
119+
allImports.push(makeImport(reactRelayImports, "react-relay"));
120+
}
121+
if (reactImports.length) {
122+
allImports.push(makeImport(reactImports, "react"));
123+
}
124+
if (typeImports) {
125+
allImports.push(typeImports.join("\n"));
126+
}
127+
128+
const docTextComment = docText ? "\n/*\n" + docText.trim() + "\n*/\n" : "";
129+
let nodeStatement = `const node: ${
130+
documentType || "never"
131+
} = ${concreteText};`;
132+
if (compilerOptions.noImplicitAny) {
133+
nodeStatement = addAnyTypeCast(nodeStatement).trim();
134+
}
135+
return `/* tslint:disable */
136+
/* eslint-disable */
137+
// @ts-nocheck
138+
${hash ? `/* ${hash} */\n` : ""}
139+
${allImports.join("\n")}
140+
${typeText || ""}
141+
142+
${docTextComment}
143+
${nodeStatement}
144+
(node as any).hash = '${sourceHash}';
145+
146+
export default node;
147+
148+
${allHooks.join("\n")}
149+
`;
150+
};
151+
152+
return {
153+
...relayCompilerLanguageTypescript(),
154+
formatModule,
155+
};
156+
}
157+
158+
function capitalize(str: string) {
159+
return `${str[0].toUpperCase()}${str.slice(1)}`;
160+
}
161+
162+
interface CompilerMeta {
163+
derivedFrom?: string;
164+
connection?: unknown[];
165+
refetch?: {
166+
connection: unknown;
167+
operation: string;
168+
fragmentPathInResult: unknown[];
169+
identifierField: unknown;
170+
};
171+
}
172+
173+
function makeImport(idents: string[], from: string) {
174+
return `import { ${Array.from(new Set(idents))
175+
.sort()
176+
.join(", ")} } from "${from}";`;
177+
}
178+
179+
function makeFragmentBlock(name: string) {
180+
const n = capitalize(name);
181+
182+
// NOTE: These declares ensure that the type of the returned data is:
183+
// - non-nullable if the provided ref type is non-nullable
184+
// - nullable if the provided ref type is nullable
185+
// - array of non-nullable if the provided ref type is an array of
186+
// non-nullable refs
187+
// - array of nullable if the provided ref type is an array of nullable refs
188+
189+
return `export function use${n}Fragment<TKey extends ${n}$key>(fragmentRef: TKey): Required<TKey>[" $data"]
190+
export function use${n}Fragment<TKey extends ${n}$key>(fragmentRef: TKey | null): Required<TKey>[" $data"] | null
191+
export function use${n}Fragment<TKey extends ${n}$key>(fragmentRef: ReadonlyArray<TKey>): ReadonlyArray<Required<TKey>[" $data"]>
192+
export function use${n}Fragment<TKey extends ${n}$key>(fragmentRef: ReadonlyArray<TKey | null>): ReadonlyArray<Required<TKey>[" $data"] | null>
193+
export function use${n}Fragment<TKey extends ${n}$key>(fragmentRef: ReadonlyArray<TKey> | null): ReadonlyArray<Required<TKey>[" $data"]> | null
194+
export function use${n}Fragment<TKey extends ${n}$key>(fragmentRef: ReadonlyArray<TKey | null> | null): ReadonlyArray<Required<TKey>[" $data"] | null> | null
195+
export function use${n}Fragment(fragmentRef: any) {
196+
return useFragment(node, fragmentRef)
197+
}`;
198+
}
199+
200+
function makeRefetchableFragmentBlock(name: string, operation: string) {
201+
const n = capitalize(name);
202+
return `export function useRefetchable${n}Fragment<TKey extends ${n}$key>(fragmentRef: TKey): [Required<TKey>[" $data"], RefetchFnDynamic<${operation}, ${n}$key>]
203+
export function useRefetchable${n}Fragment<TKey extends ${n}$key>(fragmentRef: TKey | null): [Required<TKey>[" $data"] | null, RefetchFnDynamic<${operation}, ${n}$key | null>]
204+
export function useRefetchable${n}Fragment(fragmentRef: any) {
205+
return useRefetchableFragment(node, fragmentRef)
206+
}`;
207+
}
208+
209+
function makePaginationFragmentBlock(name: string, operation: string) {
210+
const n = capitalize(name);
211+
212+
// Note: It'd be nice if react-relay exported this type for us
213+
return `interface usePaginationFragmentHookType<TQuery extends OperationType, TKey extends ${name}$key | null, TFragmentData> {
214+
data: TFragmentData;
215+
loadNext: LoadMoreFn<TQuery>;
216+
loadPrevious: LoadMoreFn<TQuery>;
217+
hasNext: boolean;
218+
hasPrevious: boolean;
219+
isLoadingNext: boolean;
220+
isLoadingPrevious: boolean;
221+
refetch: RefetchFnDynamic<TQuery, TKey>;
222+
}
223+
224+
export function usePaginated${n}<K extends ${name}$key>(fragmentRef: K): usePaginationFragmentHookType<${operation}, ${name}$key, Required<K>[" $data"]>
225+
export function usePaginated${n}<K extends ${name}$key>(fragmentRef: K | null): usePaginationFragmentHookType<${operation}, ${name}$key | null, Required<K>[" $data"] | null>;
226+
export function usePaginated${n}(fragmentRef: any) {
227+
return usePaginationFragment<${operation}, ${name}$key>(node, fragmentRef)
228+
}`;
229+
}
230+
231+
function makePreloadedQueryBlock(name: string) {
232+
const n = capitalize(name);
233+
return `export function usePreloaded${n}(preloadedQuery: PreloadedQuery<${name}>, options?: {
234+
UNSTABLE_renderPolicy?: RenderPolicy;
235+
}) {
236+
return usePreloadedQuery<${name}>(node, preloadedQuery, options)
237+
}`;
238+
}
239+
240+
function makeQueryLoaderBlock(name: string) {
241+
const fn = capitalize(name);
242+
return `export function use${fn}Loader(initialQueryReference?: PreloadedQuery<${name}> | null) {
243+
return useQueryLoader(node, initialQueryReference)
244+
}`;
245+
}
246+
247+
function makeLazyLoadBlock(
248+
name: string,
249+
args: ReadonlyArray<LocalArgumentDefinition>
250+
) {
251+
const n = capitalize(name);
252+
const noVars = args.length === 0;
253+
return `export function use${n}(variables: VariablesOf<${name}>${
254+
noVars ? " = {}" : ""
255+
}, options?: {
256+
fetchKey?: string | number;
257+
fetchPolicy?: FetchPolicy;
258+
networkCacheConfig?: CacheConfig;
259+
UNSTABLE_renderPolicy?: RenderPolicy;
260+
}) {
261+
return useLazyLoadQuery<${name}>(node, variables, options)
262+
}`;
263+
}
264+
265+
function makeLoadBlock(
266+
name: string,
267+
args: ReadonlyArray<LocalArgumentDefinition>
268+
) {
269+
const n = capitalize(name);
270+
const noVars = args.length === 0;
271+
return `export function load${n}<TEnvironmentProviderOptions extends EnvironmentProviderOptions = {}>(
272+
environment: IEnvironment,
273+
variables: VariablesOf<${name}>${noVars ? " = {}" : ""},
274+
options?: LoadQueryOptions,
275+
environmentProviderOptions?: TEnvironmentProviderOptions,
276+
): PreloadedQuery<${name}, TEnvironmentProviderOptions> {
277+
return loadQuery(environment, node, variables, options, environmentProviderOptions)
278+
}`;
279+
}
280+
281+
function makeSubscriptionBlock(
282+
name: string,
283+
args: ReadonlyArray<LocalArgumentDefinition>
284+
) {
285+
const n = capitalize(name);
286+
const noVars = args.length === 0;
287+
return `export function use${n}(
288+
config${noVars ? "?:" : ":"} Omit<
289+
GraphQLSubscriptionConfig<${name}>,
290+
'subscription' ${noVars ? `| 'variables'` : ""}
291+
>${noVars ? `& { variables?: ${name}['variables'] },` : ","}
292+
requestSubscriptionFn?: typeof requestSubscription
293+
) {
294+
const memoConfig = useMemo(() => {
295+
return {
296+
variables: ${noVars ? "{}" : "config.variables"},
297+
...config,
298+
subscription: node,
299+
}
300+
}, [config]);
301+
return useSubscription<${name}>(
302+
memoConfig,
303+
requestSubscriptionFn
304+
);
305+
}`;
306+
}
307+
308+
function makeMutationBlock(name: string) {
309+
const n = capitalize(name);
310+
return `
311+
export function use${n}(mutationConfig?: (environment: IEnvironment, config: MutationConfig<${name}>) => Disposable) {
312+
return useMutation<${name}>(node, mutationConfig)
313+
}`;
314+
}

0 commit comments

Comments
 (0)