Mirror: Modular GraphQL.js import paths without the hassle.
1const fs = require('fs');
2const path = require('path');
3
4const { rollup } = require('rollup');
5
6/** Generates a map of exports from a given graphql package to list of import locations. */
7async function traceImports(moduleName) {
8 const basepath = path.resolve(process.cwd(), 'node_modules/', moduleName);
9 const exportMap = {};
10
11 const resolveFile = (to, relative = '.') => {
12 const dirname = path.join('graphql/', relative, path.dirname(to));
13 const filename = path.basename(to, '.mjs');
14 return path.join(dirname, filename);
15 };
16
17 const bundle = await rollup({
18 // This contains all top-level "sub-modules" of graphql too, since not all exports of
19 // them may be exposed in the main index.mjs file.
20 input: {
21 graphql: path.join(basepath, 'index.mjs'),
22 'graphql/error': path.join(basepath, 'error/index.mjs'),
23 'graphql/execution': path.join(basepath, 'execution/index.mjs'),
24 'graphql/language': path.join(basepath, 'language/index.mjs'),
25 'graphql/subscription': path.join(basepath, 'subscription/index.mjs'),
26 'graphql/type': path.join(basepath, 'type/index.mjs'),
27 'graphql/utilities': path.join(basepath, 'utilities/index.mjs'),
28 'graphql/validation': path.join(basepath, 'validation/index.mjs'),
29 },
30 external: (id) => !/^\.{0,2}\//.test(id),
31 preserveModules: true,
32 plugins: [
33 require('@rollup/plugin-node-resolve').nodeResolve(),
34 {
35 transform(code, id) {
36 const relative = path.relative(basepath, id);
37 const dirname = path.dirname(relative);
38 const exports = {};
39
40 this.parse(code)
41 .body.filter((x) => x.type === 'ExportNamedDeclaration')
42 .forEach((node) => {
43 const from = node.source
44 ? resolveFile(node.source.value, dirname)
45 : resolveFile(relative);
46
47 node.specifiers.forEach((specifier) => {
48 const { name: local } = specifier.exported;
49 exports[local] = { local, from };
50 });
51
52 if (node.declaration) {
53 (node.declaration.declarations || [node.declaration]).forEach((declaration) => {
54 if (declaration && declaration.id) {
55 const { name: local } = declaration.id;
56 exports[local] = { local, from };
57 }
58 });
59 }
60 });
61
62 exportMap[resolveFile(relative)] = exports;
63 return null;
64 },
65 },
66 ],
67 });
68
69 await bundle.generate({});
70 return exportMap;
71}
72
73function isDeclarationEqual(a, b) {
74 return a.local === b.local && a.from === b.from;
75}
76
77function mergeTraces(traces) {
78 const trace = {};
79
80 // Iterate over all known filenames in all traces
81 const ids = new Set(
82 traces.map((trace) => Object.keys(trace)).reduce((acc, names) => acc.concat(names), [])
83 );
84 for (const id of ids) {
85 // Each file must exist in all traces
86 if (!traces.every((trace) => !!trace[id])) continue;
87
88 const exports = {};
89
90 // Iterate over all known exports in each trace's set of exports for this file
91 const exportNames = new Set(
92 traces.map((trace) => Object.keys(trace[id])).reduce((acc, names) => acc.concat(names), [])
93 );
94 for (const name of exportNames) {
95 // Each export must exist in all traces
96 if (traces.every((trace) => !!trace[id][name])) {
97 // Collect known declarations and deduplicate
98 exports[name] = traces
99 .map((trace) => trace[id][name])
100 .filter((val, index, all) => {
101 const firstIndex = all.findIndex((item) => isDeclarationEqual(item, val));
102 return firstIndex === index;
103 });
104 }
105 }
106
107 if (Object.keys(exports).length) trace[id] = exports;
108 }
109
110 // For a given declaration, find the first deepest one that works for the trace
111 // NOTE: This doesn't find the absolute deepest one, since it assumes that each
112 // export only has one functional trace
113 const resolveDeclaration = (declaration) => {
114 const declarations = trace[declaration.from];
115 if (!declarations || !declarations[declaration.local]) return null;
116 for (const childDeclaration of declarations[declaration.local]) {
117 if (childDeclaration.from === declaration.from) continue;
118 const resolved = resolveDeclaration(childDeclaration);
119 if (resolved && resolved.from !== declaration.from) return resolved;
120 }
121
122 return declaration;
123 };
124
125 // Resolve all known (and consistent) exports to a common, deepest declaration
126 const ROOT_MODULE = 'graphql/index';
127 for (const local in trace[ROOT_MODULE])
128 exports[local] = resolveDeclaration({ local, from: ROOT_MODULE });
129 return exports;
130}
131
132(async () => {
133 const traces = await Promise.all([
134 traceImports('graphql-14'),
135 traceImports('graphql-15'),
136 traceImports('graphql-16'),
137 ]);
138
139 const trace = mergeTraces(traces);
140
141 fs.writeFileSync('import-map.json', JSON.stringify(trace, null, 2));
142})().catch((err) => {
143 console.error(`${err.name}: ${err.stack}`);
144 process.exit(1);
145});